mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-05 12:54:35 +00:00
Merge branch 'develop' into feature/fga/timeline-multi-forward
This commit is contained in:
@@ -56,7 +56,7 @@ jobs:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Assemble debug APKs
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Assemble debug Gplay Enterprise APK
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Set up Python 3.12
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Assemble debug APK
|
||||
@@ -124,6 +124,19 @@ jobs:
|
||||
disk-size: 3G
|
||||
script: |
|
||||
.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh app-gplay-x86_64-debug.apk
|
||||
- name: Set up Python
|
||||
if: always()
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: 3.14
|
||||
- name: End all the remaining sessions of the Maestro account
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
env:
|
||||
MAS_PASSWORD: ${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }}
|
||||
run: |
|
||||
pip install requests
|
||||
python3 ./tools/mas/delete_all_sessions.py --homeserver matrix.org --username maestroelement
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
java-version: '21'
|
||||
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: false
|
||||
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Dependency analysis
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Set up Python 3.12
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Run Konsist tests
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Run compose tests
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Build Gplay Debug
|
||||
@@ -233,7 +233,7 @@ jobs:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Run Detekt
|
||||
@@ -274,7 +274,7 @@ jobs:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Run Ktlint check
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
java-version: '21'
|
||||
# Add gradle cache, this should speed up the process
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Record screenshots
|
||||
|
||||
@@ -37,13 +37,15 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check Matrix Rust SDK is not a nightly version
|
||||
run: ./tools/release/check_no_nightly_sdk.sh
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
- name: Create app bundle
|
||||
env:
|
||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||
@@ -87,13 +89,15 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check Matrix Rust SDK is not a nightly version
|
||||
run: ./tools/release/check_no_nightly_sdk.sh
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
- name: Create APKs
|
||||
env:
|
||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Build debug code and test fixtures
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Set up Python 3.12
|
||||
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ PRs must meet these rules.
|
||||
|
||||
- Create previews for **all main states** of a Composable.
|
||||
- Use `@PreviewsDayNight` for consistency.
|
||||
- Use `PreviewParameterProvider` (e.g., `FooStatePreviewParam`) to provide states.
|
||||
- Use `PreviewParameterProvider` (e.g., `FooStatePreviewParam`) to provide states. Always add new states to existing providers at the end of the sequence.
|
||||
- Wrap previews in `ElementPreview { ... }`.
|
||||
- When writing tests, never try to record the screenshots, the CI will do it.
|
||||
|
||||
|
||||
@@ -280,7 +280,6 @@ dependencies {
|
||||
implementation(projects.features.migration.api)
|
||||
implementation(projects.appnav)
|
||||
implementation(projects.appconfig)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.services.analytics.compose)
|
||||
|
||||
if (ModulesConfig.pushProvidersConfig.includeFirebase) {
|
||||
@@ -298,18 +297,13 @@ dependencies {
|
||||
implementation(libs.androidx.splash)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.androidx.lifecycle.runtime)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.startup)
|
||||
implementation(libs.androidx.preference)
|
||||
implementation(libs.coil)
|
||||
|
||||
implementation(platform(libs.network.okhttp.bom))
|
||||
implementation(libs.network.okhttp.logging)
|
||||
implementation(libs.serialization.json)
|
||||
|
||||
implementation(libs.matrix.emojibase.bindings)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
|
||||
@@ -11,12 +11,12 @@ package io.element.android.x.di
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.Multibinds
|
||||
import io.element.android.features.api.MigrationEntryPoint
|
||||
import io.element.android.features.callnative.api.NativeCallPip
|
||||
import io.element.android.features.enterprise.api.AppStartupHook
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
import io.element.android.features.lockscreen.api.LockScreenService
|
||||
import io.element.android.features.migration.api.MigrationEntryPoint
|
||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
|
||||
@@ -48,7 +48,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(libs.androidx.annotationjvm)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
||||
@@ -44,7 +44,6 @@ dependencies {
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.libraries.uiCommon)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.wellknown.api)
|
||||
implementation(projects.features.login.api)
|
||||
|
||||
implementation(libs.coil)
|
||||
|
||||
@@ -33,7 +33,9 @@ import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrixmedia.api.ImageLoaderHolder
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
@@ -59,6 +61,7 @@ class LoggedInAppScopeFlowNode(
|
||||
interface Callback : Plugin {
|
||||
fun navigateToBugReport()
|
||||
fun navigateToAddAccount()
|
||||
fun switchAccountAndOpenRoom(sessionId: SessionId, roomId: RoomId?)
|
||||
}
|
||||
|
||||
private val callback: Callback = callback()
|
||||
@@ -91,6 +94,10 @@ class LoggedInAppScopeFlowNode(
|
||||
override fun navigateToAddAccount() {
|
||||
callback.navigateToAddAccount()
|
||||
}
|
||||
|
||||
override fun switchAccountAndOpenRoom(sessionId: SessionId, roomId: RoomId?) {
|
||||
callback.switchAccountAndOpenRoom(sessionId, roomId)
|
||||
}
|
||||
}
|
||||
return createNode<LoggedInFlowNode>(buildContext, listOf(callback))
|
||||
}
|
||||
|
||||
@@ -8,28 +8,51 @@
|
||||
|
||||
package io.element.android.appnav
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.coroutine.childScope
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class LoggedInEventProcessor(
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
@Assisted private val snackbarDispatcher: SnackbarDispatcher,
|
||||
private val roomMembershipObserver: RoomMembershipObserver,
|
||||
private val sessionId: SessionId,
|
||||
private val roomListService: RoomListService,
|
||||
private val notificationCleaner: NotificationCleaner,
|
||||
private val dispatchers: CoroutineDispatchers
|
||||
) {
|
||||
private var observingJob: Job? = null
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(snackbarDispatcher: SnackbarDispatcher): LoggedInEventProcessor
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal var currentChildScope: CoroutineScope? = null
|
||||
|
||||
fun observeEvents(coroutineScope: CoroutineScope) {
|
||||
observingJob = roomMembershipObserver.updates
|
||||
if (currentChildScope != null) return
|
||||
|
||||
val childScope = coroutineScope.childScope(dispatchers.computation, "LoggedInEventProcessor")
|
||||
.also { currentChildScope = it }
|
||||
|
||||
roomMembershipObserver.updates
|
||||
.filter { !it.isUserInRoom }
|
||||
.distinctUntilChanged()
|
||||
.onEach { roomMemberShipUpdate ->
|
||||
@@ -48,12 +71,20 @@ class LoggedInEventProcessor(
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
.launchIn(childScope)
|
||||
|
||||
// Use the room list service state as a 'heartbeat' update to check existing notifications
|
||||
roomListService.state
|
||||
.filter { it == RoomListService.State.Running }
|
||||
.onEach {
|
||||
notificationCleaner.clearReadRoomsNotifications(sessionId)
|
||||
}
|
||||
.launchIn(childScope)
|
||||
}
|
||||
|
||||
fun stopObserving() {
|
||||
observingJob?.cancel()
|
||||
observingJob = null
|
||||
currentChildScope?.cancel()
|
||||
currentChildScope = null
|
||||
}
|
||||
|
||||
private fun displayMessage(message: Int) {
|
||||
|
||||
@@ -88,6 +88,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
@@ -157,6 +158,7 @@ class LoggedInFlowNode(
|
||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
|
||||
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
|
||||
private val loggedInEventProcessorFactory: LoggedInEventProcessor.Factory,
|
||||
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Placeholder,
|
||||
@@ -172,13 +174,11 @@ class LoggedInFlowNode(
|
||||
interface Callback : Plugin {
|
||||
fun navigateToBugReport()
|
||||
fun navigateToAddAccount()
|
||||
fun switchAccountAndOpenRoom(sessionId: SessionId, roomId: RoomId?)
|
||||
}
|
||||
|
||||
private val callback: Callback = callback()
|
||||
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
roomMembershipObserver = matrixClient.roomMembershipObserver,
|
||||
)
|
||||
private val loggedInFlowProcessor = loggedInEventProcessorFactory.create(snackbarDispatcher)
|
||||
|
||||
/**
|
||||
* Display the incoming verification request, the app is expected to be in foreground.
|
||||
@@ -361,6 +361,10 @@ class LoggedInFlowNode(
|
||||
override fun navigateToBugReport() {
|
||||
callback.navigateToBugReport()
|
||||
}
|
||||
|
||||
override fun navigateToAddAccount() {
|
||||
callback.navigateToAddAccount()
|
||||
}
|
||||
}
|
||||
homeEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
@@ -572,10 +576,16 @@ class LoggedInFlowNode(
|
||||
buildContext = buildContext,
|
||||
params = ShareEntryPoint.Params(shareIntentData = navTarget.shareIntentData),
|
||||
callback = object : ShareEntryPoint.Callback {
|
||||
override fun onDone(roomIds: List<RoomId>) {
|
||||
override fun onDone(sessionId: SessionId, roomIds: List<RoomId>) {
|
||||
// Remove the incoming share screen
|
||||
backstack.pop()
|
||||
|
||||
if (sessionId != matrixClient.sessionId) {
|
||||
// The data has been shared using another session, switch to it
|
||||
callback.switchAccountAndOpenRoom(sessionId, roomIds.singleOrNull())
|
||||
return
|
||||
}
|
||||
|
||||
// Navigate to the room if the text/media was shared to a single one
|
||||
roomIds.singleOrNull()?.let { roomId ->
|
||||
lifecycleScope.launch {
|
||||
|
||||
@@ -13,6 +13,7 @@ package io.element.android.appnav
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.annotation.DelicateCoilApi
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
@@ -24,6 +25,8 @@ import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.announcement.api.AnnouncementService
|
||||
import io.element.android.features.login.api.LoginEntryPoint
|
||||
import io.element.android.features.login.api.LoginParams
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
@@ -33,8 +36,11 @@ import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices
|
||||
import io.element.android.libraries.designsystem.utils.ScreenOrientation
|
||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrixmedia.api.ImageLoaderHolder
|
||||
import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@@ -45,6 +51,8 @@ class NotLoggedInFlowNode(
|
||||
private val loginEntryPoint: LoginEntryPoint,
|
||||
private val imageLoaderHolder: ImageLoaderHolder,
|
||||
private val analyticsColdStartWatcher: AnalyticsColdStartWatcher,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val announcementService: AnnouncementService,
|
||||
) : BaseFlowNode<NotLoggedInFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
@@ -68,6 +76,13 @@ class NotLoggedInFlowNode(
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
analyticsColdStartWatcher.whenLoggingIn()
|
||||
lifecycleScope.launch {
|
||||
// Users who enter the login flow while the feature is available, either for the first time or to add
|
||||
// another account, do not need to be informed about the multi-account feature.
|
||||
if (featureFlagService.isFeatureEnabled(FeatureFlags.MultiAccount)) {
|
||||
announcementService.onAnnouncementDismissed(Announcement.MultiAccount)
|
||||
}
|
||||
}
|
||||
lifecycle.subscribe(
|
||||
onResume = {
|
||||
SingletonImageLoader.setUnsafe(imageLoaderHolder.get())
|
||||
|
||||
@@ -62,6 +62,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.accountprovider.AccountProvider
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.asEventId
|
||||
@@ -349,8 +350,7 @@ class RootFlowNode(
|
||||
|
||||
@Parcelize data class AccountSelect(
|
||||
val currentSessionId: SessionId,
|
||||
val shareIntentData: ShareIntentData?,
|
||||
val permalinkData: PermalinkData?,
|
||||
val permalinkData: PermalinkData,
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize data class NotLoggedInFlow(
|
||||
@@ -389,6 +389,15 @@ class RootFlowNode(
|
||||
override fun navigateToAddAccount() {
|
||||
backstack.push(NavTarget.NotLoggedInFlow(null))
|
||||
}
|
||||
|
||||
override fun switchAccountAndOpenRoom(sessionId: SessionId, roomId: RoomId?) {
|
||||
lifecycleScope.launch {
|
||||
val loggedInFlowNode = attachSession(sessionId)
|
||||
roomId?.let {
|
||||
loggedInFlowNode.attachRoom(it.toRoomIdOrAlias(), clearBackstack = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val savedNavState = extractSavedStateForNavTarget(navTarget, this.buildContext.savedStateMap)
|
||||
val buildContext = if (savedNavState != null) {
|
||||
@@ -463,13 +472,7 @@ class RootFlowNode(
|
||||
// Do not pop when the account is changed to avoid a UI flicker.
|
||||
backstack.pop()
|
||||
}
|
||||
attachSession(sessionId).apply {
|
||||
if (navTarget.shareIntentData != null) {
|
||||
attachIncomingShare(navTarget.shareIntentData)
|
||||
} else if (navTarget.permalinkData != null) {
|
||||
attachPermalinkData(navTarget.permalinkData)
|
||||
}
|
||||
}
|
||||
attachSession(sessionId).attachPermalinkData(navTarget.permalinkData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,21 +542,9 @@ class RootFlowNode(
|
||||
// No session, open login
|
||||
switchToNotLoggedInFlow(null)
|
||||
} else {
|
||||
// wait for the current session to be restored
|
||||
val loggedInFlowNode = attachSession(latestSessionId)
|
||||
if (sessionStore.numberOfSessions() > 1) {
|
||||
// Several accounts, let the user choose which one to use
|
||||
backstack.push(
|
||||
NavTarget.AccountSelect(
|
||||
currentSessionId = latestSessionId,
|
||||
shareIntentData = shareIntentData,
|
||||
permalinkData = null,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// Only one account, directly attach the incoming share node.
|
||||
loggedInFlowNode.attachIncomingShare(shareIntentData)
|
||||
}
|
||||
// wait for the current session to be restored, then attach the incoming share node.
|
||||
// In case of multiple accounts, the user can switch account from the room select screen.
|
||||
attachSession(latestSessionId).attachIncomingShare(shareIntentData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,7 +567,6 @@ class RootFlowNode(
|
||||
backstack.push(
|
||||
NavTarget.AccountSelect(
|
||||
currentSessionId = latestSessionId,
|
||||
shareIntentData = null,
|
||||
permalinkData = permalinkData,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -28,8 +28,8 @@ import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||
import io.element.android.appnav.room.joined.LoadingRoomNodeView
|
||||
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
|
||||
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
|
||||
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint.Params
|
||||
import io.element.android.features.roomaliasresolver.api.RoomAliasResolverEntryPoint
|
||||
import io.element.android.features.roomaliasresolver.api.RoomAliasResolverEntryPoint.Params
|
||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
|
||||
@@ -13,7 +13,7 @@ import com.bumble.appyx.core.state.SavedStateMap
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.appnav.session.MatrixSessionCache
|
||||
import io.element.android.features.preferences.api.CacheService
|
||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||
import io.element.android.libraries.matrixmedia.api.ImageLoaderHolder
|
||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.appnav
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class LoggedInEventProcessorTest {
|
||||
@Test
|
||||
fun `test observeEvents with left membership change`() = runTest {
|
||||
val snackbarDispatcher = SnackbarDispatcher()
|
||||
val roomMembershipObserver = RoomMembershipObserver()
|
||||
|
||||
val loggedInEventProcessor = createLoggedInEventProcessor(
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
)
|
||||
|
||||
loggedInEventProcessor.observeEvents(backgroundScope)
|
||||
|
||||
testScheduler.runCurrent()
|
||||
|
||||
roomMembershipObserver.notifyUserLeftRoom(
|
||||
roomId = A_ROOM_ID,
|
||||
isSpace = false,
|
||||
membershipBeforeLeft = CurrentUserMembership.JOINED,
|
||||
)
|
||||
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// Verify that the snackbar message was displayed
|
||||
assertThat(snackbarDispatcher.snackbarMessage.first()).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test observeEvents with left membership change for space`() = runTest {
|
||||
val snackbarDispatcher = SnackbarDispatcher()
|
||||
val roomMembershipObserver = RoomMembershipObserver()
|
||||
|
||||
val loggedInEventProcessor = createLoggedInEventProcessor(
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
)
|
||||
|
||||
loggedInEventProcessor.observeEvents(backgroundScope)
|
||||
|
||||
testScheduler.runCurrent()
|
||||
|
||||
roomMembershipObserver.notifyUserLeftRoom(
|
||||
roomId = A_ROOM_ID,
|
||||
isSpace = true,
|
||||
membershipBeforeLeft = CurrentUserMembership.JOINED,
|
||||
)
|
||||
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// Verify that the snackbar message was displayed
|
||||
assertThat(snackbarDispatcher.snackbarMessage.first()).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test observeEvents with invitation rejected membership change`() = runTest {
|
||||
val snackbarDispatcher = SnackbarDispatcher()
|
||||
val roomMembershipObserver = RoomMembershipObserver()
|
||||
|
||||
val loggedInEventProcessor = createLoggedInEventProcessor(
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
)
|
||||
|
||||
loggedInEventProcessor.observeEvents(backgroundScope)
|
||||
|
||||
testScheduler.runCurrent()
|
||||
|
||||
roomMembershipObserver.notifyUserLeftRoom(
|
||||
roomId = A_ROOM_ID,
|
||||
isSpace = false,
|
||||
membershipBeforeLeft = CurrentUserMembership.INVITED,
|
||||
)
|
||||
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// Verify that the snackbar message was displayed
|
||||
assertThat(snackbarDispatcher.snackbarMessage.first()).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test observeEvents with knock retracted membership change`() = runTest {
|
||||
val snackbarDispatcher = SnackbarDispatcher()
|
||||
val roomMembershipObserver = RoomMembershipObserver()
|
||||
|
||||
val loggedInEventProcessor = createLoggedInEventProcessor(
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
)
|
||||
|
||||
loggedInEventProcessor.observeEvents(backgroundScope)
|
||||
|
||||
testScheduler.runCurrent()
|
||||
|
||||
roomMembershipObserver.notifyUserLeftRoom(
|
||||
roomId = A_ROOM_ID,
|
||||
isSpace = false,
|
||||
membershipBeforeLeft = CurrentUserMembership.KNOCKED,
|
||||
)
|
||||
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// Verify that the snackbar message was displayed
|
||||
assertThat(snackbarDispatcher.snackbarMessage.first()).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test stopObserving cancels the child scope`() = runTest {
|
||||
val snackbarDispatcher = SnackbarDispatcher()
|
||||
val roomMembershipObserver = RoomMembershipObserver()
|
||||
|
||||
val loggedInEventProcessor = createLoggedInEventProcessor(
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
)
|
||||
|
||||
loggedInEventProcessor.observeEvents(backgroundScope)
|
||||
|
||||
assertThat(loggedInEventProcessor.currentChildScope?.isActive).isTrue()
|
||||
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// Stop observing events
|
||||
loggedInEventProcessor.stopObserving()
|
||||
|
||||
// Verify that the child scope is cancelled (the scope is null)
|
||||
assertThat(loggedInEventProcessor.currentChildScope).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test notifications are cleared when room list service state is running`() = runTest {
|
||||
val clearReadNotificationsLambda = lambdaRecorder { _: SessionId -> }
|
||||
val snackbarDispatcher = SnackbarDispatcher()
|
||||
val notificationCleaner = FakeNotificationCleaner(clearReadRoomsNotificationsLambda = clearReadNotificationsLambda)
|
||||
val roomListService = FakeRoomListService()
|
||||
|
||||
val loggedInEventProcessor = createLoggedInEventProcessor(
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
notificationCleaner = notificationCleaner,
|
||||
roomListService = roomListService,
|
||||
)
|
||||
|
||||
loggedInEventProcessor.observeEvents(backgroundScope)
|
||||
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// Simulate the room list service state changing to Running
|
||||
roomListService.postState(RoomListService.State.Running)
|
||||
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// Verify that the notifications were cleared
|
||||
clearReadNotificationsLambda.assertions().isCalledOnce().with(value(A_SESSION_ID))
|
||||
}
|
||||
|
||||
private fun TestScope.createLoggedInEventProcessor(
|
||||
snackbarDispatcher: SnackbarDispatcher,
|
||||
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
||||
sessionId: SessionId = A_SESSION_ID,
|
||||
roomListService: FakeRoomListService = FakeRoomListService(),
|
||||
notificationCleaner: FakeNotificationCleaner = FakeNotificationCleaner(),
|
||||
) = LoggedInEventProcessor(
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
sessionId = sessionId,
|
||||
roomListService = roomListService,
|
||||
notificationCleaner = notificationCleaner,
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
)
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import io.element.android.appnav.root.RootPresenter
|
||||
import io.element.android.appnav.session.FakeSyncOrchestratorFactory
|
||||
import io.element.android.appnav.session.MatrixSessionCache
|
||||
import io.element.android.appnav.verification.IncomingVerificationRequestObserver
|
||||
import io.element.android.features.announcement.test.FakeAnnouncementService
|
||||
import io.element.android.features.login.api.LoginEntryPoint
|
||||
import io.element.android.features.login.api.LoginParams
|
||||
import io.element.android.features.login.test.FakeLoginEntryPoint
|
||||
@@ -33,7 +34,6 @@ import io.element.android.features.login.test.FakeLoginIntentResolver
|
||||
import io.element.android.features.login.test.accesscontrol.FakeAccountProviderAccessControl
|
||||
import io.element.android.features.preferences.test.FakeCacheService
|
||||
import io.element.android.features.rageshake.test.FakeBugReportEntryPoint
|
||||
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
|
||||
import io.element.android.features.share.test.FakeShareIntentHandler
|
||||
import io.element.android.features.signedout.test.FakeSignedOutEntryPoint
|
||||
import io.element.android.libraries.accountselect.test.FakeAccountSelectEntryPoint
|
||||
@@ -42,7 +42,7 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.test.FakeSdkMetadata
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||
import io.element.android.libraries.matrix.ui.media.test.FakeImageLoaderHolder
|
||||
import io.element.android.libraries.matrixmedia.test.FakeImageLoaderHolder
|
||||
import io.element.android.libraries.oauth.test.FakeOAuthActionFlow
|
||||
import io.element.android.libraries.oauth.test.FakeOAuthIntentResolver
|
||||
import io.element.android.libraries.preferences.test.FakeSessionPreferencesStoreFactory
|
||||
@@ -141,6 +141,8 @@ class RootFlowNodeTest : RobolectricTest() {
|
||||
loginEntryPoint = loginEntryPoint,
|
||||
imageLoaderHolder = FakeImageLoaderHolder(),
|
||||
analyticsColdStartWatcher = FakeAnalyticsColdStartWatcher(),
|
||||
featureFlagService = FakeFeatureFlagService(),
|
||||
announcementService = FakeAnnouncementService(),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
+1
-1
Submodule enterprise updated: 073abe3b44...819746a734
@@ -16,6 +16,4 @@ android {
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
}
|
||||
|
||||
+2
@@ -17,4 +17,6 @@ sealed interface Announcement {
|
||||
}
|
||||
|
||||
data object NewNotificationSound : Announcement
|
||||
|
||||
data object MultiAccount : Announcement
|
||||
}
|
||||
|
||||
+10
-3
@@ -29,10 +29,11 @@ class DefaultAnnouncementService(
|
||||
) : AnnouncementService {
|
||||
override suspend fun showAnnouncement(announcement: Announcement) {
|
||||
when (announcement) {
|
||||
is Announcement.Fullscreen -> showFullscreenAnnouncement(announcement)
|
||||
is Announcement.Fullscreen -> showAnnouncementIfNeverShown(announcement)
|
||||
Announcement.NewNotificationSound -> {
|
||||
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
|
||||
}
|
||||
Announcement.MultiAccount -> showAnnouncementIfNeverShown(announcement)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,16 +45,22 @@ class DefaultAnnouncementService(
|
||||
return combine(
|
||||
flowOf(Unit),
|
||||
announcementStore.announcementStatusFlow(Announcement.NewNotificationSound),
|
||||
) { _, newNotificationSoundStatus ->
|
||||
announcementStore.announcementStatusFlow(Announcement.MultiAccount),
|
||||
) { _, newNotificationSoundStatus, multiAccountStatus ->
|
||||
buildList {
|
||||
if (newNotificationSoundStatus == AnnouncementStatus.Show) {
|
||||
add(Announcement.NewNotificationSound)
|
||||
}
|
||||
// The multi-account announcement is pending until it is dismissed. Users who were already
|
||||
// logged in when the feature became available still have the default NeverShown status.
|
||||
if (multiAccountStatus != AnnouncementStatus.Shown) {
|
||||
add(Announcement.MultiAccount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun showFullscreenAnnouncement(announcement: Announcement.Fullscreen) {
|
||||
private suspend fun showAnnouncementIfNeverShown(announcement: Announcement) {
|
||||
val currentValue = announcementStore.announcementStatusFlow(announcement).first()
|
||||
if (currentValue == AnnouncementStatus.NeverShown) {
|
||||
announcementStore.setAnnouncementStatus(announcement, AnnouncementStatus.Show)
|
||||
|
||||
+8
-2
@@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.map
|
||||
|
||||
private val spaceAnnouncementKey = intPreferencesKey("spaceAnnouncement")
|
||||
private val newNotificationSoundKey = intPreferencesKey("newNotificationSound")
|
||||
private val multiAccountKey = intPreferencesKey("multiAccountAnnouncement")
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultAnnouncementStore(
|
||||
@@ -36,11 +37,15 @@ class DefaultAnnouncementStore(
|
||||
override fun announcementStatusFlow(announcement: Announcement): Flow<AnnouncementStatus> {
|
||||
val key = announcement.toKey()
|
||||
// Default-status convention: Shown for one-shot intro/feature announcements that should
|
||||
// NOT replay on first launch (Fullscreen.Space is disabled; NewNotificationSound is opted
|
||||
// in by AppMigration08).
|
||||
// NOT replay on first launch.
|
||||
// - Fullscreen.Space is disabled
|
||||
// - NewNotificationSound is opted in by AppMigration08
|
||||
// - MultiAccount is NeverShown by default, which means pending. It is set to Shown when the user enters
|
||||
// the login flow while the feature is enabled (see NotLoggedInFlowNode), or dismisses the announcement.
|
||||
val defaultStatus = when (announcement) {
|
||||
Announcement.Fullscreen.Space -> AnnouncementStatus.Shown
|
||||
Announcement.NewNotificationSound -> AnnouncementStatus.Shown
|
||||
Announcement.MultiAccount -> AnnouncementStatus.NeverShown
|
||||
}
|
||||
return store.data.map { prefs ->
|
||||
val ordinal = prefs[key] ?: defaultStatus.ordinal
|
||||
@@ -56,4 +61,5 @@ class DefaultAnnouncementStore(
|
||||
private fun Announcement.toKey() = when (this) {
|
||||
Announcement.Fullscreen.Space -> spaceAnnouncementKey
|
||||
Announcement.NewNotificationSound -> newNotificationSoundKey
|
||||
Announcement.MultiAccount -> multiAccountKey
|
||||
}
|
||||
|
||||
+24
-2
@@ -51,6 +51,22 @@ class DefaultAnnouncementServiceTest {
|
||||
assertThat(announcementStore.announcementStatusFlow(Announcement.NewNotificationSound).first()).isEqualTo(AnnouncementStatus.Show)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when showing MultiAccount announcement, announcement is set to show only if it was never shown`() = runTest {
|
||||
val announcementStore = InMemoryAnnouncementStore()
|
||||
val sut = createDefaultAnnouncementService(
|
||||
announcementStore = announcementStore,
|
||||
)
|
||||
assertThat(announcementStore.announcementStatusFlow(Announcement.MultiAccount).first()).isEqualTo(AnnouncementStatus.NeverShown)
|
||||
sut.showAnnouncement(Announcement.MultiAccount)
|
||||
assertThat(announcementStore.announcementStatusFlow(Announcement.MultiAccount).first()).isEqualTo(AnnouncementStatus.Show)
|
||||
// Simulate user close the announcement
|
||||
sut.onAnnouncementDismissed(Announcement.MultiAccount)
|
||||
// Calling again showAnnouncement should not change the value
|
||||
sut.showAnnouncement(Announcement.MultiAccount)
|
||||
assertThat(announcementStore.announcementStatusFlow(Announcement.MultiAccount).first()).isEqualTo(AnnouncementStatus.Shown)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test announcementsToShowFlow`() = runTest {
|
||||
val announcementStore = InMemoryAnnouncementStore()
|
||||
@@ -58,10 +74,16 @@ class DefaultAnnouncementServiceTest {
|
||||
announcementStore = announcementStore,
|
||||
)
|
||||
sut.announcementsToShowFlow().test {
|
||||
assertThat(awaitItem()).isEmpty()
|
||||
// MultiAccount is pending by default
|
||||
assertThat(awaitItem()).containsExactly(Announcement.MultiAccount)
|
||||
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
|
||||
assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound)
|
||||
assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound, Announcement.MultiAccount)
|
||||
announcementStore.setAnnouncementStatus(Announcement.MultiAccount, AnnouncementStatus.Show)
|
||||
// Still pending
|
||||
assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound, Announcement.MultiAccount)
|
||||
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Shown)
|
||||
assertThat(awaitItem()).containsExactly(Announcement.MultiAccount)
|
||||
announcementStore.setAnnouncementStatus(Announcement.MultiAccount, AnnouncementStatus.Shown)
|
||||
assertThat(awaitItem()).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -16,9 +16,11 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
class InMemoryAnnouncementStore(
|
||||
initialFullscreenAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
|
||||
initialNewNotificationSoundAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
|
||||
initialMultiAccountAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
|
||||
) : AnnouncementStore {
|
||||
private val fullScreenAnnouncement = MutableStateFlow(initialFullscreenAnnouncementStatus)
|
||||
private val newNotificationSoundAnnouncement = MutableStateFlow(initialNewNotificationSoundAnnouncementStatus)
|
||||
private val multiAccountAnnouncement = MutableStateFlow(initialMultiAccountAnnouncementStatus)
|
||||
|
||||
override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStatus) {
|
||||
announcement.toMutableStateFlow().value = status
|
||||
@@ -31,10 +33,12 @@ class InMemoryAnnouncementStore(
|
||||
override suspend fun reset() {
|
||||
fullScreenAnnouncement.value = AnnouncementStatus.NeverShown
|
||||
newNotificationSoundAnnouncement.value = AnnouncementStatus.NeverShown
|
||||
multiAccountAnnouncement.value = AnnouncementStatus.NeverShown
|
||||
}
|
||||
|
||||
private fun Announcement.toMutableStateFlow() = when (this) {
|
||||
is Announcement.Fullscreen -> fullScreenAnnouncement
|
||||
Announcement.NewNotificationSound -> newNotificationSoundAnnouncement
|
||||
Announcement.MultiAccount -> multiAccountAnnouncement
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.rageshake.test.logs
|
||||
package io.element.android.features.announcement.test
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -17,6 +17,5 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
||||
@@ -90,7 +90,6 @@ dependencies {
|
||||
api(projects.features.call.api)
|
||||
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.features.call.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||
import io.element.android.libraries.matrixmedia.api.ImageLoaderHolder
|
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||
import io.element.android.libraries.matrixmedia.api.ImageLoaderHolder
|
||||
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
|
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||
import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
|
||||
|
||||
+3
-4
@@ -6,17 +6,16 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call
|
||||
package io.element.android.features.call.impl
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.call.api.CallData
|
||||
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
|
||||
import io.element.android.features.call.impl.notifications.CallNotificationData
|
||||
import io.element.android.features.call.impl.ui.ElementCallActivity
|
||||
import io.element.android.features.call.utils.FakeActiveCallManager
|
||||
import io.element.android.features.call.utils.FakeNativeCallEntryPoint
|
||||
import io.element.android.features.call.impl.utils.FakeActiveCallManager
|
||||
import io.element.android.features.call.impl.utils.FakeNativeCallEntryPoint
|
||||
import io.element.android.features.callnative.api.NativeCallEntryPoint
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call
|
||||
package io.element.android.features.call.impl
|
||||
|
||||
import android.Manifest
|
||||
import android.webkit.PermissionRequest
|
||||
+2
-3
@@ -6,13 +6,12 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.notifications
|
||||
package io.element.android.features.call.impl.notifications
|
||||
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import coil3.ImageLoader
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
|
||||
import io.element.android.features.call.impl.ui.IncomingCallActivity
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
@@ -21,7 +20,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.matrix.ui.media.test.FakeImageLoaderHolder
|
||||
import io.element.android.libraries.matrixmedia.test.FakeImageLoaderHolder
|
||||
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.ui
|
||||
package io.element.android.features.call.impl.ui
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.call.api.CallData
|
||||
+1
-3
@@ -5,11 +5,9 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.ui
|
||||
package io.element.android.features.call.impl.ui
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.call.impl.ui.CallScreenBackPressAction
|
||||
import io.element.android.features.call.impl.ui.CallScreenBackPressPolicy
|
||||
import org.junit.Test
|
||||
|
||||
class CallScreenBackPressPolicyTest {
|
||||
+4
-7
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.ui
|
||||
package io.element.android.features.call.impl.ui
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
@@ -14,13 +14,10 @@ import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.features.call.api.CallData
|
||||
import io.element.android.features.call.impl.ui.CallScreenEvent
|
||||
import io.element.android.features.call.impl.ui.CallScreenNavigator
|
||||
import io.element.android.features.call.impl.ui.CallScreenPresenter
|
||||
import io.element.android.features.call.impl.utils.FakeActiveCallManager
|
||||
import io.element.android.features.call.impl.utils.FakeCallWidgetProvider
|
||||
import io.element.android.features.call.impl.utils.FakeWidgetMessageInterceptor
|
||||
import io.element.android.features.call.impl.utils.WidgetMessageSerializer
|
||||
import io.element.android.features.call.utils.FakeActiveCallManager
|
||||
import io.element.android.features.call.utils.FakeCallWidgetProvider
|
||||
import io.element.android.features.call.utils.FakeWidgetMessageInterceptor
|
||||
import io.element.android.libraries.androidutils.json.DefaultJsonProvider
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
+1
-6
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.ui
|
||||
package io.element.android.features.call.impl.ui
|
||||
|
||||
import android.view.KeyEvent
|
||||
import android.webkit.WebView
|
||||
@@ -18,11 +18,6 @@ import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||
import io.element.android.features.call.impl.pip.PictureInPictureEvent
|
||||
import io.element.android.features.call.impl.pip.PictureInPictureState
|
||||
import io.element.android.features.call.impl.pip.aPictureInPictureState
|
||||
import io.element.android.features.call.impl.ui.CallScreenEvent
|
||||
import io.element.android.features.call.impl.ui.CallScreenState
|
||||
import io.element.android.features.call.impl.ui.CallScreenView
|
||||
import io.element.android.features.call.impl.ui.JavascriptBackHandlerBridge
|
||||
import io.element.android.features.call.impl.ui.aCallScreenState
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.pressBackKey
|
||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||
+1
-3
@@ -6,9 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.ui
|
||||
|
||||
import io.element.android.features.call.impl.ui.CallScreenNavigator
|
||||
package io.element.android.features.call.impl.ui
|
||||
|
||||
class FakeCallScreenNavigator : CallScreenNavigator {
|
||||
var closeCalled = false
|
||||
+1
-2
@@ -6,10 +6,9 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.ui
|
||||
package io.element.android.features.call.impl.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.features.call.impl.ui.LanguageTagProvider
|
||||
|
||||
class FakeLanguageTagProvider(private val languageTag: String?) : LanguageTagProvider {
|
||||
@Composable
|
||||
+2
-6
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.utils
|
||||
package io.element.android.features.call.impl.utils
|
||||
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
@@ -16,10 +16,6 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.call.api.CallData
|
||||
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
|
||||
import io.element.android.features.call.impl.notifications.aCallNotificationData
|
||||
import io.element.android.features.call.impl.utils.ActiveCall
|
||||
import io.element.android.features.call.impl.utils.CallState
|
||||
import io.element.android.features.call.impl.utils.DefaultActiveCallManager
|
||||
import io.element.android.features.call.impl.utils.DefaultCurrentCallService
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
@@ -34,7 +30,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.libraries.matrix.ui.media.test.FakeImageLoaderHolder
|
||||
import io.element.android.libraries.matrixmedia.test.FakeImageLoaderHolder
|
||||
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
|
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||
import io.element.android.libraries.push.test.notifications.FakeOnMissedCallNotificationHandler
|
||||
+1
-2
@@ -6,10 +6,9 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.utils
|
||||
package io.element.android.features.call.impl.utils
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.call.impl.utils.DefaultCallWidgetProvider
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
+1
-3
@@ -6,12 +6,10 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.utils
|
||||
package io.element.android.features.call.impl.utils
|
||||
|
||||
import io.element.android.features.call.api.CallData
|
||||
import io.element.android.features.call.impl.notifications.CallNotificationData
|
||||
import io.element.android.features.call.impl.utils.ActiveCall
|
||||
import io.element.android.features.call.impl.utils.ActiveCallManager
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
+1
-2
@@ -6,9 +6,8 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.utils
|
||||
package io.element.android.features.call.impl.utils
|
||||
|
||||
import io.element.android.features.call.impl.utils.CallWidgetProvider
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.utils
|
||||
package io.element.android.features.call.impl.utils
|
||||
|
||||
import io.element.android.features.call.api.CallData
|
||||
import io.element.android.features.callnative.api.NativeCallEntryPoint
|
||||
+1
-2
@@ -6,9 +6,8 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.utils
|
||||
package io.element.android.features.call.impl.utils
|
||||
|
||||
import io.element.android.features.call.impl.utils.WidgetMessageInterceptor
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
class FakeWidgetMessageInterceptor : WidgetMessageInterceptor {
|
||||
@@ -17,7 +17,6 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
|
||||
api(projects.features.call.api)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
||||
@@ -47,8 +47,6 @@ dependencies {
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.element.call.test)
|
||||
testImplementation(projects.features.call.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
}
|
||||
|
||||
@@ -8,16 +8,8 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
||||
@@ -34,7 +34,6 @@ dependencies {
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.deeplink.api)
|
||||
implementation(projects.libraries.mediapickers.api)
|
||||
implementation(projects.libraries.mediaupload.api)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
@@ -52,7 +51,6 @@ dependencies {
|
||||
testImplementation(projects.libraries.mediapickers.test)
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.features.startchat.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.features.enterprise.test)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<string name="screen_create_room_new_room_title">"新房间"</string>
|
||||
<string name="screen_create_room_new_space_title">"新空间"</string>
|
||||
<string name="screen_create_room_private_option_description">"仅限受邀人员加入。"</string>
|
||||
<string name="screen_create_room_private_option_title">"私密"</string>
|
||||
<string name="screen_create_room_private_option_title">"私有"</string>
|
||||
<string name="screen_create_room_public_option_description">"任何人都能找到此房间。
|
||||
你可以随时在房间设置中更改。"</string>
|
||||
<string name="screen_create_room_public_option_short_description">"任何人都可以加入,包括你的组织之外的人员。"</string>
|
||||
@@ -18,7 +18,7 @@
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"%1$s 中的任何人都可以加入,但其他人必须申请访问。"</string>
|
||||
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"申请加入"</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_description">"仅限受邀人员加入。"</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_title">"私密"</string>
|
||||
<string name="screen_create_room_room_access_section_private_option_title">"私有"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_description">"任何人都可以加入,包括你的组织之外的人员。"</string>
|
||||
<string name="screen_create_room_room_access_section_public_option_title">"公共"</string>
|
||||
<string name="screen_create_room_room_access_section_restricted_option_description">"%1$s 中的任何人都可以加入。"</string>
|
||||
|
||||
+2
-1
@@ -6,10 +6,11 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.api
|
||||
package io.element.android.features.createroom.test
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
@@ -28,7 +28,6 @@ setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.logout.impl
|
||||
package io.element.android.features.deactivation.impl
|
||||
|
||||
sealed interface AccountDeactivationEvent {
|
||||
data class SetEraseData(val eraseData: Boolean) : AccountDeactivationEvent
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.logout.impl
|
||||
package io.element.android.features.deactivation.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.logout.impl
|
||||
package io.element.android.features.deactivation.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.logout.impl
|
||||
package io.element.android.features.deactivation.impl
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.logout.impl
|
||||
package io.element.android.features.deactivation.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
+2
-3
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.logout.impl
|
||||
package io.element.android.features.deactivation.impl
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -44,8 +44,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.deactivation.impl.R
|
||||
import io.element.android.features.logout.impl.ui.AccountDeactivationActionDialog
|
||||
import io.element.android.features.deactivation.impl.ui.AccountDeactivationActionDialog
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.form.textFieldState
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.logout.impl
|
||||
package io.element.android.features.deactivation.impl
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.logout.impl.ui
|
||||
package io.element.android.features.deactivation.impl.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.logout.impl.ui
|
||||
package io.element.android.features.deactivation.impl.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.logout.impl
|
||||
package io.element.android.features.deactivation.impl
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
+1
-2
@@ -8,7 +8,7 @@
|
||||
|
||||
@file:OptIn(ExperimentalTestApi::class)
|
||||
|
||||
package io.element.android.features.logout.impl
|
||||
package io.element.android.features.deactivation.impl
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.AndroidComposeUiTest
|
||||
@@ -16,7 +16,6 @@ import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||
import io.element.android.features.deactivation.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_PASSWORD
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.logout.impl
|
||||
package io.element.android.features.deactivation.impl
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
@@ -17,5 +17,4 @@ dependencies {
|
||||
implementation(projects.libraries.compound)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.wellknown.api)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.wellknown.api)
|
||||
implementation(libs.serialization.json)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
|
||||
@@ -18,6 +18,5 @@ dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.compound)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.wellknown.api)
|
||||
implementation(projects.tests.testutils)
|
||||
}
|
||||
|
||||
+5
-1
@@ -28,6 +28,7 @@ import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||
@@ -40,6 +41,7 @@ class ForwardMessagesNode(
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: ForwardMessagesPresenter.Factory,
|
||||
private val roomSelectEntryPoint: RoomSelectEntryPoint,
|
||||
private val sessionId: SessionId,
|
||||
) : ParentNode<ForwardMessagesNode.NavTarget>(
|
||||
navModel = PermanentNavModel(
|
||||
navTargets = setOf(NavTarget),
|
||||
@@ -62,7 +64,8 @@ class ForwardMessagesNode(
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
val callback = object : RoomSelectEntryPoint.Callback {
|
||||
override fun onRoomSelected(roomIds: List<RoomId>) {
|
||||
override fun onRoomSelected(sessionId: SessionId, roomIds: List<RoomId>) {
|
||||
// In Forward mode, the session cannot be changed
|
||||
presenter.onRoomSelected(roomIds)
|
||||
}
|
||||
|
||||
@@ -75,6 +78,7 @@ class ForwardMessagesNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = RoomSelectEntryPoint.Params(
|
||||
sessionId = sessionId,
|
||||
mode = RoomSelectMode.Forward,
|
||||
maxNumberOfRooms = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS,
|
||||
),
|
||||
|
||||
+2
@@ -15,6 +15,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.forward.api.ForwardEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimelineProvider
|
||||
import io.element.android.libraries.roomselect.test.FakeRoomSelectEntryPoint
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
@@ -39,6 +40,7 @@ class DefaultForwardEntryPointTest {
|
||||
plugins = plugins,
|
||||
presenterFactory = { _, _ -> createForwardMessagesPresenter() },
|
||||
roomSelectEntryPoint = FakeRoomSelectEntryPoint(),
|
||||
sessionId = A_SESSION_ID,
|
||||
)
|
||||
}
|
||||
val callback = object : ForwardEntryPoint.Callback {
|
||||
|
||||
@@ -15,7 +15,6 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.features.forward.api)
|
||||
implementation(projects.tests.testutils)
|
||||
}
|
||||
|
||||
@@ -32,12 +32,10 @@ dependencies {
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.uiCommon)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.features.analytics.api)
|
||||
implementation(projects.features.logout.api)
|
||||
implementation(projects.features.securebackup.api)
|
||||
|
||||
+1
@@ -32,5 +32,6 @@ interface HomeEntryPoint : FeatureEntryPoint {
|
||||
fun navigateToEnterRecoveryKey()
|
||||
fun navigateToRoomSettings(roomId: RoomId)
|
||||
fun navigateToBugReport()
|
||||
fun navigateToAddAccount()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,14 +45,11 @@ dependencies {
|
||||
implementation(projects.libraries.indicator.api)
|
||||
implementation(projects.libraries.deeplink.api)
|
||||
implementation(projects.libraries.fullscreenintent.api)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.permissions.noop)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.features.announcement.api)
|
||||
implementation(projects.features.invite.api)
|
||||
implementation(projects.features.networkmonitor.api)
|
||||
implementation(projects.features.leaveroom.api)
|
||||
implementation(projects.features.rageshake.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
@@ -72,11 +69,8 @@ dependencies {
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.libraries.eventformatter.test)
|
||||
testImplementation(projects.libraries.indicator.test)
|
||||
testImplementation(projects.libraries.permissions.noop)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
}
|
||||
|
||||
+2
-7
@@ -8,8 +8,8 @@
|
||||
|
||||
package io.element.android.features.home.impl
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.api.user.toMatrixUser
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -32,12 +32,7 @@ class CurrentUserWithNeighborsBuilder {
|
||||
matrixUser
|
||||
} else {
|
||||
// Use the data from the DB
|
||||
MatrixUser(
|
||||
userId = UserId(it.userId),
|
||||
displayName = it.userDisplayName,
|
||||
avatarUrl = it.userAvatarUrl,
|
||||
avatarThumbnail = it.userAvatarData,
|
||||
)
|
||||
it.toMatrixUser()
|
||||
}
|
||||
}
|
||||
.let { sessionList ->
|
||||
|
||||
@@ -13,4 +13,5 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
||||
sealed interface HomeEvent {
|
||||
data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvent
|
||||
data class SwitchToAccount(val sessionId: SessionId) : HomeEvent
|
||||
data object DismissMultiAccountAnnouncement : HomeEvent
|
||||
}
|
||||
|
||||
+1
@@ -221,6 +221,7 @@ class HomeFlowNode(
|
||||
homeState = state,
|
||||
onRoomClick = ::navigateToRoom,
|
||||
onSettingsClick = callback::navigateToSettings,
|
||||
onAddAccountClick = callback::navigateToAddAccount,
|
||||
onStartChatClick = callback::navigateToCreateRoom,
|
||||
onCreateSpaceClick = callback::navigateToCreateSpace,
|
||||
onSetUpRecoveryClick = callback::navigateToSetUpRecovery,
|
||||
|
||||
+35
@@ -19,18 +19,23 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.announcement.api.AnnouncementService
|
||||
import io.element.android.features.home.impl.roomlist.RoomListState
|
||||
import io.element.android.features.home.impl.spaces.HomeSpacesState
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.indicator.api.IndicatorService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
@@ -43,6 +48,8 @@ class HomePresenter(
|
||||
private val homeSpacesPresenter: Presenter<HomeSpacesState>,
|
||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||
private val sessionStore: SessionStore,
|
||||
private val announcementService: AnnouncementService,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : Presenter<HomeState> {
|
||||
private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder()
|
||||
|
||||
@@ -59,6 +66,30 @@ class HomePresenter(
|
||||
}.collectAsState(initial = persistentListOf(matrixUser))
|
||||
val isOnline by syncService.isOnline.collectAsState()
|
||||
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
|
||||
val showMultiAccountAnnouncement by remember {
|
||||
combine(
|
||||
announcementService.announcementsToShowFlow(),
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MultiAccount),
|
||||
sessionStore.sessionsFlow(),
|
||||
) { announcements, isMultiAccountEnabled, sessions ->
|
||||
// Do not announce the feature to users who already have several accounts
|
||||
isMultiAccountEnabled && sessions.size == 1 && announcements.contains(Announcement.MultiAccount)
|
||||
}
|
||||
}.collectAsState(false)
|
||||
LaunchedEffect(Unit) {
|
||||
// The user already uses several accounts, so the feature does not need to be announced anymore,
|
||||
// even if they later remove accounts.
|
||||
combine(
|
||||
announcementService.announcementsToShowFlow(),
|
||||
sessionStore.sessionsFlow(),
|
||||
) { announcements, sessions ->
|
||||
announcements.contains(Announcement.MultiAccount) && sessions.size > 1
|
||||
}
|
||||
.filter { it }
|
||||
.collect {
|
||||
announcementService.onAnnouncementDismissed(Announcement.MultiAccount)
|
||||
}
|
||||
}
|
||||
val roomListState = roomListPresenter.present()
|
||||
val homeSpacesState = homeSpacesPresenter.present()
|
||||
var currentHomeNavigationBarItemOrdinal by rememberSaveable { mutableIntStateOf(HomeNavigationBarItem.Chats.ordinal) }
|
||||
@@ -82,6 +113,9 @@ class HomePresenter(
|
||||
is HomeEvent.SwitchToAccount -> coroutineState.launch {
|
||||
sessionStore.setLatestSession(event.sessionId.value)
|
||||
}
|
||||
HomeEvent.DismissMultiAccountAnnouncement -> coroutineState.launch {
|
||||
announcementService.onAnnouncementDismissed(Announcement.MultiAccount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +129,7 @@ class HomePresenter(
|
||||
homeSpacesState = homeSpacesState,
|
||||
snackbarMessage = snackbarMessage,
|
||||
canReportBug = canReportBug,
|
||||
showMultiAccountAnnouncement = showMultiAccountAnnouncement,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ data class HomeState(
|
||||
val homeSpacesState: HomeSpacesState,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val canReportBug: Boolean,
|
||||
val showMultiAccountAnnouncement: Boolean,
|
||||
val eventSink: (HomeEvent) -> Unit,
|
||||
) {
|
||||
val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected
|
||||
|
||||
+4
@@ -48,6 +48,8 @@ open class HomeStatePreviewParam : PreviewParameterProvider<HomeState> {
|
||||
homeSpacesState = aHomeSpacesState(
|
||||
spaceRooms = emptyList(),
|
||||
),
|
||||
) + aHomeState(
|
||||
showMultiAccountAnnouncement = true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,6 +63,7 @@ internal fun aHomeState(
|
||||
roomListState: RoomListState = aRoomListState(),
|
||||
homeSpacesState: HomeSpacesState = aHomeSpacesState(),
|
||||
canReportBug: Boolean = true,
|
||||
showMultiAccountAnnouncement: Boolean = false,
|
||||
eventSink: (HomeEvent) -> Unit = {}
|
||||
) = HomeState(
|
||||
currentUserAndNeighbors = currentUserAndNeighbors.toImmutableList(),
|
||||
@@ -68,6 +71,7 @@ internal fun aHomeState(
|
||||
hasNetworkConnection = hasNetworkConnection,
|
||||
snackbarMessage = snackbarMessage,
|
||||
canReportBug = canReportBug,
|
||||
showMultiAccountAnnouncement = showMultiAccountAnnouncement,
|
||||
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
||||
roomListState = roomListState,
|
||||
homeSpacesState = homeSpacesState,
|
||||
|
||||
@@ -39,6 +39,7 @@ import androidx.compose.ui.zIndex
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.home.impl.components.HomeTopBar
|
||||
import io.element.android.features.home.impl.components.MultiAccountAnnouncementBottomSheet
|
||||
import io.element.android.features.home.impl.components.RoomListContentView
|
||||
import io.element.android.features.home.impl.components.RoomListMenuAction
|
||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||
@@ -76,6 +77,7 @@ fun HomeView(
|
||||
homeState: HomeState,
|
||||
onRoomClick: (RoomId, EventId?) -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onAddAccountClick: () -> Unit,
|
||||
onSetUpRecoveryClick: () -> Unit,
|
||||
onConfirmRecoveryKeyClick: () -> Unit,
|
||||
onStartChatClick: () -> Unit,
|
||||
@@ -110,6 +112,13 @@ fun HomeView(
|
||||
)
|
||||
}
|
||||
|
||||
if (homeState.showMultiAccountAnnouncement) {
|
||||
MultiAccountAnnouncementBottomSheet(
|
||||
onDismiss = { homeState.eventSink(HomeEvent.DismissMultiAccountAnnouncement) },
|
||||
onAddAccountClick = { if (firstThrottler.canHandle()) onAddAccountClick() },
|
||||
)
|
||||
}
|
||||
|
||||
leaveRoomView()
|
||||
|
||||
HomeScaffold(
|
||||
@@ -366,6 +375,7 @@ internal fun HomeViewPreview(@PreviewParameter(HomeStatePreviewParam::class) sta
|
||||
homeState = state,
|
||||
onRoomClick = { _, _ -> },
|
||||
onSettingsClick = {},
|
||||
onAddAccountClick = {},
|
||||
onSetUpRecoveryClick = {},
|
||||
onConfirmRecoveryKeyClick = {},
|
||||
onStartChatClick = {},
|
||||
@@ -386,6 +396,7 @@ internal fun HomeViewA11yPreview() = ElementPreview {
|
||||
homeState = aHomeState(),
|
||||
onRoomClick = { _, _ -> },
|
||||
onSettingsClick = {},
|
||||
onAddAccountClick = {},
|
||||
onSetUpRecoveryClick = {},
|
||||
onConfirmRecoveryKeyClick = {},
|
||||
onStartChatClick = {},
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.home.impl.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.home.impl.R
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Bottom sheet announcing that several accounts can now be added.
|
||||
* Ref: https://www.figma.com/design/kEAcfun9iSpszeUDvdKZ6b/ER-351--Multi-account-in-EX?node-id=195-43073
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun MultiAccountAnnouncementBottomSheet(
|
||||
onDismiss: () -> Unit,
|
||||
onAddAccountClick: () -> Unit,
|
||||
) {
|
||||
val sheetState = rememberBottomSheetState(
|
||||
initialValue = SheetValue.Hidden,
|
||||
)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
ModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = onDismiss,
|
||||
scrollable = false,
|
||||
) {
|
||||
MultiAccountAnnouncementContent(
|
||||
onAddAccountClick = {
|
||||
coroutineScope.launch {
|
||||
sheetState.hide()
|
||||
onDismiss()
|
||||
onAddAccountClick()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MultiAccountAnnouncementContent(
|
||||
onAddAccountClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(all = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
BigIcon(
|
||||
style = BigIcon.Style.Default(CompoundIcons.UserProfileSolid()),
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.semantics { heading() },
|
||||
text = stringResource(R.string.screen_multi_account_announcement_title),
|
||||
style = ElementTheme.typography.fontHeadingMdBold,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
val path = stringResource(CommonStrings.common_settings) + " > " + stringResource(CommonStrings.common_add_another_account)
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
text = annotatedTextWithBold(
|
||||
text = stringResource(R.string.screen_multi_account_announcement_description, path),
|
||||
boldText = path,
|
||||
),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(40.dp))
|
||||
Button(
|
||||
text = stringResource(R.string.screen_multi_account_announcement_action),
|
||||
leadingIcon = IconSource.Vector(CompoundIcons.Plus()),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onAddAccountClick,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun MultiAccountAnnouncementBottomSheetPreview() = ElementPreview(fillMaxSize = true) {
|
||||
MultiAccountAnnouncementBottomSheet(
|
||||
onDismiss = {},
|
||||
onAddAccountClick = {},
|
||||
)
|
||||
}
|
||||
+2
-1
@@ -47,6 +47,7 @@ import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.api.ShowAllActivityInRoomListFeature
|
||||
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
@@ -119,7 +120,7 @@ class RoomListPresenter(
|
||||
val declineInviteMenu = remember { mutableStateOf<RoomListState.DeclineInviteMenu>(RoomListState.DeclineInviteMenu.Hidden) }
|
||||
|
||||
val showAllActivity by produceState(initialValue = false) {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.ShowAllActivityInRoomList).collect { value = it }
|
||||
featureFlagService.isFeatureEnabledFlow(ShowAllActivityInRoomListFeature).collect { value = it }
|
||||
}
|
||||
|
||||
fun handleEvent(event: RoomListEvent) {
|
||||
|
||||
+35
@@ -10,12 +10,14 @@ package io.element.android.features.home.impl.search
|
||||
import androidx.compose.foundation.text.input.clearText
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.home.impl.search.history.SearchHistoryResult
|
||||
@@ -38,6 +40,7 @@ import io.element.android.libraries.matrix.api.search.MessageSearch
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchPaginationState
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchResult
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchService
|
||||
import io.element.android.libraries.matrix.api.search.SearchBackfillStrategy
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||
@@ -81,6 +84,7 @@ class GlobalSearchPresenter(
|
||||
val isEnabled by produceState(false) {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MessageSearch).collectLatest { value = it }
|
||||
}
|
||||
|
||||
val roomListSearchDataSource = remember { roomListSearchDataSourceFactory.create(coroutineScope = coroutineScope) }
|
||||
val queryState = rememberTextFieldState()
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
@@ -88,6 +92,8 @@ class GlobalSearchPresenter(
|
||||
var currentTarget: GlobalSearchTarget by remember { mutableStateOf(GlobalSearchTarget.ROOMS) }
|
||||
val currentMessageSearch: MessageSearch = remember { messageSearchService.createMessageSearch(scope = coroutineScope) }
|
||||
|
||||
StartSearchBackfillWhenVisible(isEnabled = isEnabled, isSearchActive = isSearchActive)
|
||||
|
||||
val searchHistory by produceState<AsyncData<ImmutableList<SearchHistoryListItem>>>(initialValue = AsyncData.Uninitialized) {
|
||||
searchHistoryStore.history.collectLatest { history ->
|
||||
val mappedHistoryItems = history.mapNotNull { result ->
|
||||
@@ -297,6 +303,35 @@ class GlobalSearchPresenter(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StartSearchBackfillWhenVisible(
|
||||
isEnabled: Boolean,
|
||||
isSearchActive: Boolean,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var hasBackfillRun by rememberSaveable { mutableStateOf(false) }
|
||||
if (isEnabled && isSearchActive && !hasBackfillRun) {
|
||||
DisposableEffect(Unit) {
|
||||
// When we open the message search, we want to backfill the search results so that we can show the user more results if they scroll down.
|
||||
val handle = matrixClient.searchBackfillService.startSearchBackfill(SearchBackfillStrategy.FOREGROUND).getOrNull()
|
||||
?: return@DisposableEffect onDispose {
|
||||
Timber.w("Could not start search backfill, it may already be running.")
|
||||
}
|
||||
|
||||
val job = coroutineScope.launch(coroutineDispatchers.io) {
|
||||
while (handle.isRunning()) {
|
||||
delay(100.milliseconds)
|
||||
}
|
||||
hasBackfillRun = true
|
||||
}
|
||||
onDispose {
|
||||
job.cancel()
|
||||
handle.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapMediaContent(
|
||||
result: MessageSearchResult,
|
||||
roomInfo: RoomInfo,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<string name="confirm_recovery_key_banner_title">"Penyimpanan kunci Anda tidak sinkron"</string>
|
||||
<string name="full_screen_intent_banner_message">"Untuk memastikan Anda tidak melewatkan panggilan penting, silakan ubah pengaturan Anda untuk memperbolehkan notifikasi layar penuh ketika ponsel Anda terkunci."</string>
|
||||
<string name="full_screen_intent_banner_title">"Tingkatkan pengalaman panggilan Anda"</string>
|
||||
<string name="screen_home_tab_chats">"Semua Obrolan"</string>
|
||||
<string name="screen_home_tab_chats">"Obrolan"</string>
|
||||
<string name="screen_home_tab_spaces">"Space"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Apakah Anda yakin ingin menolak undangan untuk bergabung ke %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Tolak undangan"</string>
|
||||
@@ -46,7 +46,7 @@ Untuk sementara, Anda dapat membatalkan pilihan saringan untuk melihat percakapa
|
||||
<string name="screen_roomlist_filter_unreads_empty_state_title">"Selamat!
|
||||
Anda tidak memiliki pesan yang belum dibaca!"</string>
|
||||
<string name="screen_roomlist_knock_event_sent_description">"Permintaan untuk bergabung dikirim"</string>
|
||||
<string name="screen_roomlist_main_space_title">"Semua Obrolan"</string>
|
||||
<string name="screen_roomlist_main_space_title">"Obrolan"</string>
|
||||
<string name="screen_roomlist_mark_as_read">"Tandai sebagai dibaca"</string>
|
||||
<string name="screen_roomlist_mark_as_unread">"Tandai sebagai belum dibaca"</string>
|
||||
<string name="screen_roomlist_tombstoned_room_description">"Ruangan ini telah ditingkatkan"</string>
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) invited you"</string>
|
||||
<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_multi_account_announcement_action">"Add account"</string>
|
||||
<string name="screen_multi_account_announcement_description">"Go to %1$s to sign into another account. Then you can easily switch between them."</string>
|
||||
<string name="screen_multi_account_announcement_title">"You can now add multiple accounts"</string>
|
||||
<string name="screen_roomlist_a11y_create_message">"Create a new conversation or room"</string>
|
||||
<string name="screen_roomlist_clear_filters">"Clear filters"</string>
|
||||
<string name="screen_roomlist_empty_message">"Get started by messaging someone."</string>
|
||||
|
||||
+1
@@ -52,6 +52,7 @@ class DefaultHomeEntryPointTest : RobolectricTest() {
|
||||
override fun navigateToEnterRecoveryKey() = lambdaError()
|
||||
override fun navigateToRoomSettings(roomId: RoomId) = lambdaError()
|
||||
override fun navigateToBugReport() = lambdaError()
|
||||
override fun navigateToAddAccount() = lambdaError()
|
||||
}
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
|
||||
+105
@@ -9,12 +9,18 @@
|
||||
package io.element.android.features.home.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.announcement.api.AnnouncementService
|
||||
import io.element.android.features.announcement.test.FakeAnnouncementService
|
||||
import io.element.android.features.home.impl.roomlist.aRoomListState
|
||||
import io.element.android.features.home.impl.spaces.HomeSpacesState
|
||||
import io.element.android.features.home.impl.spaces.aHomeSpacesState
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.indicator.api.IndicatorService
|
||||
import io.element.android.libraries.indicator.test.FakeIndicatorService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
@@ -30,6 +36,8 @@ import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -144,6 +152,99 @@ class HomePresenterTest {
|
||||
assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - multi account announcement is shown when announced, feature enabled and a single account`() = runTest {
|
||||
val presenter = createHomePresenter(
|
||||
sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(aSessionData()),
|
||||
updateUserProfileResult = { _, _, _, _ -> },
|
||||
),
|
||||
announcementService = FakeAnnouncementService(
|
||||
initialAnnouncementsToShowFlowValue = listOf(Announcement.MultiAccount),
|
||||
),
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(FeatureFlags.MultiAccount.key to true),
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
assertThat(awaitItem().showMultiAccountAnnouncement).isFalse()
|
||||
assertThat(expectMostRecentItem().showMultiAccountAnnouncement).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - multi account announcement is not shown when the feature is disabled`() = runTest {
|
||||
val presenter = createHomePresenter(
|
||||
sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(aSessionData()),
|
||||
updateUserProfileResult = { _, _, _, _ -> },
|
||||
),
|
||||
announcementService = FakeAnnouncementService(
|
||||
initialAnnouncementsToShowFlowValue = listOf(Announcement.MultiAccount),
|
||||
),
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(FeatureFlags.MultiAccount.key to false),
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
assertThat(expectMostRecentItem().showMultiAccountAnnouncement).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - multi account announcement is not shown and is dismissed when there are several accounts`() = runTest {
|
||||
val onAnnouncementDismissedResult = lambdaRecorder<Announcement, Unit> { }
|
||||
val presenter = createHomePresenter(
|
||||
sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(sessionId = "@alice:server.org"),
|
||||
aSessionData(sessionId = "@bob:server.org"),
|
||||
),
|
||||
updateUserProfileResult = { _, _, _, _ -> },
|
||||
),
|
||||
announcementService = FakeAnnouncementService(
|
||||
initialAnnouncementsToShowFlowValue = listOf(Announcement.MultiAccount),
|
||||
onAnnouncementDismissedResult = onAnnouncementDismissedResult,
|
||||
),
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(FeatureFlags.MultiAccount.key to true),
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
assertThat(expectMostRecentItem().showMultiAccountAnnouncement).isFalse()
|
||||
onAnnouncementDismissedResult.assertions().isCalledOnce()
|
||||
.with(value(Announcement.MultiAccount))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - dismiss multi account announcement`() = runTest {
|
||||
val onAnnouncementDismissedResult = lambdaRecorder<Announcement, Unit> { }
|
||||
val announcementService = FakeAnnouncementService(
|
||||
initialAnnouncementsToShowFlowValue = listOf(Announcement.MultiAccount),
|
||||
onAnnouncementDismissedResult = onAnnouncementDismissedResult,
|
||||
)
|
||||
val presenter = createHomePresenter(
|
||||
sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(aSessionData()),
|
||||
updateUserProfileResult = { _, _, _, _ -> },
|
||||
),
|
||||
announcementService = announcementService,
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(FeatureFlags.MultiAccount.key to true),
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
val state = expectMostRecentItem()
|
||||
assertThat(state.showMultiAccountAnnouncement).isTrue()
|
||||
state.eventSink(HomeEvent.DismissMultiAccountAnnouncement)
|
||||
announcementService.emitAnnouncementsToShow(emptyList())
|
||||
assertThat(awaitItem().showMultiAccountAnnouncement).isFalse()
|
||||
onAnnouncementDismissedResult.assertions().isCalledOnce()
|
||||
.with(value(Announcement.MultiAccount))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createHomePresenter(
|
||||
@@ -154,6 +255,8 @@ internal fun createHomePresenter(
|
||||
indicatorService: IndicatorService = FakeIndicatorService(),
|
||||
homeSpacesPresenter: Presenter<HomeSpacesState> = Presenter { aHomeSpacesState() },
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
announcementService: AnnouncementService = FakeAnnouncementService(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
) = HomePresenter(
|
||||
client = client,
|
||||
syncService = syncService,
|
||||
@@ -163,4 +266,6 @@ internal fun createHomePresenter(
|
||||
homeSpacesPresenter = homeSpacesPresenter,
|
||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||
sessionStore = sessionStore,
|
||||
announcementService = announcementService,
|
||||
featureFlagService = featureFlagService,
|
||||
)
|
||||
|
||||
+4
-3
@@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.announcement.api.AnnouncementService
|
||||
import io.element.android.features.announcement.test.FakeAnnouncementService
|
||||
import io.element.android.features.home.impl.FakeDateTimeObserver
|
||||
import io.element.android.features.home.impl.datasource.RoomListDataSource
|
||||
import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory
|
||||
@@ -32,7 +33,6 @@ import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.preferences.impl.tasks.MarkRoomAsRead
|
||||
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
@@ -40,6 +40,7 @@ import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
|
||||
import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.api.ShowAllActivityInRoomListFeature
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
@@ -655,7 +656,7 @@ class RoomListPresenterTest {
|
||||
)
|
||||
val featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(
|
||||
FeatureFlags.ShowAllActivityInRoomList.key to true,
|
||||
ShowAllActivityInRoomListFeature.key to true,
|
||||
FeatureFlags.UnreadIndicatorCount.key to true,
|
||||
)
|
||||
)
|
||||
@@ -672,7 +673,7 @@ class RoomListPresenterTest {
|
||||
assertThat(enabledState.contentAsRooms().showAllActivity).isTrue()
|
||||
assertThat(enabledState.contentAsRooms().showUnreadCount).isTrue()
|
||||
|
||||
featureFlagService.setFeatureEnabled(FeatureFlags.ShowAllActivityInRoomList, false)
|
||||
featureFlagService.setFeatureEnabled(ShowAllActivityInRoomListFeature, false)
|
||||
val allActivityDisabledState = consumeItemsUntilPredicate { state ->
|
||||
state.contentState is RoomListContentState.Rooms && !state.contentAsRooms().showAllActivity
|
||||
}.last()
|
||||
|
||||
+27
-1
@@ -19,6 +19,7 @@ import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTouchInput
|
||||
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||
import io.element.android.features.home.impl.HomeEvent
|
||||
import io.element.android.features.home.impl.HomeView
|
||||
import io.element.android.features.home.impl.R
|
||||
import io.element.android.features.home.impl.aHomeState
|
||||
@@ -259,6 +260,23 @@ class RoomListViewTest : RobolectricTest() {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on add account in the multi-account announcement emits the expected Event and invokes the callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<HomeEvent>()
|
||||
ensureCalledOnce { callback ->
|
||||
setRoomListView(
|
||||
state = aRoomListState(),
|
||||
showMultiAccountAnnouncement = true,
|
||||
homeEventSink = eventsRecorder,
|
||||
onAddAccountClick = callback,
|
||||
)
|
||||
clickOn(R.string.screen_multi_account_announcement_action)
|
||||
// Wait for the bottom sheet to be hidden
|
||||
waitForIdle()
|
||||
}
|
||||
eventsRecorder.assertSingle(HomeEvent.DismissMultiAccountAnnouncement)
|
||||
}
|
||||
}
|
||||
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setRoomListView(
|
||||
@@ -273,12 +291,20 @@ private fun AndroidComposeUiTest<ComponentActivity>.setRoomListView(
|
||||
onMenuActionClick: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onReportRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onDeclineInviteAndBlockUser: (RoomListRoomSummary) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onAddAccountClick: () -> Unit = EnsureNeverCalled(),
|
||||
showMultiAccountAnnouncement: Boolean = false,
|
||||
homeEventSink: (HomeEvent) -> Unit = {},
|
||||
) {
|
||||
setSafeContent {
|
||||
HomeView(
|
||||
homeState = aHomeState(roomListState = state),
|
||||
homeState = aHomeState(
|
||||
roomListState = state,
|
||||
showMultiAccountAnnouncement = showMultiAccountAnnouncement,
|
||||
eventSink = homeEventSink,
|
||||
),
|
||||
onRoomClick = { roomId, _ -> onRoomClick(roomId) },
|
||||
onSettingsClick = onSettingsClick,
|
||||
onAddAccountClick = onAddAccountClick,
|
||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||
onStartChatClick = onCreateRoomClick,
|
||||
|
||||
+52
-4
@@ -33,13 +33,16 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchService
|
||||
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.FakeSdkPendingTask
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.libraries.matrix.test.search.FakeMessageSearch
|
||||
import io.element.android.libraries.matrix.test.search.FakeMessageSearchService
|
||||
import io.element.android.libraries.matrix.test.search.FakeSearchBackfillService
|
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.test
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -205,7 +208,9 @@ class GlobalSearchPresenterTest {
|
||||
@Test
|
||||
fun `present - message search results are mapped when the message search emits`() = runTest {
|
||||
val messageSearch = FakeMessageSearch()
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
searchBackfillService = FakeSearchBackfillService(startSearchBackfillLambda = { Result.success(FakeSdkPendingTask()) }),
|
||||
).apply {
|
||||
getRoomInfoLambda = { Result.success(aRoomInfo()) }
|
||||
}
|
||||
val presenter = createGlobalSearchPresenter(
|
||||
@@ -232,7 +237,9 @@ class GlobalSearchPresenterTest {
|
||||
@Test
|
||||
fun `present - message search displays the uninitialized state after removing the query`() = runTest {
|
||||
val messageSearch = FakeMessageSearch()
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
searchBackfillService = FakeSearchBackfillService(startSearchBackfillLambda = { Result.success(FakeSdkPendingTask()) }),
|
||||
).apply {
|
||||
getRoomInfoLambda = { Result.success(aRoomInfo()) }
|
||||
}
|
||||
val presenter = createGlobalSearchPresenter(
|
||||
@@ -267,7 +274,9 @@ class GlobalSearchPresenterTest {
|
||||
@Test
|
||||
fun `present - UpdateVisibleRange triggers pagination for messages when near the end`() = runTest {
|
||||
val messageSearch = FakeMessageSearch()
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
searchBackfillService = FakeSearchBackfillService(startSearchBackfillLambda = { Result.success(FakeSdkPendingTask()) }),
|
||||
).apply {
|
||||
getRoomInfoLambda = { Result.success(aRoomInfo()) }
|
||||
}
|
||||
val presenter = createGlobalSearchPresenter(
|
||||
@@ -432,6 +441,44 @@ class GlobalSearchPresenterTest {
|
||||
assertThat(awaitItem().results.isLoading()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - will automatically start search backfill and it will be cancelled when the presenter is closed`() = runTest {
|
||||
var isRunning = false
|
||||
val closeLambda = lambdaRecorder<Unit> { isRunning = false }
|
||||
val matrixClient = FakeMatrixClient(
|
||||
searchBackfillService = FakeSearchBackfillService(
|
||||
startSearchBackfillLambda = {
|
||||
isRunning = true
|
||||
Result.success(FakeSdkPendingTask(isRunningLambda = { isRunning }, closeLambda = closeLambda))
|
||||
},
|
||||
),
|
||||
)
|
||||
val presenter = createGlobalSearchPresenter(
|
||||
matrixClient = matrixClient,
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(FeatureFlags.MessageSearch.key to true),
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
// Await the initial state, with the feature enabled
|
||||
val initialState = consumeItemsUntilPredicate { it.isEnabled }.last()
|
||||
assertThat(isRunning).isFalse()
|
||||
|
||||
// Opening the search starts the backfill
|
||||
initialState.eventSink(GlobalSearchEvent.ToggleSearchVisibility)
|
||||
consumeItemsUntilPredicate { it.isSearchActive }
|
||||
// The backfill polling loop never ends while the task is running, so don't wait until the scheduler is idle
|
||||
testScheduler.runCurrent()
|
||||
assertThat(isRunning).isTrue()
|
||||
closeLambda.assertions().isNeverCalled()
|
||||
|
||||
// Closing the presenter closes the task
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
closeLambda.assertions().isCalledOnce()
|
||||
assertThat(isRunning).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createGlobalSearchPresenter(
|
||||
@@ -442,7 +489,8 @@ private fun TestScope.createGlobalSearchPresenter(
|
||||
dateFormatter: DateFormatter = FakeDateFormatter(),
|
||||
fileSizeFormatter: FileSizeFormatter = FakeFileSizeFormatter(),
|
||||
permalinkParser: PermalinkParser = FakePermalinkParser(),
|
||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||
matrixClient: MatrixClient =
|
||||
FakeMatrixClient(searchBackfillService = FakeSearchBackfillService(startSearchBackfillLambda = { Result.success(FakeSdkPendingTask()) })),
|
||||
searchHistoryStore: SearchHistoryStore = FakeSearchHistoryStore(),
|
||||
): GlobalSearchPresenter {
|
||||
return GlobalSearchPresenter(
|
||||
|
||||
@@ -18,7 +18,4 @@ android {
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.services.analytics.api)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ dependencies {
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user