mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 08:15:02 +00:00
Merge remote-tracking branch 'origin/develop' into HEAD
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
=============================
|
||||
|
||||
|
||||
+6
-6
@@ -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,
|
||||
)
|
||||
|
||||
+4
-3
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
|
||||
+1
-1
Submodule enterprise updated: e633a343b1...53750903f2
@@ -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
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
+2
-2
@@ -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,
|
||||
)
|
||||
|
||||
+2
-2
@@ -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(
|
||||
|
||||
+3
-3
@@ -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()
|
||||
|
||||
+2
-2
@@ -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
|
||||
)
|
||||
|
||||
+3
-3
@@ -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)
|
||||
|
||||
+3
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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()
|
||||
}
|
||||
|
||||
+3
-3
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+9
-9
@@ -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
|
||||
}
|
||||
+9
-9
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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) &&
|
||||
|
||||
+1
-1
@@ -143,7 +143,7 @@ fun aConfigureRoomState(
|
||||
)
|
||||
},
|
||||
spaces: List<SpaceRoom> = emptyList(),
|
||||
eventSink: (ConfigureRoomEvents) -> Unit = { },
|
||||
eventSink: (ConfigureRoomEvent) -> Unit = { },
|
||||
) = ConfigureRoomState(
|
||||
config = config,
|
||||
isSpace = isSpace,
|
||||
|
||||
+9
-9
@@ -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(
|
||||
|
||||
+36
-36
@@ -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()
|
||||
|
||||
+5
-5
@@ -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
|
||||
}
|
||||
+5
-5
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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 &&
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+7
-7
@@ -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(),
|
||||
|
||||
+15
-15
@@ -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)
|
||||
}
|
||||
|
||||
+13
-13
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ fun ForwardMessagesView(
|
||||
stringResource(id = CommonStrings.error_unknown)
|
||||
},
|
||||
onErrorDismiss = {
|
||||
state.eventSink(ForwardMessagesEvents.ClearError)
|
||||
state.eventSink(ForwardMessagesEvent.ClearError)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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()
|
||||
}
|
||||
|
||||
+3
-3
@@ -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(
|
||||
|
||||
+3
-3
@@ -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
|
||||
}
|
||||
+3
-3
@@ -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()
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
)
|
||||
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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()
|
||||
|
||||
+3
-3
@@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
+3
-3
@@ -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(),
|
||||
)
|
||||
|
||||
+55
-49
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -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 -> {
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
-3
@@ -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
|
||||
|
||||
+1
-1
@@ -8,4 +8,4 @@
|
||||
|
||||
package io.element.android.features.home.impl.spaces
|
||||
|
||||
sealed interface HomeSpacesEvents
|
||||
sealed interface HomeSpacesEvent
|
||||
+1
-1
@@ -39,7 +39,7 @@ class HomeSpacesPresenter(
|
||||
seenInvitesStore.seenRoomIds().map { it.toImmutableSet() }
|
||||
}.collectAsState(persistentSetOf())
|
||||
|
||||
fun handleEvent(event: HomeSpacesEvents) {
|
||||
fun handleEvent(event: HomeSpacesEvent) {
|
||||
// when (event) { }
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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(),
|
||||
|
||||
+1
-1
@@ -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,
|
||||
)
|
||||
|
||||
+26
@@ -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" },
|
||||
|
||||
+4
-4
@@ -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))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
|
||||
}
|
||||
+1
-1
@@ -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,
|
||||
)
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+6
-6
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+4
-4
@@ -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
|
||||
}
|
||||
+6
-6
@@ -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
|
||||
}
|
||||
+6
-6
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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()
|
||||
}
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+6
-6
@@ -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()
|
||||
|
||||
+11
-11
@@ -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 ->
|
||||
|
||||
+7
-7
@@ -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 ->
|
||||
|
||||
+11
-11
@@ -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!"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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
|
||||
}
|
||||
+1
-1
@@ -28,5 +28,5 @@ interface InvitePeopleState {
|
||||
val createRoomFromDmAction: AsyncAction<RoomId>
|
||||
|
||||
/** Where the UI sends its events. */
|
||||
val eventSink: (InvitePeopleEvents) -> Unit
|
||||
val eventSink: (InvitePeopleEvent) -> Unit
|
||||
}
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+6
-6
@@ -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
|
||||
}
|
||||
+10
-10
@@ -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
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+7
-7
@@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+29
-29
@@ -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)
|
||||
|
||||
|
||||
+11
-11
@@ -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
|
||||
}
|
||||
+14
-14
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+15
-15
@@ -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) },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
+18
-18
@@ -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)
|
||||
|
||||
+26
-26
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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
|
||||
}
|
||||
+3
-3
@@ -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()
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
Reference in New Issue
Block a user