Merge remote-tracking branch 'origin/develop' into HEAD

This commit is contained in:
bxdxnn
2026-08-27 18:40:10 +00:00
596 changed files with 5364 additions and 2447 deletions
-38
View File
@@ -63,44 +63,6 @@ jobs:
path: |
app/build/outputs/bundle/gplayRelease/app-gplay-release.aab
enterprise:
name: Create App Bundle Enterprise
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
concurrency:
group: ${{ format('build-release-main-enterprise-{0}', github.sha) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
- name: Create Enterprise app bundle
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload bundle as artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: elementx-enterprise-app-gplay-bundle-unsigned
path: |
app/build/outputs/bundle/gplayRelease/app-gplay-release.aab
fdroid:
name: Create APKs (FDroid)
runs-on: ubuntu-latest
+2 -1
View File
@@ -50,9 +50,10 @@ PRs must meet these rules.
- Title = changelog entry — descriptive, no "Fixes #…".
- Leave description template for the developer. Redirect them to the [contributing etiquette](CONTRIBUTING.md#etiquette).
- Screenshots/videos for visual changes.
- 500 additions max — split large changes.
- 500 additions max into production code. Test code can be larger — split large changes.
- Commits need a title and description; no tiny or massive commits.
- No history rewrites.
- When creating a pull request and if changes contain new or update(s) of the Composable Previews, add the label "Record-Screenshots" to the PR so that the CI will record the screenshots.
---
+84
View File
@@ -1,3 +1,87 @@
Changes in Element X v26.08.4
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.4 -->
## What's Changed
### 🧱 Build
* Add releaseV2.sh, a release script automating the GitHub release by @bmarty in https://github.com/element-hq/element-x-android/pull/7579
* Update submodule ref. by @bmarty in https://github.com/element-hq/element-x-android/pull/7582
### Others
* Keep the formatting of a caption that is being edited by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7572
* Say when the server cannot be reached instead of showing nothing by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7567
* Keep saving files to Downloads once 32 files share the same name by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7344
* Fix removed messages hiding its thread on deletion by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7480
* Drop the sender's name from the room list preview of two-person rooms by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7562
* Document how the Element Call base URL developer option is used by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7546
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.3...v26.08.4
Changes in Element X v26.08.3
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.3 -->
## What's Changed
### ✨ Features
* Let users enter their full Matrix user ID on the account provider screen by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7500
* Enable user status feature and hide status text in the timeline by @bmarty in https://github.com/element-hq/element-x-android/pull/7517
### 🐛 Bugfixes
* Fix cold start with a mobile deep link by @bmarty in https://github.com/element-hq/element-x-android/pull/7488
* Make the account provider screen scrollable so its description is not clipped by the keyboard by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7521
### 🧱 Build
* Update Gradle Wrapper from 9.7.0 to 9.7.1 by @ElementBot in https://github.com/element-hq/element-x-android/pull/7485
* Fix flaky test in `DefaultVoiceMessageComposerPresenterTest` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7487
* Fix warning about deprecated Gradle `keepRules.file` API by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7498
* Update Enterprise Copyright in idea settings, so that new files get an expected header. by @bmarty in https://github.com/element-hq/element-x-android/pull/7501
* Remove workaround for Renovate. by @bmarty in https://github.com/element-hq/element-x-android/pull/7502
### 🚧 In development 🚧
* Make PIP react to camera orientation changes in the other participant's device by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6951
* Feature: global search by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7410
### Dependency upgrades
* Update dependency org.maplibre.gl:android-sdk to v13.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7486
* Update dependency androidx.webkit:webkit to v1.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7484
* Update dependency io.sentry:sentry-android to v8.53.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7481
* Update dependency io.element.android:element-call-embedded to v0.24.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7474
* Update roborazzi to v1.72.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7489
* Update metro to v1.4.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7493
* Update dependency com.squareup.okhttp3:okhttp-bom to v5.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7512
* Update dependency com.google.guava:guava to v33.7.0-android by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7516
* Update sdk to version 26.08.25 by @bmarty in https://github.com/element-hq/element-x-android/pull/7550
### 💼 Element Pro
* Add `ClientEnterpriseHook` and `ClientBuilderEnterpriseHook` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7495
* Remove dependency on enterprise module. by @bmarty in https://github.com/element-hq/element-x-android/pull/7505
### Others
* Show the right image when opening one that has not finished sending by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7359
* Move strings to the login module by @bmarty in https://github.com/element-hq/element-x-android/pull/7494
* Allow empty problem description if the GitHub issue number is present for rageshakes by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7497
* Apply the system theme when switching back to it from a forced light or dark theme by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7453
* Show dialog with reason when user clicks on a message with a failure by @bmarty in https://github.com/element-hq/element-x-android/pull/7490
* Keep a focused timeline focused until the event has been rendered by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7463
* Do not render in-chat key verification requests in the timeline by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7443
* Share a file whose mime type is not one of the supported ones by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7435
* Show reactions in the pinned messages list by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6676
* Send a concrete mime type for media shared with a wildcard one by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7449
* Rename module and ensure that naming convention is followed by @bmarty in https://github.com/element-hq/element-x-android/pull/7514
* Fix lint issue, ensure that PiP methods are not called on old devices not supporting them. by @bmarty in https://github.com/element-hq/element-x-android/pull/7515
* Rename PreviewParameterProvider subclasses to the PreviewParam suffix by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7468
* Update Ref. by @bmarty in https://github.com/element-hq/element-x-android/pull/7518
* Remove the reference to the decommissioned OIDC playground by @sandhose in https://github.com/element-hq/element-x-android/pull/7519
* Hide the encryption badge when key storage is set up on the server by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7447
* Rename remaining events sealed interface by @bmarty in https://github.com/element-hq/element-x-android/pull/7533
* Stop waiting on a verification request the other session never accepts by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7457
* Point the proxy debugging doc at the method that builds the clients by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7543
* Show the build date in the screenshot gallery footer by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7544
* Run the database migrations again when a database is upgraded by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7570
* Update fonts on the headings by @bmarty in https://github.com/element-hq/element-x-android/pull/7520
* Add a "Push rules" entry in the developer options by @bmarty in https://github.com/element-hq/element-x-android/pull/7577
## New Contributors
* @sandhose made their first contribution in https://github.com/element-hq/element-x-android/pull/7519
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.2...v26.08.3
Changes in Element X v26.08.2
=============================
@@ -8,11 +8,11 @@
package io.element.android.appnav.loggedin
sealed interface LoggedInEvents {
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents
data object CheckSlidingSyncProxyAvailability : LoggedInEvents
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents
data object DismissLocalNetworkPermissionPrompt : LoggedInEvents
sealed interface LoggedInEvent {
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvent
data object CheckSlidingSyncProxyAvailability : LoggedInEvent
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvent
data object DismissLocalNetworkPermissionPrompt : LoggedInEvent
data object RequestLocationNetworkPermission : LoggedInEvents
data object RequestLocationNetworkPermission : LoggedInEvent
}
@@ -105,9 +105,18 @@ class LoggedInPresenter(
}
val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState()
val isOnline by syncService.isOnline.collectAsState()
val showSyncSpinner by remember {
val hasNetwork by networkMonitor.connectivity.collectAsState()
val syncIndicatorState by remember {
derivedStateOf {
isOnline && syncIndicator == RoomListService.SyncIndicator.Show
when {
isOnline -> if (syncIndicator == RoomListService.SyncIndicator.Show) {
SyncIndicatorState.Syncing
} else {
SyncIndicatorState.Hidden
}
hasNetwork == NetworkStatus.Connected -> SyncIndicatorState.ServerUnreachable
else -> SyncIndicatorState.Hidden
}
}
}
var forceNativeSlidingSyncMigration by remember { mutableStateOf(false) }
@@ -149,9 +158,9 @@ class LoggedInPresenter(
else -> LocalNetworkPermissionDialog.Settings
}
fun handleEvent(event: LoggedInEvents) {
fun handleEvent(event: LoggedInEvent) {
when (event) {
is LoggedInEvents.CloseErrorDialog -> {
is LoggedInEvent.CloseErrorDialog -> {
pusherRegistrationState.value = AsyncData.Uninitialized
if (event.doNotShowAgain) {
coroutineScope.launch {
@@ -159,17 +168,17 @@ class LoggedInPresenter(
}
}
}
LoggedInEvents.CheckSlidingSyncProxyAvailability -> coroutineScope.launch {
LoggedInEvent.CheckSlidingSyncProxyAvailability -> coroutineScope.launch {
forceNativeSlidingSyncMigration = matrixClient.needsForcedNativeSlidingSyncMigration().getOrDefault(false)
}
LoggedInEvents.LogoutAndMigrateToNativeSlidingSync -> coroutineScope.launch {
LoggedInEvent.LogoutAndMigrateToNativeSlidingSync -> coroutineScope.launch {
// Force the logout since Native Sliding Sync is already enforced by the SDK
matrixClient.logout(userInitiated = true, ignoreSdkError = true)
}
LoggedInEvents.DismissLocalNetworkPermissionPrompt -> {
LoggedInEvent.DismissLocalNetworkPermissionPrompt -> {
localNetworkPromptDismissedThisSession = true
}
LoggedInEvents.RequestLocationNetworkPermission -> {
LoggedInEvent.RequestLocationNetworkPermission -> {
if (localNetworkPermissionDialog == LocalNetworkPermissionDialog.Settings) {
localNetworkPermissionState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog)
} else {
@@ -181,7 +190,7 @@ class LoggedInPresenter(
}
return LoggedInState(
showSyncSpinner = showSyncSpinner,
syncIndicatorState = syncIndicatorState,
pusherRegistrationState = pusherRegistrationState.value,
ignoreRegistrationError = ignoreRegistrationError,
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
@@ -11,12 +11,18 @@ package io.element.android.appnav.loggedin
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
enum class SyncIndicatorState {
Hidden,
Syncing,
ServerUnreachable,
}
data class LoggedInState(
val showSyncSpinner: Boolean,
val syncIndicatorState: SyncIndicatorState,
val pusherRegistrationState: AsyncData<Unit>,
val ignoreRegistrationError: Boolean,
val forceNativeSlidingSyncMigration: Boolean,
val appName: String,
val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
val eventSink: (LoggedInEvents) -> Unit,
val eventSink: (LoggedInEvent) -> Unit,
)
@@ -17,7 +17,8 @@ open class LoggedInStatePreviewParam : PreviewParameterProvider<LoggedInState> {
override val values: Sequence<LoggedInState>
get() = sequenceOf(
aLoggedInState(),
aLoggedInState(showSyncSpinner = true),
aLoggedInState(syncIndicatorState = SyncIndicatorState.Syncing),
aLoggedInState(syncIndicatorState = SyncIndicatorState.ServerUnreachable),
aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())),
aLoggedInState(forceNativeSlidingSyncMigration = true),
aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Rationale),
@@ -26,13 +27,13 @@ open class LoggedInStatePreviewParam : PreviewParameterProvider<LoggedInState> {
}
fun aLoggedInState(
showSyncSpinner: Boolean = false,
syncIndicatorState: SyncIndicatorState = SyncIndicatorState.Hidden,
pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized,
forceNativeSlidingSyncMigration: Boolean = false,
appName: String = "Element X",
localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None,
) = LoggedInState(
showSyncSpinner = showSyncSpinner,
syncIndicatorState = syncIndicatorState,
pusherRegistrationState = pusherRegistrationState,
ignoreRegistrationError = false,
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
@@ -37,7 +37,7 @@ fun LoggedInView(
) {
OnLifecycleEvent { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
state.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
state.eventSink(LoggedInEvent.CheckSlidingSyncProxyAvailability)
}
}
Box(
@@ -47,7 +47,7 @@ fun LoggedInView(
) {
SyncStateView(
modifier = Modifier.align(Alignment.TopCenter),
isVisible = state.showSyncSpinner,
state = state.syncIndicatorState,
)
}
when (state.pusherRegistrationState) {
@@ -63,10 +63,10 @@ fun LoggedInView(
content = stringResource(id = CommonStrings.common_error_registering_pusher_android, reason),
cancelText = stringResource(id = CommonStrings.common_settings),
onDismiss = {
state.eventSink(LoggedInEvents.CloseErrorDialog(it))
state.eventSink(LoggedInEvent.CloseErrorDialog(it))
},
onCancel = {
state.eventSink(LoggedInEvents.CloseErrorDialog(false))
state.eventSink(LoggedInEvent.CloseErrorDialog(false))
navigateToNotificationTroubleshoot()
}
)
@@ -79,15 +79,15 @@ fun LoggedInView(
ForceNativeSlidingSyncMigrationDialog(
appName = state.appName,
onSubmit = {
state.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync)
state.eventSink(LoggedInEvent.LogoutAndMigrateToNativeSlidingSync)
}
)
}
LocalNetworkPermissionDialogView(
dialog = state.localNetworkPermissionDialog,
onSubmit = { state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) },
onDismiss = { state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) },
onSubmit = { state.eventSink(LoggedInEvent.RequestLocationNetworkPermission) },
onDismiss = { state.eventSink(LoggedInEvent.DismissLocalNetworkPermissionPrompt) },
)
}
@@ -25,18 +25,23 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun SyncStateView(
isVisible: Boolean,
state: SyncIndicatorState,
modifier: Modifier = Modifier
) {
AnimatedVisibility(
visible = isVisible,
visible = state != SyncIndicatorState.Hidden,
modifier = modifier,
enter = fadeIn(spring(stiffness = 500F)),
exit = fadeOut(spring(stiffness = 500F)),
) {
AsyncIndicator.Loading(
text = stringResource(id = CommonStrings.common_syncing),
)
when (state) {
SyncIndicatorState.ServerUnreachable -> AsyncIndicator.Failure(
text = stringResource(id = CommonStrings.common_server_unreachable),
)
else -> AsyncIndicator.Loading(
text = stringResource(id = CommonStrings.common_syncing),
)
}
}
}
@@ -46,7 +51,18 @@ internal fun SyncStateViewPreview() = ElementPreview {
// Add a box to see the shadow
Box(modifier = Modifier.padding(24.dp)) {
SyncStateView(
isVisible = true
state = SyncIndicatorState.Syncing
)
}
}
@PreviewsDayNight
@Composable
internal fun SyncStateViewServerUnreachablePreview() = ElementPreview {
// Add a box to see the shadow
Box(modifier = Modifier.padding(24.dp)) {
SyncStateView(
state = SyncIndicatorState.ServerUnreachable
)
}
}
@@ -67,7 +67,7 @@ class LoggedInPresenterTest {
fun `present - initial state`() = runTest {
createLoggedInPresenter().test {
val initialState = awaitItem()
assertThat(initialState.showSyncSpinner).isFalse()
assertThat(initialState.syncIndicatorState).isEqualTo(SyncIndicatorState.Hidden)
assertThat(initialState.pusherRegistrationState.isUninitialized()).isTrue()
assertThat(initialState.ignoreRegistrationError).isFalse()
}
@@ -97,11 +97,33 @@ class LoggedInPresenterTest {
matrixClient = FakeMatrixClient(roomListService = roomListService),
).test {
val initialState = awaitItem()
assertThat(initialState.showSyncSpinner).isFalse()
assertThat(initialState.syncIndicatorState).isEqualTo(SyncIndicatorState.Hidden)
roomListService.postSyncIndicator(RoomListService.SyncIndicator.Show)
consumeItemsUntilPredicate { it.showSyncSpinner }
consumeItemsUntilPredicate { it.syncIndicatorState == SyncIndicatorState.Syncing }
roomListService.postSyncIndicator(RoomListService.SyncIndicator.Hide)
consumeItemsUntilPredicate { !it.showSyncSpinner }
consumeItemsUntilPredicate { it.syncIndicatorState != SyncIndicatorState.Syncing }
}
}
@Test
fun `present - say the server is unreachable when the device has network but the sync is offline`() = runTest {
createLoggedInPresenter(
syncState = SyncState.Offline,
networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected),
).test {
consumeItemsUntilPredicate { it.syncIndicatorState == SyncIndicatorState.ServerUnreachable }
cancelAndConsumeRemainingEvents()
}
}
@Test
fun `present - say nothing when the device itself has no network`() = runTest {
createLoggedInPresenter(
syncState = SyncState.Offline,
networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected),
).test {
consumeItemsUntilPredicate { it.syncIndicatorState == SyncIndicatorState.Hidden }
cancelAndConsumeRemainingEvents()
}
}
@@ -210,7 +232,7 @@ class LoggedInPresenterTest {
lambda.assertions()
.isCalledOnce()
// Reset the error and do not show again
finalState.eventSink(LoggedInEvents.CloseErrorDialog(doNotShowAgain = false))
finalState.eventSink(LoggedInEvent.CloseErrorDialog(doNotShowAgain = false))
val lastState = awaitItem()
assertThat(lastState.pusherRegistrationState.isUninitialized()).isTrue()
assertThat(lastState.ignoreRegistrationError).isFalse()
@@ -240,7 +262,7 @@ class LoggedInPresenterTest {
lambda.assertions()
.isCalledOnce()
// Reset the error and do not show again
finalState.eventSink(LoggedInEvents.CloseErrorDialog(doNotShowAgain = true))
finalState.eventSink(LoggedInEvent.CloseErrorDialog(doNotShowAgain = true))
skipItems(1)
setIgnoreRegistrationErrorLambda.assertions()
.isCalledOnce()
@@ -296,7 +318,7 @@ class LoggedInPresenterTest {
).test {
val initialState = awaitItem()
assertThat(initialState.forceNativeSlidingSyncMigration).isFalse()
initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
initialState.eventSink(LoggedInEvent.CheckSlidingSyncProxyAvailability)
assertThat(awaitItem().forceNativeSlidingSyncMigration).isTrue()
}
}
@@ -318,7 +340,7 @@ class LoggedInPresenterTest {
).test {
val initialState = awaitItem()
initialState.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync)
initialState.eventSink(LoggedInEvent.LogoutAndMigrateToNativeSlidingSync)
advanceUntilIdle()
+1 -1
View File
@@ -322,7 +322,7 @@ Example for a new feature called RoomDetails:
4. Verify that the structure looks ok and click on Finish
5. The modules api/impl should be created under `features/roomdetails` directory.
6. Sync project with Gradle so the modules are recognized (no need to add them to settings.gradle).
7. You can now add more Presentation classes (Events, State, StatePreviewParam, View, Presenter) in the impl module with the `Template Presentation Classes`.
7. You can now add more Presentation classes (Event, State, StatePreviewParam, View, Presenter) in the impl module with the `Template Presentation Classes`.
To use it, just right click on the package where you want to generate classes, and click on `New` / `Template Presentation Classes`.
Fill the text field with the base name of the classes: the generated files are `<BaseName>Event.kt`, `<BaseName>State.kt`,
`<BaseName>StatePreviewParam.kt`, `<BaseName>Presenter.kt`, `<BaseName>Node.kt` and `<BaseName>View.kt`. For instance
+6 -1
View File
@@ -9,13 +9,18 @@
1) Set `127.0.0.1` as "Host name" and `8080` as "Port number".
1) Click "Apply" and verify that "Proxy status" is "Success" and close the settings window.
<img width="932" alt="Screenshot 2023-10-04 at 14 48 47" src="https://github.com/element-hq/element-x-android/assets/1273124/bf99a053-20b0-42a4-91d3-9602f709f684">
1) Alternatively, set the proxy on the device itself, which the app honours through `DefaultProxyProvider`, and which also works on a physical device:
```
adb shell settings put global http_proxy 10.0.2.2:8080
```
Use `adb shell settings delete global http_proxy` to remove it afterwards. On a physical device, replace `10.0.2.2` with the address of the machine running mitmproxy.
1) Install the mitmproxy CA cert (this is needed to see traffic from java/kotlin code, it's not needed for traffic coming from native code e.g. the matrix-rust-sdk).
1) Open the emulator Chrome browser app
1) Go to the url `mitm.it`
1) Follow the instructions to install the CA cert on Android devices.
<img width="606" alt="Screenshot 2023-10-04 at 14 51 27" src="https://github.com/element-hq/element-x-android/assets/1273124/5f2b6f27-6958-4ea7-97fe-c7f06d105da5">
1) Slightly modify the Element X app source code.
1) Go to the `RustMatrixClientFactory.create()` method.
1) Go to the `RustMatrixClientFactory.getBaseClientBuilder()` method, which builds every client the app creates, whether it is logging in or restoring a session.
1) Add `.disableSslVerification()` in the `ClientBuilder` method chain.
1) Build and run the Element X app.
1) Enjoy, you will see all the traffic in mitmproxy's web interface.
+53
View File
@@ -0,0 +1,53 @@
# Element Call
<!--- TOC -->
* [Overview](#overview)
* [Setting a custom base URL](#setting-a-custom-base-url)
* [Why the application does not add the room path itself](#why-the-application-does-not-add-the-room-path-itself)
* [Where the value is read](#where-the-value-is-read)
<!--- END -->
## Overview
Element X embeds a copy of [Element Call](https://github.com/element-hq/element-call) in the
application assets and loads it from
`https://appassets.androidplatform.net/element-call/index.html`. Calls therefore work out of the box
with no extra deployment, and the embedded copy is the one every user gets.
Developers can point the application at another Element Call deployment instead — a local
development server, a staging deployment, or a self-hosted one — with the **Element Call base URL**
developer option.
## Setting a custom base URL
The option lives in *Settings* → *Developer options* → *Element Call*, under "Element Call base
URL". On a release build the *Developer options* entry is hidden until the version number at the
bottom of *Settings* is tapped seven times. Leaving the field empty restores the embedded copy.
The value is used verbatim as the widget base URL, so it must be the URL of the page that joins a
room, not the URL of the deployment root. For the public deployments that means:
| Deployment | Value to enter |
|---|---|
| `call.element.io` | `https://call.element.io/room` |
| `call.element.dev` | `https://call.element.dev/room` |
Entering `https://call.element.io` without the `/room` suffix loads the Element Call lobby rather
than the room, so the call is never joined.
## Why the application does not add the room path itself
The path that joins a room is a property of the deployment, not of Element Call. Unpacking the
Element Call release tarball behind a reverse proxy can put it at any path — `https://call.foo.tld`,
`https://call.foo.tld/some/path/somewhere` — and a proxy that already maps a subdomain onto
`https://1.2.3.4/room` would end up with the suffix twice. The application passes the value through
unchanged so that every one of those deployments can be reached.
## Where the value is read
`AppDeveloperSettingsView` renders the field and sends
`AppDeveloperSettingsEvent.SetCustomElementCallBaseUrl`, which `DefaultAppPreferencesStore` persists.
`DefaultCallWidgetProvider.getWidget` reads it back and falls back to the embedded copy when it is
unset.
+3 -3
View File
@@ -64,16 +64,16 @@ brew install bundletool
### Steps
1. Open the GitHub release that you want to install from https://github.com/element-hq/element-x-android/releases
2. Download the asset `app-release-signed.aab`
2. Download the asset `app-gplay-release-signed.aab`
3. Navigate to the folder where you cloned the project and run the following command:
```bash
bundletool build-apks --bundle=<PATH_TO_YOUR_APP-RELEASE-SIGNED.AAB_FILE> --output=./tmp/elementx.apks \
bundletool build-apks --bundle=<PATH_TO_YOUR_APP-GPLAY-RELEASE-SIGNED.AAB_FILE> --output=./tmp/elementx.apks \
--ks=./app/signature/debug.keystore --ks-pass=pass:android --ks-key-alias=androiddebugkey --key-pass=pass:android \
--overwrite
```
For instance:
```bash
bundletool build-apks --bundle=./tmp/Element/0.1.5/app-release-signed.aab --output=./tmp/elementx.apks \
bundletool build-apks --bundle=./tmp/Element/26.08.3/app-gplay-release-signed.aab --output=./tmp/elementx.apks \
--ks=./app/signature/debug.keystore --ks-pass=pass:android --ks-key-alias=androiddebugkey --key-pass=pass:android \
--overwrite
```
@@ -0,0 +1,2 @@
Main changes in this version: bug fixes and improvements.
Full changelog: https://github.com/element-hq/element-x-android/releases
@@ -0,0 +1,2 @@
Main changes in this version: bug fixes and improvements.
Full changelog: https://github.com/element-hq/element-x-android/releases
@@ -8,6 +8,6 @@
package io.element.android.features.analytics.api
sealed interface AnalyticsOptInEvents {
data class EnableAnalytics(val isEnabled: Boolean) : AnalyticsOptInEvents
sealed interface AnalyticsOptInEvent {
data class EnableAnalytics(val isEnabled: Boolean) : AnalyticsOptInEvent
}
@@ -8,11 +8,11 @@
package io.element.android.features.analytics.api.preferences
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
data class AnalyticsPreferencesState(
val applicationName: String,
val isEnabled: Boolean,
val policyUrl: String,
val eventSink: (AnalyticsOptInEvents) -> Unit,
val eventSink: (AnalyticsOptInEvent) -> Unit,
)
@@ -13,7 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
import io.element.android.features.analytics.api.R
import io.element.android.libraries.designsystem.components.LINK_TAG
import io.element.android.libraries.designsystem.components.list.ListItemContent
@@ -30,7 +30,7 @@ fun AnalyticsPreferencesView(
modifier: Modifier = Modifier,
) {
fun onEnabledChanged(isEnabled: Boolean) {
state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled))
state.eventSink(AnalyticsOptInEvent.EnableAnalytics(isEnabled = isEnabled))
}
val supportingText = stringResource(
@@ -12,7 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.appconfig.AnalyticsConfig
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.services.analytics.api.AnalyticsService
@@ -28,9 +28,9 @@ class AnalyticsOptInPresenter(
override fun present(): AnalyticsOptInState {
val localCoroutineScope = rememberCoroutineScope()
fun handleEvent(event: AnalyticsOptInEvents) {
fun handleEvent(event: AnalyticsOptInEvent) {
when (event) {
is AnalyticsOptInEvents.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
is AnalyticsOptInEvent.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
}
localCoroutineScope.launch {
analyticsService.setDidAskUserConsent()
@@ -8,10 +8,10 @@
package io.element.android.features.analytics.impl
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
data class AnalyticsOptInState(
val applicationName: String,
val hasPolicyLink: Boolean,
val eventSink: (AnalyticsOptInEvents) -> Unit
val eventSink: (AnalyticsOptInEvent) -> Unit
)
@@ -28,7 +28,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.appconfig.AnalyticsConfig
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
@@ -55,11 +55,11 @@ fun AnalyticsOptInView(
val eventSink = state.eventSink
fun onAcceptTerms() {
eventSink(AnalyticsOptInEvents.EnableAnalytics(true))
eventSink(AnalyticsOptInEvent.EnableAnalytics(true))
}
fun onDeclineTerms() {
eventSink(AnalyticsOptInEvents.EnableAnalytics(false))
eventSink(AnalyticsOptInEvent.EnableAnalytics(false))
}
BackHandler(onBack = ::onDeclineTerms)
@@ -13,7 +13,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.appconfig.AnalyticsConfig
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
@@ -31,9 +31,9 @@ class AnalyticsPreferencesPresenter(
val localCoroutineScope = rememberCoroutineScope()
val isEnabled = analyticsService.userConsentFlow.collectAsState(initial = false)
fun handleEvent(event: AnalyticsOptInEvents) {
fun handleEvent(event: AnalyticsOptInEvent) {
when (event) {
is AnalyticsOptInEvents.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
is AnalyticsOptInEvent.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
}
}
@@ -12,7 +12,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
@@ -37,7 +37,7 @@ class AnalyticsOptInPresenterTest {
}.test {
val initialState = awaitItem()
assertThat(analyticsService.didAskUserConsentFlow.first()).isFalse()
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(true))
initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(true))
assertThat(analyticsService.didAskUserConsentFlow.first()).isTrue()
assertThat(analyticsService.userConsentFlow.first()).isTrue()
}
@@ -55,7 +55,7 @@ class AnalyticsOptInPresenterTest {
}.test {
val initialState = awaitItem()
assertThat(analyticsService.didAskUserConsentFlow.first()).isFalse()
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(false))
initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(false))
assertThat(analyticsService.didAskUserConsentFlow.first()).isTrue()
assertThat(analyticsService.userConsentFlow.first()).isFalse()
}
@@ -13,7 +13,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.AnalyticsConfig
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
@@ -67,9 +67,9 @@ class AnalyticsPreferencesPresenterTest {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.isEnabled).isTrue()
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(false))
initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(false))
assertThat(awaitItem().isEnabled).isFalse()
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(true))
initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(true))
assertThat(awaitItem().isEnabled).isTrue()
}
}
@@ -21,7 +21,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.createroom.impl.R
import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.features.invitepeople.api.InvitePeopleState
import io.element.android.features.invitepeople.api.InvitePeopleStatePreviewParam
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
@@ -56,7 +56,7 @@ fun AddPeopleView(
Button(
text = stringResource(CommonStrings.action_finish),
onClick = {
state.eventSink(InvitePeopleEvents.SendInvites)
state.eventSink(InvitePeopleEvent.SendInvites)
},
enabled = state.canInvite,
modifier = Modifier
@@ -11,13 +11,13 @@ package io.element.android.features.createroom.impl.configureroom
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.ui.media.AvatarAction
sealed interface ConfigureRoomEvents {
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
data class TopicChanged(val topic: String) : ConfigureRoomEvents
data class JoinRuleChanged(val joinRuleItem: JoinRuleItem) : ConfigureRoomEvents
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
data object CreateRoom : ConfigureRoomEvents
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
data class SetParentSpace(val space: SpaceRoom?) : ConfigureRoomEvents
data object CancelCreateRoom : ConfigureRoomEvents
sealed interface ConfigureRoomEvent {
data class RoomNameChanged(val name: String) : ConfigureRoomEvent
data class TopicChanged(val topic: String) : ConfigureRoomEvent
data class JoinRuleChanged(val joinRuleItem: JoinRuleItem) : ConfigureRoomEvent
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvent
data object CreateRoom : ConfigureRoomEvent
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvent
data class SetParentSpace(val space: SpaceRoom?) : ConfigureRoomEvent
data object CancelCreateRoom : ConfigureRoomEvent
}
@@ -188,14 +188,14 @@ class ConfigureRoomPresenter(
localCoroutineScope.createRoom(config, createRoomAction)
}
fun handleEvent(event: ConfigureRoomEvents) {
fun handleEvent(event: ConfigureRoomEvent) {
when (event) {
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
is ConfigureRoomEvents.JoinRuleChanged -> dataStore.setJoinRule(event.joinRuleItem)
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)
is ConfigureRoomEvents.HandleAvatarAction -> {
is ConfigureRoomEvent.RoomNameChanged -> dataStore.setRoomName(event.name)
is ConfigureRoomEvent.TopicChanged -> dataStore.setTopic(event.topic)
is ConfigureRoomEvent.JoinRuleChanged -> dataStore.setJoinRule(event.joinRuleItem)
is ConfigureRoomEvent.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
is ConfigureRoomEvent.CreateRoom -> createRoom(createRoomConfig)
is ConfigureRoomEvent.HandleAvatarAction -> {
when (event.action) {
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) {
@@ -207,10 +207,10 @@ class ConfigureRoomPresenter(
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
}
}
is ConfigureRoomEvents.SetParentSpace -> {
is ConfigureRoomEvent.SetParentSpace -> {
dataStore.setParentSpace(event.space, false)
}
ConfigureRoomEvents.CancelCreateRoom -> {
ConfigureRoomEvent.CancelCreateRoom -> {
createRoomAction.value = AsyncAction.Uninitialized
}
}
@@ -26,7 +26,7 @@ data class ConfigureRoomState(
val homeserverName: String,
val availableJoinRules: ImmutableList<JoinRuleItem>,
val spaces: ImmutableList<SpaceRoom>,
val eventSink: (ConfigureRoomEvents) -> Unit
val eventSink: (ConfigureRoomEvent) -> Unit
) {
val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
(config.visibilityState is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid) &&
@@ -143,7 +143,7 @@ fun aConfigureRoomState(
)
},
spaces: List<SpaceRoom> = emptyList(),
eventSink: (ConfigureRoomEvents) -> Unit = { },
eventSink: (ConfigureRoomEvent) -> Unit = { },
) = ConfigureRoomState(
config = config,
isSpace = isSpace,
@@ -96,7 +96,7 @@ fun ConfigureRoomView(
onBackClick = onBackClick,
onNextClick = {
focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.CreateRoom)
state.eventSink(ConfigureRoomEvent.CreateRoom)
},
)
}
@@ -114,20 +114,20 @@ fun ConfigureRoomView(
avatarUri = state.config.avatarUri,
roomName = state.config.roomName.orEmpty(),
onAvatarClick = ::onAvatarClick,
onChangeRoomName = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
onChangeRoomName = { state.eventSink(ConfigureRoomEvent.RoomNameChanged(it)) },
)
Spacer(modifier = Modifier.height(16.dp))
RoomTopic(
modifier = Modifier.padding(horizontal = 16.dp),
topic = state.config.topic.orEmpty(),
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
onTopicChange = { state.eventSink(ConfigureRoomEvent.TopicChanged(it)) },
)
Spacer(modifier = Modifier.height(16.dp))
if (!state.isSpace && state.spaces.isNotEmpty()) {
SelectParentSpaceOptions(
spaces = state.spaces,
selectedSpace = state.config.parentSpace,
onSelectSpace = { state.eventSink(ConfigureRoomEvents.SetParentSpace(it)) },
onSelectSpace = { state.eventSink(ConfigureRoomEvent.SetParentSpace(it)) },
)
}
RoomJoinRuleOptions(
@@ -136,7 +136,7 @@ fun ConfigureRoomView(
parentSpace = state.config.parentSpace,
onOptionClick = {
focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.JoinRuleChanged(it))
state.eventSink(ConfigureRoomEvent.JoinRuleChanged(it))
},
)
if (state.config.visibilityState !is RoomVisibilityState.Private) {
@@ -146,7 +146,7 @@ fun ConfigureRoomView(
address = state.config.visibilityState.roomAddress().getOrNull().orEmpty(),
homeserverName = state.homeserverName,
addressValidity = state.roomAddressValidity,
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
onAddressChange = { state.eventSink(ConfigureRoomEvent.RoomAddressChanged(it)) },
label = null,
supportingText = stringResource(R.string.screen_create_room_room_address_section_footer),
)
@@ -158,7 +158,7 @@ fun ConfigureRoomView(
actions = state.avatarActions,
isVisible = isAvatarActionsSheetVisible.value,
onDismiss = { isAvatarActionsSheetVisible.value = false },
onSelectAction = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) }
onSelectAction = { state.eventSink(ConfigureRoomEvent.HandleAvatarAction(it)) }
)
AsyncActionView(
@@ -170,8 +170,8 @@ fun ConfigureRoomView(
},
onSuccess = { onCreateRoomSuccess(it) },
errorMessage = { stringResource(if (isSpace) R.string.screen_create_room_error_creating_space else R.string.screen_create_room_error_creating_room) },
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom) },
onErrorDismiss = { state.eventSink(ConfigureRoomEvents.CancelCreateRoom) },
onRetry = { state.eventSink(ConfigureRoomEvent.CreateRoom) },
onErrorDismiss = { state.eventSink(ConfigureRoomEvent.CancelCreateRoom) },
)
PermissionsView(
@@ -12,7 +12,7 @@ import androidx.core.net.toUri
import app.cash.turbine.TurbineTestContext
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomEvents
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomEvent
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomPresenter
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomState
import io.element.android.features.createroom.impl.configureroom.CreateRoomConfig
@@ -106,14 +106,14 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
assertThat(initialState.isValid).isFalse()
// Room name not empty
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
initialState.eventSink(ConfigureRoomEvent.RoomNameChanged(A_ROOM_NAME))
var newState: ConfigureRoomState = awaitItem()
config = config.copy(roomName = A_ROOM_NAME)
assertThat(newState.config).isEqualTo(config)
assertThat(newState.isValid).isTrue()
// Clear room name
newState.eventSink(ConfigureRoomEvents.RoomNameChanged(""))
newState.eventSink(ConfigureRoomEvent.RoomNameChanged(""))
newState = awaitItem()
config = config.copy(roomName = null)
assertThat(newState.config).isEqualTo(config)
@@ -136,13 +136,13 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
var expectedConfig = CreateRoomConfig()
assertThat(initialState.config).isEqualTo(expectedConfig)
// Room name
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
initialState.eventSink(ConfigureRoomEvent.RoomNameChanged(A_ROOM_NAME))
var newState = awaitItem()
expectedConfig = expectedConfig.copy(roomName = A_ROOM_NAME)
assertThat(newState.config).isEqualTo(expectedConfig)
// Room topic
newState.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE))
newState.eventSink(ConfigureRoomEvent.TopicChanged(A_MESSAGE))
newState = awaitItem()
expectedConfig = expectedConfig.copy(topic = A_MESSAGE)
assertThat(newState.config).isEqualTo(expectedConfig)
@@ -153,7 +153,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
// From gallery
val uriFromGallery = AN_URI_FROM_GALLERY
pickerProvider.givenResult(uriFromGallery.toUri())
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery)
assertThat(newState.config).isEqualTo(expectedConfig)
@@ -161,7 +161,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
val uriFromCamera = AN_URI_FROM_CAMERA
pickerProvider.givenResult(uriFromCamera.toUri())
assertThat(newState.cameraPermissionState.permissionGranted).isFalse()
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.TakePhoto))
newState = awaitItem()
assertThat(newState.cameraPermissionState.showDialog).isTrue()
permissionsPresenter.setPermissionGranted()
@@ -173,18 +173,18 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
// Do it again, no permission is requested
val uriFromCamera2 = AN_URI_FROM_CAMERA_2
pickerProvider.givenResult(uriFromCamera2.toUri())
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.TakePhoto))
newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2)
assertThat(newState.config).isEqualTo(expectedConfig)
// Remove
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.Remove))
newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.Remove))
newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = null)
assertThat(newState.config).isEqualTo(expectedConfig)
// Room privacy
newState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
newState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
newState = awaitItem()
expectedConfig = expectedConfig.copy(
visibilityState = RoomVisibilityState.Public(
@@ -208,7 +208,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
matrixClient.givenCreateRoomResult(createRoomResult)
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
@@ -239,13 +239,13 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
// Use a public parent space so AskToJoin is a valid option
val parentSpace = aSpaceRoom(joinRule = JoinRule.Public)
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
initialState.eventSink(ConfigureRoomEvent.SetParentSpace(parentSpace))
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
assertThat(awaitItem().config.visibilityState.joinRuleItem).isEqualTo(JoinRuleItem.PublicVisibility.AskToJoin)
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem()
@@ -282,13 +282,13 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
// Use a public parent space so AskToJoin is a valid option
val parentSpace = aSpaceRoom(joinRule = JoinRule.Public)
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
initialState.eventSink(ConfigureRoomEvent.SetParentSpace(parentSpace))
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
assertThat(awaitItem().config.visibilityState.joinRuleItem).isEqualTo(JoinRuleItem.PublicVisibility.AskToJoin)
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
// We immediately receive the room power levels info needed for adding the child to a space
val powerLevels = RoomPowerLevels(
@@ -339,7 +339,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
matrixClient.givenCreateRoomResult(createRoomResult)
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
skipItems(2)
val analyticsEvent = analyticsService.capturedEvents.filterIsInstance<CreatedRoom>().firstOrNull()
@@ -369,14 +369,14 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(file, mockk(), mockk())))
matrixClient.givenUploadMediaResult(Result.failure(AN_EXCEPTION))
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat(analyticsService.capturedEvents.filterIsInstance<CreatedRoom>()).isEmpty()
matrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL))
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom)
stateAfterCreateRoom.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
@@ -399,14 +399,14 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
fakeMatrixClient.givenCreateRoomResult(createRoomResult)
// Create
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
// Retry
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom)
stateAfterCreateRoom.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterRetry = awaitItem()
@@ -414,7 +414,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
assertThat((stateAfterRetry.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
// Cancel
stateAfterRetry.eventSink(ConfigureRoomEvents.CancelCreateRoom)
stateAfterRetry.eventSink(ConfigureRoomEvent.CancelCreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
}
@@ -430,9 +430,9 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
skipItems(1)
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address"))
initialState.eventSink(ConfigureRoomEvent.RoomAddressChanged("invalid address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
@@ -450,9 +450,9 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
skipItems(1)
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
initialState.eventSink(ConfigureRoomEvent.RoomAddressChanged("address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
@@ -470,9 +470,9 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
skipItems(1)
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
initialState.eventSink(ConfigureRoomEvent.RoomAddressChanged("address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
@@ -488,22 +488,22 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
val initialState = initialState()
// First change the join rule to public
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
assertThat(awaitItem().config.visibilityState).isInstanceOf(RoomVisibilityState.Public::class.java)
// Then check changing the parent space resets it to private
// (via LaunchedEffect fallback since Public is not in availableJoinRules for non-public parent)
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(aSpaceRoom()))
initialState.eventSink(ConfigureRoomEvent.SetParentSpace(aSpaceRoom()))
skipItems(1) // Skip intermediate state
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
// If we change the join rule back to public
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
skipItems(1) // Skip intermediate state (Public is still invalid)
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
// Then remove the parent space, the join rule stays private
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(null))
initialState.eventSink(ConfigureRoomEvent.SetParentSpace(null))
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
}
}
@@ -514,7 +514,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(aSpaceRoom()))
initialState.eventSink(ConfigureRoomEvent.SetParentSpace(aSpaceRoom()))
assertThat(awaitError())
}
@@ -534,8 +534,8 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PrivateVisibility.Private))
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PrivateVisibility.Private))
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction.isLoading()).isTrue()
assertThat(awaitItem().createRoomAction.isSuccess()).isTrue()
@@ -8,9 +8,9 @@
package io.element.android.features.logout.impl
sealed interface AccountDeactivationEvents {
data class SetEraseData(val eraseData: Boolean) : AccountDeactivationEvents
data class SetPassword(val password: String) : AccountDeactivationEvents
data class DeactivateAccount(val isRetry: Boolean) : AccountDeactivationEvents
data object CloseDialogs : AccountDeactivationEvents
sealed interface AccountDeactivationEvent {
data class SetEraseData(val eraseData: Boolean) : AccountDeactivationEvent
data class SetPassword(val password: String) : AccountDeactivationEvent
data class DeactivateAccount(val isRetry: Boolean) : AccountDeactivationEvent
data object CloseDialogs : AccountDeactivationEvent
}
@@ -34,19 +34,19 @@ class AccountDeactivationPresenter(
val formState = remember { mutableStateOf(DeactivateFormState.Default) }
fun handleEvent(event: AccountDeactivationEvents) {
fun handleEvent(event: AccountDeactivationEvent) {
when (event) {
is AccountDeactivationEvents.SetEraseData -> {
is AccountDeactivationEvent.SetEraseData -> {
updateFormState(formState) {
copy(eraseData = event.eraseData)
}
}
is AccountDeactivationEvents.SetPassword -> {
is AccountDeactivationEvent.SetPassword -> {
updateFormState(formState) {
copy(password = event.password)
}
}
is AccountDeactivationEvents.DeactivateAccount ->
is AccountDeactivationEvent.DeactivateAccount ->
if (action.value.isConfirming() || event.isRetry) {
localCoroutineScope.deactivateAccount(
formState = formState.value,
@@ -55,7 +55,7 @@ class AccountDeactivationPresenter(
} else {
action.value = AsyncAction.ConfirmingNoParams
}
AccountDeactivationEvents.CloseDialogs -> {
AccountDeactivationEvent.CloseDialogs -> {
action.value = AsyncAction.Uninitialized
}
}
@@ -15,7 +15,7 @@ import kotlinx.parcelize.Parcelize
data class AccountDeactivationState(
val deactivateFormState: DeactivateFormState,
val accountDeactivationAction: AsyncAction<Unit>,
val eventSink: (AccountDeactivationEvents) -> Unit,
val eventSink: (AccountDeactivationEvent) -> Unit,
) {
val submitEnabled: Boolean
get() = accountDeactivationAction is AsyncAction.Uninitialized &&
@@ -45,7 +45,7 @@ internal fun aDeactivateFormState(
internal fun anAccountDeactivationState(
deactivateFormState: DeactivateFormState = aDeactivateFormState(),
accountDeactivationAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (AccountDeactivationEvents) -> Unit = {},
eventSink: (AccountDeactivationEvent) -> Unit = {},
) = AccountDeactivationState(
deactivateFormState = deactivateFormState,
accountDeactivationAction = accountDeactivationAction,
@@ -102,14 +102,14 @@ fun AccountDeactivationView(
Content(
state = state,
onSubmitClick = {
eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
}
)
Spacer(modifier = Modifier.height(32.dp))
Buttons(
state = state,
onSubmitClick = {
eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
}
)
}
@@ -117,13 +117,13 @@ fun AccountDeactivationView(
AccountDeactivationActionDialog(
state.accountDeactivationAction,
onConfirmClick = {
eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
},
onRetryClick = {
eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = true))
eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = true))
},
onDismissDialog = {
eventSink(AccountDeactivationEvents.CloseDialogs)
eventSink(AccountDeactivationEvent.CloseDialogs)
},
)
}
@@ -239,7 +239,7 @@ private fun Content(
headline = stringResource(R.string.screen_deactivate_account_delete_all_messages),
value = eraseData,
onChange = {
eventSink(AccountDeactivationEvents.SetEraseData(it))
eventSink(AccountDeactivationEvent.SetEraseData(it))
},
enabled = !isLoading,
)
@@ -275,7 +275,7 @@ private fun Content(
onValueChange = {
val sanitized = it.sanitize()
passwordFieldState = sanitized
eventSink(AccountDeactivationEvents.SetPassword(sanitized))
eventSink(AccountDeactivationEvent.SetPassword(sanitized))
},
placeholder = stringResource(CommonStrings.common_password),
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
@@ -47,11 +47,11 @@ class AccountDeactivationPresenterTest {
}.test {
val initialState = awaitItem()
assertThat(initialState.deactivateFormState).isEqualTo(DeactivateFormState.Default)
initialState.eventSink(AccountDeactivationEvents.SetEraseData(true))
initialState.eventSink(AccountDeactivationEvent.SetEraseData(true))
val updatedState = awaitItem()
assertThat(updatedState.deactivateFormState).isEqualTo(DeactivateFormState.Default.copy(eraseData = true))
assertThat(updatedState.submitEnabled).isFalse()
updatedState.eventSink(AccountDeactivationEvents.SetPassword("password"))
updatedState.eventSink(AccountDeactivationEvent.SetPassword("password"))
val updatedState2 = awaitItem()
assertThat(updatedState2.deactivateFormState).isEqualTo(DeactivateFormState(password = "password", eraseData = true))
assertThat(updatedState2.submitEnabled).isTrue()
@@ -71,12 +71,12 @@ class AccountDeactivationPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(AccountDeactivationEvents.SetPassword("password"))
initialState.eventSink(AccountDeactivationEvent.SetPassword("password"))
skipItems(1)
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
initialState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState = awaitItem()
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
updatedState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState2 = awaitItem()
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
val finalState = awaitItem()
@@ -98,20 +98,20 @@ class AccountDeactivationPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(AccountDeactivationEvents.SetPassword("password"))
initialState.eventSink(AccountDeactivationEvents.SetEraseData(true))
initialState.eventSink(AccountDeactivationEvent.SetPassword("password"))
initialState.eventSink(AccountDeactivationEvent.SetEraseData(true))
skipItems(2)
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
initialState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState = awaitItem()
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
updatedState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState2 = awaitItem()
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
val finalState = awaitItem()
assertThat(finalState.accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
recorder.assertions().isCalledOnce().with(value("password"), value(true))
// Retry
finalState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = true))
finalState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = true))
val finalState2 = awaitItem()
assertThat(finalState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
assertThat(awaitItem().accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
@@ -131,20 +131,20 @@ class AccountDeactivationPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(AccountDeactivationEvents.SetPassword("password"))
initialState.eventSink(AccountDeactivationEvents.SetEraseData(true))
initialState.eventSink(AccountDeactivationEvent.SetPassword("password"))
initialState.eventSink(AccountDeactivationEvent.SetEraseData(true))
skipItems(2)
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
initialState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState = awaitItem()
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
updatedState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState2 = awaitItem()
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
val finalState = awaitItem()
assertThat(finalState.accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
recorder.assertions().isCalledOnce().with(value("password"), value(true))
// Cancel
finalState.eventSink(AccountDeactivationEvents.CloseDialogs)
finalState.eventSink(AccountDeactivationEvent.CloseDialogs)
val finalState2 = awaitItem()
assertThat(finalState2.accountDeactivationAction).isEqualTo(AsyncAction.Uninitialized)
}
@@ -35,7 +35,7 @@ import org.robolectric.annotation.Config
class AccountDeactivationViewTest : RobolectricTest() {
@Test
fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>(expectEvents = false)
ensureCalledOnce {
setAccountDeactivationView(
state = anAccountDeactivationState(eventSink = eventsRecorder),
@@ -48,7 +48,7 @@ class AccountDeactivationViewTest : RobolectricTest() {
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on Deactivate emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
@@ -58,12 +58,12 @@ class AccountDeactivationViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_delete)
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false))
eventsRecorder.assertSingle(AccountDeactivationEvent.DeactivateAccount(false))
}
@Test
fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
@@ -74,12 +74,12 @@ class AccountDeactivationViewTest : RobolectricTest() {
),
)
pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false))
eventsRecorder.assertSingle(AccountDeactivationEvent.DeactivateAccount(false))
}
@Test
fun `clicking on retry on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
@@ -90,24 +90,24 @@ class AccountDeactivationViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_retry)
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(true))
eventsRecorder.assertSingle(AccountDeactivationEvent.DeactivateAccount(true))
}
@Test
fun `switching on the erase all switch emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
setAccountDeactivationView(
state = anAccountDeactivationState(
eventSink = eventsRecorder,
),
)
clickOn(R.string.screen_deactivate_account_delete_all_messages)
eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(true))
eventsRecorder.assertSingle(AccountDeactivationEvent.SetEraseData(true))
}
@Test
fun `switching off the erase all switch emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
@@ -117,13 +117,13 @@ class AccountDeactivationViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_deactivate_account_delete_all_messages)
eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(false))
eventsRecorder.assertSingle(AccountDeactivationEvent.SetEraseData(false))
}
@Config(qualifiers = "h1024dp")
@Test
fun `typing text in the password field emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
@@ -133,7 +133,7 @@ class AccountDeactivationViewTest : RobolectricTest() {
),
)
onNodeWithTag(TestTags.loginPassword.value).performTextInput("A")
eventsRecorder.assertSingle(AccountDeactivationEvents.SetPassword("A$A_PASSWORD"))
eventsRecorder.assertSingle(AccountDeactivationEvent.SetPassword("A$A_PASSWORD"))
}
}
@@ -8,6 +8,6 @@
package io.element.android.features.forward.impl
sealed interface ForwardMessagesEvents {
data object ClearError : ForwardMessagesEvents
sealed interface ForwardMessagesEvent {
data object ClearError : ForwardMessagesEvent
}
@@ -48,9 +48,9 @@ class ForwardMessagesPresenter(
@Composable
override fun present(): ForwardMessagesState {
fun handleEvent(event: ForwardMessagesEvents) {
fun handleEvent(event: ForwardMessagesEvent) {
when (event) {
ForwardMessagesEvents.ClearError -> forwardingActionState.value = AsyncAction.Uninitialized
ForwardMessagesEvent.ClearError -> forwardingActionState.value = AsyncAction.Uninitialized
}
}
@@ -13,5 +13,5 @@ import io.element.android.libraries.matrix.api.core.RoomId
data class ForwardMessagesState(
val forwardAction: AsyncAction<List<RoomId>>,
val eventSink: (ForwardMessagesEvents) -> Unit
val eventSink: (ForwardMessagesEvent) -> Unit
)
@@ -32,7 +32,7 @@ open class ForwardMessagesStatePreviewParam : PreviewParameterProvider<ForwardMe
fun aForwardMessagesState(
forwardAction: AsyncAction<List<RoomId>> = AsyncAction.Uninitialized,
eventSink: (ForwardMessagesEvents) -> Unit = {}
eventSink: (ForwardMessagesEvent) -> Unit = {}
) = ForwardMessagesState(
forwardAction = forwardAction,
eventSink = eventSink
@@ -31,7 +31,7 @@ fun ForwardMessagesView(
stringResource(id = CommonStrings.error_unknown)
},
onErrorDismiss = {
state.eventSink(ForwardMessagesEvents.ClearError)
state.eventSink(ForwardMessagesEvent.ClearError)
},
)
}
@@ -86,7 +86,7 @@ class ForwardMessagesPresenterTest {
val failedForwardState = awaitItem()
assertThat(failedForwardState.forwardAction.isFailure()).isTrue()
// Then clear error
failedForwardState.eventSink(ForwardMessagesEvents.ClearError)
failedForwardState.eventSink(ForwardMessagesEvent.ClearError)
assertThat(awaitItem().forwardAction.isUninitialized()).isTrue()
forwardEventLambda.assertions().isCalledOnce()
}
@@ -29,7 +29,7 @@ import org.junit.Test
class ForwardMessagesViewTest : RobolectricTest() {
@Test
fun `cancel error emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ForwardMessagesEvents>()
val eventsRecorder = EventsRecorder<ForwardMessagesEvent>()
setForwardMessagesView(
aForwardMessagesState(
forwardAction = AsyncAction.Failure(AN_EXCEPTION),
@@ -37,13 +37,13 @@ class ForwardMessagesViewTest : RobolectricTest() {
),
)
pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(ForwardMessagesEvents.ClearError)
eventsRecorder.assertSingle(ForwardMessagesEvent.ClearError)
}
@Test
fun `success invokes onForwardSuccess`() = runAndroidComposeUiTest {
val data = listOf(A_ROOM_ID)
val eventsRecorder = EventsRecorder<ForwardMessagesEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<ForwardMessagesEvent>(expectEvents = false)
ensureCalledOnceWithParam<List<RoomId>?>(data) { callback ->
setForwardMessagesView(
aForwardMessagesState(
@@ -8,7 +8,7 @@
package io.element.android.features.ftue.impl.notifications
sealed interface NotificationsOptInEvents {
data object ContinueClicked : NotificationsOptInEvents
data object NotNowClicked : NotificationsOptInEvents
sealed interface NotificationsOptInEvent {
data object ContinueClicked : NotificationsOptInEvent
data object NotNowClicked : NotificationsOptInEvent
}
@@ -52,16 +52,16 @@ class NotificationsOptInPresenter(
override fun present(): NotificationsOptInState {
val notificationsPermissionsState = postNotificationPermissionsPresenter.present()
fun handleEvent(event: NotificationsOptInEvents) {
fun handleEvent(event: NotificationsOptInEvent) {
when (event) {
NotificationsOptInEvents.ContinueClicked -> {
NotificationsOptInEvent.ContinueClicked -> {
if (notificationsPermissionsState.permissionGranted) {
callback.onNotificationsOptInFinished()
} else {
notificationsPermissionsState.eventSink(PermissionsEvent.RequestPermissions)
}
}
NotificationsOptInEvents.NotNowClicked -> {
NotificationsOptInEvent.NotNowClicked -> {
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
appCoroutineScope.setPermissionDenied()
}
@@ -12,5 +12,5 @@ import io.element.android.libraries.permissions.api.PermissionsState
data class NotificationsOptInState(
val notificationsPermissionState: PermissionsState,
val eventSink: (NotificationsOptInEvents) -> Unit
val eventSink: (NotificationsOptInEvent) -> Unit
)
@@ -86,14 +86,14 @@ private fun NotificationsOptInFooter(state: NotificationsOptInState) {
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_ok),
onClick = {
state.eventSink(NotificationsOptInEvents.ContinueClicked)
state.eventSink(NotificationsOptInEvent.ContinueClicked)
}
)
TextButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_not_now),
onClick = {
state.eventSink(NotificationsOptInEvents.NotNowClicked)
state.eventSink(NotificationsOptInEvent.NotNowClicked)
}
)
}
@@ -15,7 +15,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutEvent
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
@@ -67,7 +67,7 @@ class ChooseSelfVerificationModePresenter(
fun handleEvent(event: ChooseSelfVerificationModeEvent) {
when (event) {
ChooseSelfVerificationModeEvent.SignOut -> directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
ChooseSelfVerificationModeEvent.SignOut -> directLogoutState.eventSink(DirectLogoutEvent.Logout(ignoreSdkError = false))
}
}
@@ -55,7 +55,7 @@ class NotificationsOptInPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(NotificationsOptInEvents.ContinueClicked)
initialState.eventSink(NotificationsOptInEvent.ContinueClicked)
assertThat(awaitItem().notificationsPermissionState.showDialog).isTrue()
}
}
@@ -70,7 +70,7 @@ class NotificationsOptInPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(NotificationsOptInEvents.ContinueClicked)
initialState.eventSink(NotificationsOptInEvent.ContinueClicked)
assertThat(isFinished).isTrue()
}
}
@@ -86,7 +86,7 @@ class NotificationsOptInPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(NotificationsOptInEvents.NotNowClicked)
initialState.eventSink(NotificationsOptInEvent.NotNowClicked)
assertThat(isFinished).isTrue()
}
}
@@ -105,7 +105,7 @@ class NotificationsOptInPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(NotificationsOptInEvents.NotNowClicked)
initialState.eventSink(NotificationsOptInEvent.NotNowClicked)
// Allow background coroutines to run
runCurrent()
@@ -9,7 +9,7 @@
package io.element.android.features.ftue.impl.sessionverification.choosemode
import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutEvent
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.libraries.architecture.AsyncData
@@ -112,7 +112,7 @@ class ChooseSessionVerificationModePresenterTest {
@Test
fun `sing out action triggers a direct logout`() = runTest {
val logoutEventRecorder = lambdaRecorder<DirectLogoutEvents, Unit> {}
val logoutEventRecorder = lambdaRecorder<DirectLogoutEvent, Unit> {}
val logoutPresenter = Presenter<DirectLogoutState> {
aDirectLogoutState(eventSink = logoutEventRecorder)
}
@@ -121,7 +121,7 @@ class ChooseSessionVerificationModePresenterTest {
val initial = awaitItem()
initial.eventSink(ChooseSelfVerificationModeEvent.SignOut)
logoutEventRecorder.assertions().isCalledOnce()
.with(value(DirectLogoutEvents.Logout(ignoreSdkError = false)))
.with(value(DirectLogoutEvent.Logout(ignoreSdkError = false)))
}
}
@@ -16,7 +16,7 @@ import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.push.api.battery.BatteryOptimizationEvents
import io.element.android.libraries.push.api.battery.BatteryOptimizationEvent
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
@@ -31,8 +31,8 @@ internal fun BatteryOptimizationBanner(
description = stringResource(R.string.banner_battery_optimization_content_android),
type = AnnouncementType.Actionable(
actionText = stringResource(R.string.banner_battery_optimization_submit_android),
onActionClick = { state.eventSink(BatteryOptimizationEvents.RequestDisableOptimizations) },
onDismissClick = { state.eventSink(BatteryOptimizationEvents.Dismiss) },
onActionClick = { state.eventSink(BatteryOptimizationEvent.RequestDisableOptimizations) },
onDismissClick = { state.eventSink(BatteryOptimizationEvent.Dismiss) },
),
)
}
@@ -16,7 +16,7 @@ import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvent
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.ui.strings.CommonStrings
@@ -31,8 +31,8 @@ fun FullScreenIntentPermissionBanner(
description = stringResource(R.string.full_screen_intent_banner_message),
type = AnnouncementType.Actionable(
actionText = stringResource(CommonStrings.action_continue),
onDismissClick = { state.eventSink(FullScreenIntentPermissionsEvents.Dismiss) },
onActionClick = { state.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) },
onDismissClick = { state.eventSink(FullScreenIntentPermissionsEvent.Dismiss) },
onActionClick = { state.eventSink(FullScreenIntentPermissionsEvent.OpenSettings) },
),
modifier = modifier.roomListBannerPadding(),
)
@@ -53,8 +53,6 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
@@ -80,7 +78,7 @@ import io.element.android.libraries.designsystem.modifiers.backgroundVerticalGra
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.text.AdaptativeTitle
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
import io.element.android.libraries.designsystem.theme.components.Icon
@@ -141,15 +139,21 @@ fun HomeTopBar(
else -> stringResource(selectedNavigationItem.labelRes)
}
}
HomeNavigationBarItem.Spaces -> stringResource(selectedNavigationItem.labelRes)
HomeNavigationBarItem.Spaces -> null
}
displayTitle?.let {
val style = when (spaceFiltersState) {
// Space name
is SpaceFiltersState.Selected -> ElementTheme.typography.fontHeadingSmMedium
// "Chats"
else -> ElementTheme.typography.fontHeadingLgBold
}
AdaptativeTitle(
title = displayTitle,
style = style,
twoLinesStyle = ElementTheme.typography.fontHeadingSmMedium,
)
}
Text(
modifier = Modifier.semantics {
heading()
},
style = ElementTheme.typography.aliasScreenTitle,
text = displayTitle,
)
},
navigationIcon = {
NavigationIcon(
@@ -179,7 +183,9 @@ fun HomeTopBar(
TopAppBarScrollBehaviorLayout(scrollBehavior = scrollBehavior) {
RoomListFiltersView(
state = filtersState,
modifier = Modifier.padding(bottom = 16.dp).padding(contentPadding)
modifier = Modifier
.padding(bottom = 16.dp)
.padding(contentPadding)
)
}
}
@@ -348,45 +354,45 @@ private fun AccountIcon(
),
contentAlignment = Alignment.Center,
) {
val avatarData by remember(matrixUser) {
derivedStateOf {
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
}
val avatarData by remember(matrixUser) {
derivedStateOf {
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
}
val statusEmoji = matrixUser.displayedStatus?.toEmojiText()
val avatarModifier = if (statusEmoji != null) {
Modifier.eraseStatusEmojiBackground(
parentSize = AvatarSize.CurrentUserTopBar.dp,
layoutDirection = LocalLayoutDirection.current,
)
} else {
Modifier
}
Avatar(
avatarData = avatarData,
avatarType = AvatarType.User,
modifier = avatarModifier,
contentDescription = if (isCurrentAccount) {
if (showAvatarIndicator) {
stringResource(CommonStrings.a11y_settings_with_required_action)
} else {
stringResource(CommonStrings.common_settings)
}
} else {
null
},
}
val statusEmoji = matrixUser.displayedStatus?.toEmojiText()
val avatarModifier = if (statusEmoji != null) {
Modifier.eraseStatusEmojiBackground(
parentSize = AvatarSize.CurrentUserTopBar.dp,
layoutDirection = LocalLayoutDirection.current,
)
if (statusEmoji != null) {
StatusEmojiBadge(
emoji = statusEmoji,
modifier = Modifier.align(Alignment.BottomEnd),
)
}
if (showAvatarIndicator) {
RedIndicatorAtom(
modifier = Modifier.align(Alignment.TopEnd)
)
}
} else {
Modifier
}
Avatar(
avatarData = avatarData,
avatarType = AvatarType.User,
modifier = avatarModifier,
contentDescription = if (isCurrentAccount) {
if (showAvatarIndicator) {
stringResource(CommonStrings.a11y_settings_with_required_action)
} else {
stringResource(CommonStrings.common_settings)
}
} else {
null
},
)
if (statusEmoji != null) {
StatusEmojiBadge(
emoji = statusEmoji,
modifier = Modifier.align(Alignment.BottomEnd),
)
}
if (showAvatarIndicator) {
RedIndicatorAtom(
modifier = Modifier.align(Alignment.TopEnd)
)
}
}
}
@@ -19,6 +19,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
import io.element.android.libraries.matrix.api.room.CallIntentConsensus
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.ui.model.dmUserStatus
@@ -47,7 +48,7 @@ class RoomListRoomSummaryFactory(
mode = DateFormatterMode.TimeOrDate,
useRelative = true,
),
latestEvent = computeLatestEvent(roomSummary.latestEvent, roomInfo.isDm),
latestEvent = computeLatestEvent(roomSummary.latestEvent, roomInfo.hasOnlyTwoMembers()),
avatarData = avatarData,
userDefinedNotificationMode = roomInfo.userDefinedNotificationMode,
hasRoomCall = roomInfo.hasRoomCall,
@@ -81,6 +82,10 @@ class RoomListRoomSummaryFactory(
)
}
private fun RoomInfo.hasOnlyTwoMembers(): Boolean {
return isDm || activeMembersCount <= 2
}
private fun computeLatestEvent(latestEvent: LatestEventValue, dm: Boolean): LatestEvent {
return when (latestEvent) {
is LatestEventValue.None -> {
@@ -37,8 +37,8 @@ import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spacefilters.into
import io.element.android.features.home.impl.spacefilters.selectedFilter
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.DeclineInvite
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent.AcceptInvite
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent.DeclineInvite
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomState
@@ -7,9 +7,6 @@
package io.element.android.features.home.impl.search
import androidx.compose.runtime.Immutable
@Immutable
sealed interface GlobalSearchEvent {
object ClearQuery : GlobalSearchEvent
object ToggleSearchVisibility : GlobalSearchEvent
@@ -8,4 +8,4 @@
package io.element.android.features.home.impl.spaces
sealed interface HomeSpacesEvents
sealed interface HomeSpacesEvent
@@ -39,7 +39,7 @@ class HomeSpacesPresenter(
seenInvitesStore.seenRoomIds().map { it.toImmutableSet() }
}.collectAsState(persistentSetOf())
fun handleEvent(event: HomeSpacesEvents) {
fun handleEvent(event: HomeSpacesEvent) {
// when (event) { }
}
@@ -19,7 +19,7 @@ data class HomeSpacesState(
val seenSpaceInvites: ImmutableSet<RoomId>,
val hideInvitesAvatar: Boolean,
val canExploreSpaces: Boolean,
val eventSink: (HomeSpacesEvents) -> Unit,
val eventSink: (HomeSpacesEvent) -> Unit,
)
sealed interface CurrentSpace {
@@ -43,7 +43,7 @@ internal fun aHomeSpacesState(
seenSpaceInvites: Set<RoomId> = emptySet(),
hideInvitesAvatar: Boolean = false,
canExploreSpaces: Boolean = true,
eventSink: (HomeSpacesEvents) -> Unit = {},
eventSink: (HomeSpacesEvent) -> Unit = {},
) = HomeSpacesState(
space = space,
spaceRooms = spaceRooms.toImmutableList(),
@@ -143,7 +143,7 @@ private fun EmptySpaceHomeView(
)
Text(
text = stringResource(CommonStrings.screen_space_list_empty_state_title),
style = ElementTheme.typography.fontHeadingLgBold,
style = ElementTheme.typography.fontHeadingMdBold,
color = ElementTheme.colors.textPrimary,
textAlign = TextAlign.Center,
)
@@ -8,10 +8,36 @@
package io.element.android.features.home.impl.datasource
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomSummary
import org.junit.Test
class RoomListRoomSummaryFactoryTest {
@Test
fun `a room with two members does not prefix its preview with the sender`() {
val formatter = FakeRoomLatestEventFormatter()
val factory = aRoomListRoomSummaryFactory(roomLatestEventFormatter = formatter)
factory.create(aRoomSummary(info = aRoomInfo(isDm = false, activeMembersCount = 2)))
assertThat(formatter.lastIsDmRoom).isTrue()
}
@Test
fun `a room with more than two members prefixes its preview with the sender`() {
val formatter = FakeRoomLatestEventFormatter()
val factory = aRoomListRoomSummaryFactory(roomLatestEventFormatter = formatter)
factory.create(aRoomSummary(info = aRoomInfo(isDm = false, activeMembersCount = 3)))
assertThat(formatter.lastIsDmRoom).isFalse()
}
}
fun aRoomListRoomSummaryFactory(
dateFormatter: DateFormatter = FakeDateFormatter { _, _, _ -> "Today" },
@@ -25,7 +25,7 @@ import io.element.android.features.home.impl.search.aRoomListSearchState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spacefilters.aDisabledSpaceFiltersState
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
import io.element.android.features.invite.test.InMemorySeenInvitesStore
@@ -474,7 +474,7 @@ class RoomListPresenterTest {
@Test
fun `present - when a room is invited then accept and decline events are sent to acceptDeclinePresenter`() = runTest {
val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvents -> }
val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvent -> }
val acceptDeclinePresenter = Presenter {
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
}
@@ -513,8 +513,8 @@ class RoomListPresenterTest {
assert(eventSinkRecorder)
.isCalledExactly(2)
.withSequence(
listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))),
listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false))),
listOf(value(AcceptDeclineInviteEvent.AcceptInvite(inviteData))),
listOf(value(AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false))),
)
}
}
@@ -13,7 +13,7 @@ import io.element.android.features.invite.api.InviteData
/**
* Events sent to the accept/decline invite presenter, from whichever screen is showing the invitation.
*/
interface AcceptDeclineInviteEvents {
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents
data class DeclineInvite(val invite: InviteData, val blockUser: Boolean, val shouldConfirm: Boolean) : AcceptDeclineInviteEvents
interface AcceptDeclineInviteEvent {
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvent
data class DeclineInvite(val invite: InviteData, val blockUser: Boolean, val shouldConfirm: Boolean) : AcceptDeclineInviteEvent
}
@@ -14,5 +14,5 @@ import io.element.android.libraries.matrix.api.core.RoomId
data class AcceptDeclineInviteState(
val acceptAction: AsyncAction<RoomId>,
val declineAction: AsyncAction<RoomId>,
val eventSink: (AcceptDeclineInviteEvents) -> Unit,
val eventSink: (AcceptDeclineInviteEvent) -> Unit,
)
@@ -14,7 +14,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
fun anAcceptDeclineInviteState(
acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
eventSink: (AcceptDeclineInviteEvents) -> Unit = {},
eventSink: (AcceptDeclineInviteEvent) -> Unit = {},
) = AcceptDeclineInviteState(
acceptAction = acceptAction,
declineAction = declineAction,
@@ -15,7 +15,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
import io.element.android.features.invite.impl.AcceptInvite
@@ -40,13 +40,13 @@ class AcceptDeclineInvitePresenter(
val declinedAction: MutableState<AsyncAction<RoomId>> =
remember { mutableStateOf(AsyncAction.Uninitialized) }
fun handleEvent(event: AcceptDeclineInviteEvents) {
fun handleEvent(event: AcceptDeclineInviteEvent) {
when (event) {
is AcceptDeclineInviteEvents.AcceptInvite -> {
is AcceptDeclineInviteEvent.AcceptInvite -> {
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
}
is AcceptDeclineInviteEvents.DeclineInvite -> {
is AcceptDeclineInviteEvent.DeclineInvite -> {
val inviteData = event.invite
if (event.shouldConfirm) {
declinedAction.value = ConfirmingDeclineInvite(inviteData, event.blockUser)
@@ -58,11 +58,11 @@ class AcceptDeclineInvitePresenter(
)
}
}
is InternalAcceptDeclineInviteEvents.ClearAcceptActionState -> {
is InternalAcceptDeclineInviteEvent.ClearAcceptActionState -> {
acceptedAction.value = AsyncAction.Uninitialized
}
is InternalAcceptDeclineInviteEvents.ClearDeclineActionState -> {
is InternalAcceptDeclineInviteEvent.ClearDeclineActionState -> {
declinedAction.value = AsyncAction.Uninitialized
}
}
@@ -14,7 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
import io.element.android.features.invite.impl.AcceptInvite
@@ -37,11 +37,11 @@ fun AcceptDeclineInviteView(
AsyncActionView(
async = state.acceptAction,
onSuccess = { roomId ->
state.eventSink(InternalAcceptDeclineInviteEvents.ClearAcceptActionState)
state.eventSink(InternalAcceptDeclineInviteEvent.ClearAcceptActionState)
onAcceptInviteSuccess(roomId)
},
onErrorDismiss = {
state.eventSink(InternalAcceptDeclineInviteEvents.ClearAcceptActionState)
state.eventSink(InternalAcceptDeclineInviteEvent.ClearAcceptActionState)
},
errorTitle = {
stringResource(CommonStrings.common_something_went_wrong)
@@ -57,11 +57,11 @@ fun AcceptDeclineInviteView(
AsyncActionView(
async = state.declineAction,
onSuccess = { roomId ->
state.eventSink(InternalAcceptDeclineInviteEvents.ClearDeclineActionState)
state.eventSink(InternalAcceptDeclineInviteEvent.ClearDeclineActionState)
onDeclineInviteSuccess(roomId)
},
onErrorDismiss = {
state.eventSink(InternalAcceptDeclineInviteEvents.ClearDeclineActionState)
state.eventSink(InternalAcceptDeclineInviteEvent.ClearDeclineActionState)
},
errorTitle = {
stringResource(CommonStrings.common_something_went_wrong)
@@ -77,7 +77,7 @@ fun AcceptDeclineInviteView(
blockUser = confirming.blockUser,
onConfirmClick = {
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(
AcceptDeclineInviteEvent.DeclineInvite(
confirming.inviteData,
blockUser = confirming.blockUser,
shouldConfirm = false
@@ -85,7 +85,7 @@ fun AcceptDeclineInviteView(
)
},
onDismissClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.ClearDeclineActionState)
state.eventSink(InternalAcceptDeclineInviteEvent.ClearDeclineActionState)
}
)
}
@@ -8,9 +8,9 @@
package io.element.android.features.invite.impl.acceptdecline
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
data object ClearAcceptActionState : InternalAcceptDeclineInviteEvents
data object ClearDeclineActionState : InternalAcceptDeclineInviteEvents
sealed interface InternalAcceptDeclineInviteEvent : AcceptDeclineInviteEvent {
data object ClearAcceptActionState : InternalAcceptDeclineInviteEvent
data object ClearDeclineActionState : InternalAcceptDeclineInviteEvent
}
@@ -8,10 +8,10 @@
package io.element.android.features.invite.impl.declineandblock
sealed interface DeclineAndBlockEvents {
data class UpdateReportReason(val reason: String) : DeclineAndBlockEvents
data object ToggleReportRoom : DeclineAndBlockEvents
data object ToggleBlockUser : DeclineAndBlockEvents
data object Decline : DeclineAndBlockEvents
data object ClearDeclineAction : DeclineAndBlockEvents
sealed interface DeclineAndBlockEvent {
data class UpdateReportReason(val reason: String) : DeclineAndBlockEvent
data object ToggleReportRoom : DeclineAndBlockEvent
data object ToggleBlockUser : DeclineAndBlockEvent
data object Decline : DeclineAndBlockEvent
data object ClearDeclineAction : DeclineAndBlockEvent
}
@@ -49,13 +49,13 @@ class DeclineAndBlockPresenter(
val coroutineScope = rememberCoroutineScope()
fun handleEvent(event: DeclineAndBlockEvents) {
fun handleEvent(event: DeclineAndBlockEvent) {
when (event) {
DeclineAndBlockEvents.ClearDeclineAction -> declineAction.value = AsyncAction.Uninitialized
DeclineAndBlockEvents.Decline -> coroutineScope.decline(reportReason, blockUser, reportRoom, declineAction)
DeclineAndBlockEvents.ToggleBlockUser -> blockUser = !blockUser
DeclineAndBlockEvents.ToggleReportRoom -> reportRoom = !reportRoom
is DeclineAndBlockEvents.UpdateReportReason -> reportReason = event.reason
DeclineAndBlockEvent.ClearDeclineAction -> declineAction.value = AsyncAction.Uninitialized
DeclineAndBlockEvent.Decline -> coroutineScope.decline(reportReason, blockUser, reportRoom, declineAction)
DeclineAndBlockEvent.ToggleBlockUser -> blockUser = !blockUser
DeclineAndBlockEvent.ToggleReportRoom -> reportRoom = !reportRoom
is DeclineAndBlockEvent.UpdateReportReason -> reportReason = event.reason
}
}
@@ -15,7 +15,7 @@ data class DeclineAndBlockState(
val reportReason: String,
val blockUser: Boolean,
val declineAction: AsyncAction<Unit>,
val eventSink: (DeclineAndBlockEvents) -> Unit
val eventSink: (DeclineAndBlockEvent) -> Unit
) {
val canDecline = blockUser || reportRoom && reportReason.isNotEmpty()
}
@@ -36,7 +36,7 @@ fun aDeclineAndBlockState(
reportReason: String = "",
blockUser: Boolean = false,
declineAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (DeclineAndBlockEvents) -> Unit = {},
eventSink: (DeclineAndBlockEvent) -> Unit = {},
) = DeclineAndBlockState(
reportRoom = reportRoom,
reportReason = reportReason,
@@ -56,8 +56,8 @@ fun DeclineAndBlockView(
async = state.declineAction,
onSuccess = { onDeclineSuccess() },
errorMessage = { stringResource(CommonStrings.error_unknown) },
onRetry = { state.eventSink(DeclineAndBlockEvents.Decline) },
onErrorDismiss = { state.eventSink(DeclineAndBlockEvents.ClearDeclineAction) }
onRetry = { state.eventSink(DeclineAndBlockEvent.Decline) },
onErrorDismiss = { state.eventSink(DeclineAndBlockEvent.ClearDeclineAction) }
)
Scaffold(
@@ -89,7 +89,7 @@ fun DeclineAndBlockView(
Text(text = stringResource(R.string.screen_decline_and_block_block_user_option_description))
},
onClick = {
state.eventSink(DeclineAndBlockEvents.ToggleBlockUser)
state.eventSink(DeclineAndBlockEvent.ToggleBlockUser)
},
trailingContent = ListItemContent.Switch(checked = state.blockUser)
)
@@ -104,7 +104,7 @@ fun DeclineAndBlockView(
Text(text = stringResource(R.string.screen_decline_and_block_report_user_option_description))
},
onClick = {
state.eventSink(DeclineAndBlockEvents.ToggleReportRoom)
state.eventSink(DeclineAndBlockEvent.ToggleReportRoom)
},
trailingContent = ListItemContent.Switch(checked = state.reportRoom)
)
@@ -113,7 +113,7 @@ fun DeclineAndBlockView(
Spacer(modifier = Modifier.height(24.dp))
TextField(
value = state.reportReason,
onValueChange = { state.eventSink(DeclineAndBlockEvents.UpdateReportReason(it)) },
onValueChange = { state.eventSink(DeclineAndBlockEvent.UpdateReportReason(it)) },
placeholder = stringResource(R.string.screen_decline_and_block_report_user_reason_placeholder),
minLines = 3,
enabled = !isDeclining,
@@ -132,7 +132,7 @@ fun DeclineAndBlockView(
enabled = !isDeclining && state.canDecline,
onClick = {
focusManager.clearFocus(force = true)
state.eventSink(DeclineAndBlockEvents.Decline)
state.eventSink(DeclineAndBlockEvent.Decline)
},
modifier = Modifier
.fillMaxWidth()
@@ -10,7 +10,7 @@ package io.element.android.features.invite.impl.acceptdecline
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
import io.element.android.features.invite.impl.AcceptInvite
import io.element.android.features.invite.impl.DeclineInvite
@@ -51,13 +51,13 @@ class AcceptDeclineInvitePresenterTest {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
InternalAcceptDeclineInviteEvents.ClearDeclineActionState
InternalAcceptDeclineInviteEvent.ClearDeclineActionState
)
}
awaitItem().also { state ->
@@ -78,20 +78,20 @@ class AcceptDeclineInvitePresenterTest {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(
InternalAcceptDeclineInviteEvents.ClearDeclineActionState
InternalAcceptDeclineInviteEvent.ClearDeclineActionState
)
}
awaitItem().also { state ->
@@ -114,13 +114,13 @@ class AcceptDeclineInvitePresenterTest {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, blockUser = false))
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
@@ -146,7 +146,7 @@ class AcceptDeclineInvitePresenterTest {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
AcceptDeclineInviteEvent.AcceptInvite(inviteData)
)
}
awaitItem().also { state ->
@@ -155,7 +155,7 @@ class AcceptDeclineInvitePresenterTest {
awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(
InternalAcceptDeclineInviteEvents.ClearAcceptActionState
InternalAcceptDeclineInviteEvent.ClearAcceptActionState
)
}
awaitItem().also { state ->
@@ -178,7 +178,7 @@ class AcceptDeclineInvitePresenterTest {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
AcceptDeclineInviteEvent.AcceptInvite(inviteData)
)
}
awaitItem().also { state ->
@@ -53,21 +53,21 @@ class DeclineAndBlockPresenterTest {
assertThat(state.blockUser).isTrue()
assertThat(state.reportReason).isEmpty()
assertThat(state.canDecline).isTrue()
state.eventSink(DeclineAndBlockEvents.ToggleBlockUser)
state.eventSink(DeclineAndBlockEvent.ToggleBlockUser)
}
awaitItem().also { state ->
assertThat(state.reportRoom).isFalse()
assertThat(state.blockUser).isFalse()
assertThat(state.reportReason).isEmpty()
assertThat(state.canDecline).isFalse()
state.eventSink(DeclineAndBlockEvents.ToggleReportRoom)
state.eventSink(DeclineAndBlockEvent.ToggleReportRoom)
}
awaitItem().also { state ->
assertThat(state.reportRoom).isTrue()
assertThat(state.blockUser).isFalse()
assertThat(state.reportReason).isEmpty()
assertThat(state.canDecline).isFalse()
state.eventSink(DeclineAndBlockEvents.UpdateReportReason("Spam"))
state.eventSink(DeclineAndBlockEvent.UpdateReportReason("Spam"))
}
awaitItem().also { state ->
assertThat(state.reportRoom).isTrue()
@@ -87,7 +87,7 @@ class DeclineAndBlockPresenterTest {
)
presenter.test {
awaitItem().also { state ->
state.eventSink(DeclineAndBlockEvents.Decline)
state.eventSink(DeclineAndBlockEvent.Decline)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
@@ -110,12 +110,12 @@ class DeclineAndBlockPresenterTest {
)
presenter.test {
awaitItem().also { state ->
state.eventSink(DeclineAndBlockEvents.Decline)
state.eventSink(DeclineAndBlockEvent.Decline)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(DeclineAndBlockEvents.ClearDeclineAction)
state.eventSink(DeclineAndBlockEvent.ClearDeclineAction)
}
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
@@ -137,7 +137,7 @@ class DeclineAndBlockPresenterTest {
)
presenter.test {
awaitItem().also { state ->
state.eventSink(DeclineAndBlockEvents.Decline)
state.eventSink(DeclineAndBlockEvent.Decline)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
@@ -30,7 +30,7 @@ import org.junit.Test
class DeclineAndBlockViewTest : RobolectricTest() {
@Test
fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<DeclineAndBlockEvent>(expectEvents = false)
ensureCalledOnce {
setDeclineAndBlockView(
aDeclineAndBlockState(
@@ -44,7 +44,7 @@ class DeclineAndBlockViewTest : RobolectricTest() {
@Test
fun `a successful decline invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<DeclineAndBlockEvent>(expectEvents = false)
ensureCalledOnce {
setDeclineAndBlockView(
aDeclineAndBlockState(
@@ -58,7 +58,7 @@ class DeclineAndBlockViewTest : RobolectricTest() {
@Test
fun `clicking on decline when enabled emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
val eventsRecorder = EventsRecorder<DeclineAndBlockEvent>()
setDeclineAndBlockView(
aDeclineAndBlockState(
blockUser = true,
@@ -66,12 +66,12 @@ class DeclineAndBlockViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_decline)
eventsRecorder.assertSingle(DeclineAndBlockEvents.Decline)
eventsRecorder.assertSingle(DeclineAndBlockEvent.Decline)
}
@Test
fun `clicking on decline when disabled does not emit event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<DeclineAndBlockEvent>(expectEvents = false)
setDeclineAndBlockView(
aDeclineAndBlockState(
blockUser = false,
@@ -85,7 +85,7 @@ class DeclineAndBlockViewTest : RobolectricTest() {
@Test
fun `clicking on block option emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
val eventsRecorder = EventsRecorder<DeclineAndBlockEvent>()
setDeclineAndBlockView(
aDeclineAndBlockState(
blockUser = true,
@@ -93,12 +93,12 @@ class DeclineAndBlockViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_decline_and_block_block_user_option_title)
eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleBlockUser)
eventsRecorder.assertSingle(DeclineAndBlockEvent.ToggleBlockUser)
}
@Test
fun `clicking on report room option emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
val eventsRecorder = EventsRecorder<DeclineAndBlockEvent>()
setDeclineAndBlockView(
aDeclineAndBlockState(
reportRoom = true,
@@ -106,12 +106,12 @@ class DeclineAndBlockViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_report_room)
eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleReportRoom)
eventsRecorder.assertSingle(DeclineAndBlockEvent.ToggleReportRoom)
}
@Test
fun `typing text in the reason field emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
val eventsRecorder = EventsRecorder<DeclineAndBlockEvent>()
setDeclineAndBlockView(
aDeclineAndBlockState(
reportRoom = true,
@@ -120,7 +120,7 @@ class DeclineAndBlockViewTest : RobolectricTest() {
),
)
onNodeWithText("").performTextInput("Spam!")
eventsRecorder.assertSingle(DeclineAndBlockEvents.UpdateReportReason("Spam!"))
eventsRecorder.assertSingle(DeclineAndBlockEvent.UpdateReportReason("Spam!"))
}
}
@@ -11,8 +11,8 @@ package io.element.android.features.invitepeople.api
/**
* Events the invite people UI sends to its presenter through [InvitePeopleState.eventSink].
*/
interface InvitePeopleEvents {
data object SendInvites : InvitePeopleEvents
data object CloseSearch : InvitePeopleEvents
data object ClearError : InvitePeopleEvents
interface InvitePeopleEvent {
data object SendInvites : InvitePeopleEvent
data object CloseSearch : InvitePeopleEvent
data object ClearError : InvitePeopleEvent
}
@@ -28,5 +28,5 @@ interface InvitePeopleState {
val createRoomFromDmAction: AsyncAction<RoomId>
/** Where the UI sends its events. */
val eventSink: (InvitePeopleEvents) -> Unit
val eventSink: (InvitePeopleEvent) -> Unit
}
@@ -27,7 +27,7 @@ private data class PreviewInvitePeopleState(
override val isSearchActive: Boolean,
override val sendInvitesAction: AsyncAction<Unit>,
override val createRoomFromDmAction: AsyncAction<RoomId>,
override val eventSink: (InvitePeopleEvents) -> Unit,
override val eventSink: (InvitePeopleEvent) -> Unit,
) : InvitePeopleState
private fun aPreviewInvitePeopleState(
@@ -35,7 +35,7 @@ private fun aPreviewInvitePeopleState(
isSearchActive: Boolean = false,
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
createRoomFromDmAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
eventSink: (InvitePeopleEvents) -> Unit = {},
eventSink: (InvitePeopleEvent) -> Unit = {},
) = PreviewInvitePeopleState(
canInvite = canInvite,
isSearchActive = isSearchActive,
@@ -8,12 +8,12 @@
package io.element.android.features.invitepeople.impl
import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.libraries.matrix.api.user.MatrixUser
sealed interface DefaultInvitePeopleEvents : InvitePeopleEvents {
data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents
data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents
data object DismissUnknownUsersModal : DefaultInvitePeopleEvents
data object RemoveUnknownUsers : DefaultInvitePeopleEvents
sealed interface DefaultInvitePeopleEvent : InvitePeopleEvent {
data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvent
data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvent
data object DismissUnknownUsersModal : DefaultInvitePeopleEvent
data object RemoveUnknownUsers : DefaultInvitePeopleEvent
}
@@ -24,7 +24,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
import io.element.android.features.invitepeople.api.InvitePeopleState
import io.element.android.libraries.architecture.AsyncAction
@@ -179,26 +179,26 @@ class DefaultInvitePeoplePresenter(
)
}
fun handleEvent(event: InvitePeopleEvents) {
fun handleEvent(event: InvitePeopleEvent) {
when (event) {
// Dedicated `when` for exhaustivity.
is DefaultInvitePeopleEvents -> when (event) {
is DefaultInvitePeopleEvents.OnSearchActiveChanged -> {
is DefaultInvitePeopleEvent -> when (event) {
is DefaultInvitePeopleEvent.OnSearchActiveChanged -> {
searchActive = event.active
if (!event.active) {
queryState.clearText()
}
}
is DefaultInvitePeopleEvents.ToggleUser -> {
is DefaultInvitePeopleEvent.ToggleUser -> {
selectedUsers.toggleUser(event.user)
searchResults.toggleUser(event.user)
// suggestions will automatically update via derivedStateOf when selectedUsers changes
}
is DefaultInvitePeopleEvents.DismissUnknownUsersModal -> {
is DefaultInvitePeopleEvent.DismissUnknownUsersModal -> {
sendInvitesAction.value = AsyncAction.Uninitialized
}
is DefaultInvitePeopleEvents.RemoveUnknownUsers -> {
is DefaultInvitePeopleEvent.RemoveUnknownUsers -> {
val usersToRemove = selectedUsers.value.filter { it in unknownUsers }
usersToRemove.forEach { user ->
selectedUsers.toggleUser(user)
@@ -207,7 +207,7 @@ class DefaultInvitePeoplePresenter(
sendInvitesAction.value = AsyncAction.Uninitialized
}
}
is InvitePeopleEvents.SendInvites -> {
is InvitePeopleEvent.SendInvites -> {
if (unknownUsers.isNotEmpty() && sendInvitesAction.value !is ConfirmingUnknownUserInvitation) {
sendInvitesAction.value = ConfirmingUnknownUserInvitation(
unknownUsers
@@ -224,11 +224,11 @@ class DefaultInvitePeoplePresenter(
}
}
}
is InvitePeopleEvents.CloseSearch -> {
is InvitePeopleEvent.CloseSearch -> {
searchActive = false
queryState.clearText()
}
is InvitePeopleEvents.ClearError -> {
is InvitePeopleEvent.ClearError -> {
sendInvitesAction.value = AsyncAction.Uninitialized
createRoomFromDmAction.value = AsyncAction.Uninitialized
}
@@ -9,7 +9,7 @@
package io.element.android.features.invitepeople.impl
import androidx.compose.foundation.text.input.TextFieldState
import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.features.invitepeople.api.InvitePeopleState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
@@ -29,5 +29,5 @@ data class DefaultInvitePeopleState(
override val sendInvitesAction: AsyncAction<Unit>,
override val createRoomFromDmAction: AsyncAction<RoomId>,
val suggestions: ImmutableList<InvitableUser>,
override val eventSink: (InvitePeopleEvents) -> Unit
override val eventSink: (InvitePeopleEvent) -> Unit
) : InvitePeopleState
@@ -30,7 +30,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.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
@@ -102,7 +102,7 @@ private fun InvitePeopleContentView(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
fun toggleUser(user: MatrixUser) {
state.eventSink(DefaultInvitePeopleEvents.ToggleUser(user))
state.eventSink(DefaultInvitePeopleEvent.ToggleUser(user))
}
InvitePeopleSearchBar(
@@ -114,7 +114,7 @@ private fun InvitePeopleContentView(
active = state.isSearchActive,
onActiveChange = {
state.eventSink(
DefaultInvitePeopleEvents.OnSearchActiveChanged(
DefaultInvitePeopleEvent.OnSearchActiveChanged(
it
)
)
@@ -144,7 +144,7 @@ private fun InvitePeopleContentView(
CheckableUserRow(
checked = invitableUser.isSelected,
onCheckedChange = {
state.eventSink(DefaultInvitePeopleEvents.ToggleUser(invitableUser.matrixUser))
state.eventSink(DefaultInvitePeopleEvent.ToggleUser(invitableUser.matrixUser))
},
data = CheckableUserRowData.Resolved(
avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.UserListItem),
@@ -163,9 +163,9 @@ private fun InvitePeopleContentView(
if (state.sendInvitesAction is ConfirmingUnknownUserInvitation) {
InvitePeopleConfirmModal(
users = state.sendInvitesAction.users,
onDismiss = { state.eventSink.invoke(DefaultInvitePeopleEvents.DismissUnknownUsersModal) },
onInvite = { state.eventSink.invoke(InvitePeopleEvents.SendInvites) },
onRemove = { state.eventSink.invoke(DefaultInvitePeopleEvents.RemoveUnknownUsers) }
onDismiss = { state.eventSink.invoke(DefaultInvitePeopleEvent.DismissUnknownUsersModal) },
onInvite = { state.eventSink.invoke(InvitePeopleEvent.SendInvites) },
onRemove = { state.eventSink.invoke(DefaultInvitePeopleEvent.RemoveUnknownUsers) }
)
}
}
@@ -11,7 +11,7 @@ package io.element.android.features.invitepeople.impl
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import app.cash.turbine.ReceiveTurbine
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
@@ -88,13 +88,13 @@ internal class DefaultInvitePeoplePresenterTest {
val initialState = awaitItem()
skipItems(1)
initialState.eventSink(DefaultInvitePeopleEvents.OnSearchActiveChanged(true))
initialState.eventSink(DefaultInvitePeopleEvent.OnSearchActiveChanged(true))
val resultState = awaitItemAsDefault()
assertThat(resultState.isSearchActive).isTrue()
resultState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
assertThat(awaitItemAsDefault().searchQuery.text.toString()).isEqualTo("some query")
resultState.eventSink(InvitePeopleEvents.CloseSearch)
resultState.eventSink(InvitePeopleEvent.CloseSearch)
skipItems(1)
awaitItemAsDefault().also {
assertThat(it.isSearchActive).isFalse()
@@ -290,18 +290,18 @@ internal class DefaultInvitePeoplePresenterTest {
skipItems(1)
// When we toggle a user not in the list, they are added
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(aMatrixUser()))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(aMatrixUser()))
assertThat(awaitItemAsDefault().selectedUsers).containsExactly(aMatrixUser())
// Toggling a different user also adds them
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(aMatrixUser(id = A_USER_ID_2.value)))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(aMatrixUser(id = A_USER_ID_2.value)))
assertThat(awaitItemAsDefault().selectedUsers).containsExactly(
aMatrixUser(),
aMatrixUser(id = A_USER_ID_2.value)
)
// Toggling the first user removes them
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(aMatrixUser()))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(aMatrixUser()))
assertThat(awaitItemAsDefault().selectedUsers).containsExactly(aMatrixUser(id = A_USER_ID_2.value))
}
}
@@ -319,7 +319,7 @@ internal class DefaultInvitePeoplePresenterTest {
val selectedUser = aMatrixUser(displayName = "John Doe")
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(selectedUser))
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
skipItems(1)
@@ -373,7 +373,7 @@ internal class DefaultInvitePeoplePresenterTest {
}
// And then a user is toggled
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(selectedUser))
skipItems(1)
val resultState = awaitItemAsDefault()
@@ -414,13 +414,13 @@ internal class DefaultInvitePeoplePresenterTest {
repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
skipItems(1)
// And then a user is toggled
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(selectedUser))
skipItems(1)
val resultState = awaitItemAsDefault()
// The results are updated...
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
// Send invites
initialState.eventSink(InvitePeopleEvents.SendInvites)
initialState.eventSink(InvitePeopleEvent.SendInvites)
// Can't invite in the loading state
awaitItem().run {
@@ -468,13 +468,13 @@ internal class DefaultInvitePeoplePresenterTest {
repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
skipItems(1)
// And then a user is toggled
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(selectedUser))
skipItems(1)
val resultState = awaitItemAsDefault()
// The results are updated...
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
// Send invites
initialState.eventSink(InvitePeopleEvents.SendInvites)
initialState.eventSink(InvitePeopleEvent.SendInvites)
// Can't invite in the loading state
awaitItem().run {
@@ -645,17 +645,17 @@ internal class DefaultInvitePeoplePresenterTest {
skipItems(1)
// When we toggle a user not in the list, they are added, and we fetch their identity.
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(alice))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob))
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(bob))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie))
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(charlie))
delay(100)
// If we do not have their identity cached, or fail to fetch it, we should mark them as unknown.
awaitItemAsDefault().run {
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
eventSink(InvitePeopleEvents.SendInvites)
eventSink(InvitePeopleEvent.SendInvites)
}
getUserIdentityResult.assertions().isCalledExactly(3).withSequence(
@@ -668,7 +668,7 @@ internal class DefaultInvitePeoplePresenterTest {
awaitItemAsDefault().run {
assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java)
assertThat(canInvite).isTrue()
eventSink(InvitePeopleEvents.SendInvites)
eventSink(InvitePeopleEvent.SendInvites)
}
delay(1_000)
@@ -712,11 +712,11 @@ internal class DefaultInvitePeoplePresenterTest {
skipItems(1)
// When we toggle a user not in the list, they are added, and we fetch their identity.
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(alice))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob))
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(bob))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie))
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(charlie))
delay(100)
// And the search is matching Alice and Bob
@@ -738,7 +738,7 @@ internal class DefaultInvitePeoplePresenterTest {
searchResults.users().map { Pair(it.matrixUser, it.isSelected) }
).containsExactly(Pair(alice, true), Pair(bob, true))
eventSink(InvitePeopleEvents.SendInvites)
eventSink(InvitePeopleEvent.SendInvites)
}
getUserIdentityResult.assertions().isCalledExactly(3).withSequence(
@@ -751,7 +751,7 @@ internal class DefaultInvitePeoplePresenterTest {
awaitItemAsDefault().run {
assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java)
assertThat(canInvite).isTrue()
eventSink(DefaultInvitePeopleEvents.RemoveUnknownUsers)
eventSink(DefaultInvitePeopleEvent.RemoveUnknownUsers)
}
// Selecting "remove" should remove all unknown users, but keeps those who are known.
@@ -794,16 +794,16 @@ internal class DefaultInvitePeoplePresenterTest {
skipItems(1)
// When we toggle a user not in the list, they are added, and we fetch their identity.
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(alice))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob))
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(bob))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie))
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(charlie))
delay(100)
awaitItemAsDefault().run {
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
eventSink(InvitePeopleEvents.SendInvites)
eventSink(InvitePeopleEvent.SendInvites)
}
getUserIdentityResult.assertions().isCalledExactly(3).withSequence(
@@ -816,7 +816,7 @@ internal class DefaultInvitePeoplePresenterTest {
awaitItemAsDefault().run {
assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java)
assertThat(canInvite).isTrue()
eventSink(DefaultInvitePeopleEvents.DismissUnknownUsersModal)
eventSink(DefaultInvitePeopleEvent.DismissUnknownUsersModal)
}
// Dismissing should not modify the selection at all
@@ -853,10 +853,10 @@ internal class DefaultInvitePeoplePresenterTest {
skipItems(1)
// We want to add a new user to a DM
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(alice))
// And we send the invites
initialState.eventSink(InvitePeopleEvents.SendInvites)
initialState.eventSink(InvitePeopleEvent.SendInvites)
skipItems(1)
@@ -10,15 +10,15 @@ package io.element.android.features.joinroom.impl
import io.element.android.features.invite.api.InviteData
sealed interface JoinRoomEvents {
data object RetryFetchingContent : JoinRoomEvents
data object DismissErrorAndHideContent : JoinRoomEvents
data object JoinRoom : JoinRoomEvents
data object KnockRoom : JoinRoomEvents
data object ForgetRoom : JoinRoomEvents
data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvents
data class UpdateKnockMessage(val message: String) : JoinRoomEvents
data object ClearActionStates : JoinRoomEvents
data class AcceptInvite(val inviteData: InviteData) : JoinRoomEvents
data class DeclineInvite(val inviteData: InviteData, val blockUser: Boolean) : JoinRoomEvents
sealed interface JoinRoomEvent {
data object RetryFetchingContent : JoinRoomEvent
data object DismissErrorAndHideContent : JoinRoomEvent
data object JoinRoom : JoinRoomEvent
data object KnockRoom : JoinRoomEvent
data object ForgetRoom : JoinRoomEvent
data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvent
data class UpdateKnockMessage(val message: String) : JoinRoomEvent
data object ClearActionStates : JoinRoomEvent
data class AcceptInvite(val inviteData: InviteData) : JoinRoomEvent
data class DeclineInvite(val inviteData: InviteData, val blockUser: Boolean) : JoinRoomEvent
}
@@ -26,7 +26,7 @@ import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.toInviteData
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
@@ -151,37 +151,37 @@ class JoinRoomPresenter(
contentState.markRoomInviteAsSeen()
}
fun handleEvent(event: JoinRoomEvents) {
fun handleEvent(event: JoinRoomEvent) {
when (event) {
JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction)
is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage)
is JoinRoomEvents.AcceptInvite -> {
JoinRoomEvent.JoinRoom -> coroutineScope.joinRoom(joinAction)
is JoinRoomEvent.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage)
is JoinRoomEvent.AcceptInvite -> {
acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(event.inviteData)
AcceptDeclineInviteEvent.AcceptInvite(event.inviteData)
)
}
is JoinRoomEvents.DeclineInvite -> {
is JoinRoomEvent.DeclineInvite -> {
acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(invite = event.inviteData, blockUser = event.blockUser, shouldConfirm = true)
AcceptDeclineInviteEvent.DeclineInvite(invite = event.inviteData, blockUser = event.blockUser, shouldConfirm = true)
)
}
is JoinRoomEvents.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction)
JoinRoomEvents.RetryFetchingContent -> {
is JoinRoomEvent.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction)
JoinRoomEvent.RetryFetchingContent -> {
retryCount++
}
JoinRoomEvents.ClearActionStates -> {
JoinRoomEvent.ClearActionStates -> {
knockAction.value = AsyncAction.Uninitialized
joinAction.value = AsyncAction.Uninitialized
cancelKnockAction.value = AsyncAction.Uninitialized
forgetRoomAction.value = AsyncAction.Uninitialized
}
is JoinRoomEvents.UpdateKnockMessage -> {
is JoinRoomEvent.UpdateKnockMessage -> {
knockMessage = event.message.take(MAX_KNOCK_MESSAGE_LENGTH)
}
JoinRoomEvents.DismissErrorAndHideContent -> {
JoinRoomEvent.DismissErrorAndHideContent -> {
isDismissingContent = true
}
JoinRoomEvents.ForgetRoom -> coroutineScope.forgetRoom(forgetRoomAction)
JoinRoomEvent.ForgetRoom -> coroutineScope.forgetRoom(forgetRoomAction)
}
}
@@ -37,7 +37,7 @@ data class JoinRoomState(
val knockMessage: String,
val hideInviteAvatars: Boolean,
val canReportRoom: Boolean,
val eventSink: (JoinRoomEvents) -> Unit
val eventSink: (JoinRoomEvent) -> Unit
) {
val isJoinActionUnauthorized = joinAction is AsyncAction.Failure && joinAction.error is JoinRoom.Failures.UnauthorizedJoin
val joinAuthorisationStatus = when (contentState) {
@@ -207,7 +207,7 @@ fun aJoinRoomState(
knockMessage: String = "",
hideInviteAvatars: Boolean = false,
canReportRoom: Boolean = true,
eventSink: (JoinRoomEvents) -> Unit = {}
eventSink: (JoinRoomEvent) -> Unit = {}
) = JoinRoomState(
roomIdOrAlias = roomIdOrAlias,
contentState = contentState,
@@ -115,33 +115,33 @@ fun JoinRoomView(
contentState = state.contentState,
knockMessage = state.knockMessage,
hideAvatarsImages = state.hideAvatarsImages,
onKnockMessageUpdate = { state.eventSink(JoinRoomEvents.UpdateKnockMessage(it)) },
onKnockMessageUpdate = { state.eventSink(JoinRoomEvent.UpdateKnockMessage(it)) },
)
},
footer = {
JoinRoomFooter(
joinAuthorisationStatus = state.joinAuthorisationStatus,
onAcceptInvite = { inviteData ->
state.eventSink(JoinRoomEvents.AcceptInvite(inviteData))
state.eventSink(JoinRoomEvent.AcceptInvite(inviteData))
},
onDeclineInvite = { inviteData, blockUser ->
if (state.canReportRoom && blockUser) {
onDeclineInviteAndBlockUser(inviteData)
} else {
state.eventSink(JoinRoomEvents.DeclineInvite(inviteData, blockUser = blockUser))
state.eventSink(JoinRoomEvent.DeclineInvite(inviteData, blockUser = blockUser))
}
},
onJoinRoom = {
state.eventSink(JoinRoomEvents.JoinRoom)
state.eventSink(JoinRoomEvent.JoinRoom)
},
onKnockRoom = {
state.eventSink(JoinRoomEvents.KnockRoom)
state.eventSink(JoinRoomEvent.KnockRoom)
},
onCancelKnock = {
state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = true))
state.eventSink(JoinRoomEvent.CancelKnock(requiresConfirmation = true))
},
onForgetRoom = {
state.eventSink(JoinRoomEvents.ForgetRoom)
state.eventSink(JoinRoomEvent.ForgetRoom)
},
onGoBack = onBackClick,
)
@@ -152,9 +152,9 @@ fun JoinRoomView(
RetryDialog(
title = stringResource(R.string.screen_join_room_loading_alert_title),
content = stringResource(CommonStrings.error_network_or_server_issue),
onRetry = { state.eventSink(JoinRoomEvents.RetryFetchingContent) },
onRetry = { state.eventSink(JoinRoomEvent.RetryFetchingContent) },
onDismiss = {
state.eventSink(JoinRoomEvents.DismissErrorAndHideContent)
state.eventSink(JoinRoomEvent.DismissErrorAndHideContent)
onBackClick()
}
)
@@ -166,7 +166,7 @@ fun JoinRoomView(
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
onSuccess = { onJoinSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
onErrorDismiss = { state.eventSink(JoinRoomEvent.ClearActionStates) },
)
}
AsyncActionView(
@@ -174,19 +174,19 @@ fun JoinRoomView(
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
onSuccess = { onKnockSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
onErrorDismiss = { state.eventSink(JoinRoomEvent.ClearActionStates) },
)
AsyncActionView(
async = state.forgetAction,
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
onSuccess = { onForgetSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
onErrorDismiss = { state.eventSink(JoinRoomEvent.ClearActionStates) },
)
AsyncActionView(
async = state.cancelKnockAction,
onSuccess = { onCancelKnockSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
onErrorDismiss = { state.eventSink(JoinRoomEvent.ClearActionStates) },
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
confirmationDialog = {
@@ -195,8 +195,8 @@ fun JoinRoomView(
title = stringResource(R.string.screen_join_room_cancel_knock_alert_title),
submitText = stringResource(R.string.screen_join_room_cancel_knock_alert_confirmation),
cancelText = stringResource(CommonStrings.action_no),
onSubmitClick = { state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = false)) },
onDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
onSubmitClick = { state.eventSink(JoinRoomEvent.CancelKnock(requiresConfirmation = false)) },
onDismiss = { state.eventSink(JoinRoomEvent.ClearActionStates) },
)
},
)
@@ -12,7 +12,7 @@ import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
import io.element.android.features.invite.api.toInviteData
@@ -365,7 +365,7 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is invited then accept and decline events are sent to acceptDeclinePresenter`() = runTest {
val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvents -> }
val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvent -> }
val acceptDeclinePresenter = Presenter {
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
}
@@ -388,14 +388,14 @@ class JoinRoomPresenterTest {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.AcceptInvite(inviteData))
state.eventSink(JoinRoomEvents.DeclineInvite(inviteData, false))
state.eventSink(JoinRoomEvent.AcceptInvite(inviteData))
state.eventSink(JoinRoomEvent.DeclineInvite(inviteData, false))
assert(eventSinkRecorder)
.isCalledExactly(2)
.withSequence(
listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))),
listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true))),
listOf(value(AcceptDeclineInviteEvent.AcceptInvite(inviteData))),
listOf(value(AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true))),
)
}
}
@@ -422,7 +422,7 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.JoinRoom)
state.eventSink(JoinRoomEvent.JoinRoom)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Loading)
@@ -453,14 +453,14 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.JoinRoom)
state.eventSink(JoinRoomEvent.JoinRoom)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Loading)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
state.eventSink(JoinRoomEvents.ClearActionStates)
state.eventSink(JoinRoomEvent.ClearActionStates)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Uninitialized)
@@ -480,7 +480,7 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.JoinRoom)
state.eventSink(JoinRoomEvent.JoinRoom)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Loading)
@@ -683,17 +683,17 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.UpdateKnockMessage(knockMessage))
state.eventSink(JoinRoomEvent.UpdateKnockMessage(knockMessage))
}
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.KnockRoom)
state.eventSink(JoinRoomEvent.KnockRoom)
}
assertThat(awaitItem().knockAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
assertThat(state.knockAction).isEqualTo(AsyncAction.Success(Unit))
fakeKnockRoom.lambda = knockRoomFailure
state.eventSink(JoinRoomEvents.KnockRoom)
state.eventSink(JoinRoomEvent.KnockRoom)
}
assertThat(awaitItem().knockAction).isEqualTo(AsyncAction.Loading)
@@ -731,17 +731,17 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.CancelKnock(true))
state.eventSink(JoinRoomEvent.CancelKnock(true))
}
awaitItem().also { state ->
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.ConfirmingNoParams)
state.eventSink(JoinRoomEvents.CancelKnock(false))
state.eventSink(JoinRoomEvent.CancelKnock(false))
}
assertThat(awaitItem().cancelKnockAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.Success(Unit))
cancelKnockRoom.lambda = cancelKnockRoomFailure
state.eventSink(JoinRoomEvents.CancelKnock(false))
state.eventSink(JoinRoomEvent.CancelKnock(false))
}
assertThat(awaitItem().cancelKnockAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
@@ -778,14 +778,14 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.ForgetRoom)
state.eventSink(JoinRoomEvent.ForgetRoom)
}
assertThat(awaitItem().forgetAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
assertThat(state.forgetAction).isEqualTo(AsyncAction.Success(Unit))
fakeForgetRoom.lambda = forgetRoomFailure
state.eventSink(JoinRoomEvents.ForgetRoom)
state.eventSink(JoinRoomEvent.ForgetRoom)
}
assertThat(awaitItem().forgetAction).isEqualTo(AsyncAction.Loading)
@@ -34,7 +34,7 @@ import org.junit.Test
class JoinRoomViewTest : RobolectricTest() {
@Test
fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<JoinRoomEvent>(expectEvents = false)
ensureCalledOnce {
setJoinRoomView(
aJoinRoomState(
@@ -48,7 +48,7 @@ class JoinRoomViewTest : RobolectricTest() {
@Test
fun `clicking on Join room on CanJoin room emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin),
@@ -56,12 +56,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_join_room_join_action)
eventsRecorder.assertSingle(JoinRoomEvents.JoinRoom)
eventsRecorder.assertSingle(JoinRoomEvent.JoinRoom)
}
@Test
fun `clicking on Knock room on CanKnock room emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
@@ -70,12 +70,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_join_room_knock_action)
eventsRecorder.assertSingle(JoinRoomEvents.KnockRoom)
eventsRecorder.assertSingle(JoinRoomEvent.KnockRoom)
}
@Test
fun `clicking on closing Knock error emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
@@ -84,12 +84,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
eventsRecorder.assertSingle(JoinRoomEvent.ClearActionStates)
}
@Test
fun `clicking on cancel knock request emit the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked),
@@ -97,12 +97,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_join_room_cancel_knock_action)
eventsRecorder.assertSingle(JoinRoomEvents.CancelKnock(true))
eventsRecorder.assertSingle(JoinRoomEvent.CancelKnock(true))
}
@Test
fun `clicking on closing Cancel Knock error emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked),
@@ -111,12 +111,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
eventsRecorder.assertSingle(JoinRoomEvent.ClearActionStates)
}
@Test
fun `clicking on closing Join error emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
@@ -125,12 +125,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
eventsRecorder.assertSingle(JoinRoomEvent.ClearActionStates)
}
@Test
fun `when joining room is successful, the expected callback is invoked`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<JoinRoomEvent>(expectEvents = false)
ensureCalledOnce {
setJoinRoomView(
aJoinRoomState(
@@ -144,7 +144,7 @@ class JoinRoomViewTest : RobolectricTest() {
@Test
fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
val inviteData = anInviteData()
setJoinRoomView(
aJoinRoomState(
@@ -153,12 +153,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_accept)
eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite(inviteData))
eventsRecorder.assertSingle(JoinRoomEvent.AcceptInvite(inviteData))
}
@Test
fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
val inviteData = anInviteData()
setJoinRoomView(
aJoinRoomState(
@@ -167,13 +167,13 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_decline)
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, false))
eventsRecorder.assertSingle(JoinRoomEvent.DeclineInvite(inviteData, false))
}
@Test
fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and can report room, the expected callback is invoked`() {
runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<JoinRoomEvent>(expectEvents = false)
val inviteData = anInviteData()
val joinRoomState = aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())),
@@ -192,7 +192,7 @@ class JoinRoomViewTest : RobolectricTest() {
@Test
fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and cant report room, emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
val inviteData = anInviteData()
val joinRoomState = aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())),
@@ -201,12 +201,12 @@ class JoinRoomViewTest : RobolectricTest() {
)
setJoinRoomView(state = joinRoomState)
clickOn(R.string.screen_join_room_decline_and_block_button_title)
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, true))
eventsRecorder.assertSingle(JoinRoomEvent.DeclineInvite(inviteData, true))
}
@Test
fun `clicking on Retry when an error occurs emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aFailureContentState(),
@@ -214,12 +214,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_retry)
eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent)
eventsRecorder.assertSingle(JoinRoomEvent.RetryFetchingContent)
}
@Test
fun `clicking on ok when user is unauthorized the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<JoinRoomEvent>(expectEvents = false)
ensureCalledOnce {
setJoinRoomView(
aJoinRoomState(
@@ -235,7 +235,7 @@ class JoinRoomViewTest : RobolectricTest() {
@Test
fun `clicking on forget when user is banned invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(null, null)),
@@ -243,7 +243,7 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_join_room_forget_action)
eventsRecorder.assertSingle(JoinRoomEvents.ForgetRoom)
eventsRecorder.assertSingle(JoinRoomEvent.ForgetRoom)
}
}
@@ -8,7 +8,7 @@
package io.element.android.features.knockrequests.impl.banner
sealed interface KnockRequestsBannerEvents {
data object AcceptSingleRequest : KnockRequestsBannerEvents
data object Dismiss : KnockRequestsBannerEvents
sealed interface KnockRequestsBannerEvent {
data object AcceptSingleRequest : KnockRequestsBannerEvent
data object Dismiss : KnockRequestsBannerEvent
}
@@ -53,15 +53,15 @@ class KnockRequestsBannerPresenter(
}
}
fun handleEvent(event: KnockRequestsBannerEvents) {
fun handleEvent(event: KnockRequestsBannerEvent) {
when (event) {
is KnockRequestsBannerEvents.AcceptSingleRequest -> {
is KnockRequestsBannerEvent.AcceptSingleRequest -> {
sessionCoroutineScope.acceptSingleKnockRequest(
knockRequests = knockRequests,
displayAcceptError = showAcceptError,
)
}
is KnockRequestsBannerEvents.Dismiss -> {
is KnockRequestsBannerEvent.Dismiss -> {
sessionCoroutineScope.launch {
knockRequestsService.markAllKnockRequestsAsSeen()
}
@@ -20,7 +20,7 @@ data class KnockRequestsBannerState(
val knockRequests: ImmutableList<KnockRequestPresentable>,
val displayAcceptError: Boolean,
val canAccept: Boolean,
val eventSink: (KnockRequestsBannerEvents) -> Unit,
val eventSink: (KnockRequestsBannerEvent) -> Unit,
) {
val subtitle = knockRequests.singleOrNull()?.userId?.value
val reason = knockRequests.singleOrNull()?.reason
@@ -64,7 +64,7 @@ fun aKnockRequestsBannerState(
displayAcceptError: Boolean = false,
canAccept: Boolean = true,
isVisible: Boolean = true,
eventSink: (KnockRequestsBannerEvents) -> Unit = {}
eventSink: (KnockRequestsBannerEvent) -> Unit = {}
) = KnockRequestsBannerState(
knockRequests = knockRequests.toImmutableList(),
displayAcceptError = displayAcceptError,

Some files were not shown because too many files have changed in this diff Show More