Merge branch 'develop' of https://github.com/vector-im/element-x-android into dla/feature/connect_sdk_to_global_notifications_ui
This commit is contained in:
commit
c3fbac4678
686 changed files with 7212 additions and 2257 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/develop' && format('build-develop-{0}-{1}', matrix.variant, github.sha) || format('build-{0}-{1}', matrix.variant, github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
|
|
|
|||
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
|
|
@ -7,7 +7,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
name: Danger main check
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
||||
- name: Danger
|
||||
|
|
|
|||
2
.github/workflows/gradle-wrapper-update.yml
vendored
2
.github/workflows/gradle-wrapper-update.yml
vendored
|
|
@ -8,7 +8,7 @@ jobs:
|
|||
update-gradle-wrapper:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Update Gradle Wrapper
|
||||
uses: gradle-update/update-gradle-wrapper-action@v1
|
||||
# Skip in forks
|
||||
|
|
|
|||
|
|
@ -11,5 +11,5 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
# No concurrency required, this is a prerequisite to other actions and should run every time.
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
|
|
|
|||
4
.github/workflows/maestro.yml
vendored
4
.github/workflows/maestro.yml
vendored
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
group: ${{ format('maestro-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.4.1
|
||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.5.0
|
||||
with:
|
||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||
# Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android):
|
||||
|
|
|
|||
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'vector-im/element-x-android' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
|
|
|
|||
2
.github/workflows/nightlyReports.yml
vendored
2
.github/workflows/nightlyReports.yml
vendored
|
|
@ -55,7 +55,7 @@ jobs:
|
|||
name: Dependency analysis
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
|
|
|
|||
10
.github/workflows/quality.yml
vendored
10
.github/workflows/quality.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
name: Search for forbidden patterns
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run code quality check suite
|
||||
run: ./tools/check/check_code_quality.sh
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/main' && format('check-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-develop-{0}', github.sha) || format('check-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
|
|
@ -52,12 +52,6 @@ jobs:
|
|||
name: linting-report
|
||||
path: |
|
||||
*/build/reports/**/*.*
|
||||
- name: 🔊 Publish results to Sonar
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
|
||||
if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }}
|
||||
run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Prepare Danger
|
||||
if: always()
|
||||
run: |
|
||||
|
|
|
|||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/head/main' && format('build-release-main-{0}', github.sha) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
|
|
|
|||
14
.github/workflows/sonar.yml
vendored
14
.github/workflows/sonar.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Code Quality Checks
|
||||
name: Sonar
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
|
@ -10,18 +10,18 @@ on:
|
|||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options="-Xmx2g" -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon --warn
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 --no-daemon --warn
|
||||
|
||||
jobs:
|
||||
sonar:
|
||||
name: Project Check Suite
|
||||
name: Sonar Quality Checks
|
||||
runs-on: ubuntu-latest
|
||||
# Allow all jobs on main and develop. Just one per PR.
|
||||
concurrency:
|
||||
group: ${{ github.ref == 'refs/heads/main' && format('sonar-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('sonar-develop-{0}', github.sha) || format('sonar-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
|
|
@ -41,9 +41,3 @@ jobs:
|
|||
ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
|
||||
if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }}
|
||||
run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Prepare Danger
|
||||
if: always()
|
||||
run: |
|
||||
npm install --save-dev @babel/core
|
||||
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
||||
yarn add danger-plugin-lint-report --dev
|
||||
|
|
|
|||
2
.github/workflows/sync-localazy.yml
vendored
2
.github/workflows/sync-localazy.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
# Skip in forks
|
||||
if: github.repository == 'vector-im/element-x-android'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
|
|
|
|||
10
.github/workflows/tests.yml
vendored
10
.github/workflows/tests.yml
vendored
|
|
@ -22,6 +22,16 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
# Increase swapfile size to prevent screenshot tests getting terminated
|
||||
# https://github.com/actions/runner-images/discussions/7188#discussioncomment-6750749
|
||||
- name: 💽 Increase swapfile size
|
||||
run: |
|
||||
sudo swapoff -a
|
||||
sudo fallocate -l 8G /mnt/swapfile
|
||||
sudo chmod 600 /mnt/swapfile
|
||||
sudo mkswap /mnt/swapfile
|
||||
sudo swapon /mnt/swapfile
|
||||
sudo swapon --show
|
||||
- name: ⏬ Checkout with LFS
|
||||
uses: nschloe/action-cached-lfs-checkout@v1.2.2
|
||||
with:
|
||||
|
|
|
|||
13
.maestro/tests/roomList/timeline/messages/poll.yaml
Normal file
13
.maestro/tests/roomList/timeline/messages/poll.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
appId: ${APP_ID}
|
||||
---
|
||||
- takeScreenshot: build/maestro/530-Timeline
|
||||
- tapOn: "Add attachment"
|
||||
- tapOn: "Poll"
|
||||
- tapOn: "What is the poll about?"
|
||||
- inputText: "I am a poll"
|
||||
- tapOn: "Option 1"
|
||||
- inputText: "Answer 1"
|
||||
- tapOn: "Option 2"
|
||||
- inputText: "Answer 2"
|
||||
- tapOn: "Create"
|
||||
- takeScreenshot: build/maestro/531-Timeline
|
||||
|
|
@ -5,5 +5,6 @@ appId: ${APP_ID}
|
|||
- takeScreenshot: build/maestro/500-Timeline
|
||||
- runFlow: messages/text.yaml
|
||||
- runFlow: messages/location.yaml
|
||||
- runFlow: messages/poll.yaml
|
||||
- back
|
||||
- runFlow: ../../assertions/assertHomeDisplayed.yaml
|
||||
|
|
|
|||
23
CHANGES.md
23
CHANGES.md
|
|
@ -1,3 +1,26 @@
|
|||
Changes in Element X v0.1.6 (2023-09-04)
|
||||
========================================
|
||||
|
||||
Features ✨
|
||||
----------
|
||||
- Enable the Polls feature. Allows to create, view, vote and end polls. ([#1196](https://github.com/vector-im/element-x-android/issues/1196))
|
||||
- Create poll. ([#1143](https://github.com/vector-im/element-x-android/issues/1143))
|
||||
|
||||
Bugfixes 🐛
|
||||
----------
|
||||
- Ensure notification for Event from encrypted room get decrypted content. ([#1178](https://github.com/vector-im/element-x-android/issues/1178))
|
||||
- Make sure Snackbars are only displayed once. ([#928](https://github.com/vector-im/element-x-android/issues/928))
|
||||
- Fix the orientation of sent images. ([#1135](https://github.com/vector-im/element-x-android/issues/1135))
|
||||
- Bug reporter crashes when 'send logs' is disabled. ([#1168](https://github.com/vector-im/element-x-android/issues/1168))
|
||||
- Add missing link to the terms on the analytics setting screen. ([#1177](https://github.com/vector-im/element-x-android/issues/1177))
|
||||
- Re-enable `SyncService.withEncryptionSync` to improve decryption of notifications. ([#1198](https://github.com/vector-im/element-x-android/issues/1198))
|
||||
- Crash with `aspectRatio` modifier when `Float.NaN` was used as input. ([#1995](https://github.com/vector-im/element-x-android/issues/1995))
|
||||
|
||||
Other changes
|
||||
-------------
|
||||
- Remove unnecessary year in copyright mention. ([#1187](https://github.com/vector-im/element-x-android/issues/1187))
|
||||
|
||||
|
||||
Changes in Element X v0.1.5 (2023-08-28)
|
||||
========================================
|
||||
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ dependencies {
|
|||
implementation(libs.network.okhttp.logging)
|
||||
implementation(libs.serialization.json)
|
||||
|
||||
implementation(libs.vanniktech.emoji)
|
||||
implementation(libs.matrix.emojibase.bindings)
|
||||
|
||||
implementation(libs.dagger)
|
||||
kapt(libs.dagger.compiler)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
<application
|
||||
android:name=".ElementXApplication"
|
||||
android:allowBackup="true"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import io.element.android.x.di.AppComponent
|
|||
import io.element.android.x.di.DaggerAppComponent
|
||||
import io.element.android.x.info.logApplicationInfo
|
||||
import io.element.android.x.initializer.CrashInitializer
|
||||
import io.element.android.x.initializer.EmojiInitializer
|
||||
import io.element.android.x.initializer.TracingInitializer
|
||||
|
||||
class ElementXApplication : Application(), DaggerComponentOwner {
|
||||
|
|
@ -39,7 +38,6 @@ class ElementXApplication : Application(), DaggerComponentOwner {
|
|||
AppInitializer.getInstance(this).apply {
|
||||
initializeComponent(CrashInitializer::class.java)
|
||||
initializeComponent(TracingInitializer::class.java)
|
||||
initializeComponent(EmojiInitializer::class.java)
|
||||
}
|
||||
logApplicationInfo()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import androidx.preference.PreferenceManager
|
|||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.DefaultEmojibaseProvider
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.EmojibaseProvider
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
|
|
@ -105,4 +107,10 @@ object AppModule {
|
|||
fun provideSnackbarDispatcher(): SnackbarDispatcher {
|
||||
return SnackbarDispatcher()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesEmojibaseProvider(@ApplicationContext context: Context): EmojibaseProvider {
|
||||
return DefaultEmojibaseProvider(context)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@
|
|||
package io.element.android.x.initializer
|
||||
|
||||
import android.content.Context
|
||||
import android.system.Os
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.startup.Initializer
|
||||
import io.element.android.features.preferences.impl.developer.tracing.SharedPrefTracingConfigurationStore
|
||||
import io.element.android.features.preferences.impl.developer.tracing.TargetLogLevelMapBuilder
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
|
||||
import io.element.android.libraries.matrix.api.tracing.TracingFilterConfigurations
|
||||
|
|
@ -34,8 +38,11 @@ class TracingInitializer : Initializer<Unit> {
|
|||
val bugReporter = appBindings.bugReporter()
|
||||
Timber.plant(tracingService.createTimberTree())
|
||||
val tracingConfiguration = if (BuildConfig.DEBUG) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val store = SharedPrefTracingConfigurationStore(prefs)
|
||||
val builder = TargetLogLevelMapBuilder(store)
|
||||
TracingConfiguration(
|
||||
filterConfiguration = TracingFilterConfigurations.debug,
|
||||
filterConfiguration = TracingFilterConfigurations.custom(builder.getCurrentMap()),
|
||||
writesToLogcat = true,
|
||||
writesToFilesConfiguration = WriteToFilesConfiguration.Disabled
|
||||
)
|
||||
|
|
@ -51,6 +58,8 @@ class TracingInitializer : Initializer<Unit> {
|
|||
}
|
||||
bugReporter.cleanLogDirectoryIfNeeded()
|
||||
tracingService.setupTracing(tracingConfiguration)
|
||||
// Also set env variable for rust back trace
|
||||
Os.setenv("RUST_BACKTRACE", "1", true)
|
||||
}
|
||||
|
||||
override fun dependencies(): List<Class<out Initializer<*>>> = mutableListOf()
|
||||
|
|
|
|||
|
|
@ -15,15 +15,8 @@
|
|||
-->
|
||||
|
||||
<!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older that API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
All backup is disabled since it would clash with encryption.
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
<exclude domain="root" path="." />
|
||||
</full-backup-content>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2022 New Vector Ltd
|
||||
~ Copyright (c) 2023 New Vector Ltd
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
|
|
@ -15,21 +15,13 @@
|
|||
-->
|
||||
|
||||
<!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
All backup is disabled since it would clash with encryption.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
<exclude domain="root" path="." />
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
<exclude domain="root" path="." />
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
|
|
|
|||
|
|
@ -48,8 +48,6 @@ dependencies {
|
|||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.permissions.noop)
|
||||
|
||||
implementation(libs.coil)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import dagger.assisted.AssistedInject
|
|||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.login.api.LoginEntryPoint
|
||||
import io.element.android.features.onboarding.api.OnBoardingEntryPoint
|
||||
import io.element.android.features.preferences.api.ConfigureTracingEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
|
@ -43,6 +44,7 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
|||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val onBoardingEntryPoint: OnBoardingEntryPoint,
|
||||
private val configureTracingEntryPoint: ConfigureTracingEntryPoint,
|
||||
private val loginEntryPoint: LoginEntryPoint,
|
||||
private val notLoggedInImageLoaderFactory: NotLoggedInImageLoaderFactory,
|
||||
) : BackstackNode<NotLoggedInFlowNode.NavTarget>(
|
||||
|
|
@ -70,6 +72,9 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
|||
data class LoginFlow(
|
||||
val isAccountCreation: Boolean,
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object ConfigureTracing : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -83,6 +88,10 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
|||
override fun onSignIn() {
|
||||
backstack.push(NavTarget.LoginFlow(isAccountCreation = false))
|
||||
}
|
||||
|
||||
override fun onOpenDeveloperSettings() {
|
||||
backstack.push(NavTarget.ConfigureTracing)
|
||||
}
|
||||
}
|
||||
onBoardingEntryPoint
|
||||
.nodeBuilder(this, buildContext)
|
||||
|
|
@ -94,6 +103,9 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
|||
.params(LoginEntryPoint.Params(isAccountCreation = navTarget.isAccountCreation))
|
||||
.build()
|
||||
}
|
||||
NavTarget.ConfigureTracing -> {
|
||||
configureTracingEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,44 +16,26 @@
|
|||
|
||||
package io.element.android.appnav.loggedin
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
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.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import kotlinx.coroutines.delay
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val DELAY_BEFORE_SHOWING_SYNC_SPINNER_IN_MILLIS = 1500L
|
||||
|
||||
class LoggedInPresenter @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val pushService: PushService,
|
||||
) : Presenter<LoggedInState> {
|
||||
|
||||
private val postNotificationPermissionsPresenter by lazy {
|
||||
// Ask for POST_NOTIFICATION PERMISSION on Android 13+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
permissionsPresenterFactory.create(Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
NoopPermissionsPresenter()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): LoggedInState {
|
||||
LaunchedEffect(Unit) {
|
||||
|
|
@ -64,25 +46,15 @@ class LoggedInPresenter @Inject constructor(
|
|||
pushService.registerWith(matrixClient, pushProvider, distributor)
|
||||
}
|
||||
|
||||
val roomListState by matrixClient.roomListService.state.collectAsState()
|
||||
val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState()
|
||||
val networkStatus by networkMonitor.connectivity.collectAsState()
|
||||
val permissionsState = postNotificationPermissionsPresenter.present()
|
||||
var showSyncSpinner by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
LaunchedEffect(roomListState, networkStatus) {
|
||||
showSyncSpinner = when {
|
||||
networkStatus == NetworkStatus.Offline -> false
|
||||
roomListState == RoomListService.State.Running -> false
|
||||
else -> {
|
||||
delay(DELAY_BEFORE_SHOWING_SYNC_SPINNER_IN_MILLIS)
|
||||
true
|
||||
}
|
||||
val showSyncSpinner by remember {
|
||||
derivedStateOf {
|
||||
networkStatus == NetworkStatus.Online && syncIndicator == RoomListService.SyncIndicator.Show
|
||||
}
|
||||
}
|
||||
return LoggedInState(
|
||||
showSyncSpinner = showSyncSpinner,
|
||||
permissionsState = permissionsState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@
|
|||
|
||||
package io.element.android.appnav.loggedin
|
||||
|
||||
import io.element.android.libraries.permissions.api.PermissionsState
|
||||
|
||||
data class LoggedInState(
|
||||
val showSyncSpinner: Boolean,
|
||||
val permissionsState: PermissionsState,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package io.element.android.appnav.loggedin
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState
|
||||
|
||||
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
||||
override val values: Sequence<LoggedInState>
|
||||
|
|
@ -32,5 +31,4 @@ fun aLoggedInState(
|
|||
showSyncSpinner: Boolean = true,
|
||||
) = LoggedInState(
|
||||
showSyncSpinner = showSyncSpinner,
|
||||
permissionsState = createDummyPostNotificationPermissionsState(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,21 +23,16 @@ import androidx.compose.foundation.layout.systemBarsPadding
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.androidutils.system.openAppSettingsPage
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.permissions.api.PermissionsView
|
||||
|
||||
@Composable
|
||||
fun LoggedInView(
|
||||
state: LoggedInState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -49,10 +44,6 @@ fun LoggedInView(
|
|||
.align(Alignment.TopCenter),
|
||||
isVisible = state.showSyncSpinner,
|
||||
)
|
||||
PermissionsView(
|
||||
state = state.permissionsState,
|
||||
openSystemSettings = context::openAppSettingsPage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,10 +31,16 @@ import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolde
|
|||
import io.element.android.services.apperror.api.AppErrorState
|
||||
import io.element.android.services.apperror.api.AppErrorStateService
|
||||
import io.element.android.services.apperror.impl.DefaultAppErrorStateService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class RootPresenterTest {
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
|
|
|
|||
|
|
@ -26,16 +26,21 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
|||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class LoggedInPresenterTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
|
|
@ -43,7 +48,7 @@ class LoggedInPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.permissionsState.permission).isEmpty()
|
||||
assertThat(initialState.showSyncSpinner).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -68,11 +73,6 @@ class LoggedInPresenterTest {
|
|||
): LoggedInPresenter {
|
||||
return LoggedInPresenter(
|
||||
matrixClient = FakeMatrixClient(roomListService = roomListService),
|
||||
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
||||
override fun create(permission: String): PermissionsPresenter {
|
||||
return NoopPermissionsPresenter()
|
||||
}
|
||||
},
|
||||
networkMonitor = FakeNetworkMonitor(networkStatus),
|
||||
pushService = object : PushService {
|
||||
override fun notificationStyleChanged() {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ allprojects {
|
|||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||
}
|
||||
dependencies {
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.2.1")
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.2.2")
|
||||
}
|
||||
|
||||
// KtLint
|
||||
|
|
@ -143,22 +143,6 @@ sonar {
|
|||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
val projectDir = projectDir.toString()
|
||||
sonar {
|
||||
properties {
|
||||
// Note: folders `kotlin` are not supported (yet), I asked on their side: https://community.sonarsource.com/t/82824
|
||||
// As a workaround provide the path in `sonar.sources` property.
|
||||
if (File("$projectDir/src/main/kotlin").exists()) {
|
||||
property("sonar.sources", "src/main/kotlin")
|
||||
}
|
||||
if (File("$projectDir/src/test/kotlin").exists()) {
|
||||
property("sonar.tests", "src/test/kotlin")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
tasks.withType<Test> {
|
||||
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
|
||||
|
|
@ -261,6 +245,8 @@ koverMerged {
|
|||
includes += "*Presenter"
|
||||
excludes += "*Fake*Presenter"
|
||||
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
|
||||
// Some options can't be tested at the moment
|
||||
excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*"
|
||||
}
|
||||
bound {
|
||||
minValue = 85
|
||||
|
|
|
|||
1
changelog.d/+sdk_bump.feature
Normal file
1
changelog.d/+sdk_bump.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Bump Rust SDK to `v0.1.49`
|
||||
|
|
@ -1 +0,0 @@
|
|||
Create poll.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Bug reporter crashes when 'send logs' is disabled.
|
||||
2
changelog.d/1172.feature
Normal file
2
changelog.d/1172.feature
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[Rich text editor] Integrate rich text editor library. Note that markdown is now not supported and further formatting support will be introduced through the rich text editor.
|
||||
|
||||
1
changelog.d/1173.bugfix
Normal file
1
changelog.d/1173.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Reply action: harmonize conditions in bottom sheet and swipe to reply.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Add missing link to the terms on the analytics setting screen.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Remove unnecessary year in copyright mention.
|
||||
1
changelog.d/1191.misc
Normal file
1
changelog.d/1191.misc
Normal file
|
|
@ -0,0 +1 @@
|
|||
Exclude some groups related to analytics to be included.
|
||||
1
changelog.d/1217.feature
Normal file
1
changelog.d/1217.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Implement Bloom effect modifier.
|
||||
1
changelog.d/1222.bugfix
Normal file
1
changelog.d/1222.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Fix system bar color after login on light theme.
|
||||
1
changelog.d/1224.feature
Normal file
1
changelog.d/1224.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Set color on display name and default avatar in the timeline.
|
||||
1
changelog.d/1241.bugfix
Normal file
1
changelog.d/1241.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Enable polls in release build.
|
||||
1
changelog.d/1244.misc
Normal file
1
changelog.d/1244.misc
Normal file
|
|
@ -0,0 +1 @@
|
|||
Use the new SyncIndicator API.
|
||||
1
changelog.d/1251.misc
Normal file
1
changelog.d/1251.misc
Normal file
|
|
@ -0,0 +1 @@
|
|||
Improve RoomSummary mapping by using RoomInfo.
|
||||
1
changelog.d/1261.feature
Normal file
1
changelog.d/1261.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
[Rich text editor] Add formatting menu (accessible via the '+' button)
|
||||
1
changelog.d/1269.misc
Normal file
1
changelog.d/1269.misc
Normal file
|
|
@ -0,0 +1 @@
|
|||
Ensure Posthog data are sent to "https://posthog.element.io"
|
||||
1
changelog.d/612.bugfix
Normal file
1
changelog.d/612.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Make links in room topic clickable
|
||||
1
changelog.d/897.feature
Normal file
1
changelog.d/897.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Add a notification permission screen to the initial flow.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Make sure Snackbars are only displayed once.
|
||||
69
docs/continuous_integration.md
Normal file
69
docs/continuous_integration.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Continuous integration strategy
|
||||
|
||||
<!--- TOC -->
|
||||
|
||||
* [Introduction](#introduction)
|
||||
* [CI tools](#ci-tools)
|
||||
* [Rules](#rules)
|
||||
* [What is the CI checking](#what-is-the-ci-checking)
|
||||
* [What is the CI reporting](#what-is-the-ci-reporting)
|
||||
* [Current choices](#current-choices)
|
||||
* [R8 task](#r8-task)
|
||||
* [Android test (connected test)](#android-test-connected-test)
|
||||
|
||||
<!--- END -->
|
||||
|
||||
## Introduction
|
||||
|
||||
This document gives some information about how we take advantage of the continuous integration (CI).
|
||||
|
||||
## CI tools
|
||||
|
||||
We use GitHub Actions to configure and perform the CI.
|
||||
|
||||
## Rules
|
||||
|
||||
We want:
|
||||
|
||||
1. The CI to detect as soon as possible any issue in the code
|
||||
2. The CI to be fast - it's run on all the Pull Requests, and developers do not like to wait too long
|
||||
3. The CI to be reliable - it should not fail randomly
|
||||
4. The CI to generate artifacts which can be used by the team and the community
|
||||
5. The CI to generate useful logs and reports, not too verbose, not too short
|
||||
6. The developer to be able to run the CI locally - to help with this we have [a script](../tools/check/check_code_quality.sh) the can be run locally and which does more checks that just building and deploying the app.
|
||||
7. The CI to be used as a common environment for the team: generate the screenshots image for the screenshot test, build the release build (unsigned)
|
||||
8. The CI to run repeated tasks, like building the nightly builds, integrating data from external tools (translations, etc.)
|
||||
9. The CI to upgrade our dependencies (Renovate)
|
||||
10. The CI to do some issue triaging
|
||||
|
||||
## What is the CI checking
|
||||
|
||||
The CI checks that:
|
||||
|
||||
1. The code is compiling, without any warnings, for all the app build types and variants and for the minimal app
|
||||
2. The tests are passing
|
||||
3. The code quality is good (detekt, ktlint, lint)
|
||||
4. The code is running and smoke tests are passing (maestro)
|
||||
5. The PullRequest itself is good (with danger)
|
||||
6. Files that must be added with git-lfs are added with git-lfs
|
||||
|
||||
## What is the CI reporting
|
||||
|
||||
The CI reports:
|
||||
|
||||
1. Code coverage reports
|
||||
2. Sonar reports
|
||||
|
||||
## Current choices
|
||||
|
||||
### R8 task
|
||||
|
||||
The CI does not run R8 because it's too slow, and it breaks rule 2.
|
||||
|
||||
The drawback is that the nightly build can fail, as well as the release build.
|
||||
|
||||
Since the nightly build is failing, the team can detect the failure quite fast and react to it.
|
||||
|
||||
### Android test (connected test)
|
||||
|
||||
We limit the number of connected tests (tests under folder `androidTest`), because it often break rule 2 and 3.
|
||||
64
docs/install_from_github_release.md
Normal file
64
docs/install_from_github_release.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Installing Element X Android from a Github Release
|
||||
|
||||
This document explains how to install Element X Android from a Github Release.
|
||||
|
||||
<!--- TOC -->
|
||||
|
||||
* [Requirements](#requirements)
|
||||
* [Steps](#steps)
|
||||
* [I already have the application on my phone](#i-already-have-the-application-on-my-phone)
|
||||
|
||||
<!--- END -->
|
||||
|
||||
## Requirements
|
||||
|
||||
The Github release will contain an Android App Bundle (with `aab` extension) file, unlike in the Element Android project where releases directly provide the APKs. So there are some steps to perform to generate and sign App Bundle APKs. An APK suitable for the targeted device will then be generated.
|
||||
|
||||
The easiest way to do that is to use the debug signature that is shared between the developers and stored in the Element X Android project. So we recommend to clone the project first, to be able to use the debug signature it contains. But note that you can use any other signature. You don't need to install Android Studio, you will only need a shell terminal.
|
||||
|
||||
You can clone the project by running:
|
||||
```bash
|
||||
git clone git@github.com:vector-im/element-x-android.git
|
||||
```
|
||||
or
|
||||
```bash
|
||||
git clone https://github.com/vector-im/element-x-android.git
|
||||
```
|
||||
|
||||
You will also need to install [bundletool](https://developer.android.com/studio/command-line/bundletool). On MacOS, you can run the following command:
|
||||
|
||||
```bash
|
||||
brew install bundletool
|
||||
```
|
||||
|
||||
## Steps
|
||||
|
||||
1. Open the GitHub release that you want to install from https://github.com/vector-im/element-x-android/releases
|
||||
2. Download the asset `app-release-signed.aab`
|
||||
3. Navigate to the folder where you cloned the project and run the following command:
|
||||
```bash
|
||||
bundletool build-apks --bundle=<PATH_TO_YOUR_APP-RELEASE-SIGNED.AAB_FILE> --output=./tmp/elementx.apks \
|
||||
--ks=./app/signature/debug.keystore --ks-pass=pass:android --ks-key-alias=androiddebugkey --key-pass=pass:android \
|
||||
--overwrite
|
||||
```
|
||||
For instance:
|
||||
```bash
|
||||
bundletool build-apks --bundle=./tmp/Element/0.1.5/app-release-signed.aab --output=./tmp/elementx.apks \
|
||||
--ks=./app/signature/debug.keystore --ks-pass=pass:android --ks-key-alias=androiddebugkey --key-pass=pass:android \
|
||||
--overwrite
|
||||
```
|
||||
4. Run an Android emulator, or connect a real device to your computer
|
||||
5. Install the APKs on the device:
|
||||
```bash
|
||||
bundletool install-apks --apks=./tmp/elementx.apks
|
||||
```
|
||||
|
||||
That's it, the application should be installed on your device, you can start it from the launcher icon.
|
||||
|
||||
## I already have the application on my phone
|
||||
|
||||
If the application was already installed on your phone, there are several cases:
|
||||
|
||||
- it was installed from the PlayStore, you will have to uninstall it first because the signature will not match.
|
||||
- it was installed from a previous GitHub release, this is like an application upgrade, so no need to uninstall the existing app.
|
||||
- it was installed from a more recent GitHub release, you will have to uninstall it first.
|
||||
2
fastlane/metadata/android/en-US/changelogs/40001060.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40001060.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: bugfixes.
|
||||
Full changelog: https://github.com/vector-im/element-x-android/releases
|
||||
|
|
@ -51,4 +51,5 @@ dependencies {
|
|||
testImplementation(libs.test.mockk)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,18 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class AnalyticsOptInPresenterTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - enable`() = runTest {
|
||||
val analyticsService = FakeAnalyticsService(isEnabled = false)
|
||||
|
|
|
|||
|
|
@ -23,10 +23,17 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class AnalyticsPreferencesPresenterTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state available`() = runTest {
|
||||
val presenter = DefaultAnalyticsPreferencesPresenter(
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ dependencies {
|
|||
testImplementation(projects.libraries.mediapickers.test)
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
testImplementation(projects.libraries.usersearch.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers
|
|||
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListState> {
|
||||
|
|
@ -36,7 +37,11 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListSta
|
|||
selectionMode = SelectionMode.Multiple,
|
||||
),
|
||||
aUserListState().copy(
|
||||
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
searchResults = SearchBarResultState.Results(aMatrixUserList()
|
||||
.mapIndexed { index, matrixUser ->
|
||||
UserSearchResult(matrixUser, index % 2 == 0)
|
||||
}
|
||||
.toImmutableList()),
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
isSearchActive = true,
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
|
|
|
|||
|
|
@ -24,12 +24,18 @@ import io.element.android.features.createroom.impl.CreateRoomDataStore
|
|||
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
|
||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class AddPeoplePresenterTests {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
private lateinit var presenter: AddPeoplePresenter
|
||||
|
||||
@Before
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
|||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
|
|
@ -47,6 +48,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
|
@ -58,6 +60,10 @@ private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery"
|
|||
@RunWith(RobolectricTestRunner::class)
|
||||
class ConfigureRoomPresenterTests {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
private lateinit var presenter: ConfigureRoomPresenter
|
||||
private lateinit var userListDataStore: UserListDataStore
|
||||
private lateinit var createRoomDataStore: CreateRoomDataStore
|
||||
|
|
|
|||
|
|
@ -35,13 +35,19 @@ import io.element.android.libraries.matrix.test.core.aBuildMeta
|
|||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class CreateRoomRootPresenterTests {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
private lateinit var userRepository: FakeUserRepository
|
||||
private lateinit var presenter: CreateRoomRootPresenter
|
||||
private lateinit var fakeUserListPresenter: FakeUserListPresenter
|
||||
|
|
|
|||
|
|
@ -25,12 +25,18 @@ import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
|||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultUserListPresenterTests {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
private val userRepository = FakeUserRepository()
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ dependencies {
|
|||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.features.analytics.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.permissions.noop)
|
||||
implementation(projects.services.toolbox.api)
|
||||
implementation(projects.services.toolbox.test)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
|
@ -51,6 +55,9 @@ dependencies {
|
|||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.libraries.permissions.impl)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import io.element.android.anvilannotations.ContributesNode
|
|||
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||
import io.element.android.features.ftue.impl.migration.MigrationScreenNode
|
||||
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
|
||||
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
||||
import io.element.android.features.ftue.impl.state.FtueStep
|
||||
import io.element.android.features.ftue.impl.welcome.WelcomeNode
|
||||
|
|
@ -79,6 +80,9 @@ class FtueFlowNode @AssistedInject constructor(
|
|||
@Parcelize
|
||||
data object WelcomeScreen : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object NotificationsOptIn : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object AnalyticsOptIn : NavTarget
|
||||
}
|
||||
|
|
@ -124,6 +128,14 @@ class FtueFlowNode @AssistedInject constructor(
|
|||
}
|
||||
createNode<WelcomeNode>(buildContext, listOf(callback))
|
||||
}
|
||||
NavTarget.NotificationsOptIn -> {
|
||||
val callback = object : NotificationsOptInNode.Callback {
|
||||
override fun onNotificationsOptInFinished() {
|
||||
lifecycleScope.launch { moveToNextStep() }
|
||||
}
|
||||
}
|
||||
createNode<NotificationsOptInNode>(buildContext, listOf(callback))
|
||||
}
|
||||
NavTarget.AnalyticsOptIn -> {
|
||||
analyticsEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
|
|
@ -138,6 +150,9 @@ class FtueFlowNode @AssistedInject constructor(
|
|||
FtueStep.WelcomeScreen -> {
|
||||
backstack.newRoot(NavTarget.WelcomeScreen)
|
||||
}
|
||||
FtueStep.NotificationsOptIn -> {
|
||||
backstack.newRoot(NavTarget.NotificationsOptIn)
|
||||
}
|
||||
FtueStep.AnalyticsOptIn -> {
|
||||
backstack.replace(NavTarget.AnalyticsOptIn)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.ftue.impl.notifications
|
||||
|
||||
sealed interface NotificationsOptInEvents {
|
||||
data object ContinueClicked : NotificationsOptInEvents
|
||||
data object NotNowClicked : NotificationsOptInEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.ftue.impl.notifications
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
class NotificationsOptInNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenterFactory: NotificationsOptInPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
interface Callback: NodeInputs {
|
||||
fun onNotificationsOptInFinished()
|
||||
}
|
||||
|
||||
private val callback = inputs<Callback>()
|
||||
|
||||
private val presenter: NotificationsOptInPresenter by lazy {
|
||||
presenterFactory.create(callback)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
NotificationsOptInView(
|
||||
state = state,
|
||||
onBack = { callback.onNotificationsOptInFinished() },
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.ftue.impl.notifications
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
|
||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NotificationsOptInPresenter @AssistedInject constructor(
|
||||
private val permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
@Assisted private val callback: NotificationsOptInNode.Callback,
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
private val permissionStateProvider: PermissionStateProvider,
|
||||
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||
) : Presenter<NotificationsOptInState> {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(callback: NotificationsOptInNode.Callback): NotificationsOptInPresenter
|
||||
}
|
||||
|
||||
private val postNotificationPermissionsPresenter by lazy {
|
||||
// Ask for POST_NOTIFICATION PERMISSION on Android 13+
|
||||
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
|
||||
permissionsPresenterFactory.create(Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
NoopPermissionsPresenter()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): NotificationsOptInState {
|
||||
val notificationsPermissionsState = postNotificationPermissionsPresenter.present()
|
||||
|
||||
fun handleEvents(event: NotificationsOptInEvents) {
|
||||
when (event) {
|
||||
NotificationsOptInEvents.ContinueClicked -> {
|
||||
if (notificationsPermissionsState.permissionGranted) {
|
||||
callback.onNotificationsOptInFinished()
|
||||
} else {
|
||||
notificationsPermissionsState.eventSink(PermissionsEvents.OpenSystemDialog)
|
||||
}
|
||||
}
|
||||
NotificationsOptInEvents.NotNowClicked -> {
|
||||
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
|
||||
appCoroutineScope.setPermissionDenied()
|
||||
}
|
||||
callback.onNotificationsOptInFinished()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(notificationsPermissionsState) {
|
||||
if (notificationsPermissionsState.permissionGranted
|
||||
|| notificationsPermissionsState.permissionAlreadyDenied) {
|
||||
callback.onNotificationsOptInFinished()
|
||||
}
|
||||
}
|
||||
|
||||
return NotificationsOptInState(
|
||||
notificationsPermissionState = notificationsPermissionsState,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
private fun CoroutineScope.setPermissionDenied() = launch {
|
||||
permissionStateProvider.setPermissionDenied(Manifest.permission.POST_NOTIFICATIONS, true)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.ftue.impl.notifications
|
||||
|
||||
import io.element.android.libraries.permissions.api.PermissionsState
|
||||
|
||||
data class NotificationsOptInState(
|
||||
val notificationsPermissionState: PermissionsState,
|
||||
val eventSink: (NotificationsOptInEvents) -> Unit
|
||||
)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.ftue.impl.notifications
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||
|
||||
open class NotificationsOptInStateProvider : PreviewParameterProvider<NotificationsOptInState> {
|
||||
override val values: Sequence<NotificationsOptInState>
|
||||
get() = sequenceOf(
|
||||
aNotificationsOptInState(),
|
||||
// Add other states here
|
||||
)
|
||||
}
|
||||
|
||||
fun aNotificationsOptInState() = NotificationsOptInState(
|
||||
notificationsPermissionState = aPermissionsState(),
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.ftue.impl.notifications
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.ftue.impl.R
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun NotificationsOptInView(
|
||||
state: NotificationsOptInState,
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BackHandler(onBack = onBack)
|
||||
|
||||
HeaderFooterPage(
|
||||
modifier = modifier
|
||||
.systemBarsPadding()
|
||||
.fillMaxSize(),
|
||||
header = { NotificationsOptInHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) },
|
||||
footer = { NotificationsOptInFooter(state) },
|
||||
) {
|
||||
NotificationsOptInContent(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationsOptInHeader(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = modifier,
|
||||
title = stringResource(R.string.screen_notification_optin_title),
|
||||
subTitle = stringResource(R.string.screen_notification_optin_subtitle),
|
||||
iconImageVector = Icons.Default.Notifications,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationsOptInFooter(state: NotificationsOptInState) {
|
||||
ButtonColumnMolecule {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(CommonStrings.action_ok),
|
||||
onClick = {
|
||||
state.eventSink(NotificationsOptInEvents.ContinueClicked)
|
||||
}
|
||||
)
|
||||
TextButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(CommonStrings.action_not_now),
|
||||
onClick = {
|
||||
state.eventSink(NotificationsOptInEvents.NotNowClicked)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationsOptInContent(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
16.dp,
|
||||
alignment = Alignment.CenterVertically
|
||||
)
|
||||
) {
|
||||
NotificationRow(
|
||||
avatarLetter = "M",
|
||||
avatarColorsId = "5",
|
||||
firstRowPercent = 1f,
|
||||
secondRowPercent = 0.4f
|
||||
)
|
||||
|
||||
NotificationRow(
|
||||
avatarLetter = "A",
|
||||
avatarColorsId = "1",
|
||||
firstRowPercent = 1f,
|
||||
secondRowPercent = 1f
|
||||
)
|
||||
|
||||
NotificationRow(
|
||||
avatarLetter = "T",
|
||||
avatarColorsId = "4",
|
||||
firstRowPercent = 0.65f,
|
||||
secondRowPercent = 0f
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationRow(
|
||||
avatarLetter: String,
|
||||
avatarColorsId: String,
|
||||
firstRowPercent: Float,
|
||||
secondRowPercent: Float,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
color = ElementTheme.colors.bgCanvasDisabled,
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
shadowElevation = 2.dp,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = AvatarData(id = avatarColorsId, name = avatarLetter, size = AvatarSize.NotificationsOptIn),
|
||||
)
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.fillMaxWidth(firstRowPercent)
|
||||
.height(10.dp)
|
||||
.background(ElementTheme.colors.borderInteractiveSecondary)
|
||||
)
|
||||
if (secondRowPercent > 0f) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.fillMaxWidth(secondRowPercent)
|
||||
.height(10.dp)
|
||||
.background(ElementTheme.colors.borderInteractiveSecondary)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
internal fun NotificationsOptInViewPreview(
|
||||
@PreviewParameter(NotificationsOptInStateProvider::class) state: NotificationsOptInState
|
||||
) {
|
||||
ElementPreview {
|
||||
NotificationsOptInView(
|
||||
onBack = {},
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package io.element.android.features.ftue.impl.state
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.ftue.api.state.FtueState
|
||||
|
|
@ -23,7 +25,9 @@ import io.element.android.features.ftue.impl.migration.MigrationScreenStore
|
|||
import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -34,10 +38,12 @@ import javax.inject.Inject
|
|||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultFtueState @Inject constructor(
|
||||
private val sdkVersionProvider: BuildVersionSdkIntProvider,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val welcomeScreenState: WelcomeScreenState,
|
||||
private val migrationScreenStore: MigrationScreenStore,
|
||||
private val permissionStateProvider: PermissionStateProvider,
|
||||
private val matrixClient: MatrixClient,
|
||||
) : FtueState {
|
||||
|
||||
|
|
@ -47,6 +53,9 @@ class DefaultFtueState @Inject constructor(
|
|||
welcomeScreenState.reset()
|
||||
analyticsService.reset()
|
||||
migrationScreenStore.reset()
|
||||
if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
|
||||
permissionStateProvider.resetPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
|
|
@ -63,7 +72,10 @@ class DefaultFtueState @Inject constructor(
|
|||
FtueStep.MigrationScreen -> if (shouldDisplayWelcomeScreen()) FtueStep.WelcomeScreen else getNextStep(
|
||||
FtueStep.WelcomeScreen
|
||||
)
|
||||
FtueStep.WelcomeScreen -> if (needsAnalyticsOptIn()) FtueStep.AnalyticsOptIn else getNextStep(
|
||||
FtueStep.WelcomeScreen -> if (shouldAskNotificationPermissions()) FtueStep.NotificationsOptIn else getNextStep(
|
||||
FtueStep.NotificationsOptIn
|
||||
)
|
||||
FtueStep.NotificationsOptIn -> if (needsAnalyticsOptIn()) FtueStep.AnalyticsOptIn else getNextStep(
|
||||
FtueStep.AnalyticsOptIn
|
||||
)
|
||||
FtueStep.AnalyticsOptIn -> null
|
||||
|
|
@ -73,6 +85,7 @@ class DefaultFtueState @Inject constructor(
|
|||
return listOf(
|
||||
shouldDisplayMigrationScreen(),
|
||||
shouldDisplayWelcomeScreen(),
|
||||
shouldAskNotificationPermissions(),
|
||||
needsAnalyticsOptIn()
|
||||
).any { it }
|
||||
}
|
||||
|
|
@ -90,6 +103,15 @@ class DefaultFtueState @Inject constructor(
|
|||
return welcomeScreenState.isWelcomeScreenNeeded()
|
||||
}
|
||||
|
||||
private fun shouldAskNotificationPermissions(): Boolean {
|
||||
return if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
|
||||
val permission = Manifest.permission.POST_NOTIFICATIONS
|
||||
val isPermissionDenied = runBlocking { permissionStateProvider.isPermissionDenied(permission).first() }
|
||||
val isPermissionGranted = permissionStateProvider.isPermissionGranted(permission)
|
||||
!isPermissionGranted && !isPermissionDenied
|
||||
} else false
|
||||
}
|
||||
|
||||
fun setWelcomeScreenShown() {
|
||||
welcomeScreenState.setWelcomeScreenShown()
|
||||
updateState()
|
||||
|
|
@ -104,5 +126,6 @@ class DefaultFtueState @Inject constructor(
|
|||
sealed interface FtueStep {
|
||||
data object MigrationScreen : FtueStep
|
||||
data object WelcomeScreen : FtueStep
|
||||
data object NotificationsOptIn : FtueStep
|
||||
data object AnalyticsOptIn : FtueStep
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_migration_message">"Toto je jednorázový proces, děkujeme za čekání."</string>
|
||||
<string name="screen_migration_message">"Jedná se o jednorázový proces, prosíme o strpení."</string>
|
||||
<string name="screen_migration_title">"Nastavení vašeho účtu"</string>
|
||||
<string name="screen_welcome_bullet_1">"Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."</string>
|
||||
<string name="screen_welcome_bullet_2">"Historie zpráv šifrovaných místností nebude v této aktualizaci k dispozici."</string>
|
||||
|
|
|
|||
11
features/ftue/impl/src/main/res/values-ro/translations.xml
Normal file
11
features/ftue/impl/src/main/res/values-ro/translations.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_migration_message">"Acesta este un proces care se desfășoară o singură dată, vă mulțumim pentru așteptare."</string>
|
||||
<string name="screen_migration_title">"Contul dumneavoastră se configurează"</string>
|
||||
<string name="screen_welcome_bullet_1">"Apelurile, sondajele, căutare și multe altele vor fi adăugate în cursul acestui an."</string>
|
||||
<string name="screen_welcome_bullet_2">"Istoricul mesajelor pentru camerele criptate nu va fi disponibil în această actualizare."</string>
|
||||
<string name="screen_welcome_bullet_3">"Ne-ar plăcea să auzim de la dumneavoastră, spuneți-ne ce părere aveți prin intermediul paginii de setări."</string>
|
||||
<string name="screen_welcome_button">"Să începem!"</string>
|
||||
<string name="screen_welcome_subtitle">"Iată ce trebuie să știți:"</string>
|
||||
<string name="screen_welcome_title">"Bun venit la%1$s!"</string>
|
||||
</resources>
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_migration_message">"This is a one time process, thanks for waiting."</string>
|
||||
<string name="screen_migration_title">"Setting up your account."</string>
|
||||
<string name="screen_notification_optin_subtitle">"You can change your settings later."</string>
|
||||
<string name="screen_notification_optin_title">"Allow notifications and never miss a message"</string>
|
||||
<string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string>
|
||||
<string name="screen_welcome_bullet_2">"Message history for encrypted rooms won’t be available in this update."</string>
|
||||
<string name="screen_welcome_bullet_3">"We’d love to hear from you, let us know what you think via the settings page."</string>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.features.ftue.impl
|
||||
|
||||
import android.os.Build
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.ftue.impl.migration.InMemoryMigrationScreenStore
|
||||
import io.element.android.features.ftue.impl.migration.MigrationScreenStore
|
||||
|
|
@ -25,8 +26,10 @@ import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState
|
|||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
|
|
@ -51,13 +54,21 @@ class DefaultFtueStateTests {
|
|||
val welcomeState = FakeWelcomeState()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true)
|
||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
|
||||
val state = createState(coroutineScope, welcomeState, analyticsService, migrationScreenStore)
|
||||
val state = createState(
|
||||
coroutineScope = coroutineScope,
|
||||
welcomeState = welcomeState,
|
||||
analyticsService = analyticsService,
|
||||
migrationScreenStore = migrationScreenStore,
|
||||
permissionStateProvider = permissionStateProvider
|
||||
)
|
||||
|
||||
welcomeState.setWelcomeScreenShown()
|
||||
analyticsService.setDidAskUserConsent()
|
||||
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
|
||||
permissionStateProvider.setPermissionGranted()
|
||||
state.updateState()
|
||||
|
||||
assertThat(state.shouldDisplayFlow.value).isFalse()
|
||||
|
|
@ -71,9 +82,16 @@ class DefaultFtueStateTests {
|
|||
val welcomeState = FakeWelcomeState()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
|
||||
val state = createState(coroutineScope, welcomeState, analyticsService, migrationScreenStore)
|
||||
val state = createState(
|
||||
coroutineScope = coroutineScope,
|
||||
welcomeState = welcomeState,
|
||||
analyticsService = analyticsService,
|
||||
migrationScreenStore = migrationScreenStore,
|
||||
permissionStateProvider = permissionStateProvider
|
||||
)
|
||||
val steps = mutableListOf<FtueStep?>()
|
||||
|
||||
// First step, migration screen
|
||||
|
|
@ -84,7 +102,11 @@ class DefaultFtueStateTests {
|
|||
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||
welcomeState.setWelcomeScreenShown()
|
||||
|
||||
// Third step, analytics opt in
|
||||
// Third step, notifications opt in
|
||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||
permissionStateProvider.setPermissionGranted()
|
||||
|
||||
// Fourth step, analytics opt in
|
||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||
analyticsService.setDidAskUserConsent()
|
||||
|
||||
|
|
@ -94,6 +116,7 @@ class DefaultFtueStateTests {
|
|||
assertThat(steps).containsExactly(
|
||||
FtueStep.MigrationScreen,
|
||||
FtueStep.WelcomeScreen,
|
||||
FtueStep.NotificationsOptIn,
|
||||
FtueStep.AnalyticsOptIn,
|
||||
null, // Final state
|
||||
)
|
||||
|
|
@ -107,11 +130,40 @@ class DefaultFtueStateTests {
|
|||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
||||
|
||||
val state = createState(
|
||||
coroutineScope = coroutineScope,
|
||||
analyticsService = analyticsService,
|
||||
migrationScreenStore = migrationScreenStore,
|
||||
permissionStateProvider = permissionStateProvider,
|
||||
)
|
||||
|
||||
// Skip first 3 steps
|
||||
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
|
||||
state.setWelcomeScreenShown()
|
||||
permissionStateProvider.setPermissionGranted()
|
||||
|
||||
assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
||||
|
||||
analyticsService.setDidAskUserConsent()
|
||||
assertThat(state.getNextStep(FtueStep.WelcomeScreen)).isNull()
|
||||
|
||||
// Cleanup
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if version is older than 13 we don't display the notification opt in screen`() = runTest {
|
||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||
|
||||
val state = createState(
|
||||
sdkIntVersion = Build.VERSION_CODES.M,
|
||||
coroutineScope = coroutineScope,
|
||||
analyticsService = analyticsService,
|
||||
migrationScreenStore = migrationScreenStore,
|
||||
)
|
||||
|
||||
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
|
||||
|
|
@ -132,12 +184,16 @@ class DefaultFtueStateTests {
|
|||
welcomeState: FakeWelcomeState = FakeWelcomeState(),
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||
migrationScreenStore: MigrationScreenStore = InMemoryMigrationScreenStore(),
|
||||
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
|
||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU, // First version where notification permission is required
|
||||
) = DefaultFtueState(
|
||||
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
|
||||
coroutineScope = coroutineScope,
|
||||
analyticsService = analyticsService,
|
||||
welcomeScreenState = welcomeState,
|
||||
migrationScreenStore = migrationScreenStore,
|
||||
permissionStateProvider = permissionStateProvider,
|
||||
matrixClient = matrixClient,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,10 +25,17 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
|||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class MigrationScreenPresenterTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.ftue.impl.notifications
|
||||
|
||||
import android.os.Build
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class NotificationsOptInPresenterTests {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
private var isFinished = false
|
||||
|
||||
@Test
|
||||
fun `initial state`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
Truth.assertThat(initialState.notificationsPermissionState.showDialog).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `show dialog on continue clicked`() = runTest {
|
||||
val permissionPresenter = FakePermissionsPresenter()
|
||||
val presenter = createPresenter(permissionPresenter)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(NotificationsOptInEvents.ContinueClicked)
|
||||
Truth.assertThat(awaitItem().notificationsPermissionState.showDialog).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `finish flow on continue clicked with permission already granted`() = runTest {
|
||||
val permissionPresenter = FakePermissionsPresenter().apply {
|
||||
setPermissionGranted()
|
||||
}
|
||||
val presenter = createPresenter(permissionPresenter)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(NotificationsOptInEvents.ContinueClicked)
|
||||
Truth.assertThat(isFinished).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `finish flow on not now clicked`() = runTest {
|
||||
val permissionPresenter = FakePermissionsPresenter()
|
||||
val presenter = createPresenter(
|
||||
permissionsPresenter = permissionPresenter,
|
||||
sdkIntVersion = Build.VERSION_CODES.M
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(NotificationsOptInEvents.NotNowClicked)
|
||||
Truth.assertThat(isFinished).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `set permission denied on not now clicked in API 33`() = runTest(StandardTestDispatcher()) {
|
||||
val permissionPresenter = FakePermissionsPresenter()
|
||||
val permissionStateProvider = FakePermissionStateProvider()
|
||||
val presenter = createPresenter(
|
||||
permissionsPresenter = permissionPresenter,
|
||||
permissionStateProvider = permissionStateProvider,
|
||||
sdkIntVersion = Build.VERSION_CODES.TIRAMISU
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(NotificationsOptInEvents.NotNowClicked)
|
||||
|
||||
// Allow background coroutines to run
|
||||
runCurrent()
|
||||
|
||||
val isPermissionDenied = runBlocking {
|
||||
permissionStateProvider.isPermissionDenied("notifications").first()
|
||||
}
|
||||
Truth.assertThat(isPermissionDenied).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createPresenter(
|
||||
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
||||
permissionStateProvider: PermissionStateProvider = FakePermissionStateProvider(),
|
||||
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
||||
) = NotificationsOptInPresenter(
|
||||
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
||||
override fun create(permission: String): PermissionsPresenter {
|
||||
return permissionsPresenter
|
||||
}
|
||||
},
|
||||
callback = object : NotificationsOptInNode.Callback {
|
||||
override fun onNotificationsOptInFinished() {
|
||||
isFinished = true
|
||||
}
|
||||
},
|
||||
appCoroutineScope = this,
|
||||
permissionStateProvider = permissionStateProvider,
|
||||
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
|
||||
)
|
||||
}
|
||||
|
|
@ -54,6 +54,7 @@ dependencies {
|
|||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.features.invitelist.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,10 +44,15 @@ import io.element.android.libraries.push.api.notifications.NotificationDrawerMan
|
|||
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class InviteListPresenterTests {
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - starts empty, adds invites when received`() = runTest {
|
||||
|
|
|
|||
|
|
@ -29,14 +29,20 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MembershipCha
|
|||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class LeaveRoomPresenterImplTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state hides all dialogs`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
|
|
|
|||
|
|
@ -57,4 +57,5 @@ dependencies {
|
|||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.features.messages.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.setValue
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.features.location.impl.common.actions.LocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||
import io.element.android.features.location.impl.common.actions.LocationActions
|
||||
import io.element.android.features.messages.api.MessageComposerContext
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
|
|
@ -119,9 +119,8 @@ class SendLocationPresenter @Inject constructor(
|
|||
Composer(
|
||||
inThread = messageComposerContext.composerMode.inThread,
|
||||
isEditing = messageComposerContext.composerMode.isEditing,
|
||||
isLocation = true,
|
||||
isReply = messageComposerContext.composerMode.isReply,
|
||||
locationType = Composer.LocationType.PinDrop,
|
||||
messageType = Composer.MessageType.LocationPin,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -138,9 +137,8 @@ class SendLocationPresenter @Inject constructor(
|
|||
Composer(
|
||||
inThread = messageComposerContext.composerMode.inThread,
|
||||
isEditing = messageComposerContext.composerMode.isEditing,
|
||||
isLocation = true,
|
||||
isReply = messageComposerContext.composerMode.isReply,
|
||||
locationType = Composer.LocationType.MyLocation,
|
||||
messageType = Composer.MessageType.LocationUser,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,12 +34,18 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
|||
import io.element.android.libraries.matrix.test.room.SendLocationInvocation
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class SendLocationPresenterTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
private val permissionsPresenterFake = PermissionsPresenterFake()
|
||||
private val fakeMatrixRoom = FakeMatrixRoom()
|
||||
private val fakeAnalyticsService = FakeAnalyticsService()
|
||||
|
|
@ -302,9 +308,8 @@ class SendLocationPresenterTest {
|
|||
Composer(
|
||||
inThread = false,
|
||||
isEditing = false,
|
||||
isLocation = true,
|
||||
isReply = false,
|
||||
locationType = Composer.LocationType.MyLocation,
|
||||
messageType = Composer.MessageType.LocationUser,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -359,9 +364,8 @@ class SendLocationPresenterTest {
|
|||
Composer(
|
||||
inThread = false,
|
||||
isEditing = false,
|
||||
isLocation = true,
|
||||
isReply = false,
|
||||
locationType = Composer.LocationType.PinDrop,
|
||||
messageType = Composer.MessageType.LocationPin,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -406,9 +410,8 @@ class SendLocationPresenterTest {
|
|||
Composer(
|
||||
inThread = false,
|
||||
isEditing = true,
|
||||
isLocation = true,
|
||||
isReply = false,
|
||||
locationType = Composer.LocationType.PinDrop,
|
||||
messageType = Composer.MessageType.LocationPin,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,12 +27,18 @@ import io.element.android.features.location.impl.common.permissions.PermissionsP
|
|||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenterFake
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class ShowLocationPresenterTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
private val permissionsPresenterFake = PermissionsPresenterFake()
|
||||
private val fakeLocationActions = FakeLocationActions()
|
||||
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@
|
|||
<string name="screen_account_provider_form_notice">"Introduceţi un termen de căutare sau o adresă de domeniu."</string>
|
||||
<string name="screen_account_provider_form_subtitle">"Căutați o companie, o comunitate sau un server privat."</string>
|
||||
<string name="screen_account_provider_form_title">"Găsiți un furnizor de cont"</string>
|
||||
<string name="screen_account_provider_signin_subtitle">"Aici vor trăi conversațiile - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile."</string>
|
||||
<string name="screen_account_provider_signin_subtitle">"Aici vor trăi conversațiile dumneavoastră - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile."</string>
|
||||
<string name="screen_account_provider_signin_title">"Sunteți pe cale să vă conectați la %s"</string>
|
||||
<string name="screen_account_provider_signup_subtitle">"Aici vor trăi conversațiile - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile."</string>
|
||||
<string name="screen_account_provider_signup_subtitle">"Aici vor trăi conversațiile dumneavoastră - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile."</string>
|
||||
<string name="screen_account_provider_signup_title">"Sunteți pe cale să creați un cont pe %s"</string>
|
||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org este un server mare și gratuit din rețeaua publică Matrix pentru comunicații sigure și descentralizate, administrat de Fundația Matrix.org."</string>
|
||||
<string name="screen_change_account_provider_other">"Altul"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"Utilizați un alt furnizor de cont, cum ar fi propriul server privat sau un cont de serviciu."</string>
|
||||
<string name="screen_change_account_provider_title">"Schimbați furnizorul contului"</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<string name="screen_account_provider_signin_title">"Вы собираетесь войти в %s"</string>
|
||||
<string name="screen_account_provider_signup_subtitle">"Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."</string>
|
||||
<string name="screen_account_provider_signup_title">"Вы собираетесь создать учетную запись на %s"</string>
|
||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org — это большой бесплатный сервер в общедоступной сети Matrix для безопасной децентрализованной связи, управляемый Matrix.org Foundation."</string>
|
||||
<string name="screen_change_account_provider_other">"Другое"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"Используйте другого поставщика учетных записей, например, собственный частный сервер или рабочую учетную запись."</string>
|
||||
<string name="screen_change_account_provider_title">"Сменить поставщика учетной записи"</string>
|
||||
|
|
|
|||
|
|
@ -26,10 +26,17 @@ import io.element.android.libraries.architecture.Async
|
|||
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class ChangeServerPresenterTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = ChangeServerPresenter(
|
||||
|
|
|
|||
|
|
@ -27,11 +27,18 @@ import io.element.android.libraries.architecture.Async
|
|||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class OidcPresenterTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = OidcPresenter(
|
||||
|
|
|
|||
|
|
@ -24,10 +24,17 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
|
|||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class ChangeAccountProviderPresenterTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val changeServerPresenter = ChangeServerPresenter(
|
||||
|
|
|
|||
|
|
@ -31,11 +31,18 @@ import io.element.android.libraries.matrix.test.A_HOMESERVER
|
|||
import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
|
||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.waitForPredicate
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class ConfirmAccountProviderPresenterTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial test`() = runTest {
|
||||
val presenter = createConfirmAccountProviderPresenter()
|
||||
|
|
|
|||
|
|
@ -31,10 +31,17 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
|||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class LoginPasswordPresenterTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
|
|
|
|||
|
|
@ -30,11 +30,18 @@ import io.element.android.features.login.impl.resolver.network.WellKnownSlidingS
|
|||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class SearchAccountProviderPresenterTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val fakeWellknownRequest = FakeWellknownRequest()
|
||||
|
|
|
|||
|
|
@ -30,10 +30,17 @@ import io.element.android.libraries.matrix.test.A_THROWABLE
|
|||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class WaitListPresenterTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService().apply {
|
||||
|
|
|
|||
|
|
@ -48,4 +48,5 @@ dependencies {
|
|||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,10 +25,17 @@ import io.element.android.features.logout.api.LogoutPreferenceState
|
|||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class LogoutPreferencePresenterTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = DefaultLogoutPreferencePresenter(
|
||||
|
|
|
|||
|
|
@ -25,5 +25,5 @@ android {
|
|||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.libraries.textcomposer)
|
||||
api(projects.libraries.textcomposer.impl)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ dependencies {
|
|||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.textcomposer)
|
||||
implementation(projects.libraries.textcomposer.impl)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.dateformatter.api)
|
||||
implementation(projects.libraries.eventformatter.api)
|
||||
|
|
@ -61,7 +61,7 @@ dependencies {
|
|||
implementation(libs.accompanist.systemui)
|
||||
implementation(libs.vanniktech.blurhash)
|
||||
implementation(libs.telephoto.zoomableimage)
|
||||
implementation(libs.vanniktech.emoji)
|
||||
implementation(libs.matrix.emojibase.bindings)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
|
@ -76,6 +76,7 @@ dependencies {
|
|||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
testImplementation(projects.libraries.mediapickers.test)
|
||||
testImplementation(projects.libraries.textcomposer.test)
|
||||
testImplementation(libs.test.mockk)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import androidx.compose.runtime.setValue
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.PollEnd
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
|
|
@ -59,7 +60,6 @@ import io.element.android.features.networkmonitor.api.NetworkStatus
|
|||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
|
|
@ -75,6 +75,7 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
|||
import io.element.android.libraries.matrix.ui.room.canRedactAsState
|
||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -93,6 +94,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
private val messageSummaryFormatter: MessageSummaryFormatter,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val clipboardHelper: ClipboardHelper,
|
||||
private val analyticsService: AnalyticsService,
|
||||
@Assisted private val navigator: MessagesNavigator,
|
||||
) : Presenter<MessagesState> {
|
||||
|
||||
|
|
@ -176,7 +178,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
snackbarMessage = snackbarMessage,
|
||||
showReinvitePrompt = showReinvitePrompt,
|
||||
inviteProgress = inviteProgress.value,
|
||||
eventSink = ::handleEvents
|
||||
eventSink = { handleEvents(it) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -202,6 +204,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
TimelineItemAction.Developer -> handleShowDebugInfoAction(targetEvent)
|
||||
TimelineItemAction.Forward -> handleForwardAction(targetEvent)
|
||||
TimelineItemAction.ReportContent -> handleReportAction(targetEvent)
|
||||
TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -214,7 +217,8 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState<Async<Unit>>) = launch(dispatchers.io) {
|
||||
suspend {
|
||||
inviteProgress.value = Async.Loading()
|
||||
runCatching {
|
||||
room.updateMembers()
|
||||
|
||||
val memberList = when (val memberState = room.membersStateFlow.value) {
|
||||
|
|
@ -227,7 +231,14 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
room.inviteUserById(member.userId).onFailure { t ->
|
||||
Timber.e(t, "Failed to reinvite DM partner")
|
||||
}.getOrThrow()
|
||||
}.runCatchingUpdatingState(inviteProgress)
|
||||
}.fold(
|
||||
onSuccess = {
|
||||
inviteProgress.value = Async.Success(Unit)
|
||||
},
|
||||
onFailure = {
|
||||
inviteProgress.value = Async.Failure(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun handleActionRedact(event: TimelineItem.Event) {
|
||||
|
|
@ -242,7 +253,9 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
private fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
|
||||
val composerMode = MessageComposerMode.Edit(
|
||||
targetEvent.eventId,
|
||||
(targetEvent.content as? TimelineItemTextBasedContent)?.body.orEmpty(),
|
||||
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
|
||||
it.htmlBody ?: it.body
|
||||
}.orEmpty(),
|
||||
targetEvent.transactionId,
|
||||
)
|
||||
composerState.eventSink(
|
||||
|
|
@ -310,6 +323,13 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
navigator.onReportContentClicked(event.eventId, event.senderId)
|
||||
}
|
||||
|
||||
private suspend fun handleEndPollAction(event: TimelineItem.Event) {
|
||||
event.eventId?.let {
|
||||
room.endPoll(it, "The poll with event id: $it has ended.")
|
||||
analyticsService.capture(PollEnd())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleCopyContents(event: TimelineItem.Event) {
|
||||
val content = when (event.content) {
|
||||
is TimelineItemTextBasedContent -> event.content.body
|
||||
|
|
|
|||
|
|
@ -30,6 +30,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.RoomId
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||
|
|
@ -54,7 +55,9 @@ fun aMessagesState() = MessagesState(
|
|||
userHasPermissionToSendMessage = true,
|
||||
userHasPermissionToRedact = false,
|
||||
composerState = aMessageComposerState().copy(
|
||||
text = "Hello",
|
||||
richTextEditorState = RichTextEditorState("Hello", fake = true).apply {
|
||||
requestFocus()
|
||||
},
|
||||
isFullScreen = false,
|
||||
mode = MessageComposerMode.Normal("Hello"),
|
||||
),
|
||||
|
|
@ -67,7 +70,7 @@ fun aMessagesState() = MessagesState(
|
|||
),
|
||||
actionListState = anActionListState(),
|
||||
customReactionState = CustomReactionState(
|
||||
selectedEventId = null,
|
||||
target = CustomReactionState.Target.None,
|
||||
eventSink = {},
|
||||
selectedEmoji = persistentSetOf(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -123,7 +123,13 @@ fun MessagesView(
|
|||
fun onMessageLongClicked(event: TimelineItem.Event) {
|
||||
Timber.v("OnMessageLongClicked= ${event.id}")
|
||||
localView.hideKeyboard()
|
||||
state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event, state.userHasPermissionToRedact))
|
||||
state.actionListState.eventSink(
|
||||
ActionListEvents.ComputeForMessage(
|
||||
event = event,
|
||||
canRedact = state.userHasPermissionToRedact,
|
||||
canSendMessage = state.userHasPermissionToSendMessage,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
|
||||
|
|
@ -141,7 +147,7 @@ fun MessagesView(
|
|||
}
|
||||
|
||||
fun onMoreReactionsClicked(event: TimelineItem.Event) {
|
||||
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event))
|
||||
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
|
|
@ -194,18 +200,17 @@ fun MessagesView(
|
|||
state = state.actionListState,
|
||||
onActionSelected = ::onActionSelected,
|
||||
onCustomReactionClicked = { event ->
|
||||
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event))
|
||||
if (event.eventId == null) return@ActionListView
|
||||
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
||||
},
|
||||
onEmojiReactionClicked = ::onEmojiReactionClicked,
|
||||
)
|
||||
|
||||
CustomReactionBottomSheet(
|
||||
state = state.customReactionState,
|
||||
onEmojiSelected = { emoji ->
|
||||
state.customReactionState.selectedEventId?.let { eventId ->
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
|
||||
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
||||
}
|
||||
onEmojiSelected = { eventId, emoji ->
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
|
||||
state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
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