Merge branch 'release/26.02.0'
This commit is contained in:
commit
b9c6a8e530
3067 changed files with 10841 additions and 8400 deletions
3
.github/renovate.json
vendored
3
.github/renovate.json
vendored
|
|
@ -22,7 +22,8 @@
|
||||||
{
|
{
|
||||||
"versioning": "semver",
|
"versioning": "semver",
|
||||||
"matchPackageNames": [
|
"matchPackageNames": [
|
||||||
"/^org.maplibre/"
|
"/^org.maplibre/",
|
||||||
|
"/^org.jetbrains.kotlinx:kotlinx-datetime/"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
22
.github/workflows/stale-issues.yml
vendored
Normal file
22
.github/workflows/stale-issues.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
name: Close stale issues that are missing info.
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v10
|
||||||
|
with:
|
||||||
|
only-labels: "X-Needs-Info"
|
||||||
|
days-before-issue-stale: 30
|
||||||
|
days-before-issue-close: 7
|
||||||
|
days-before-pr-stale: -1
|
||||||
|
stale-issue-label: "stale"
|
||||||
|
labels-to-remove-when-unstale: "X-Needs-Info"
|
||||||
|
stale-issue-message: "This issue has been awaiting further information for the past 30 days so will now be marked as stale. Please provide the requested information within the next 7 days to keep it open."
|
||||||
|
close-issue-message: "This issue is being closed due to inactivity after further information was requested."
|
||||||
4
.idea/kotlinc.xml
generated
4
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="2.3.0" />
|
<option name="version" value="2.3.10" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -9,29 +9,35 @@ appId: ${MAESTRO_APP_ID}
|
||||||
id: "login-continue"
|
id: "login-continue"
|
||||||
## MAS page
|
## MAS page
|
||||||
## Conditional workflow to pass the Chrome first launch welcome page.
|
## Conditional workflow to pass the Chrome first launch welcome page.
|
||||||
- runFlow:
|
- retry:
|
||||||
when:
|
maxRetries: 3
|
||||||
visible: 'Use without an account'
|
|
||||||
commands:
|
commands:
|
||||||
- tapOn: "Use without an account"
|
- runFlow:
|
||||||
## For older chrome versions
|
when:
|
||||||
- runFlow:
|
visible: 'Use without an account'
|
||||||
when:
|
commands:
|
||||||
visible: 'Accept & continue'
|
- tapOn: "Use without an account"
|
||||||
commands:
|
## For older chrome versions
|
||||||
- tapOn: "Accept & continue"
|
- runFlow:
|
||||||
- runFlow:
|
when:
|
||||||
when:
|
visible: 'Accept & continue'
|
||||||
visible: 'No thanks'
|
commands:
|
||||||
commands:
|
- tapOn: "Accept & continue"
|
||||||
- tapOn: "No thanks"
|
- runFlow:
|
||||||
|
when:
|
||||||
|
visible: 'No thanks'
|
||||||
|
commands:
|
||||||
|
- tapOn: "No thanks"
|
||||||
## Working when running Maestro locally, but not on the CI yet.
|
## Working when running Maestro locally, but not on the CI yet.
|
||||||
- extendedWaitUntil:
|
- retry:
|
||||||
visible:
|
maxRetries: 3
|
||||||
id: "form-1"
|
commands:
|
||||||
timeout: 10000
|
- extendedWaitUntil:
|
||||||
|
visible:
|
||||||
|
id: "form-1"
|
||||||
|
timeout: 10000
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "form-1"
|
id: "form-1"
|
||||||
- inputText: ${MAESTRO_USERNAME}
|
- inputText: ${MAESTRO_USERNAME}
|
||||||
- pressKey: Enter
|
- pressKey: Enter
|
||||||
- tapOn:
|
- tapOn:
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,8 @@ appId: ${MAESTRO_APP_ID}
|
||||||
- scroll
|
- scroll
|
||||||
- tapOn: "Leave room"
|
- tapOn: "Leave room"
|
||||||
- tapOn: "Leave"
|
- tapOn: "Leave"
|
||||||
|
- runFlow:
|
||||||
|
when:
|
||||||
|
visible: 'You need an invite in order to join'
|
||||||
|
commands:
|
||||||
|
- tapOn: "Back"
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ appId: ${MAESTRO_APP_ID}
|
||||||
# Purpose: Test the creation and deletion of a room
|
# Purpose: Test the creation and deletion of a room
|
||||||
- tapOn: "Create a new conversation or room"
|
- tapOn: "Create a new conversation or room"
|
||||||
- tapOn: "New room"
|
- tapOn: "New room"
|
||||||
- tapOn: "e.g. your project name"
|
- tapOn: "Add name…"
|
||||||
- inputText: "aRoomName"
|
- inputText: "aRoomName"
|
||||||
- tapOn: "What is this room about?"
|
- tapOn: "Add description…"
|
||||||
- inputText: "aRoomTopic"
|
- inputText: "aRoomTopic"
|
||||||
- tapOn: "Create"
|
- tapOn: "Create"
|
||||||
- takeScreenshot: build/maestro/320-createAndDeleteRoom
|
- takeScreenshot: build/maestro/320-createAndDeleteRoom
|
||||||
|
|
@ -37,3 +37,8 @@ appId: ${MAESTRO_APP_ID}
|
||||||
- scroll
|
- scroll
|
||||||
- tapOn: "Leave room"
|
- tapOn: "Leave room"
|
||||||
- tapOn: "Leave"
|
- tapOn: "Leave"
|
||||||
|
- runFlow:
|
||||||
|
when:
|
||||||
|
visible: 'You need an invite in order to join'
|
||||||
|
commands:
|
||||||
|
- tapOn: "Back"
|
||||||
|
|
|
||||||
67
CHANGES.md
67
CHANGES.md
|
|
@ -1,3 +1,70 @@
|
||||||
|
Changes in Element X v26.01.2
|
||||||
|
=============================
|
||||||
|
|
||||||
|
<!-- Release notes generated using configuration in .github/release.yml at v26.01.2 -->
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
### ✨ Features
|
||||||
|
* Display a badge for messages decrypted using shared keys by @richvdh in https://github.com/element-hq/element-x-android/pull/6023
|
||||||
|
* Add empty state view for HomeSpacesView by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6047
|
||||||
|
* Create a new room in a space by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6061
|
||||||
|
* Add an empty state for the space screen if the user can modify its graph by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6064
|
||||||
|
* Add 'Create room' option to menu in space screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6095
|
||||||
|
### 🙌 Improvements
|
||||||
|
* Add suggestions section to InvitePeopleView by @ganfra in https://github.com/element-hq/element-x-android/pull/6045
|
||||||
|
* Show an icon in the room header for shared history by @richvdh in https://github.com/element-hq/element-x-android/pull/6090
|
||||||
|
* Remove "history may be shared" banner. by @kaylendog in https://github.com/element-hq/element-x-android/pull/6087
|
||||||
|
### 🐛 Bugfixes
|
||||||
|
* Make `relatedTo` in `RoomSendQueueUpdate.MediaUpload` a transaction id by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6002
|
||||||
|
* Tweak the power levels when creating a space by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6012
|
||||||
|
* Keep the child state in `AttachmentsPreviewPresenter` up to date by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6039
|
||||||
|
* Ensure screenshot is up to date by @bmarty in https://github.com/element-hq/element-x-android/pull/6040
|
||||||
|
* First try to resolve the room before checking for the alias validity by @bmarty in https://github.com/element-hq/element-x-android/pull/6066
|
||||||
|
* Use `MediaPreviewValue.Private` to check if media should be displayed in notifications by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6038
|
||||||
|
* Fix RoomDetailsEditView avatar picker for spaces by @ganfra in https://github.com/element-hq/element-x-android/pull/6074
|
||||||
|
* Try fixing performance metrics by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6046
|
||||||
|
* Fix rageshakes not uploading if they are too long by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6075
|
||||||
|
* Display a confirmation dialog when ending a poll from the event bottom sheet by @bmarty in https://github.com/element-hq/element-x-android/pull/6092
|
||||||
|
### 🗣 Translations
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6033
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6042
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6085
|
||||||
|
### 🚧 In development 🚧
|
||||||
|
* Implement Space 'Add existing rooms' feature by @ganfra in https://github.com/element-hq/element-x-android/pull/6063
|
||||||
|
### Dependency upgrades
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.1.16 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6025
|
||||||
|
* chore(deps): update plugin dependencycheck to v12.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5993
|
||||||
|
* fix(deps): update wysiwyg to v2.41.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6037
|
||||||
|
* fix(deps): update metro to v0.10.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6043
|
||||||
|
* fix(deps): update dependency io.sentry:sentry-android to v8.31.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6057
|
||||||
|
* chore(deps): update peter-evans/create-pull-request action to v8.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6055
|
||||||
|
* fix(deps): update dependency org.robolectric:robolectric to v4.16.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6050
|
||||||
|
* fix(deps): update kotlin by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6058
|
||||||
|
* fix(deps): update dependency io.nlopez.compose.rules:detekt to v0.5.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6060
|
||||||
|
* fix(deps): update dependency io.nlopez.compose.rules:detekt to v0.5.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6062
|
||||||
|
* fix(deps): update dependency com.posthog:posthog-android to v3.29.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6056
|
||||||
|
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v26.1.22 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6065
|
||||||
|
* fix(deps): update metro to v0.10.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6077
|
||||||
|
* fix(deps): update kotlin by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6076
|
||||||
|
* fix(deps): update roborazzi to v1.57.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6080
|
||||||
|
* fix(deps): update media3 to v1.9.1 - autoclosed by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6086
|
||||||
|
* fix(deps): update dependency io.mockk:mockk to v1.14.9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6089
|
||||||
|
* Update dependency io.nlopez.compose.rules:detekt to v0.5.6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6093
|
||||||
|
* Update dependency net.zetetic:sqlcipher-android to v4.13.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6094
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.1.27 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6096
|
||||||
|
* Update dependency com.google.testparameterinjector:test-parameter-injector to v1.21 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6100
|
||||||
|
### Others
|
||||||
|
* Let rageshake description mention when a log file is missing by @bmarty in https://github.com/element-hq/element-x-android/pull/6027
|
||||||
|
* Provide `ConfigureRoomState.availableVisibilityOptions` in presenter by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6024
|
||||||
|
* Attempt to fix flaky test. by @bmarty in https://github.com/element-hq/element-x-android/pull/6016
|
||||||
|
* sdk : allow passing coroutineScope to RoomList by @ganfra in https://github.com/element-hq/element-x-android/pull/6054
|
||||||
|
* Let SearchBar/SearchField use TextFieldState by @ganfra in https://github.com/element-hq/element-x-android/pull/6072
|
||||||
|
* Remove obfuscation in proguard by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6067
|
||||||
|
* Sync all strings and fix compilation issue. by @bmarty in https://github.com/element-hq/element-x-android/pull/6088
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.01.1...v26.01.2
|
||||||
|
|
||||||
Changes in Element X v26.01.1
|
Changes in Element X v26.01.1
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
|
||||||
|
|
@ -54,7 +55,8 @@ class LoadingBaseRoomStateFlowFactoryTest {
|
||||||
@Test
|
@Test
|
||||||
fun `flow should emit Loading and then Loaded when there is a room in cache after SS is loaded`() = runTest {
|
fun `flow should emit Loading and then Loaded when there is a room in cache after SS is loaded`() = runTest {
|
||||||
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID))
|
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID))
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList()
|
||||||
|
val roomListService = FakeRoomListService(allRooms = roomList)
|
||||||
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
|
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
|
||||||
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
|
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
|
||||||
flowFactory
|
flowFactory
|
||||||
|
|
@ -62,21 +64,22 @@ class LoadingBaseRoomStateFlowFactoryTest {
|
||||||
.test {
|
.test {
|
||||||
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
|
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
|
||||||
matrixClient.givenGetRoomResult(A_ROOM_ID, room)
|
matrixClient.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
roomList.loadingState.emit(RoomList.LoadingState.Loaded(1))
|
||||||
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
|
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `flow should emit Loading and then Error when there is no room in cache after SS is loaded`() = runTest {
|
fun `flow should emit Loading and then Error when there is no room in cache after SS is loaded`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList()
|
||||||
|
val roomListService = FakeRoomListService(allRooms = roomList)
|
||||||
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
|
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
|
||||||
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
|
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
|
||||||
flowFactory
|
flowFactory
|
||||||
.create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null)
|
.create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null)
|
||||||
.test {
|
.test {
|
||||||
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
|
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
roomList.loadingState.emit(RoomList.LoadingState.Loaded(1))
|
||||||
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Error)
|
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
* [Logging](#logging)
|
* [Logging](#logging)
|
||||||
* [Translations](#translations)
|
* [Translations](#translations)
|
||||||
* [Rageshake](#rageshake)
|
* [Rageshake](#rageshake)
|
||||||
|
* [Developer options](#developer-options)
|
||||||
* [Tips](#tips)
|
* [Tips](#tips)
|
||||||
* [Happy coding!](#happy-coding)
|
* [Happy coding!](#happy-coding)
|
||||||
|
|
||||||
|
|
@ -409,14 +410,31 @@ The data will be sent to an internal server, which is not publicly accessible. A
|
||||||
|
|
||||||
Rageshake can be very useful to get logs from a release version of the application.
|
Rageshake can be very useful to get logs from a release version of the application.
|
||||||
|
|
||||||
|
|
||||||
|
#### Developer options
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Developer options can result in unexpected application behavior or destructive
|
||||||
|
> actions. Use with caution and only if you are instructed by someone at Element or are
|
||||||
|
> already familiar.
|
||||||
|
|
||||||
|
These options provide advanced controls for testing and debugging. They are visible by
|
||||||
|
default in debug and nightly builds but are hidden in release versions.
|
||||||
|
|
||||||
|
**Enabling in release builds:** Navigate to application settings and tap the version
|
||||||
|
number at the bottom 7 times. After tapping, a new "Developer options" entry will appear
|
||||||
|
at the bottom of the list.
|
||||||
|
|
||||||
|
The developer options include feature flags, notification/push history, Element call
|
||||||
|
customization, Rust SDK log levels, per-feature tracing toggles, Showkase to debug UI
|
||||||
|
components, rageshake controls, app crash controls, cache details/controls, persistent
|
||||||
|
storage maintenance tasks.
|
||||||
|
|
||||||
|
Keywords: Developer settings, developer mode
|
||||||
|
|
||||||
|
|
||||||
### Tips
|
### Tips
|
||||||
|
|
||||||
- Element Android has a `developer mode` in the `Settings/Advanced settings`. Other useful options are available here; (TODO Not supported yet!)
|
|
||||||
- Show hidden Events can also help to debug feature. When developer mode is enabled, it is possible to view the source (= the Json content) of any Events; (TODO
|
|
||||||
Not supported yet!)
|
|
||||||
- Type `/devtools` in a Room composer to access a developer menu. There are some other entry points. Developer mode has to be enabled; (TODO Not supported yet!)
|
|
||||||
- Hidden debug menu: when developer mode is enabled and on debug build, there are some extra screens that can be accessible using the green wheel. In those
|
|
||||||
screens, it will be possible to toggle some feature flags; (TODO Not supported yet!)
|
|
||||||
- Using logcat, filtering with `Compositions` can help you to understand what screen are currently displayed on your device. Searching for string displayed on
|
- Using logcat, filtering with `Compositions` can help you to understand what screen are currently displayed on your device. Searching for string displayed on
|
||||||
the screen can also help to find the running code in the codebase.
|
the screen can also help to find the running code in the codebase.
|
||||||
- When this is possible, prefer using `sealed interface` instead of `sealed class`;
|
- When this is possible, prefer using `sealed interface` instead of `sealed class`;
|
||||||
|
|
|
||||||
2
fastlane/metadata/android/en-US/changelogs/202602000.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/202602000.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: bug fixes and improvements.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
|
|
@ -149,15 +149,18 @@ class CallScreenPresenter(
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
if (callType is CallType.RoomCall) {
|
||||||
// Wait for the call to be joined, if it takes too long, we display an error
|
// Note: For external calls isWidgetLoaded will always be false
|
||||||
delay(10.seconds)
|
LaunchedEffect(Unit) {
|
||||||
|
// Wait for the call to be joined, if it takes too long, we display an error
|
||||||
|
delay(10.seconds)
|
||||||
|
|
||||||
if (!isWidgetLoaded) {
|
if (!isWidgetLoaded) {
|
||||||
Timber.w("The call took too long to load. Displaying an error before exiting.")
|
Timber.w("The call took too long to load. Displaying an error before exiting.")
|
||||||
|
|
||||||
// This will display a simple 'Sorry, an error occurred' dialog and force the user to exit the call
|
// This will display a simple 'Sorry, an error occurred' dialog and force the user to exit the call
|
||||||
webViewError = ""
|
webViewError = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,10 +82,6 @@ class ConfigureRoomPresenter(
|
||||||
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
|
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
|
||||||
private var pendingPermissionRequest = false
|
private var pendingPermissionRequest = false
|
||||||
|
|
||||||
init {
|
|
||||||
dataStore.setIsSpace(isSpace)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ConfigureRoomState {
|
override fun present(): ConfigureRoomState {
|
||||||
val canAddRoomToSpace by featureFlagService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
|
val canAddRoomToSpace by featureFlagService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
|
||||||
|
|
@ -123,9 +119,10 @@ class ConfigureRoomPresenter(
|
||||||
} else {
|
} else {
|
||||||
persistentListOf()
|
persistentListOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
val parentSpace = spaces.find { it.roomId == initialParentSpaceId }
|
val parentSpace = spaces.find { it.roomId == initialParentSpaceId }
|
||||||
parentSpace?.let { dataStore.setParentSpace(it) }
|
parentSpace?.let {
|
||||||
|
dataStore.setParentSpace(parentSpace = parentSpace, updateVisibility = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(cameraPermissionState.permissionGranted) {
|
LaunchedEffect(cameraPermissionState.permissionGranted) {
|
||||||
|
|
@ -152,21 +149,42 @@ class ConfigureRoomPresenter(
|
||||||
// 2. If it has a parent space.
|
// 2. If it has a parent space.
|
||||||
// 3. If knocking is enabled.
|
// 3. If knocking is enabled.
|
||||||
val parentSpace = createRoomConfig.parentSpace
|
val parentSpace = createRoomConfig.parentSpace
|
||||||
val availableJoinRules = remember(createRoomConfig.parentSpace, isSpace, isKnockFeatureEnabled) {
|
val availableJoinRules = remember(parentSpace, isSpace, isKnockFeatureEnabled) {
|
||||||
when {
|
when {
|
||||||
isSpace && parentSpace != null -> TODO("Adding a space to a parent space is not supported yet! How did you get here?")
|
isSpace && parentSpace != null -> TODO("Adding a space to a parent space is not supported yet! How did you get here?")
|
||||||
parentSpace == null || parentSpace.joinRule == JoinRule.Public -> listOfNotNull(
|
parentSpace == null || parentSpace.joinRule == JoinRule.Public -> listOfNotNull(
|
||||||
JoinRuleItem.PublicVisibility.Public,
|
JoinRuleItem.PublicVisibility.Public,
|
||||||
JoinRuleItem.PublicVisibility.AskToJoin.takeIf { !isSpace && isKnockFeatureEnabled },
|
JoinRuleItem.PublicVisibility.AskToJoin.takeIf { !isSpace && isKnockFeatureEnabled },
|
||||||
JoinRuleItem.Private,
|
JoinRuleItem.PrivateVisibility.Private,
|
||||||
).toImmutableList()
|
).toImmutableList()
|
||||||
else -> listOfNotNull(
|
else -> listOfNotNull(
|
||||||
JoinRuleItem.PublicVisibility.Restricted(parentSpace.roomId),
|
JoinRuleItem.PrivateVisibility.Restricted(parentSpace.roomId),
|
||||||
JoinRuleItem.PublicVisibility.AskToJoinRestricted(parentSpace.roomId).takeIf { !isSpace && isKnockFeatureEnabled },
|
JoinRuleItem.PrivateVisibility.AskToJoinRestricted(parentSpace.roomId).takeIf { isKnockFeatureEnabled },
|
||||||
JoinRuleItem.Private,
|
JoinRuleItem.PrivateVisibility.Private,
|
||||||
).toImmutableList()
|
).toImmutableList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val currentJoinRule = createRoomConfig.visibilityState.joinRuleItem
|
||||||
|
LaunchedEffect(availableJoinRules, currentJoinRule) {
|
||||||
|
// Find matching rule by type (ignoring parentSpaceId parameter for Restricted types)
|
||||||
|
val matchingRule = when (currentJoinRule) {
|
||||||
|
is JoinRuleItem.PrivateVisibility.Restricted ->
|
||||||
|
availableJoinRules.filterIsInstance<JoinRuleItem.PrivateVisibility.Restricted>().firstOrNull()
|
||||||
|
is JoinRuleItem.PrivateVisibility.AskToJoinRestricted ->
|
||||||
|
availableJoinRules.filterIsInstance<JoinRuleItem.PrivateVisibility.AskToJoinRestricted>().firstOrNull()
|
||||||
|
else -> availableJoinRules.find { it == currentJoinRule }
|
||||||
|
}
|
||||||
|
when {
|
||||||
|
matchingRule == null -> {
|
||||||
|
// No matching type fallback to Private (always available)
|
||||||
|
dataStore.setJoinRule(JoinRuleItem.PrivateVisibility.Private)
|
||||||
|
}
|
||||||
|
matchingRule != currentJoinRule -> {
|
||||||
|
// Same type but different params (e.g., different parentSpaceId), update
|
||||||
|
dataStore.setJoinRule(matchingRule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun createRoom(config: CreateRoomConfig) {
|
fun createRoom(config: CreateRoomConfig) {
|
||||||
createRoomAction.value = AsyncAction.Uninitialized
|
createRoomAction.value = AsyncAction.Uninitialized
|
||||||
|
|
@ -193,7 +211,7 @@ class ConfigureRoomPresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ConfigureRoomEvents.SetParentSpace -> {
|
is ConfigureRoomEvents.SetParentSpace -> {
|
||||||
dataStore.setParentSpace(event.space)
|
dataStore.setParentSpace(event.space, false)
|
||||||
}
|
}
|
||||||
ConfigureRoomEvents.CancelCreateRoom -> {
|
ConfigureRoomEvents.CancelCreateRoom -> {
|
||||||
createRoomAction.value = AsyncAction.Uninitialized
|
createRoomAction.value = AsyncAction.Uninitialized
|
||||||
|
|
@ -210,6 +228,7 @@ class ConfigureRoomPresenter(
|
||||||
roomAddressValidity = roomAddressValidity.value,
|
roomAddressValidity = roomAddressValidity.value,
|
||||||
availableJoinRules = availableJoinRules,
|
availableJoinRules = availableJoinRules,
|
||||||
spaces = spaces,
|
spaces = spaces,
|
||||||
|
isSpace = isSpace,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -220,35 +239,41 @@ class ConfigureRoomPresenter(
|
||||||
) = launch {
|
) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
val avatarUrl = config.avatarUri?.let { uploadAvatar(it.toUri()) }
|
val avatarUrl = config.avatarUri?.let { uploadAvatar(it.toUri()) }
|
||||||
val params = if (config.visibilityState is RoomVisibilityState.Public) {
|
val params = when (config.visibilityState) {
|
||||||
CreateRoomParameters(
|
is RoomVisibilityState.Public -> {
|
||||||
name = config.roomName,
|
CreateRoomParameters(
|
||||||
topic = config.topic,
|
name = config.roomName,
|
||||||
isEncrypted = false,
|
topic = config.topic,
|
||||||
isDirect = false,
|
isEncrypted = false,
|
||||||
visibility = RoomVisibility.Public,
|
isDirect = false,
|
||||||
joinRuleOverride = config.visibilityState.joinRuleItem.toJoinRule()
|
visibility = RoomVisibility.Public,
|
||||||
// No need to specify the public join rule override, since the preset is already PUBLIC_CHAT
|
joinRuleOverride = config.visibilityState.joinRuleItem.toJoinRule()
|
||||||
.takeIf { it != JoinRule.Public },
|
// No need to specify the public join rule override, since the preset is already PUBLIC_CHAT
|
||||||
preset = RoomPreset.PUBLIC_CHAT,
|
.takeIf { it != JoinRule.Public },
|
||||||
invite = config.invites.map { it.userId },
|
preset = RoomPreset.PUBLIC_CHAT,
|
||||||
avatar = avatarUrl,
|
invite = config.invites.map { it.userId },
|
||||||
roomAliasName = config.visibilityState.roomAddress(),
|
avatar = avatarUrl,
|
||||||
isSpace = isSpace,
|
roomAliasName = config.visibilityState.roomAddress(),
|
||||||
)
|
isSpace = isSpace,
|
||||||
} else {
|
)
|
||||||
CreateRoomParameters(
|
}
|
||||||
name = config.roomName,
|
is RoomVisibilityState.Private -> {
|
||||||
topic = config.topic,
|
CreateRoomParameters(
|
||||||
isEncrypted = config.visibilityState is RoomVisibilityState.Private,
|
name = config.roomName,
|
||||||
isDirect = false,
|
topic = config.topic,
|
||||||
visibility = RoomVisibility.Private,
|
isEncrypted = true,
|
||||||
historyVisibilityOverride = RoomHistoryVisibility.Invited,
|
isDirect = false,
|
||||||
preset = RoomPreset.PRIVATE_CHAT,
|
visibility = RoomVisibility.Private,
|
||||||
invite = config.invites.map { it.userId },
|
historyVisibilityOverride = RoomHistoryVisibility.Invited,
|
||||||
avatar = avatarUrl,
|
joinRuleOverride = config.visibilityState.joinRuleItem.toJoinRule()
|
||||||
isSpace = isSpace,
|
// No need to specify the Invite join rule override, since the preset is already PRIVATE_CHAT
|
||||||
)
|
.takeIf { it != JoinRule.Invite },
|
||||||
|
preset = RoomPreset.PRIVATE_CHAT,
|
||||||
|
invite = config.invites.map { it.userId },
|
||||||
|
avatar = avatarUrl,
|
||||||
|
isSpace = isSpace,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val roomId = matrixClient.createRoom(params)
|
val roomId = matrixClient.createRoom(params)
|
||||||
.onFailure { failure ->
|
.onFailure { failure ->
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import io.element.android.libraries.permissions.api.PermissionsState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class ConfigureRoomState(
|
data class ConfigureRoomState(
|
||||||
|
val isSpace: Boolean,
|
||||||
val config: CreateRoomConfig,
|
val config: CreateRoomConfig,
|
||||||
val avatarActions: ImmutableList<AvatarAction>,
|
val avatarActions: ImmutableList<AvatarAction>,
|
||||||
val createRoomAction: AsyncAction<RoomId>,
|
val createRoomAction: AsyncAction<RoomId>,
|
||||||
|
|
@ -28,5 +29,6 @@ data class ConfigureRoomState(
|
||||||
val eventSink: (ConfigureRoomEvents) -> Unit
|
val eventSink: (ConfigureRoomEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
|
val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
|
||||||
(config.visibilityState is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid)
|
(config.visibilityState is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid) &&
|
||||||
|
config.visibilityState.joinRuleItem in availableJoinRules
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,8 +82,8 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
roomAddressValidity = RoomAddressValidity.Valid,
|
roomAddressValidity = RoomAddressValidity.Valid,
|
||||||
),
|
),
|
||||||
aConfigureRoomState(
|
aConfigureRoomState(
|
||||||
|
isSpace = true,
|
||||||
config = CreateRoomConfig(
|
config = CreateRoomConfig(
|
||||||
isSpace = true,
|
|
||||||
roomName = "Space 101",
|
roomName = "Space 101",
|
||||||
topic = "Space topic for this space when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
topic = "Space topic for this space when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||||
visibilityState = RoomVisibilityState.Public(
|
visibilityState = RoomVisibilityState.Public(
|
||||||
|
|
@ -95,13 +95,11 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
),
|
),
|
||||||
aConfigureRoomState(
|
aConfigureRoomState(
|
||||||
config = CreateRoomConfig(
|
config = CreateRoomConfig(
|
||||||
isSpace = false,
|
|
||||||
roomName = "Room 101",
|
roomName = "Room 101",
|
||||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||||
parentSpace = null,
|
parentSpace = null,
|
||||||
visibilityState = RoomVisibilityState.Public(
|
visibilityState = RoomVisibilityState.Private(
|
||||||
roomAddress = RoomAddress.AutoFilled("Space-101"),
|
joinRuleItem = JoinRuleItem.PrivateVisibility.Restricted(aSpaceRoom().roomId),
|
||||||
joinRuleItem = JoinRuleItem.PublicVisibility.Restricted(aSpaceRoom().roomId),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
spaces = listOf(aSpaceRoom()),
|
spaces = listOf(aSpaceRoom()),
|
||||||
|
|
@ -109,13 +107,11 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
),
|
),
|
||||||
aConfigureRoomState(
|
aConfigureRoomState(
|
||||||
config = CreateRoomConfig(
|
config = CreateRoomConfig(
|
||||||
isSpace = false,
|
|
||||||
roomName = "Room 101",
|
roomName = "Room 101",
|
||||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||||
parentSpace = aSpaceRoom(canonicalAlias = RoomAlias("#a-space-room:example.org")),
|
parentSpace = aSpaceRoom(canonicalAlias = RoomAlias("#a-space-room:example.org")),
|
||||||
visibilityState = RoomVisibilityState.Public(
|
visibilityState = RoomVisibilityState.Private(
|
||||||
roomAddress = RoomAddress.AutoFilled("Space-101"),
|
joinRuleItem = JoinRuleItem.PrivateVisibility.Restricted(aSpaceRoom().roomId),
|
||||||
joinRuleItem = JoinRuleItem.PublicVisibility.Restricted(aSpaceRoom().roomId),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
spaces = listOf(aSpaceRoom()),
|
spaces = listOf(aSpaceRoom()),
|
||||||
|
|
@ -126,6 +122,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
|
|
||||||
fun aConfigureRoomState(
|
fun aConfigureRoomState(
|
||||||
config: CreateRoomConfig = CreateRoomConfig(),
|
config: CreateRoomConfig = CreateRoomConfig(),
|
||||||
|
isSpace: Boolean = false,
|
||||||
isKnockFeatureEnabled: Boolean = true,
|
isKnockFeatureEnabled: Boolean = true,
|
||||||
avatarActions: List<AvatarAction> = emptyList(),
|
avatarActions: List<AvatarAction> = emptyList(),
|
||||||
createRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
createRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||||
|
|
@ -134,21 +131,22 @@ fun aConfigureRoomState(
|
||||||
roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Valid,
|
roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Valid,
|
||||||
availableVisibilityOptions: List<JoinRuleItem> = if (config.parentSpace != null) {
|
availableVisibilityOptions: List<JoinRuleItem> = if (config.parentSpace != null) {
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
JoinRuleItem.PublicVisibility.Restricted(config.parentSpace.roomId),
|
JoinRuleItem.PrivateVisibility.Restricted(config.parentSpace.roomId),
|
||||||
JoinRuleItem.PublicVisibility.AskToJoinRestricted(config.parentSpace.roomId).takeIf { isKnockFeatureEnabled },
|
JoinRuleItem.PrivateVisibility.AskToJoinRestricted(config.parentSpace.roomId).takeIf { isKnockFeatureEnabled },
|
||||||
JoinRuleItem.Private,
|
JoinRuleItem.PrivateVisibility.Private,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
JoinRuleItem.PublicVisibility.Public,
|
JoinRuleItem.PublicVisibility.Public,
|
||||||
JoinRuleItem.PublicVisibility.AskToJoin.takeIf { isKnockFeatureEnabled },
|
JoinRuleItem.PublicVisibility.AskToJoin.takeIf { isKnockFeatureEnabled },
|
||||||
JoinRuleItem.Private,
|
JoinRuleItem.PrivateVisibility.Private,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
spaces: List<SpaceRoom> = emptyList(),
|
spaces: List<SpaceRoom> = emptyList(),
|
||||||
eventSink: (ConfigureRoomEvents) -> Unit = { },
|
eventSink: (ConfigureRoomEvents) -> Unit = { },
|
||||||
) = ConfigureRoomState(
|
) = ConfigureRoomState(
|
||||||
config = config,
|
config = config,
|
||||||
|
isSpace = isSpace,
|
||||||
avatarActions = avatarActions.toImmutableList(),
|
avatarActions = avatarActions.toImmutableList(),
|
||||||
createRoomAction = createRoomAction,
|
createRoomAction = createRoomAction,
|
||||||
cameraPermissionState = cameraPermissionState,
|
cameraPermissionState = cameraPermissionState,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
|
@ -76,7 +78,7 @@ fun ConfigureRoomView(
|
||||||
onCreateRoomSuccess: (RoomId) -> Unit,
|
onCreateRoomSuccess: (RoomId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val isSpace = state.config.isSpace
|
val isSpace = state.isSpace
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val isAvatarActionsSheetVisible = remember { mutableStateOf(false) }
|
val isAvatarActionsSheetVisible = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
|
@ -105,7 +107,6 @@ fun ConfigureRoomView(
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.consumeWindowInsets(padding),
|
.consumeWindowInsets(padding),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
) {
|
) {
|
||||||
RoomNameWithAvatar(
|
RoomNameWithAvatar(
|
||||||
isSpace = isSpace,
|
isSpace = isSpace,
|
||||||
|
|
@ -115,20 +116,20 @@ fun ConfigureRoomView(
|
||||||
onAvatarClick = ::onAvatarClick,
|
onAvatarClick = ::onAvatarClick,
|
||||||
onChangeRoomName = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
|
onChangeRoomName = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
RoomTopic(
|
RoomTopic(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
topic = state.config.topic.orEmpty(),
|
topic = state.config.topic.orEmpty(),
|
||||||
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
|
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
if (!state.config.isSpace && state.spaces.isNotEmpty()) {
|
if (!state.isSpace && state.spaces.isNotEmpty()) {
|
||||||
SelectParentSpaceOptions(
|
SelectParentSpaceOptions(
|
||||||
spaces = state.spaces,
|
spaces = state.spaces,
|
||||||
selectedSpace = state.config.parentSpace,
|
selectedSpace = state.config.parentSpace,
|
||||||
onSelectSpace = { state.eventSink(ConfigureRoomEvents.SetParentSpace(it)) },
|
onSelectSpace = { state.eventSink(ConfigureRoomEvents.SetParentSpace(it)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
RoomJoinRuleOptions(
|
RoomJoinRuleOptions(
|
||||||
options = state.availableJoinRules,
|
options = state.availableJoinRules,
|
||||||
selected = state.config.visibilityState.joinRuleItem,
|
selected = state.config.visibilityState.joinRuleItem,
|
||||||
|
|
@ -138,20 +139,17 @@ fun ConfigureRoomView(
|
||||||
state.eventSink(ConfigureRoomEvents.JoinRuleChanged(it))
|
state.eventSink(ConfigureRoomEvents.JoinRuleChanged(it))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if (state.config.visibilityState !is RoomVisibilityState.Private) {
|
if (state.config.visibilityState !is RoomVisibilityState.Private) {
|
||||||
Column {
|
ListSectionHeader(title = stringResource(R.string.screen_create_room_room_address_section_title))
|
||||||
ListSectionHeader(title = stringResource(R.string.screen_create_room_room_address_section_title))
|
RoomAddressField(
|
||||||
RoomAddressField(
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
address = state.config.visibilityState.roomAddress().getOrNull().orEmpty(),
|
||||||
address = state.config.visibilityState.roomAddress().getOrNull().orEmpty(),
|
homeserverName = state.homeserverName,
|
||||||
homeserverName = state.homeserverName,
|
addressValidity = state.roomAddressValidity,
|
||||||
addressValidity = state.roomAddressValidity,
|
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
|
||||||
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
|
label = null,
|
||||||
label = null,
|
supportingText = stringResource(R.string.screen_create_room_room_address_section_footer),
|
||||||
supportingText = stringResource(R.string.screen_create_room_room_address_section_footer),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +215,9 @@ private fun RoomNameWithAvatar(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.padding(end = 8.dp).size(AvatarSize.EditRoomDetails.dp),
|
modifier = Modifier
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
.size(AvatarSize.EditRoomDetails.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
val avatarState = remember(avatarUri) {
|
val avatarState = remember(avatarUri) {
|
||||||
|
|
@ -272,12 +272,13 @@ private fun RoomTopic(
|
||||||
internal fun ConfigureRoomOptions(
|
internal fun ConfigureRoomOptions(
|
||||||
title: String,
|
title: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
hasDivider: Boolean = true,
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.selectableGroup()
|
modifier = modifier.selectableGroup()
|
||||||
) {
|
) {
|
||||||
ListSectionHeader(title = title)
|
ListSectionHeader(title = title, hasDivider = hasDivider)
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -302,10 +303,10 @@ private fun RoomJoinRuleOptions(
|
||||||
size = RoundedIconAtomSize.Big,
|
size = RoundedIconAtomSize.Big,
|
||||||
imageVector = when (item) {
|
imageVector = when (item) {
|
||||||
JoinRuleItem.PublicVisibility.Public -> CompoundIcons.Public()
|
JoinRuleItem.PublicVisibility.Public -> CompoundIcons.Public()
|
||||||
is JoinRuleItem.PublicVisibility.Restricted -> CompoundIcons.Space()
|
is JoinRuleItem.PrivateVisibility.Restricted -> CompoundIcons.Space()
|
||||||
JoinRuleItem.PublicVisibility.AskToJoin,
|
JoinRuleItem.PublicVisibility.AskToJoin,
|
||||||
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> CompoundIcons.UserAdd()
|
is JoinRuleItem.PrivateVisibility.AskToJoinRestricted -> CompoundIcons.UserAdd()
|
||||||
JoinRuleItem.Private -> CompoundIcons.Lock()
|
JoinRuleItem.PrivateVisibility.Private -> CompoundIcons.Lock()
|
||||||
},
|
},
|
||||||
tint = if (isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary,
|
tint = if (isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary,
|
||||||
backgroundTint = Color.Transparent,
|
backgroundTint = Color.Transparent,
|
||||||
|
|
@ -314,28 +315,28 @@ private fun RoomJoinRuleOptions(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
val title = when (item) {
|
val title = when (item) {
|
||||||
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_title)
|
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_title)
|
||||||
is JoinRuleItem.PublicVisibility.Restricted -> stringResource(R.string.screen_create_room_room_access_section_restricted_option_title)
|
is JoinRuleItem.PrivateVisibility.Restricted -> stringResource(R.string.screen_create_room_room_access_section_restricted_option_title)
|
||||||
JoinRuleItem.PublicVisibility.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_title)
|
JoinRuleItem.PublicVisibility.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_title)
|
||||||
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> stringResource(
|
is JoinRuleItem.PrivateVisibility.AskToJoinRestricted -> stringResource(
|
||||||
R.string.screen_create_room_room_access_section_knocking_restricted_option_title
|
R.string.screen_create_room_room_access_section_knocking_restricted_option_title
|
||||||
)
|
)
|
||||||
JoinRuleItem.Private -> stringResource(R.string.screen_create_room_room_access_section_private_option_title)
|
JoinRuleItem.PrivateVisibility.Private -> stringResource(R.string.screen_create_room_room_access_section_private_option_title)
|
||||||
}
|
}
|
||||||
Text(text = title)
|
Text(text = title)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
val description = when (item) {
|
val description = when (item) {
|
||||||
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_description)
|
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_description)
|
||||||
is JoinRuleItem.PublicVisibility.Restricted -> stringResource(
|
is JoinRuleItem.PrivateVisibility.Restricted -> stringResource(
|
||||||
R.string.screen_create_room_room_access_section_restricted_option_description,
|
R.string.screen_create_room_room_access_section_restricted_option_description,
|
||||||
parentSpace?.displayName.orEmpty()
|
parentSpace?.displayName.orEmpty()
|
||||||
)
|
)
|
||||||
JoinRuleItem.PublicVisibility.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_description)
|
JoinRuleItem.PublicVisibility.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_description)
|
||||||
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> stringResource(
|
is JoinRuleItem.PrivateVisibility.AskToJoinRestricted -> stringResource(
|
||||||
R.string.screen_create_room_room_access_section_knocking_restricted_option_description,
|
R.string.screen_create_room_room_access_section_knocking_restricted_option_description,
|
||||||
parentSpace?.displayName.orEmpty()
|
parentSpace?.displayName.orEmpty()
|
||||||
)
|
)
|
||||||
JoinRuleItem.Private -> stringResource(R.string.screen_create_room_room_access_section_private_option_description)
|
JoinRuleItem.PrivateVisibility.Private -> stringResource(R.string.screen_create_room_room_access_section_private_option_description)
|
||||||
}
|
}
|
||||||
Text(text = description)
|
Text(text = description)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,10 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
data class CreateRoomConfig(
|
data class CreateRoomConfig(
|
||||||
val isSpace: Boolean = false,
|
|
||||||
val roomName: String? = null,
|
val roomName: String? = null,
|
||||||
val topic: String? = null,
|
val topic: String? = null,
|
||||||
val avatarUri: String? = null,
|
val avatarUri: String? = null,
|
||||||
val invites: ImmutableList<MatrixUser> = persistentListOf(),
|
val invites: ImmutableList<MatrixUser> = persistentListOf(),
|
||||||
val visibilityState: RoomVisibilityState = RoomVisibilityState.Private(),
|
val visibilityState: RoomVisibilityState = RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private),
|
||||||
val parentSpace: SpaceRoom? = null,
|
val parentSpace: SpaceRoom? = null,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,9 @@ class CreateRoomConfigStore(
|
||||||
createRoomConfigFlow.getAndUpdate { config ->
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
config.copy(
|
config.copy(
|
||||||
visibilityState = when (joinRule) {
|
visibilityState = when (joinRule) {
|
||||||
JoinRuleItem.Private -> RoomVisibilityState.Private()
|
is JoinRuleItem.PrivateVisibility -> RoomVisibilityState.Private(
|
||||||
|
joinRuleItem = joinRule
|
||||||
|
)
|
||||||
is JoinRuleItem.PublicVisibility -> {
|
is JoinRuleItem.PublicVisibility -> {
|
||||||
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
|
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
|
||||||
RoomVisibilityState.Public(
|
RoomVisibilityState.Public(
|
||||||
|
|
@ -99,17 +101,16 @@ class CreateRoomConfigStore(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setIsSpace(isSpace: Boolean) {
|
fun setParentSpace(parentSpace: SpaceRoom?, updateVisibility: Boolean) {
|
||||||
createRoomConfigFlow.getAndUpdate { config ->
|
|
||||||
config.copy(isSpace = isSpace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setParentSpace(parentSpace: SpaceRoom?) {
|
|
||||||
createRoomConfigFlow.getAndUpdate { config ->
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
|
val visibilityState = if (parentSpace != null && updateVisibility) {
|
||||||
|
RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Restricted(parentSpace.roomId))
|
||||||
|
} else {
|
||||||
|
config.visibilityState
|
||||||
|
}
|
||||||
config.copy(
|
config.copy(
|
||||||
parentSpace = parentSpace,
|
parentSpace = parentSpace,
|
||||||
visibilityState = RoomVisibilityState.Private(),
|
visibilityState = visibilityState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,11 @@ import kotlinx.collections.immutable.persistentListOf
|
||||||
*/
|
*/
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface JoinRuleItem {
|
sealed interface JoinRuleItem {
|
||||||
data object Private : JoinRuleItem
|
sealed interface PrivateVisibility : JoinRuleItem {
|
||||||
|
data object Private : PrivateVisibility
|
||||||
|
data class Restricted(val parentSpaceId: RoomId) : PrivateVisibility
|
||||||
|
data class AskToJoinRestricted(val parentSpaceId: RoomId) : PrivateVisibility
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Those join rule items that represent public visibility of the room/space.
|
* Those join rule items that represent public visibility of the room/space.
|
||||||
|
|
@ -27,18 +31,16 @@ sealed interface JoinRuleItem {
|
||||||
sealed interface PublicVisibility : JoinRuleItem {
|
sealed interface PublicVisibility : JoinRuleItem {
|
||||||
data object Public : PublicVisibility
|
data object Public : PublicVisibility
|
||||||
data object AskToJoin : PublicVisibility
|
data object AskToJoin : PublicVisibility
|
||||||
data class Restricted(val parentSpaceId: RoomId) : PublicVisibility
|
|
||||||
data class AskToJoinRestricted(val parentSpaceId: RoomId) : PublicVisibility
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms a [JoinRuleItem] option into a [JoinRule].
|
* Transforms a [JoinRuleItem] option into a [JoinRule].
|
||||||
*/
|
*/
|
||||||
fun toJoinRule(): JoinRule = when (this) {
|
fun toJoinRule(): JoinRule = when (this) {
|
||||||
Private -> JoinRule.Private
|
PrivateVisibility.Private -> JoinRule.Invite
|
||||||
|
is PrivateVisibility.Restricted -> JoinRule.Restricted(persistentListOf(AllowRule.RoomMembership(parentSpaceId)))
|
||||||
|
is PrivateVisibility.AskToJoinRestricted -> JoinRule.KnockRestricted(persistentListOf(AllowRule.RoomMembership(parentSpaceId)))
|
||||||
PublicVisibility.Public -> JoinRule.Public
|
PublicVisibility.Public -> JoinRule.Public
|
||||||
PublicVisibility.AskToJoin -> JoinRule.Knock
|
PublicVisibility.AskToJoin -> JoinRule.Knock
|
||||||
is PublicVisibility.Restricted -> JoinRule.Restricted(persistentListOf(AllowRule.RoomMembership(parentSpaceId)))
|
|
||||||
is PublicVisibility.AskToJoinRestricted -> JoinRule.KnockRestricted(persistentListOf(AllowRule.RoomMembership(parentSpaceId)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import java.util.Optional
|
||||||
|
|
||||||
sealed interface RoomVisibilityState {
|
sealed interface RoomVisibilityState {
|
||||||
val joinRuleItem: JoinRuleItem
|
val joinRuleItem: JoinRuleItem
|
||||||
data class Private(override val joinRuleItem: JoinRuleItem.Private = JoinRuleItem.Private) : RoomVisibilityState
|
data class Private(override val joinRuleItem: JoinRuleItem.PrivateVisibility) : RoomVisibilityState
|
||||||
|
|
||||||
data class Public(
|
data class Public(
|
||||||
val roomAddress: RoomAddress,
|
val roomAddress: RoomAddress,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
|
@ -21,7 +20,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.createroom.impl.R
|
import io.element.android.features.createroom.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
|
@ -30,7 +29,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
|
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
|
||||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||||
|
|
@ -55,6 +53,7 @@ internal fun SelectParentSpaceOptions(
|
||||||
var displaySelectSpaceBottomSheet by remember { mutableStateOf(false) }
|
var displaySelectSpaceBottomSheet by remember { mutableStateOf(false) }
|
||||||
ConfigureRoomOptions(
|
ConfigureRoomOptions(
|
||||||
title = stringResource(CommonStrings.common_space),
|
title = stringResource(CommonStrings.common_space),
|
||||||
|
hasDivider = false,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
|
|
@ -62,22 +61,16 @@ internal fun SelectParentSpaceOptions(
|
||||||
Text(
|
Text(
|
||||||
text = selectedSpace?.displayName
|
text = selectedSpace?.displayName
|
||||||
?: stringResource(R.string.screen_create_room_space_selection_no_space_title),
|
?: stringResource(R.string.screen_create_room_space_selection_no_space_title),
|
||||||
maxLines = 1
|
maxLines = 1,
|
||||||
|
color = ElementTheme.colors.textPrimary
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = selectedSpace?.canonicalAlias?.let { alias ->
|
||||||
Text(
|
{
|
||||||
text = if (selectedSpace != null) {
|
Text(text = alias.value, maxLines = 1)
|
||||||
selectedSpace.canonicalAlias?.value.orEmpty()
|
}
|
||||||
} else {
|
|
||||||
stringResource(R.string.screen_create_room_space_selection_no_space_description)
|
|
||||||
},
|
|
||||||
maxLines = 1
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
leadingContent = if (selectedSpace == null) {
|
leadingContent = selectedSpace?.let {
|
||||||
ListItemContent.Icon(IconSource.Vector(CompoundIcons.Home()))
|
|
||||||
} else {
|
|
||||||
ListItemContent.Custom({
|
ListItemContent.Custom({
|
||||||
val avatarData = AvatarData(
|
val avatarData = AvatarData(
|
||||||
id = selectedSpace.roomId.value,
|
id = selectedSpace.roomId.value,
|
||||||
|
|
@ -119,7 +112,7 @@ internal fun SelectParentSpaceOptions(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.SelectParentSpaceBottomSheet(
|
private fun SelectParentSpaceBottomSheet(
|
||||||
spaces: ImmutableList<SpaceRoom>,
|
spaces: ImmutableList<SpaceRoom>,
|
||||||
selectedSpace: SpaceRoom?,
|
selectedSpace: SpaceRoom?,
|
||||||
onSelectSpace: (SpaceRoom?) -> Unit,
|
onSelectSpace: (SpaceRoom?) -> Unit,
|
||||||
|
|
@ -133,19 +126,10 @@ private fun ColumnScope.SelectParentSpaceBottomSheet(
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.screen_create_room_space_selection_no_space_title),
|
text = stringResource(R.string.screen_create_room_space_selection_no_space_option),
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.screen_create_room_space_selection_no_space_description),
|
|
||||||
maxLines = 1
|
|
||||||
)
|
|
||||||
},
|
|
||||||
leadingContent = ListItemContent.Icon(
|
|
||||||
IconSource.Vector(CompoundIcons.Home())
|
|
||||||
),
|
|
||||||
trailingContent = ListItemContent.RadioButton(
|
trailingContent = ListItemContent.RadioButton(
|
||||||
selected = selectedSpace == null
|
selected = selectedSpace == null
|
||||||
),
|
),
|
||||||
|
|
@ -157,29 +141,31 @@ private fun ColumnScope.SelectParentSpaceBottomSheet(
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
space.displayName,
|
text = space.displayName,
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = space.canonicalAlias?.let { alias ->
|
||||||
Text(
|
{
|
||||||
space.canonicalAlias?.value.orEmpty(),
|
Text(
|
||||||
maxLines = 1
|
text = alias.value,
|
||||||
)
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
leadingContent = ListItemContent.Custom({
|
leadingContent = ListItemContent.Custom({
|
||||||
val avatarData =
|
val avatarData =
|
||||||
AvatarData(
|
AvatarData(
|
||||||
id = space.roomId.value,
|
id = space.roomId.value,
|
||||||
name = space.displayName,
|
name = space.displayName,
|
||||||
url = space.avatarUrl,
|
url = space.avatarUrl,
|
||||||
size = AvatarSize.SelectParentSpace,
|
size = AvatarSize.SelectParentSpace,
|
||||||
)
|
|
||||||
Avatar(
|
|
||||||
avatarData = avatarData,
|
|
||||||
avatarType = AvatarType.Space()
|
|
||||||
)
|
)
|
||||||
}),
|
Avatar(
|
||||||
|
avatarData = avatarData,
|
||||||
|
avatarType = AvatarType.Space()
|
||||||
|
)
|
||||||
|
}),
|
||||||
trailingContent = ListItemContent.RadioButton(
|
trailingContent = ListItemContent.RadioButton(
|
||||||
selected = selectedSpace == space
|
selected = selectedSpace == space
|
||||||
),
|
),
|
||||||
|
|
@ -201,7 +187,8 @@ internal fun SelectParentSpaceBottomSheetPreview() =
|
||||||
canonicalAlias = RoomAlias(
|
canonicalAlias = RoomAlias(
|
||||||
"#a-room-alias:example.org"
|
"#a-room-alias:example.org"
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
|
aSpaceRoom()
|
||||||
),
|
),
|
||||||
selectedSpace = null,
|
selectedSpace = null,
|
||||||
) {}
|
) {}
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,33 @@
|
||||||
<string name="screen_create_room_action_create_room">"Nová místnost"</string>
|
<string name="screen_create_room_action_create_room">"Nová místnost"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Pozvat přátele"</string>
|
<string name="screen_create_room_add_people_title">"Pozvat přátele"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"Při vytváření místnosti došlo k chybě"</string>
|
<string name="screen_create_room_error_creating_room">"Při vytváření místnosti došlo k chybě"</string>
|
||||||
<string name="screen_create_room_private_option_description">"Do této místnosti mají přístup pouze pozvaní lidé. Všechny zprávy jsou koncově šifrovány."</string>
|
<string name="screen_create_room_error_creating_space">"Prostor se nepodařilo vytvořit kvůli neznámé chybě. Zkuste to znovu později."</string>
|
||||||
|
<string name="screen_create_room_name_placeholder">"Přidat název…"</string>
|
||||||
|
<string name="screen_create_room_new_room_title">"Nová místnost"</string>
|
||||||
|
<string name="screen_create_room_new_space_title">"Nový prostor"</string>
|
||||||
|
<string name="screen_create_room_private_option_description">"Do této místnosti mohou vstoupit pouze pozvaní."</string>
|
||||||
|
<string name="screen_create_room_private_option_title">"Soukromý"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Tuto místnost může najít kdokoli.
|
<string name="screen_create_room_public_option_description">"Tuto místnost může najít kdokoli.
|
||||||
To můžete kdykoli změnit v nastavení místnosti."</string>
|
To můžete kdykoli změnit v nastavení místnosti."</string>
|
||||||
|
<string name="screen_create_room_public_option_short_description">"Vstoupit může kdokoli."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Veřejná místnost"</string>
|
<string name="screen_create_room_public_option_title">"Veřejná místnost"</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Kdokoli může požádat o vstup do místnosti, ale správce nebo moderátor bude muset žádost přijmout"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Kdokoli může požádat o vstup do místnosti, ale správce nebo moderátor bude muset žádost přijmout."</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Požádat o připojení"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Povolit žádost o vstup"</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_description">"Do této místnosti může vstoupit kdokoli"</string>
|
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Kdokoli v %1$s může vstoupit, ale všichni ostatní si musí o přístup požádat."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Požádat o vstup"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_private_option_description">"Vstoupit mohou pouze pozvaní lidé."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_private_option_title">"Soukromý"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_public_option_description">"Vstoupit může kdokoli."</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_title">"Kdokoliv"</string>
|
<string name="screen_create_room_room_access_section_public_option_title">"Kdokoliv"</string>
|
||||||
<string name="screen_create_room_room_address_section_footer">"Aby byla tato místnost viditelná v adresáři veřejných místností, budete potřebovat adresu místnosti."</string>
|
<string name="screen_create_room_room_access_section_restricted_option_description">"Kdokoli může vstoupit do %1$s."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Adresa místnosti"</string>
|
<string name="screen_create_room_room_access_section_restricted_option_title">"Standard"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_title">"Kdo má přístup"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"Budete potřebovat adresu, aby se zobrazovala ve veřejném adresáři."</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"Adresa"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"Viditelnost místnosti"</string>
|
<string name="screen_create_room_room_visibility_section_title">"Viditelnost místnosti"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_description">"(bez prostoru)"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_title">"Domov"</string>
|
||||||
|
<string name="screen_create_room_space_selection_sheet_title">"Přidat do prostoru"</string>
|
||||||
<string name="screen_create_room_topic_label">"Téma (nepovinné)"</string>
|
<string name="screen_create_room_topic_label">"Téma (nepovinné)"</string>
|
||||||
|
<string name="screen_create_room_topic_placeholder">"Přidat popis…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ Du kannst dies jederzeit in den Einstellungen des Chats ändern."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
|
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">" Sichtbarkeit des Chats"</string>
|
<string name="screen_create_room_room_visibility_section_title">" Sichtbarkeit des Chats"</string>
|
||||||
<string name="screen_create_room_space_selection_no_space_description">"(kein Space)"</string>
|
<string name="screen_create_room_space_selection_no_space_description">"(kein Space)"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_title">"Home"</string>
|
||||||
<string name="screen_create_room_space_selection_sheet_title">"Space hinzufügen"</string>
|
<string name="screen_create_room_space_selection_sheet_title">"Space hinzufügen"</string>
|
||||||
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
|
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
|
||||||
<string name="screen_create_room_topic_placeholder">"Beschreibung hinzufügen…"</string>
|
<string name="screen_create_room_topic_placeholder">"Beschreibung hinzufügen…"</string>
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,33 @@
|
||||||
<string name="screen_create_room_action_create_room">"Uus jututuba"</string>
|
<string name="screen_create_room_action_create_room">"Uus jututuba"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Kutsu osalejaid"</string>
|
<string name="screen_create_room_add_people_title">"Kutsu osalejaid"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"Jututoa loomisel tekkis viga"</string>
|
<string name="screen_create_room_error_creating_room">"Jututoa loomisel tekkis viga"</string>
|
||||||
<string name="screen_create_room_private_option_description">"Ligipääs siia jututuppa on vaid kutse alusel. Kõik sõnumid siin jututoas on läbivalt krüptitud."</string>
|
<string name="screen_create_room_error_creating_space">"Kogukonda polnud tundmatu vea tõttu võimalik luua. Palun proovi hiljem uuesti."</string>
|
||||||
|
<string name="screen_create_room_name_placeholder">"Sisesta nimi…"</string>
|
||||||
|
<string name="screen_create_room_new_room_title">"Uus jututuba"</string>
|
||||||
|
<string name="screen_create_room_new_space_title">"Uus kogukond"</string>
|
||||||
|
<string name="screen_create_room_private_option_description">"Ligipääs siia jututuppa on vaid kutse alusel."</string>
|
||||||
|
<string name="screen_create_room_private_option_title">"Privaatne"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Kõik saavad seda jututuba leida.
|
<string name="screen_create_room_public_option_description">"Kõik saavad seda jututuba leida.
|
||||||
Sa võid seda jututoa seadistustest alati muuta."</string>
|
Sa võid seda jututoa seadistustest alati muuta."</string>
|
||||||
|
<string name="screen_create_room_public_option_short_description">"Kõik võivad selle jututoaga liituda."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Avalik jututuba"</string>
|
<string name="screen_create_room_public_option_title">"Avalik jututuba"</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Kõik võivad paluda selle jututoaga liitumist, kuid peakasutaja või moderaator peavad selle kinnitama"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Kõik võivad paluda selle jututoaga liitumist, kuid peakasutaja või moderaator peavad selle kinnitama."</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Küsi võimalust liitumiseks"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Luba küsida liitumisvõimalust"</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_description">"Kõik võivad selle jututoaga liituda"</string>
|
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Kõik „%1$s“ kogukonna liikmed võivad liituda, kuid kõik teised peavad liitumiseks küsima luba."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Küsi võimalust liitumiseks"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_private_option_description">"Ligipääs siia jututuppa on vaid kutse alusel."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_private_option_title">"Privaatne"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_public_option_description">"Kõik võivad selle jututoaga liituda."</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_title">"Kõik kasutajad"</string>
|
<string name="screen_create_room_room_access_section_public_option_title">"Kõik kasutajad"</string>
|
||||||
<string name="screen_create_room_room_address_section_footer">"Selleks, et see jututuba oleks nähtav jututubade avalikus kataloogis, sa vajad jututoa aadressi."</string>
|
<string name="screen_create_room_room_access_section_restricted_option_description">"Liituda võivad kõik „%1$s“ kogukonna liikmed."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Jututoa aadress"</string>
|
<string name="screen_create_room_room_access_section_restricted_option_title">"Standardne"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_title">"Kellel on ligipääs"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"Selleks, et jututuba oleks nähtav jututubade avalikus kataloogis, vajab ta aadressi."</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"Aadress"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"Jututoa nähtavus"</string>
|
<string name="screen_create_room_room_visibility_section_title">"Jututoa nähtavus"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_description">"(kogukonda pole)"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_title">"Avaleht"</string>
|
||||||
|
<string name="screen_create_room_space_selection_sheet_title">"Lisa kogukonda"</string>
|
||||||
<string name="screen_create_room_topic_label">"Teema (kui soovid lisada)"</string>
|
<string name="screen_create_room_topic_label">"Teema (kui soovid lisada)"</string>
|
||||||
|
<string name="screen_create_room_topic_placeholder">"Lisa kirjeldus…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ Vous pouvez modifier cela à tout moment dans les paramètres du salon."</string
|
||||||
<string name="screen_create_room_room_access_section_private_option_description">"Seules les personnes invitées peuvent joindre."</string>
|
<string name="screen_create_room_room_access_section_private_option_description">"Seules les personnes invitées peuvent joindre."</string>
|
||||||
<string name="screen_create_room_room_access_section_private_option_title">"Privé"</string>
|
<string name="screen_create_room_room_access_section_private_option_title">"Privé"</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_description">"Tout le monde peut joindre"</string>
|
<string name="screen_create_room_room_access_section_public_option_description">"Tout le monde peut joindre"</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_title">"Tout le monde"</string>
|
<string name="screen_create_room_room_access_section_public_option_title">"Public"</string>
|
||||||
<string name="screen_create_room_room_access_section_restricted_option_description">"Toute membre de %1$s peut joindre le salon."</string>
|
<string name="screen_create_room_room_access_section_restricted_option_description">"Toute membre de %1$s peut joindre le salon."</string>
|
||||||
<string name="screen_create_room_room_access_section_restricted_option_title">"Standard"</string>
|
<string name="screen_create_room_room_access_section_restricted_option_title">"Standard"</string>
|
||||||
<string name="screen_create_room_room_access_section_title">"Qui a accès"</string>
|
<string name="screen_create_room_room_access_section_title">"Qui a accès"</string>
|
||||||
|
|
@ -28,7 +28,8 @@ Vous pouvez modifier cela à tout moment dans les paramètres du salon."</string
|
||||||
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
|
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"Visibilité du salon"</string>
|
<string name="screen_create_room_room_visibility_section_title">"Visibilité du salon"</string>
|
||||||
<string name="screen_create_room_space_selection_no_space_description">"(pas d’espace)"</string>
|
<string name="screen_create_room_space_selection_no_space_description">"(pas d’espace)"</string>
|
||||||
<string name="screen_create_room_space_selection_no_space_title">"Accueil"</string>
|
<string name="screen_create_room_space_selection_no_space_option">"Ne pas ajouter à un espace"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_title">"Aucun espace sélectionné"</string>
|
||||||
<string name="screen_create_room_space_selection_sheet_title">"Ajouter à l’espace"</string>
|
<string name="screen_create_room_space_selection_sheet_title">"Ajouter à l’espace"</string>
|
||||||
<string name="screen_create_room_topic_label">"Sujet (facultatif)"</string>
|
<string name="screen_create_room_topic_label">"Sujet (facultatif)"</string>
|
||||||
<string name="screen_create_room_topic_placeholder">"Ajouter une description…"</string>
|
<string name="screen_create_room_topic_placeholder">"Ajouter une description…"</string>
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,25 @@
|
||||||
<string name="screen_create_room_action_create_room">"Nytt rom"</string>
|
<string name="screen_create_room_action_create_room">"Nytt rom"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Inviter folk"</string>
|
<string name="screen_create_room_add_people_title">"Inviter folk"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"Det oppsto en feil under opprettelsen av rommet"</string>
|
<string name="screen_create_room_error_creating_room">"Det oppsto en feil under opprettelsen av rommet"</string>
|
||||||
|
<string name="screen_create_room_name_placeholder">"Legg til navn…"</string>
|
||||||
|
<string name="screen_create_room_new_room_title">"Nytt rom"</string>
|
||||||
|
<string name="screen_create_room_new_space_title">"Nytt område"</string>
|
||||||
<string name="screen_create_room_private_option_description">"Bare inviterte personer kan bli med."</string>
|
<string name="screen_create_room_private_option_description">"Bare inviterte personer kan bli med."</string>
|
||||||
<string name="screen_create_room_private_option_title">"Privat"</string>
|
<string name="screen_create_room_private_option_title">"Privat"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Alle kan finne dette rommet.
|
<string name="screen_create_room_public_option_description">"Alle kan finne dette rommet.
|
||||||
Du kan endre dette når som helst i rominnstillingene."</string>
|
Du kan endre dette når som helst i rominnstillingene."</string>
|
||||||
|
<string name="screen_create_room_public_option_short_description">"Alle kan bli med."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Offentlig rom"</string>
|
<string name="screen_create_room_public_option_title">"Offentlig rom"</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Alle kan be om å få bli med, men en administrator eller moderator må godta forespørselen."</string>
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Alle kan be om å få bli med, men en administrator eller moderator må godta forespørselen."</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Be om å bli med"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Be om å bli med"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_private_option_description">"Bare inviterte personer kan bli med."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_private_option_title">"Privat"</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_description">"Alle kan bli med."</string>
|
<string name="screen_create_room_room_access_section_public_option_description">"Alle kan bli med."</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_title">"Alle"</string>
|
<string name="screen_create_room_room_access_section_public_option_title">"Alle"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_title">"Hvem har tilgang"</string>
|
||||||
<string name="screen_create_room_room_address_section_footer">"Du trenger en adresse for å gjøre den synlig i den offentlige katalogen."</string>
|
<string name="screen_create_room_room_address_section_footer">"Du trenger en adresse for å gjøre den synlig i den offentlige katalogen."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
|
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"Romsynlighet"</string>
|
<string name="screen_create_room_room_visibility_section_title">"Romsynlighet"</string>
|
||||||
<string name="screen_create_room_topic_label">"Emne (valgfritt)"</string>
|
<string name="screen_create_room_topic_label">"Emne (valgfritt)"</string>
|
||||||
|
<string name="screen_create_room_topic_placeholder">"Legg til beskrivelse…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,16 @@
|
||||||
<string name="screen_create_room_action_create_room">"Nytt rum"</string>
|
<string name="screen_create_room_action_create_room">"Nytt rum"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Bjud in personer"</string>
|
<string name="screen_create_room_add_people_title">"Bjud in personer"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"Ett fel uppstod när rummet skapades"</string>
|
<string name="screen_create_room_error_creating_room">"Ett fel uppstod när rummet skapades"</string>
|
||||||
<string name="screen_create_room_private_option_description">"Endast inbjudna personer har tillgång till detta rum. Alla meddelanden är totalsträckskrypterade."</string>
|
<string name="screen_create_room_private_option_description">"Endast inbjudna personer kan gå med."</string>
|
||||||
<string name="screen_create_room_public_option_description">"Vem som helst kan hitta det här rummet.
|
<string name="screen_create_room_public_option_description">"Vem som helst kan hitta det här rummet.
|
||||||
Du kan ändra detta när som helst i rumsinställningarna."</string>
|
Du kan ändra detta när som helst i rumsinställningarna."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Offentligt rum"</string>
|
<string name="screen_create_room_public_option_title">"Offentligt rum"</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Vem som helst kan be om att gå med i rummet men en administratör eller en moderator måste acceptera begäran"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Vem som helst kan be om att gå med men en administratör eller en moderator måste acceptera begäran"</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Be om att gå med"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Tillåt att be om att gå med"</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_description">"Vem som helst kan gå med i det här rummet"</string>
|
<string name="screen_create_room_room_access_section_public_option_description">"Vem som helst kan gå med."</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_title">"Vem som helst"</string>
|
<string name="screen_create_room_room_access_section_public_option_title">"Offentligt"</string>
|
||||||
<string name="screen_create_room_room_address_section_footer">"För att detta rum ska vara synligt i den allmänna rumskatalogen behöver du en rumsadress."</string>
|
<string name="screen_create_room_room_address_section_footer">"Du behöver en adress för att den ska synas i den offentliga katalogen."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Rumsadress"</string>
|
<string name="screen_create_room_room_address_section_title">"Adress"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"Rumssynlighet"</string>
|
<string name="screen_create_room_room_visibility_section_title">"Rumssynlighet"</string>
|
||||||
<string name="screen_create_room_topic_label">"Ämne (valfritt)"</string>
|
<string name="screen_create_room_topic_label">"Ämne (valfritt)"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@
|
||||||
<string name="screen_create_room_name_placeholder">"Add name…"</string>
|
<string name="screen_create_room_name_placeholder">"Add name…"</string>
|
||||||
<string name="screen_create_room_new_room_title">"New room"</string>
|
<string name="screen_create_room_new_room_title">"New room"</string>
|
||||||
<string name="screen_create_room_new_space_title">"New space"</string>
|
<string name="screen_create_room_new_space_title">"New space"</string>
|
||||||
<string name="screen_create_room_parent_space_home_description">"(no space)"</string>
|
|
||||||
<string name="screen_create_room_parent_space_home_title">"Home"</string>
|
|
||||||
<string name="screen_create_room_private_option_description">"Only people invited can join."</string>
|
<string name="screen_create_room_private_option_description">"Only people invited can join."</string>
|
||||||
<string name="screen_create_room_private_option_title">"Private"</string>
|
<string name="screen_create_room_private_option_title">"Private"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Anyone can find this room.
|
<string name="screen_create_room_public_option_description">"Anyone can find this room.
|
||||||
|
|
@ -30,7 +28,8 @@ You can change this anytime in room settings."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Address"</string>
|
<string name="screen_create_room_room_address_section_title">"Address"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"Room visibility"</string>
|
<string name="screen_create_room_room_visibility_section_title">"Room visibility"</string>
|
||||||
<string name="screen_create_room_space_selection_no_space_description">"(no space)"</string>
|
<string name="screen_create_room_space_selection_no_space_description">"(no space)"</string>
|
||||||
<string name="screen_create_room_space_selection_no_space_title">"Home"</string>
|
<string name="screen_create_room_space_selection_no_space_option">"Do not add to a space"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_title">"No space selected"</string>
|
||||||
<string name="screen_create_room_space_selection_sheet_title">"Add to space"</string>
|
<string name="screen_create_room_space_selection_sheet_title">"Add to space"</string>
|
||||||
<string name="screen_create_room_topic_label">"Topic (optional)"</string>
|
<string name="screen_create_room_topic_label">"Topic (optional)"</string>
|
||||||
<string name="screen_create_room_topic_placeholder">"Add description…"</string>
|
<string name="screen_create_room_topic_placeholder">"Add description…"</string>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||||
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
||||||
|
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
|
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
|
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||||
|
|
@ -89,7 +90,7 @@ class ConfigureRoomPresenterTest {
|
||||||
assertThat(initialState.config.topic).isNull()
|
assertThat(initialState.config.topic).isNull()
|
||||||
assertThat(initialState.config.invites).isEmpty()
|
assertThat(initialState.config.invites).isEmpty()
|
||||||
assertThat(initialState.config.avatarUri).isNull()
|
assertThat(initialState.config.avatarUri).isNull()
|
||||||
assertThat(initialState.config.visibilityState).isEqualTo(RoomVisibilityState.Private())
|
assertThat(initialState.config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
|
||||||
assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
assertThat(initialState.homeserverName).isEqualTo("matrix.org")
|
assertThat(initialState.homeserverName).isEqualTo("matrix.org")
|
||||||
}
|
}
|
||||||
|
|
@ -218,6 +219,7 @@ class ConfigureRoomPresenterTest {
|
||||||
fun `present - when creating a room in a space if the room doesn't receive the power levels value it can't be added to the space`() = runTest {
|
fun `present - when creating a room in a space if the room doesn't receive the power levels value it can't be added to the space`() = runTest {
|
||||||
val addChildToSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ -> Result.success(Unit) }
|
val addChildToSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ -> Result.success(Unit) }
|
||||||
val spaceService = FakeSpaceService(
|
val spaceService = FakeSpaceService(
|
||||||
|
editableSpacesResult = { Result.success(emptyList()) },
|
||||||
addChildToSpaceResult = addChildToSpaceResult,
|
addChildToSpaceResult = addChildToSpaceResult,
|
||||||
)
|
)
|
||||||
val roomInfoFlow = MutableStateFlow<Optional<RoomInfo>>(Optional.empty())
|
val roomInfoFlow = MutableStateFlow<Optional<RoomInfo>>(Optional.empty())
|
||||||
|
|
@ -234,7 +236,8 @@ class ConfigureRoomPresenterTest {
|
||||||
|
|
||||||
matrixClient.givenCreateRoomResult(createRoomResult)
|
matrixClient.givenCreateRoomResult(createRoomResult)
|
||||||
|
|
||||||
val parentSpace = aSpaceRoom()
|
// Use a public parent space so AskToJoin is a valid option
|
||||||
|
val parentSpace = aSpaceRoom(joinRule = JoinRule.Public)
|
||||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
|
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
|
||||||
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
|
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
|
||||||
|
|
||||||
|
|
@ -259,6 +262,7 @@ class ConfigureRoomPresenterTest {
|
||||||
fun `present - creating a room and adding it into a parent space works when all the data is available`() = runTest {
|
fun `present - creating a room and adding it into a parent space works when all the data is available`() = runTest {
|
||||||
val addChildToSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ -> Result.success(Unit) }
|
val addChildToSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ -> Result.success(Unit) }
|
||||||
val spaceService = FakeSpaceService(
|
val spaceService = FakeSpaceService(
|
||||||
|
editableSpacesResult = { Result.success(emptyList()) },
|
||||||
addChildToSpaceResult = addChildToSpaceResult,
|
addChildToSpaceResult = addChildToSpaceResult,
|
||||||
)
|
)
|
||||||
val roomInfoFlow = MutableStateFlow<Optional<RoomInfo>>(Optional.empty())
|
val roomInfoFlow = MutableStateFlow<Optional<RoomInfo>>(Optional.empty())
|
||||||
|
|
@ -275,7 +279,8 @@ class ConfigureRoomPresenterTest {
|
||||||
|
|
||||||
matrixClient.givenCreateRoomResult(createRoomResult)
|
matrixClient.givenCreateRoomResult(createRoomResult)
|
||||||
|
|
||||||
val parentSpace = aSpaceRoom()
|
// Use a public parent space so AskToJoin is a valid option
|
||||||
|
val parentSpace = aSpaceRoom(joinRule = JoinRule.Public)
|
||||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
|
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
|
||||||
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
|
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
|
||||||
|
|
||||||
|
|
@ -484,16 +489,19 @@ class ConfigureRoomPresenterTest {
|
||||||
assertThat(awaitItem().config.visibilityState).isInstanceOf(RoomVisibilityState.Public::class.java)
|
assertThat(awaitItem().config.visibilityState).isInstanceOf(RoomVisibilityState.Public::class.java)
|
||||||
|
|
||||||
// Then check changing the parent space resets it to private
|
// 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(ConfigureRoomEvents.SetParentSpace(aSpaceRoom()))
|
||||||
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private())
|
skipItems(1) // Skip intermediate state
|
||||||
|
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
|
||||||
|
|
||||||
// If we change the join rule back to public
|
// If we change the join rule back to public
|
||||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||||
assertThat(awaitItem().config.visibilityState).isInstanceOf(RoomVisibilityState.Public::class.java)
|
skipItems(1) // Skip intermediate state (Public is still invalid)
|
||||||
|
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
|
||||||
|
|
||||||
// Then remove the parent space, it'll be private again
|
// Then remove the parent space, the join rule stays private
|
||||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(null))
|
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(null))
|
||||||
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private())
|
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -516,7 +524,9 @@ class ConfigureRoomPresenterTest {
|
||||||
|
|
||||||
private fun createMatrixClient(
|
private fun createMatrixClient(
|
||||||
isAliasAvailable: Boolean = true,
|
isAliasAvailable: Boolean = true,
|
||||||
spaceService: FakeSpaceService = FakeSpaceService(),
|
spaceService: FakeSpaceService = FakeSpaceService(
|
||||||
|
editableSpacesResult = { Result.success(emptyList()) }
|
||||||
|
),
|
||||||
) = FakeMatrixClient(
|
) = FakeMatrixClient(
|
||||||
userIdServerNameLambda = { "matrix.org" },
|
userIdServerNameLambda = { "matrix.org" },
|
||||||
resolveRoomAliasResult = {
|
resolveRoomAliasResult = {
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,12 @@ import org.junit.Test
|
||||||
class JoinRuleItemTest {
|
class JoinRuleItemTest {
|
||||||
@Test
|
@Test
|
||||||
fun `toJoinRule works as expected`() {
|
fun `toJoinRule works as expected`() {
|
||||||
assertThat(JoinRuleItem.Private.toJoinRule()).isEqualTo(JoinRule.Private)
|
assertThat(JoinRuleItem.PrivateVisibility.Private.toJoinRule()).isEqualTo(JoinRule.Invite)
|
||||||
assertThat(JoinRuleItem.PublicVisibility.Public.toJoinRule()).isEqualTo(JoinRule.Public)
|
assertThat(JoinRuleItem.PublicVisibility.Public.toJoinRule()).isEqualTo(JoinRule.Public)
|
||||||
assertThat(JoinRuleItem.PublicVisibility.AskToJoin.toJoinRule()).isEqualTo(JoinRule.Knock)
|
assertThat(JoinRuleItem.PublicVisibility.AskToJoin.toJoinRule()).isEqualTo(JoinRule.Knock)
|
||||||
assertThat(JoinRuleItem.PublicVisibility.Restricted(A_ROOM_ID).toJoinRule())
|
assertThat(JoinRuleItem.PrivateVisibility.Restricted(A_ROOM_ID).toJoinRule())
|
||||||
.isEqualTo(JoinRule.Restricted(persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))))
|
.isEqualTo(JoinRule.Restricted(persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))))
|
||||||
assertThat(JoinRuleItem.PublicVisibility.AskToJoinRestricted(A_ROOM_ID).toJoinRule())
|
assertThat(JoinRuleItem.PrivateVisibility.AskToJoinRestricted(A_ROOM_ID).toJoinRule())
|
||||||
.isEqualTo(JoinRule.KnockRestricted(persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))))
|
.isEqualTo(JoinRule.KnockRestricted(persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ package io.element.android.features.home.impl
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
sealed interface HomeEvents {
|
sealed interface HomeEvent {
|
||||||
data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvents
|
data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvent
|
||||||
data class SwitchToAccount(val sessionId: SessionId) : HomeEvents
|
data class SwitchToAccount(val sessionId: SessionId) : HomeEvent
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +34,7 @@ import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.features.home.api.HomeEntryPoint
|
import io.element.android.features.home.api.HomeEntryPoint
|
||||||
import io.element.android.features.home.impl.components.RoomListMenuAction
|
import io.element.android.features.home.impl.components.RoomListMenuAction
|
||||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListEvents
|
import io.element.android.features.home.impl.roomlist.RoomListEvent
|
||||||
import io.element.android.features.invite.api.InviteData
|
import io.element.android.features.invite.api.InviteData
|
||||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
|
||||||
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
|
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
|
||||||
|
|
@ -159,7 +159,7 @@ class HomeFlowNode(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onNewOwnersSelected(roomId: RoomId) {
|
private fun onNewOwnersSelected(roomId: RoomId) {
|
||||||
stateFlow.value.roomListState.eventSink(RoomListEvents.LeaveRoom(roomId, needsConfirmation = false))
|
stateFlow.value.roomListState.eventSink(RoomListEvent.LeaveRoom(roomId, needsConfirmation = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun rootNode(buildContext: BuildContext): Node {
|
private fun rootNode(buildContext: BuildContext): Node {
|
||||||
|
|
|
||||||
|
|
@ -80,15 +80,15 @@ class HomePresenter(
|
||||||
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
|
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
|
||||||
val directLogoutState = logoutPresenter.present()
|
val directLogoutState = logoutPresenter.present()
|
||||||
|
|
||||||
fun handleEvent(event: HomeEvents) {
|
fun handleEvent(event: HomeEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is HomeEvents.SelectHomeNavigationBarItem -> coroutineState.launch {
|
is HomeEvent.SelectHomeNavigationBarItem -> coroutineState.launch {
|
||||||
if (event.item == HomeNavigationBarItem.Spaces) {
|
if (event.item == HomeNavigationBarItem.Spaces) {
|
||||||
announcementService.showAnnouncement(Announcement.Space)
|
announcementService.showAnnouncement(Announcement.Space)
|
||||||
}
|
}
|
||||||
currentHomeNavigationBarItemOrdinal = event.item.ordinal
|
currentHomeNavigationBarItemOrdinal = event.item.ordinal
|
||||||
}
|
}
|
||||||
is HomeEvents.SwitchToAccount -> coroutineState.launch {
|
is HomeEvent.SwitchToAccount -> coroutineState.launch {
|
||||||
sessionStore.setLatestSession(event.sessionId.value)
|
sessionStore.setLatestSession(event.sessionId.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
package io.element.android.features.home.impl
|
package io.element.android.features.home.impl
|
||||||
|
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListState
|
import io.element.android.features.home.impl.roomlist.RoomListState
|
||||||
|
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
|
||||||
import io.element.android.features.home.impl.spaces.HomeSpacesState
|
import io.element.android.features.home.impl.spaces.HomeSpacesState
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
|
|
@ -29,8 +30,9 @@ data class HomeState(
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val canReportBug: Boolean,
|
val canReportBug: Boolean,
|
||||||
val directLogoutState: DirectLogoutState,
|
val directLogoutState: DirectLogoutState,
|
||||||
val eventSink: (HomeEvents) -> Unit,
|
val eventSink: (HomeEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected
|
||||||
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
|
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
|
||||||
val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters
|
val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters
|
||||||
val showNavigationBar = homeSpacesState.canCreateSpaces || homeSpacesState.spaceRooms.isNotEmpty()
|
val showNavigationBar = homeSpacesState.canCreateSpaces || homeSpacesState.spaceRooms.isNotEmpty()
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ internal fun aHomeState(
|
||||||
homeSpacesState: HomeSpacesState = aHomeSpacesState(),
|
homeSpacesState: HomeSpacesState = aHomeSpacesState(),
|
||||||
canReportBug: Boolean = true,
|
canReportBug: Boolean = true,
|
||||||
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
||||||
eventSink: (HomeEvents) -> Unit = {}
|
eventSink: (HomeEvent) -> Unit = {}
|
||||||
) = HomeState(
|
) = HomeState(
|
||||||
currentUserAndNeighbors = currentUserAndNeighbors.toImmutableList(),
|
currentUserAndNeighbors = currentUserAndNeighbors.toImmutableList(),
|
||||||
showAvatarIndicator = showAvatarIndicator,
|
showAvatarIndicator = showAvatarIndicator,
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,12 @@ import io.element.android.features.home.impl.components.RoomListMenuAction
|
||||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListContextMenu
|
import io.element.android.features.home.impl.roomlist.RoomListContextMenu
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu
|
import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListEvents
|
import io.element.android.features.home.impl.roomlist.RoomListEvent
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListState
|
import io.element.android.features.home.impl.roomlist.RoomListState
|
||||||
import io.element.android.features.home.impl.search.RoomListSearchView
|
import io.element.android.features.home.impl.search.RoomListSearchView
|
||||||
|
import io.element.android.features.home.impl.spacefilters.SpaceFiltersEvent
|
||||||
|
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
|
||||||
|
import io.element.android.features.home.impl.spacefilters.SpaceFiltersView
|
||||||
import io.element.android.features.home.impl.spaces.HomeSpacesView
|
import io.element.android.features.home.impl.spaces.HomeSpacesView
|
||||||
import io.element.android.libraries.androidutils.throttler.FirstThrottler
|
import io.element.android.libraries.androidutils.throttler.FirstThrottler
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
@ -153,10 +156,15 @@ private fun HomeScaffold(
|
||||||
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||||
val roomListState: RoomListState = state.roomListState
|
val roomListState: RoomListState = state.roomListState
|
||||||
|
|
||||||
BackHandler(
|
BackHandler(enabled = state.isBackHandlerEnabled) {
|
||||||
enabled = state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats,
|
if (state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats) {
|
||||||
) {
|
state.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats))
|
||||||
state.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats))
|
} else {
|
||||||
|
val spaceFiltersState = state.roomListState.spaceFiltersState
|
||||||
|
if (spaceFiltersState is SpaceFiltersState.Selected) {
|
||||||
|
spaceFiltersState.eventSink(SpaceFiltersEvent.Selected.ClearSelection)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val hazeState = rememberHazeState()
|
val hazeState = rememberHazeState()
|
||||||
|
|
@ -168,20 +176,20 @@ private fun HomeScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
HomeTopBar(
|
HomeTopBar(
|
||||||
selectedNavigationItem = state.currentHomeNavigationBarItem,
|
selectedNavigationItem = state.currentHomeNavigationBarItem,
|
||||||
title = stringResource(state.currentHomeNavigationBarItem.labelRes),
|
|
||||||
currentUserAndNeighbors = state.currentUserAndNeighbors,
|
currentUserAndNeighbors = state.currentUserAndNeighbors,
|
||||||
showAvatarIndicator = state.showAvatarIndicator,
|
showAvatarIndicator = state.showAvatarIndicator,
|
||||||
areSearchResultsDisplayed = roomListState.searchState.isSearchActive,
|
areSearchResultsDisplayed = roomListState.searchState.isSearchActive,
|
||||||
onToggleSearch = { roomListState.eventSink(RoomListEvents.ToggleSearchResults) },
|
onToggleSearch = { roomListState.eventSink(RoomListEvent.ToggleSearchResults) },
|
||||||
onMenuActionClick = onMenuActionClick,
|
onMenuActionClick = onMenuActionClick,
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
onAccountSwitch = {
|
onAccountSwitch = {
|
||||||
state.eventSink(HomeEvents.SwitchToAccount(it))
|
state.eventSink(HomeEvent.SwitchToAccount(it))
|
||||||
},
|
},
|
||||||
onCreateSpace = onCreateSpaceClick,
|
onCreateSpace = onCreateSpaceClick,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
displayFilters = state.displayRoomListFilters,
|
displayFilters = state.displayRoomListFilters,
|
||||||
filtersState = roomListState.filtersState,
|
filtersState = roomListState.filtersState,
|
||||||
|
spaceFiltersState = roomListState.spaceFiltersState,
|
||||||
canCreateSpaces = state.homeSpacesState.canCreateSpaces,
|
canCreateSpaces = state.homeSpacesState.canCreateSpaces,
|
||||||
canReportBug = state.canReportBug,
|
canReportBug = state.canReportBug,
|
||||||
modifier = Modifier.hazeEffect(
|
modifier = Modifier.hazeEffect(
|
||||||
|
|
@ -211,7 +219,7 @@ private fun HomeScaffold(
|
||||||
lazyListStateTarget.animateScrollToItem(0)
|
lazyListStateTarget.animateScrollToItem(0)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.eventSink(HomeEvents.SelectHomeNavigationBarItem(item))
|
state.eventSink(HomeEvent.SelectHomeNavigationBarItem(item))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.hazeEffect(
|
modifier = Modifier.hazeEffect(
|
||||||
|
|
@ -227,6 +235,7 @@ private fun HomeScaffold(
|
||||||
RoomListContentView(
|
RoomListContentView(
|
||||||
contentState = roomListState.contentState,
|
contentState = roomListState.contentState,
|
||||||
filtersState = roomListState.filtersState,
|
filtersState = roomListState.filtersState,
|
||||||
|
spaceFiltersState = roomListState.spaceFiltersState,
|
||||||
lazyListState = roomsLazyListState,
|
lazyListState = roomsLazyListState,
|
||||||
hideInvitesAvatars = roomListState.hideInvitesAvatars,
|
hideInvitesAvatars = roomListState.hideInvitesAvatars,
|
||||||
eventSink = roomListState.eventSink,
|
eventSink = roomListState.eventSink,
|
||||||
|
|
@ -256,6 +265,7 @@ private fun HomeScaffold(
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(padding)
|
||||||
.hazeSource(state = hazeState)
|
.hazeSource(state = hazeState)
|
||||||
)
|
)
|
||||||
|
SpaceFiltersView(roomListState.spaceFiltersState)
|
||||||
}
|
}
|
||||||
HomeNavigationBarItem.Spaces -> {
|
HomeNavigationBarItem.Spaces -> {
|
||||||
HomeSpacesView(
|
HomeSpacesView(
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.pager.VerticalPager
|
import androidx.compose.foundation.pager.VerticalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
|
|
@ -44,6 +45,10 @@ import io.element.android.features.home.impl.R
|
||||||
import io.element.android.features.home.impl.filters.RoomListFiltersState
|
import io.element.android.features.home.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.home.impl.filters.RoomListFiltersView
|
import io.element.android.features.home.impl.filters.RoomListFiltersView
|
||||||
import io.element.android.features.home.impl.filters.aRoomListFiltersState
|
import io.element.android.features.home.impl.filters.aRoomListFiltersState
|
||||||
|
import io.element.android.features.home.impl.spacefilters.SpaceFiltersEvent
|
||||||
|
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
|
||||||
|
import io.element.android.features.home.impl.spacefilters.aSelectedSpaceFiltersState
|
||||||
|
import io.element.android.features.home.impl.spacefilters.anUnselectedSpaceFiltersState
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
|
||||||
import io.element.android.libraries.designsystem.components.TopAppBarScrollBehaviorLayout
|
import io.element.android.libraries.designsystem.components.TopAppBarScrollBehaviorLayout
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
|
@ -75,7 +80,6 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeTopBar(
|
fun HomeTopBar(
|
||||||
selectedNavigationItem: HomeNavigationBarItem,
|
selectedNavigationItem: HomeNavigationBarItem,
|
||||||
title: String,
|
|
||||||
currentUserAndNeighbors: ImmutableList<MatrixUser>,
|
currentUserAndNeighbors: ImmutableList<MatrixUser>,
|
||||||
showAvatarIndicator: Boolean,
|
showAvatarIndicator: Boolean,
|
||||||
areSearchResultsDisplayed: Boolean,
|
areSearchResultsDisplayed: Boolean,
|
||||||
|
|
@ -89,6 +93,7 @@ fun HomeTopBar(
|
||||||
canReportBug: Boolean,
|
canReportBug: Boolean,
|
||||||
displayFilters: Boolean,
|
displayFilters: Boolean,
|
||||||
filtersState: RoomListFiltersState,
|
filtersState: RoomListFiltersState,
|
||||||
|
spaceFiltersState: SpaceFiltersState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
|
|
@ -103,12 +108,21 @@ fun HomeTopBar(
|
||||||
scrolledContainerColor = Color.Transparent,
|
scrolledContainerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
title = {
|
title = {
|
||||||
|
val displayTitle = when (selectedNavigationItem) {
|
||||||
|
HomeNavigationBarItem.Chats -> {
|
||||||
|
when (spaceFiltersState) {
|
||||||
|
is SpaceFiltersState.Selected -> spaceFiltersState.selectedFilter.spaceRoom.displayName
|
||||||
|
else -> stringResource(selectedNavigationItem.labelRes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HomeNavigationBarItem.Spaces -> stringResource(selectedNavigationItem.labelRes)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.semantics {
|
modifier = Modifier.semantics {
|
||||||
heading()
|
heading()
|
||||||
},
|
},
|
||||||
style = ElementTheme.typography.aliasScreenTitle,
|
style = ElementTheme.typography.aliasScreenTitle,
|
||||||
text = title,
|
text = displayTitle,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
|
|
@ -124,7 +138,8 @@ fun HomeTopBar(
|
||||||
HomeNavigationBarItem.Chats -> RoomListMenuItems(
|
HomeNavigationBarItem.Chats -> RoomListMenuItems(
|
||||||
onToggleSearch = onToggleSearch,
|
onToggleSearch = onToggleSearch,
|
||||||
onMenuActionClick = onMenuActionClick,
|
onMenuActionClick = onMenuActionClick,
|
||||||
canReportBug = canReportBug
|
canReportBug = canReportBug,
|
||||||
|
spaceFiltersState = spaceFiltersState,
|
||||||
)
|
)
|
||||||
HomeNavigationBarItem.Spaces -> SpacesMenuItems(
|
HomeNavigationBarItem.Spaces -> SpacesMenuItems(
|
||||||
canCreateSpaces = canCreateSpaces,
|
canCreateSpaces = canCreateSpaces,
|
||||||
|
|
@ -154,6 +169,7 @@ private fun RoomListMenuItems(
|
||||||
onToggleSearch: () -> Unit,
|
onToggleSearch: () -> Unit,
|
||||||
onMenuActionClick: (RoomListMenuAction) -> Unit,
|
onMenuActionClick: (RoomListMenuAction) -> Unit,
|
||||||
canReportBug: Boolean,
|
canReportBug: Boolean,
|
||||||
|
spaceFiltersState: SpaceFiltersState,
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onToggleSearch,
|
onClick = onToggleSearch,
|
||||||
|
|
@ -163,6 +179,7 @@ private fun RoomListMenuItems(
|
||||||
contentDescription = stringResource(CommonStrings.action_search),
|
contentDescription = stringResource(CommonStrings.action_search),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
SpaceFilterButton(spaceFiltersState = spaceFiltersState)
|
||||||
if (RoomListConfig.HAS_DROP_DOWN_MENU) {
|
if (RoomListConfig.HAS_DROP_DOWN_MENU) {
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
@ -228,6 +245,38 @@ private fun SpacesMenuItems(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SpaceFilterButton(
|
||||||
|
spaceFiltersState: SpaceFiltersState,
|
||||||
|
) {
|
||||||
|
if (spaceFiltersState == SpaceFiltersState.Disabled) return
|
||||||
|
|
||||||
|
fun onClick() {
|
||||||
|
when (spaceFiltersState) {
|
||||||
|
is SpaceFiltersState.Unselected -> spaceFiltersState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
|
||||||
|
is SpaceFiltersState.Selected -> spaceFiltersState.eventSink(SpaceFiltersEvent.Selected.ClearSelection)
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val isSelected = spaceFiltersState is SpaceFiltersState.Selected
|
||||||
|
IconButton(
|
||||||
|
onClick = ::onClick,
|
||||||
|
colors = if (isSelected) {
|
||||||
|
IconButtonDefaults.iconButtonColors(
|
||||||
|
containerColor = ElementTheme.colors.bgActionPrimaryRest,
|
||||||
|
contentColor = ElementTheme.colors.iconOnSolidPrimary,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
IconButtonDefaults.iconButtonColors()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.Filter(),
|
||||||
|
contentDescription = stringResource(R.string.screen_roomlist_your_spaces),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NavigationIcon(
|
private fun NavigationIcon(
|
||||||
currentUserAndNeighbors: ImmutableList<MatrixUser>,
|
currentUserAndNeighbors: ImmutableList<MatrixUser>,
|
||||||
|
|
@ -309,7 +358,6 @@ private fun AccountIcon(
|
||||||
internal fun HomeTopBarPreview() = ElementPreview {
|
internal fun HomeTopBarPreview() = ElementPreview {
|
||||||
HomeTopBar(
|
HomeTopBar(
|
||||||
selectedNavigationItem = HomeNavigationBarItem.Chats,
|
selectedNavigationItem = HomeNavigationBarItem.Chats,
|
||||||
title = stringResource(R.string.screen_roomlist_main_space_title),
|
|
||||||
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
|
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
|
||||||
showAvatarIndicator = false,
|
showAvatarIndicator = false,
|
||||||
areSearchResultsDisplayed = false,
|
areSearchResultsDisplayed = false,
|
||||||
|
|
@ -322,6 +370,30 @@ internal fun HomeTopBarPreview() = ElementPreview {
|
||||||
canReportBug = true,
|
canReportBug = true,
|
||||||
displayFilters = true,
|
displayFilters = true,
|
||||||
filtersState = aRoomListFiltersState(),
|
filtersState = aRoomListFiltersState(),
|
||||||
|
spaceFiltersState = anUnselectedSpaceFiltersState(),
|
||||||
|
onMenuActionClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun HomeTopBarSpaceFiltersSelectedPreview() = ElementPreview {
|
||||||
|
HomeTopBar(
|
||||||
|
selectedNavigationItem = HomeNavigationBarItem.Chats,
|
||||||
|
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
|
||||||
|
showAvatarIndicator = false,
|
||||||
|
areSearchResultsDisplayed = false,
|
||||||
|
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
|
||||||
|
onOpenSettings = {},
|
||||||
|
onAccountSwitch = {},
|
||||||
|
onToggleSearch = {},
|
||||||
|
onCreateSpace = {},
|
||||||
|
canCreateSpaces = true,
|
||||||
|
canReportBug = true,
|
||||||
|
displayFilters = true,
|
||||||
|
filtersState = aRoomListFiltersState(),
|
||||||
|
spaceFiltersState = aSelectedSpaceFiltersState(),
|
||||||
onMenuActionClick = {},
|
onMenuActionClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -332,7 +404,6 @@ internal fun HomeTopBarPreview() = ElementPreview {
|
||||||
internal fun HomeTopBarSpacesPreview() = ElementPreview {
|
internal fun HomeTopBarSpacesPreview() = ElementPreview {
|
||||||
HomeTopBar(
|
HomeTopBar(
|
||||||
selectedNavigationItem = HomeNavigationBarItem.Spaces,
|
selectedNavigationItem = HomeNavigationBarItem.Spaces,
|
||||||
title = stringResource(R.string.screen_home_tab_spaces),
|
|
||||||
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
|
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
|
||||||
showAvatarIndicator = false,
|
showAvatarIndicator = false,
|
||||||
areSearchResultsDisplayed = false,
|
areSearchResultsDisplayed = false,
|
||||||
|
|
@ -345,6 +416,7 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview {
|
||||||
canReportBug = true,
|
canReportBug = true,
|
||||||
displayFilters = false,
|
displayFilters = false,
|
||||||
filtersState = aRoomListFiltersState(),
|
filtersState = aRoomListFiltersState(),
|
||||||
|
spaceFiltersState = anUnselectedSpaceFiltersState(),
|
||||||
onMenuActionClick = {},
|
onMenuActionClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -355,7 +427,6 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview {
|
||||||
internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
|
internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
|
||||||
HomeTopBar(
|
HomeTopBar(
|
||||||
selectedNavigationItem = HomeNavigationBarItem.Chats,
|
selectedNavigationItem = HomeNavigationBarItem.Chats,
|
||||||
title = stringResource(R.string.screen_roomlist_main_space_title),
|
|
||||||
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
|
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
|
||||||
showAvatarIndicator = true,
|
showAvatarIndicator = true,
|
||||||
areSearchResultsDisplayed = false,
|
areSearchResultsDisplayed = false,
|
||||||
|
|
@ -368,6 +439,7 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
|
||||||
canReportBug = true,
|
canReportBug = true,
|
||||||
displayFilters = true,
|
displayFilters = true,
|
||||||
filtersState = aRoomListFiltersState(),
|
filtersState = aRoomListFiltersState(),
|
||||||
|
spaceFiltersState = anUnselectedSpaceFiltersState(),
|
||||||
onMenuActionClick = {},
|
onMenuActionClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -378,7 +450,6 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
|
||||||
internal fun HomeTopBarMultiAccountPreview() = ElementPreview {
|
internal fun HomeTopBarMultiAccountPreview() = ElementPreview {
|
||||||
HomeTopBar(
|
HomeTopBar(
|
||||||
selectedNavigationItem = HomeNavigationBarItem.Chats,
|
selectedNavigationItem = HomeNavigationBarItem.Chats,
|
||||||
title = stringResource(R.string.screen_roomlist_main_space_title),
|
|
||||||
currentUserAndNeighbors = aMatrixUserList().take(3).toImmutableList(),
|
currentUserAndNeighbors = aMatrixUserList().take(3).toImmutableList(),
|
||||||
showAvatarIndicator = false,
|
showAvatarIndicator = false,
|
||||||
areSearchResultsDisplayed = false,
|
areSearchResultsDisplayed = false,
|
||||||
|
|
@ -391,6 +462,7 @@ internal fun HomeTopBarMultiAccountPreview() = ElementPreview {
|
||||||
canReportBug = true,
|
canReportBug = true,
|
||||||
displayFilters = true,
|
displayFilters = true,
|
||||||
filtersState = aRoomListFiltersState(),
|
filtersState = aRoomListFiltersState(),
|
||||||
|
spaceFiltersState = anUnselectedSpaceFiltersState(),
|
||||||
onMenuActionClick = {},
|
onMenuActionClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,7 @@ import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -47,14 +43,17 @@ import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
|
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListContentState
|
import io.element.android.features.home.impl.roomlist.RoomListContentState
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListContentStateProvider
|
import io.element.android.features.home.impl.roomlist.RoomListContentStateProvider
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListEvents
|
import io.element.android.features.home.impl.roomlist.RoomListEvent
|
||||||
import io.element.android.features.home.impl.roomlist.SecurityBannerState
|
import io.element.android.features.home.impl.roomlist.SecurityBannerState
|
||||||
|
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
|
||||||
|
import io.element.android.features.home.impl.spacefilters.anUnselectedSpaceFiltersState
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
|
@ -62,9 +61,10 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
fun RoomListContentView(
|
fun RoomListContentView(
|
||||||
contentState: RoomListContentState,
|
contentState: RoomListContentState,
|
||||||
filtersState: RoomListFiltersState,
|
filtersState: RoomListFiltersState,
|
||||||
|
spaceFiltersState: SpaceFiltersState,
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
hideInvitesAvatars: Boolean,
|
hideInvitesAvatars: Boolean,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvent) -> Unit,
|
||||||
onSetUpRecoveryClick: () -> Unit,
|
onSetUpRecoveryClick: () -> Unit,
|
||||||
onConfirmRecoveryKeyClick: () -> Unit,
|
onConfirmRecoveryKeyClick: () -> Unit,
|
||||||
onRoomClick: (RoomListRoomSummary) -> Unit,
|
onRoomClick: (RoomListRoomSummary) -> Unit,
|
||||||
|
|
@ -96,6 +96,7 @@ fun RoomListContentView(
|
||||||
state = contentState,
|
state = contentState,
|
||||||
hideInvitesAvatars = hideInvitesAvatars,
|
hideInvitesAvatars = hideInvitesAvatars,
|
||||||
filtersState = filtersState,
|
filtersState = filtersState,
|
||||||
|
spaceFiltersState = spaceFiltersState,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||||
|
|
@ -131,7 +132,7 @@ private fun SkeletonView(
|
||||||
@Composable
|
@Composable
|
||||||
private fun EmptyView(
|
private fun EmptyView(
|
||||||
state: RoomListContentState.Empty,
|
state: RoomListContentState.Empty,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvent) -> Unit,
|
||||||
onSetUpRecoveryClick: () -> Unit,
|
onSetUpRecoveryClick: () -> Unit,
|
||||||
onConfirmRecoveryKeyClick: () -> Unit,
|
onConfirmRecoveryKeyClick: () -> Unit,
|
||||||
onCreateRoomClick: () -> Unit,
|
onCreateRoomClick: () -> Unit,
|
||||||
|
|
@ -155,13 +156,13 @@ private fun EmptyView(
|
||||||
SecurityBannerState.SetUpRecovery -> {
|
SecurityBannerState.SetUpRecovery -> {
|
||||||
SetUpRecoveryKeyBanner(
|
SetUpRecoveryKeyBanner(
|
||||||
onContinueClick = onSetUpRecoveryClick,
|
onContinueClick = onSetUpRecoveryClick,
|
||||||
onDismissClick = { eventSink(RoomListEvents.DismissBanner) },
|
onDismissClick = { eventSink(RoomListEvent.DismissBanner) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SecurityBannerState.RecoveryKeyConfirmation -> {
|
SecurityBannerState.RecoveryKeyConfirmation -> {
|
||||||
ConfirmRecoveryKeyBanner(
|
ConfirmRecoveryKeyBanner(
|
||||||
onContinueClick = onConfirmRecoveryKeyClick,
|
onContinueClick = onConfirmRecoveryKeyClick,
|
||||||
onDismissClick = { eventSink(RoomListEvents.DismissBanner) },
|
onDismissClick = { eventSink(RoomListEvent.DismissBanner) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SecurityBannerState.None -> Unit
|
SecurityBannerState.None -> Unit
|
||||||
|
|
@ -175,7 +176,8 @@ private fun RoomsView(
|
||||||
state: RoomListContentState.Rooms,
|
state: RoomListContentState.Rooms,
|
||||||
hideInvitesAvatars: Boolean,
|
hideInvitesAvatars: Boolean,
|
||||||
filtersState: RoomListFiltersState,
|
filtersState: RoomListFiltersState,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
spaceFiltersState: SpaceFiltersState,
|
||||||
|
eventSink: (RoomListEvent) -> Unit,
|
||||||
onSetUpRecoveryClick: () -> Unit,
|
onSetUpRecoveryClick: () -> Unit,
|
||||||
onConfirmRecoveryKeyClick: () -> Unit,
|
onConfirmRecoveryKeyClick: () -> Unit,
|
||||||
onRoomClick: (RoomListRoomSummary) -> Unit,
|
onRoomClick: (RoomListRoomSummary) -> Unit,
|
||||||
|
|
@ -183,9 +185,12 @@ private fun RoomsView(
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
|
val isSpaceFilterSelected = spaceFiltersState is SpaceFiltersState.Selected
|
||||||
|
val hasAnyFilterSelected = filtersState.hasAnyFilterSelected || isSpaceFilterSelected
|
||||||
|
if (state.summaries.isEmpty() && hasAnyFilterSelected) {
|
||||||
EmptyViewForFilterStates(
|
EmptyViewForFilterStates(
|
||||||
selectedFilters = filtersState.selectedFilters(),
|
selectedFilters = filtersState.selectedFilters(),
|
||||||
|
isSpaceFilterSelected = isSpaceFilterSelected,
|
||||||
modifier = modifier.fillMaxSize()
|
modifier = modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -207,7 +212,7 @@ private fun RoomsView(
|
||||||
private fun RoomsViewList(
|
private fun RoomsViewList(
|
||||||
state: RoomListContentState.Rooms,
|
state: RoomListContentState.Rooms,
|
||||||
hideInvitesAvatars: Boolean,
|
hideInvitesAvatars: Boolean,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvent) -> Unit,
|
||||||
onSetUpRecoveryClick: () -> Unit,
|
onSetUpRecoveryClick: () -> Unit,
|
||||||
onConfirmRecoveryKeyClick: () -> Unit,
|
onConfirmRecoveryKeyClick: () -> Unit,
|
||||||
onRoomClick: (RoomListRoomSummary) -> Unit,
|
onRoomClick: (RoomListRoomSummary) -> Unit,
|
||||||
|
|
@ -215,17 +220,8 @@ private fun RoomsViewList(
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val visibleRange by remember {
|
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||||
derivedStateOf {
|
eventSink(RoomListEvent.UpdateVisibleRange(visibleRange))
|
||||||
val layoutInfo = lazyListState.layoutInfo
|
|
||||||
val firstItemIndex = layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0
|
|
||||||
val size = layoutInfo.visibleItemsInfo.size
|
|
||||||
firstItemIndex until firstItemIndex + size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val updatedEventSink by rememberUpdatedState(newValue = eventSink)
|
|
||||||
LaunchedEffect(visibleRange) {
|
|
||||||
updatedEventSink(RoomListEvents.UpdateVisibleRange(visibleRange))
|
|
||||||
}
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
|
|
@ -237,7 +233,7 @@ private fun RoomsViewList(
|
||||||
item {
|
item {
|
||||||
SetUpRecoveryKeyBanner(
|
SetUpRecoveryKeyBanner(
|
||||||
onContinueClick = onSetUpRecoveryClick,
|
onContinueClick = onSetUpRecoveryClick,
|
||||||
onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) },
|
onDismissClick = { eventSink(RoomListEvent.DismissBanner) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +241,7 @@ private fun RoomsViewList(
|
||||||
item {
|
item {
|
||||||
ConfirmRecoveryKeyBanner(
|
ConfirmRecoveryKeyBanner(
|
||||||
onContinueClick = onConfirmRecoveryKeyClick,
|
onContinueClick = onConfirmRecoveryKeyClick,
|
||||||
onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) },
|
onDismissClick = { eventSink(RoomListEvent.DismissBanner) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -260,7 +256,7 @@ private fun RoomsViewList(
|
||||||
} else if (state.showNewNotificationSoundBanner) {
|
} else if (state.showNewNotificationSoundBanner) {
|
||||||
item {
|
item {
|
||||||
NewNotificationSoundBanner(
|
NewNotificationSoundBanner(
|
||||||
onDismissClick = { updatedEventSink(RoomListEvents.DismissNewNotificationSoundBanner) },
|
onDismissClick = { eventSink(RoomListEvent.DismissNewNotificationSoundBanner) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -290,9 +286,10 @@ private fun RoomsViewList(
|
||||||
@Composable
|
@Composable
|
||||||
private fun EmptyViewForFilterStates(
|
private fun EmptyViewForFilterStates(
|
||||||
selectedFilters: ImmutableList<RoomListFilter>,
|
selectedFilters: ImmutableList<RoomListFilter>,
|
||||||
|
isSpaceFilterSelected: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) ?: return
|
val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected) ?: return
|
||||||
EmptyScaffold(
|
EmptyScaffold(
|
||||||
title = emptyStateResources.title,
|
title = emptyStateResources.title,
|
||||||
subtitle = emptyStateResources.subtitle,
|
subtitle = emptyStateResources.subtitle,
|
||||||
|
|
@ -343,6 +340,7 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
spaceFiltersState = anUnselectedSpaceFiltersState(),
|
||||||
hideInvitesAvatars = false,
|
hideInvitesAvatars = false,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
onSetUpRecoveryClick = {},
|
onSetUpRecoveryClick = {},
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ import io.element.android.features.home.impl.model.LatestEvent
|
||||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.home.impl.model.RoomListRoomSummaryProvider
|
import io.element.android.features.home.impl.model.RoomListRoomSummaryProvider
|
||||||
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
|
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListEvents
|
import io.element.android.features.home.impl.roomlist.RoomListEvent
|
||||||
import io.element.android.libraries.core.extensions.orEmpty
|
import io.element.android.libraries.core.extensions.orEmpty
|
||||||
import io.element.android.libraries.core.extensions.toSafeLength
|
import io.element.android.libraries.core.extensions.toSafeLength
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||||
|
|
@ -74,7 +74,7 @@ internal fun RoomSummaryRow(
|
||||||
hideInviteAvatars: Boolean,
|
hideInviteAvatars: Boolean,
|
||||||
isInviteSeen: Boolean,
|
isInviteSeen: Boolean,
|
||||||
onClick: (RoomListRoomSummary) -> Unit,
|
onClick: (RoomListRoomSummary) -> Unit,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvent) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
|
|
@ -104,10 +104,10 @@ internal fun RoomSummaryRow(
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
InviteButtonsRowMolecule(
|
InviteButtonsRowMolecule(
|
||||||
onAcceptClick = {
|
onAcceptClick = {
|
||||||
eventSink(RoomListEvents.AcceptInvite(room))
|
eventSink(RoomListEvent.AcceptInvite(room))
|
||||||
},
|
},
|
||||||
onDeclineClick = {
|
onDeclineClick = {
|
||||||
eventSink(RoomListEvents.ShowDeclineInviteMenu(room))
|
eventSink(RoomListEvent.ShowDeclineInviteMenu(room))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +117,7 @@ internal fun RoomSummaryRow(
|
||||||
room = room,
|
room = room,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
eventSink(RoomListEvents.ShowContextMenu(room))
|
eventSink(RoomListEvent.ShowContextMenu(room))
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
NameAndTimestampRow(
|
NameAndTimestampRow(
|
||||||
|
|
|
||||||
|
|
@ -9,33 +9,48 @@
|
||||||
package io.element.android.features.home.impl.datasource
|
package io.element.android.features.home.impl.datasource
|
||||||
|
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
|
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
|
||||||
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
|
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
|
||||||
import io.element.android.libraries.androidutils.system.DateTimeObserver
|
import io.element.android.libraries.androidutils.system.DateTimeObserver
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.updateVisibleRange
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.lang.IllegalStateException
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
private const val PAGE_SIZE = 20
|
||||||
|
private const val EXTENDED_VISIBILITY_RANGE_SIZE = 40
|
||||||
|
private const val SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS = 300L
|
||||||
|
private const val PAGINATION_THRESHOLD = 3 * PAGE_SIZE
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
@SingleIn(SessionScope::class)
|
||||||
class RoomListDataSource(
|
class RoomListDataSource(
|
||||||
private val roomListService: RoomListService,
|
private val roomListService: RoomListService,
|
||||||
private val roomListRoomSummaryFactory: RoomListRoomSummaryFactory,
|
private val roomListRoomSummaryFactory: RoomListRoomSummaryFactory,
|
||||||
|
|
@ -51,7 +66,12 @@ class RoomListDataSource(
|
||||||
observeDateTimeChanges()
|
observeDateTimeChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _allRooms = MutableSharedFlow<ImmutableList<RoomListRoomSummary>>(replay = 1)
|
private val roomList = roomListService.createRoomList(
|
||||||
|
pageSize = PAGE_SIZE,
|
||||||
|
source = RoomList.Source.All,
|
||||||
|
coroutineScope = sessionCoroutineScope
|
||||||
|
)
|
||||||
|
private val _roomSummariesFlow = MutableSharedFlow<ImmutableList<RoomListRoomSummary>>(replay = 1)
|
||||||
|
|
||||||
private val lock = Mutex()
|
private val lock = Mutex()
|
||||||
private val diffCache = MutableListDiffCache<RoomListRoomSummary>()
|
private val diffCache = MutableListDiffCache<RoomListRoomSummary>()
|
||||||
|
|
@ -59,22 +79,49 @@ class RoomListDataSource(
|
||||||
old?.roomId == new?.roomId
|
old?.roomId == new?.roomId
|
||||||
}
|
}
|
||||||
|
|
||||||
val allRooms: Flow<ImmutableList<RoomListRoomSummary>> = _allRooms
|
val roomSummariesFlow: Flow<ImmutableList<RoomListRoomSummary>> = _roomSummariesFlow
|
||||||
|
|
||||||
val loadingState = roomListService.allRooms.loadingState
|
val loadingState = roomList.loadingState
|
||||||
|
|
||||||
fun launchIn(coroutineScope: CoroutineScope) {
|
fun launchIn(coroutineScope: CoroutineScope) {
|
||||||
roomListService
|
roomList
|
||||||
.allRooms
|
.summaries
|
||||||
.filteredSummaries
|
|
||||||
.onEach { roomSummaries ->
|
.onEach { roomSummaries ->
|
||||||
replaceWith(roomSummaries)
|
replaceWith(roomSummaries)
|
||||||
}
|
}
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>) {
|
suspend fun updateFilter(filter: RoomListFilter) {
|
||||||
roomListService.subscribeToVisibleRooms(roomIds)
|
roomList.updateFilter(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateVisibleRange(visibleRange: IntRange) = coroutineScope {
|
||||||
|
launch {
|
||||||
|
roomList.updateVisibleRange(visibleRange, PAGINATION_THRESHOLD)
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
subscribeToVisibleRoomsIfNeeded(visibleRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentSubscribeToVisibleRoomsJob: Job? = null
|
||||||
|
private fun CoroutineScope.subscribeToVisibleRoomsIfNeeded(range: IntRange) {
|
||||||
|
currentSubscribeToVisibleRoomsJob?.cancel()
|
||||||
|
currentSubscribeToVisibleRoomsJob = launch {
|
||||||
|
// Debounce the subscription to avoid subscribing to too many rooms
|
||||||
|
delay(SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS)
|
||||||
|
|
||||||
|
if (range.isEmpty()) return@launch
|
||||||
|
val currentRoomList = roomSummariesFlow.first()
|
||||||
|
// Use extended range to 'prefetch' the next rooms info
|
||||||
|
val midExtendedRangeSize = EXTENDED_VISIBILITY_RANGE_SIZE / 2
|
||||||
|
val extendedRange = range.first until range.last + midExtendedRangeSize
|
||||||
|
val roomIds = extendedRange.mapNotNull { index ->
|
||||||
|
currentRoomList.getOrNull(index)?.roomId
|
||||||
|
}
|
||||||
|
roomListService.subscribeToVisibleRooms(roomIds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
|
|
@ -82,7 +129,7 @@ class RoomListDataSource(
|
||||||
notificationSettingsService.notificationSettingsChangeFlow
|
notificationSettingsService.notificationSettingsChangeFlow
|
||||||
.debounce(0.5.seconds)
|
.debounce(0.5.seconds)
|
||||||
.onEach {
|
.onEach {
|
||||||
roomListService.allRooms.rebuildSummaries()
|
roomList.rebuildSummaries()
|
||||||
}
|
}
|
||||||
.launchIn(sessionCoroutineScope)
|
.launchIn(sessionCoroutineScope)
|
||||||
}
|
}
|
||||||
|
|
@ -108,6 +155,7 @@ class RoomListDataSource(
|
||||||
private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>, useCache: Boolean = true) {
|
private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>, useCache: Boolean = true) {
|
||||||
// Used to detect duplicates in the room list summaries - see comment below
|
// Used to detect duplicates in the room list summaries - see comment below
|
||||||
data class CacheResult(val index: Int, val fromCache: Boolean)
|
data class CacheResult(val index: Int, val fromCache: Boolean)
|
||||||
|
|
||||||
val cachingResults = mutableMapOf<RoomId, MutableList<CacheResult>>()
|
val cachingResults = mutableMapOf<RoomId, MutableList<CacheResult>>()
|
||||||
|
|
||||||
val roomListRoomSummaries = diffCache.indices().mapNotNull { index ->
|
val roomListRoomSummaries = diffCache.indices().mapNotNull { index ->
|
||||||
|
|
@ -144,14 +192,14 @@ class RoomListDataSource(
|
||||||
analyticsService.trackError(
|
analyticsService.trackError(
|
||||||
IllegalStateException(
|
IllegalStateException(
|
||||||
"Found duplicates in room summaries after a local UI update: $duplicates. " +
|
"Found duplicates in room summaries after a local UI update: $duplicates. " +
|
||||||
"This could be a race condition/caching issue of some kind"
|
"This could be a race condition/caching issue of some kind"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Remove duplicates before emitting the new values
|
// Remove duplicates before emitting the new values
|
||||||
_allRooms.emit(roomListRoomSummaries.distinctBy { it.roomId }.toImmutableList())
|
_roomSummariesFlow.emit(roomListRoomSummaries.distinctBy { it.roomId }.toImmutableList())
|
||||||
} else {
|
} else {
|
||||||
_allRooms.emit(roomListRoomSummaries.toImmutableList())
|
_roomSummariesFlow.emit(roomListRoomSummaries.toImmutableList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,7 +211,7 @@ class RoomListDataSource(
|
||||||
|
|
||||||
private suspend fun rebuildAllRoomSummaries() {
|
private suspend fun rebuildAllRoomSummaries() {
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
roomListService.allRooms.filteredSummaries.replayCache.firstOrNull()?.let { roomSummaries ->
|
roomList.summaries.replayCache.firstOrNull()?.let { roomSummaries ->
|
||||||
buildAndEmitAllRooms(roomSummaries, useCache = false)
|
buildAndEmitAllRooms(roomSummaries, useCache = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ class RoomListRoomSummaryFactory(
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is LatestEventValue.RoomInvite -> LatestEvent.None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ import io.element.android.features.home.impl.roomlist.RoomListPresenter
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListState
|
import io.element.android.features.home.impl.roomlist.RoomListState
|
||||||
import io.element.android.features.home.impl.search.RoomListSearchPresenter
|
import io.element.android.features.home.impl.search.RoomListSearchPresenter
|
||||||
import io.element.android.features.home.impl.search.RoomListSearchState
|
import io.element.android.features.home.impl.search.RoomListSearchState
|
||||||
|
import io.element.android.features.home.impl.spacefilters.SpaceFiltersPresenter
|
||||||
|
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
||||||
|
|
@ -31,4 +33,7 @@ interface RoomListModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter<RoomListFiltersState>
|
fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter<RoomListFiltersState>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
fun bindSpaceFiltersPresenter(presenter: SpaceFiltersPresenter): Presenter<SpaceFiltersState>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
package io.element.android.features.home.impl.filters
|
package io.element.android.features.home.impl.filters
|
||||||
|
|
||||||
import io.element.android.features.home.impl.R
|
import io.element.android.features.home.impl.R
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum class representing the different filters that can be applied to the room list.
|
* Enum class representing the different filters that can be applied to the room list.
|
||||||
|
|
@ -30,3 +31,13 @@ enum class RoomListFilter(val stringResource: Int) {
|
||||||
Invites -> setOf(Rooms, People, Unread, Favourites)
|
Invites -> setOf(Rooms, People, Unread, Favourites)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun RoomListFilter.into(): MatrixRoomListFilter {
|
||||||
|
return when (this) {
|
||||||
|
RoomListFilter.Rooms -> MatrixRoomListFilter.Category.Group
|
||||||
|
RoomListFilter.People -> MatrixRoomListFilter.Category.People
|
||||||
|
RoomListFilter.Unread -> MatrixRoomListFilter.Unread
|
||||||
|
RoomListFilter.Favourites -> MatrixRoomListFilter.Favorite
|
||||||
|
RoomListFilter.Invites -> MatrixRoomListFilter.Invite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,12 @@ data class RoomListFiltersEmptyStateResources(
|
||||||
/**
|
/**
|
||||||
* Create a [RoomListFiltersEmptyStateResources] from a list of selected filters.
|
* Create a [RoomListFiltersEmptyStateResources] from a list of selected filters.
|
||||||
*/
|
*/
|
||||||
fun fromSelectedFilters(selectedFilters: List<RoomListFilter>): RoomListFiltersEmptyStateResources? {
|
fun fromSelectedFilters(selectedFilters: List<RoomListFilter>, isSpaceFilterSelected: Boolean): RoomListFiltersEmptyStateResources? {
|
||||||
return when {
|
return when {
|
||||||
|
isSpaceFilterSelected -> RoomListFiltersEmptyStateResources(
|
||||||
|
title = R.string.screen_roomlist_filter_mixed_empty_state_title,
|
||||||
|
subtitle = R.string.screen_roomlist_filter_mixed_empty_state_subtitle
|
||||||
|
)
|
||||||
selectedFilters.isEmpty() -> null
|
selectedFilters.isEmpty() -> null
|
||||||
selectedFilters.size == 1 -> {
|
selectedFilters.size == 1 -> {
|
||||||
when (selectedFilters.first()) {
|
when (selectedFilters.first()) {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
package io.element.android.features.home.impl.filters
|
package io.element.android.features.home.impl.filters
|
||||||
|
|
||||||
sealed interface RoomListFiltersEvents {
|
sealed interface RoomListFiltersEvent {
|
||||||
data class ToggleFilter(val filter: RoomListFilter) : RoomListFiltersEvents
|
data class ToggleFilter(val filter: RoomListFilter) : RoomListFiltersEvent
|
||||||
data object ClearSelectedFilters : RoomListFiltersEvents
|
data object ClearSelectedFilters : RoomListFiltersEvent
|
||||||
}
|
}
|
||||||
|
|
@ -9,61 +9,33 @@
|
||||||
package io.element.android.features.home.impl.filters
|
package io.element.android.features.home.impl.filters
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.features.home.impl.filters.selection.FilterSelectionStrategy
|
import io.element.android.features.home.impl.filters.selection.FilterSelectionStrategy
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class RoomListFiltersPresenter(
|
class RoomListFiltersPresenter(
|
||||||
private val roomListService: RoomListService,
|
|
||||||
private val filterSelectionStrategy: FilterSelectionStrategy,
|
private val filterSelectionStrategy: FilterSelectionStrategy,
|
||||||
) : Presenter<RoomListFiltersState> {
|
) : Presenter<RoomListFiltersState> {
|
||||||
private val initialFilters = filterSelectionStrategy.filterSelectionStates.value.toImmutableList()
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomListFiltersState {
|
override fun present(): RoomListFiltersState {
|
||||||
fun handleEvent(event: RoomListFiltersEvents) {
|
fun handleEvent(event: RoomListFiltersEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
RoomListFiltersEvents.ClearSelectedFilters -> {
|
RoomListFiltersEvent.ClearSelectedFilters -> {
|
||||||
filterSelectionStrategy.clear()
|
filterSelectionStrategy.clear()
|
||||||
}
|
}
|
||||||
is RoomListFiltersEvents.ToggleFilter -> {
|
is RoomListFiltersEvent.ToggleFilter -> {
|
||||||
filterSelectionStrategy.toggle(event.filter)
|
filterSelectionStrategy.toggle(event.filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val filters by produceState(initialValue = initialFilters) {
|
val filters by filterSelectionStrategy.filterSelectionStates.collectAsState()
|
||||||
filterSelectionStrategy.filterSelectionStates
|
|
||||||
.map { filters ->
|
|
||||||
value = filters.toImmutableList()
|
|
||||||
filters.mapNotNull { filterState ->
|
|
||||||
if (!filterState.isSelected) {
|
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
when (filterState.filter) {
|
|
||||||
RoomListFilter.Rooms -> MatrixRoomListFilter.Category.Group
|
|
||||||
RoomListFilter.People -> MatrixRoomListFilter.Category.People
|
|
||||||
RoomListFilter.Unread -> MatrixRoomListFilter.Unread
|
|
||||||
RoomListFilter.Favourites -> MatrixRoomListFilter.Favorite
|
|
||||||
RoomListFilter.Invites -> MatrixRoomListFilter.Invite
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.collect { filters ->
|
|
||||||
val result = MatrixRoomListFilter.All(filters)
|
|
||||||
roomListService.allRooms.updateFilter(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return RoomListFiltersState(
|
return RoomListFiltersState(
|
||||||
filterSelectionStates = filters,
|
filterSelectionStates = filters.toImmutableList(),
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
data class RoomListFiltersState(
|
data class RoomListFiltersState(
|
||||||
val filterSelectionStates: ImmutableList<FilterSelectionState>,
|
val filterSelectionStates: ImmutableList<FilterSelectionState>,
|
||||||
val eventSink: (RoomListFiltersEvents) -> Unit,
|
val eventSink: (RoomListFiltersEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
val hasAnyFilterSelected = filterSelectionStates.any { it.isSelected }
|
val hasAnyFilterSelected = filterSelectionStates.any { it.isSelected }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class RoomListFiltersStateProvider : PreviewParameterProvider<RoomListFiltersSta
|
||||||
|
|
||||||
fun aRoomListFiltersState(
|
fun aRoomListFiltersState(
|
||||||
filterSelectionStates: List<FilterSelectionState> = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = false) },
|
filterSelectionStates: List<FilterSelectionState> = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = false) },
|
||||||
eventSink: (RoomListFiltersEvents) -> Unit = {},
|
eventSink: (RoomListFiltersEvent) -> Unit = {},
|
||||||
) = RoomListFiltersState(
|
) = RoomListFiltersState(
|
||||||
filterSelectionStates = filterSelectionStates.toImmutableList(),
|
filterSelectionStates = filterSelectionStates.toImmutableList(),
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,11 @@ fun RoomListFiltersView(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
fun onClearFiltersClick() {
|
fun onClearFiltersClick() {
|
||||||
state.eventSink(RoomListFiltersEvents.ClearSelectedFilters)
|
state.eventSink(RoomListFiltersEvent.ClearSelectedFilters)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleFilter(filter: RoomListFilter) {
|
fun onToggleFilter(filter: RoomListFilter) {
|
||||||
state.eventSink(RoomListFiltersEvents.ToggleFilter(filter))
|
state.eventSink(RoomListFiltersEvent.ToggleFilter(filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
var scrollToStart by remember { mutableIntStateOf(0) }
|
var scrollToStart by remember { mutableIntStateOf(0) }
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultFilterSelectionStrategy : FilterSelectionStrategy {
|
class DefaultFilterSelectionStrategy : FilterSelectionStrategy {
|
||||||
private val selectedFilters = LinkedHashSet<RoomListFilter>()
|
private val selectedFilters = LinkedHashSet<RoomListFilter>()
|
||||||
|
private val availableFilters
|
||||||
|
get() = RoomListFilter.entries.toSet()
|
||||||
|
|
||||||
override val filterSelectionStates = MutableStateFlow(buildFilters())
|
override val filterSelectionStates = MutableStateFlow(buildFilters())
|
||||||
|
|
||||||
|
|
@ -45,7 +47,7 @@ class DefaultFilterSelectionStrategy : FilterSelectionStrategy {
|
||||||
isSelected = true
|
isSelected = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val unselectedFilters = RoomListFilter.entries - selectedFilters - selectedFilters.flatMap { it.incompatibleFilters }.toSet()
|
val unselectedFilters = availableFilters - selectedFilters - selectedFilters.flatMap { it.incompatibleFilters }.toSet()
|
||||||
val unselectedFilterStates = unselectedFilters.map {
|
val unselectedFilterStates = unselectedFilters.map {
|
||||||
FilterSelectionState(
|
FilterSelectionState(
|
||||||
filter = it,
|
filter = it,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface FilterSelectionStrategy {
|
interface FilterSelectionStrategy {
|
||||||
val filterSelectionStates: StateFlow<Set<FilterSelectionState>>
|
val filterSelectionStates: StateFlow<Set<FilterSelectionState>>
|
||||||
|
|
||||||
fun select(filter: RoomListFilter)
|
fun select(filter: RoomListFilter)
|
||||||
fun deselect(filter: RoomListFilter)
|
fun deselect(filter: RoomListFilter)
|
||||||
fun isSelected(filter: RoomListFilter): Boolean
|
fun isSelected(filter: RoomListFilter): Boolean
|
||||||
|
|
|
||||||
|
|
@ -37,41 +37,41 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
fun RoomListContextMenu(
|
fun RoomListContextMenu(
|
||||||
contextMenu: RoomListState.ContextMenu.Shown,
|
contextMenu: RoomListState.ContextMenu.Shown,
|
||||||
canReportRoom: Boolean,
|
canReportRoom: Boolean,
|
||||||
eventSink: (RoomListEvents.ContextMenuEvents) -> Unit,
|
eventSink: (RoomListEvent.ContextMenuEvent) -> Unit,
|
||||||
onRoomSettingsClick: (roomId: RoomId) -> Unit,
|
onRoomSettingsClick: (roomId: RoomId) -> Unit,
|
||||||
onReportRoomClick: (roomId: RoomId) -> Unit
|
onReportRoomClick: (roomId: RoomId) -> Unit
|
||||||
) {
|
) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { eventSink(RoomListEvents.HideContextMenu) },
|
onDismissRequest = { eventSink(RoomListEvent.HideContextMenu) },
|
||||||
) {
|
) {
|
||||||
RoomListModalBottomSheetContent(
|
RoomListModalBottomSheetContent(
|
||||||
contextMenu = contextMenu,
|
contextMenu = contextMenu,
|
||||||
canReportRoom = canReportRoom,
|
canReportRoom = canReportRoom,
|
||||||
onRoomMarkReadClick = {
|
onRoomMarkReadClick = {
|
||||||
eventSink(RoomListEvents.HideContextMenu)
|
eventSink(RoomListEvent.HideContextMenu)
|
||||||
eventSink(RoomListEvents.MarkAsRead(contextMenu.roomId))
|
eventSink(RoomListEvent.MarkAsRead(contextMenu.roomId))
|
||||||
},
|
},
|
||||||
onRoomMarkUnreadClick = {
|
onRoomMarkUnreadClick = {
|
||||||
eventSink(RoomListEvents.HideContextMenu)
|
eventSink(RoomListEvent.HideContextMenu)
|
||||||
eventSink(RoomListEvents.MarkAsUnread(contextMenu.roomId))
|
eventSink(RoomListEvent.MarkAsUnread(contextMenu.roomId))
|
||||||
},
|
},
|
||||||
onRoomSettingsClick = {
|
onRoomSettingsClick = {
|
||||||
eventSink(RoomListEvents.HideContextMenu)
|
eventSink(RoomListEvent.HideContextMenu)
|
||||||
onRoomSettingsClick(contextMenu.roomId)
|
onRoomSettingsClick(contextMenu.roomId)
|
||||||
},
|
},
|
||||||
onLeaveRoomClick = {
|
onLeaveRoomClick = {
|
||||||
eventSink(RoomListEvents.HideContextMenu)
|
eventSink(RoomListEvent.HideContextMenu)
|
||||||
eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId, needsConfirmation = true))
|
eventSink(RoomListEvent.LeaveRoom(contextMenu.roomId, needsConfirmation = true))
|
||||||
},
|
},
|
||||||
onFavoriteChange = { isFavorite ->
|
onFavoriteChange = { isFavorite ->
|
||||||
eventSink(RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, isFavorite))
|
eventSink(RoomListEvent.SetRoomIsFavorite(contextMenu.roomId, isFavorite))
|
||||||
},
|
},
|
||||||
onClearCacheRoomClick = {
|
onClearCacheRoomClick = {
|
||||||
eventSink(RoomListEvents.HideContextMenu)
|
eventSink(RoomListEvent.HideContextMenu)
|
||||||
eventSink(RoomListEvents.ClearCacheOfRoom(contextMenu.roomId))
|
eventSink(RoomListEvent.ClearCacheOfRoom(contextMenu.roomId))
|
||||||
},
|
},
|
||||||
onReportRoomClick = {
|
onReportRoomClick = {
|
||||||
eventSink(RoomListEvents.HideContextMenu)
|
eventSink(RoomListEvent.HideContextMenu)
|
||||||
onReportRoomClick(contextMenu.roomId)
|
onReportRoomClick(contextMenu.roomId)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -131,16 +131,21 @@ private fun RoomListModalBottomSheetContent(
|
||||||
style = ListItemStyle.Primary,
|
style = ListItemStyle.Primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val (textResId, icon) = if (contextMenu.isFavorite) {
|
||||||
|
CommonStrings.common_favourited to CompoundIcons.FavouriteSolid()
|
||||||
|
} else {
|
||||||
|
CommonStrings.common_favourite to CompoundIcons.Favourite()
|
||||||
|
}
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = CommonStrings.common_favourite),
|
text = stringResource(id = textResId),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
leadingContent = ListItemContent.Icon(
|
leadingContent = ListItemContent.Icon(
|
||||||
iconSource = IconSource.Vector(
|
iconSource = IconSource.Vector(
|
||||||
CompoundIcons.Favourite(),
|
icon,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
trailingContent = ListItemContent.Switch(
|
trailingContent = ListItemContent.Switch(
|
||||||
|
|
|
||||||
|
|
@ -38,27 +38,27 @@ fun RoomListDeclineInviteMenu(
|
||||||
menu: RoomListState.DeclineInviteMenu.Shown,
|
menu: RoomListState.DeclineInviteMenu.Shown,
|
||||||
canReportRoom: Boolean,
|
canReportRoom: Boolean,
|
||||||
onDeclineAndBlockClick: (RoomListRoomSummary) -> Unit,
|
onDeclineAndBlockClick: (RoomListRoomSummary) -> Unit,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { eventSink(RoomListEvents.HideDeclineInviteMenu) },
|
onDismissRequest = { eventSink(RoomListEvent.HideDeclineInviteMenu) },
|
||||||
) {
|
) {
|
||||||
RoomListDeclineInviteMenuContent(
|
RoomListDeclineInviteMenuContent(
|
||||||
roomName = menu.roomSummary.name ?: menu.roomSummary.roomId.value,
|
roomName = menu.roomSummary.name ?: menu.roomSummary.roomId.value,
|
||||||
onDeclineClick = {
|
onDeclineClick = {
|
||||||
eventSink(RoomListEvents.HideDeclineInviteMenu)
|
eventSink(RoomListEvent.HideDeclineInviteMenu)
|
||||||
eventSink(RoomListEvents.DeclineInvite(menu.roomSummary, false))
|
eventSink(RoomListEvent.DeclineInvite(menu.roomSummary, false))
|
||||||
},
|
},
|
||||||
onDeclineAndBlockClick = {
|
onDeclineAndBlockClick = {
|
||||||
eventSink(RoomListEvents.HideDeclineInviteMenu)
|
eventSink(RoomListEvent.HideDeclineInviteMenu)
|
||||||
if (canReportRoom) {
|
if (canReportRoom) {
|
||||||
onDeclineAndBlockClick(menu.roomSummary)
|
onDeclineAndBlockClick(menu.roomSummary)
|
||||||
} else {
|
} else {
|
||||||
eventSink(RoomListEvents.DeclineInvite(menu.roomSummary, true))
|
eventSink(RoomListEvent.DeclineInvite(menu.roomSummary, true))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCancelClick = {
|
onCancelClick = {
|
||||||
eventSink(RoomListEvents.HideDeclineInviteMenu)
|
eventSink(RoomListEvent.HideDeclineInviteMenu)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,24 +11,24 @@ package io.element.android.features.home.impl.roomlist
|
||||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
sealed interface RoomListEvents {
|
sealed interface RoomListEvent {
|
||||||
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
|
data class UpdateVisibleRange(val range: IntRange) : RoomListEvent
|
||||||
data object DismissRequestVerificationPrompt : RoomListEvents
|
data object DismissRequestVerificationPrompt : RoomListEvent
|
||||||
data object DismissBanner : RoomListEvents
|
data object DismissBanner : RoomListEvent
|
||||||
data object DismissNewNotificationSoundBanner : RoomListEvents
|
data object DismissNewNotificationSoundBanner : RoomListEvent
|
||||||
data object ToggleSearchResults : RoomListEvents
|
data object ToggleSearchResults : RoomListEvent
|
||||||
data class ShowContextMenu(val roomSummary: RoomListRoomSummary) : RoomListEvents
|
data class ShowContextMenu(val roomSummary: RoomListRoomSummary) : RoomListEvent
|
||||||
|
|
||||||
data class AcceptInvite(val roomSummary: RoomListRoomSummary) : RoomListEvents
|
data class AcceptInvite(val roomSummary: RoomListRoomSummary) : RoomListEvent
|
||||||
data class DeclineInvite(val roomSummary: RoomListRoomSummary, val blockUser: Boolean) : RoomListEvents
|
data class DeclineInvite(val roomSummary: RoomListRoomSummary, val blockUser: Boolean) : RoomListEvent
|
||||||
data class ShowDeclineInviteMenu(val roomSummary: RoomListRoomSummary) : RoomListEvents
|
data class ShowDeclineInviteMenu(val roomSummary: RoomListRoomSummary) : RoomListEvent
|
||||||
data object HideDeclineInviteMenu : RoomListEvents
|
data object HideDeclineInviteMenu : RoomListEvent
|
||||||
|
|
||||||
sealed interface ContextMenuEvents : RoomListEvents
|
sealed interface ContextMenuEvent : RoomListEvent
|
||||||
data object HideContextMenu : ContextMenuEvents
|
data object HideContextMenu : ContextMenuEvent
|
||||||
data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : ContextMenuEvents
|
data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : ContextMenuEvent
|
||||||
data class MarkAsRead(val roomId: RoomId) : ContextMenuEvents
|
data class MarkAsRead(val roomId: RoomId) : ContextMenuEvent
|
||||||
data class MarkAsUnread(val roomId: RoomId) : ContextMenuEvents
|
data class MarkAsUnread(val roomId: RoomId) : ContextMenuEvent
|
||||||
data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : ContextMenuEvents
|
data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : ContextMenuEvent
|
||||||
data class ClearCacheOfRoom(val roomId: RoomId) : ContextMenuEvents
|
data class ClearCacheOfRoom(val roomId: RoomId) : ContextMenuEvent
|
||||||
}
|
}
|
||||||
|
|
@ -28,9 +28,14 @@ import im.vector.app.features.analytics.plan.Interaction
|
||||||
import io.element.android.features.announcement.api.Announcement
|
import io.element.android.features.announcement.api.Announcement
|
||||||
import io.element.android.features.announcement.api.AnnouncementService
|
import io.element.android.features.announcement.api.AnnouncementService
|
||||||
import io.element.android.features.home.impl.datasource.RoomListDataSource
|
import io.element.android.features.home.impl.datasource.RoomListDataSource
|
||||||
|
import io.element.android.features.home.impl.filters.RoomListFilter.Rooms
|
||||||
import io.element.android.features.home.impl.filters.RoomListFiltersState
|
import io.element.android.features.home.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.home.impl.search.RoomListSearchEvents
|
import io.element.android.features.home.impl.filters.into
|
||||||
|
import io.element.android.features.home.impl.search.RoomListSearchEvent
|
||||||
import io.element.android.features.home.impl.search.RoomListSearchState
|
import io.element.android.features.home.impl.search.RoomListSearchState
|
||||||
|
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.SeenInvitesStore
|
||||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite
|
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.AcceptDeclineInviteEvents.DeclineInvite
|
||||||
|
|
@ -44,6 +49,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
|
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
|
|
@ -57,8 +63,6 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -68,9 +72,6 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.takeWhile
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
private const val EXTENDED_RANGE_SIZE = 40
|
|
||||||
private const val SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS = 300L
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class RoomListPresenter(
|
class RoomListPresenter(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
|
|
@ -88,6 +89,7 @@ class RoomListPresenter(
|
||||||
private val seenInvitesStore: SeenInvitesStore,
|
private val seenInvitesStore: SeenInvitesStore,
|
||||||
private val announcementService: AnnouncementService,
|
private val announcementService: AnnouncementService,
|
||||||
private val coldStartWatcher: AnalyticsColdStartWatcher,
|
private val coldStartWatcher: AnalyticsColdStartWatcher,
|
||||||
|
private val spaceFiltersPresenter: Presenter<SpaceFiltersState>,
|
||||||
) : Presenter<RoomListState> {
|
) : Presenter<RoomListState> {
|
||||||
private val encryptionService = client.encryptionService
|
private val encryptionService = client.encryptionService
|
||||||
|
|
||||||
|
|
@ -97,6 +99,7 @@ class RoomListPresenter(
|
||||||
val leaveRoomState = leaveRoomPresenter.present()
|
val leaveRoomState = leaveRoomPresenter.present()
|
||||||
val filtersState = filtersPresenter.present()
|
val filtersState = filtersPresenter.present()
|
||||||
val searchState = searchPresenter.present()
|
val searchState = searchPresenter.present()
|
||||||
|
val spaceFiltersState = spaceFiltersPresenter.present()
|
||||||
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|
@ -116,45 +119,52 @@ class RoomListPresenter(
|
||||||
val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
|
val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
|
||||||
val declineInviteMenu = remember { mutableStateOf<RoomListState.DeclineInviteMenu>(RoomListState.DeclineInviteMenu.Hidden) }
|
val declineInviteMenu = remember { mutableStateOf<RoomListState.DeclineInviteMenu>(RoomListState.DeclineInviteMenu.Hidden) }
|
||||||
|
|
||||||
fun handleEvent(event: RoomListEvents) {
|
fun handleEvent(event: RoomListEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is RoomListEvents.UpdateVisibleRange -> coroutineScope.launch {
|
is RoomListEvent.UpdateVisibleRange -> coroutineScope.launch {
|
||||||
updateVisibleRange(event.range)
|
roomListDataSource.updateVisibleRange(event.range)
|
||||||
}
|
}
|
||||||
RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true
|
RoomListEvent.DismissRequestVerificationPrompt -> securityBannerDismissed = true
|
||||||
RoomListEvents.DismissBanner -> securityBannerDismissed = true
|
RoomListEvent.DismissBanner -> securityBannerDismissed = true
|
||||||
RoomListEvents.DismissNewNotificationSoundBanner -> coroutineScope.launch {
|
RoomListEvent.DismissNewNotificationSoundBanner -> coroutineScope.launch {
|
||||||
announcementService.onAnnouncementDismissed(Announcement.NewNotificationSound)
|
announcementService.onAnnouncementDismissed(Announcement.NewNotificationSound)
|
||||||
}
|
}
|
||||||
RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
RoomListEvent.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvent.ToggleSearchVisibility)
|
||||||
is RoomListEvents.ShowContextMenu -> {
|
is RoomListEvent.ShowContextMenu -> {
|
||||||
coroutineScope.showContextMenu(event, contextMenu)
|
coroutineScope.showContextMenu(event, contextMenu)
|
||||||
}
|
}
|
||||||
is RoomListEvents.HideContextMenu -> {
|
is RoomListEvent.HideContextMenu -> {
|
||||||
contextMenu.value = RoomListState.ContextMenu.Hidden
|
contextMenu.value = RoomListState.ContextMenu.Hidden
|
||||||
}
|
}
|
||||||
is RoomListEvents.LeaveRoom -> {
|
is RoomListEvent.LeaveRoom -> {
|
||||||
leaveRoomState.eventSink(LeaveRoomEvent.LeaveRoom(event.roomId, needsConfirmation = event.needsConfirmation))
|
leaveRoomState.eventSink(LeaveRoomEvent.LeaveRoom(event.roomId, needsConfirmation = event.needsConfirmation))
|
||||||
}
|
}
|
||||||
is RoomListEvents.SetRoomIsFavorite -> coroutineScope.setRoomIsFavorite(event.roomId, event.isFavorite)
|
is RoomListEvent.SetRoomIsFavorite -> coroutineScope.setRoomIsFavorite(event.roomId, event.isFavorite)
|
||||||
is RoomListEvents.MarkAsRead -> coroutineScope.markAsRead(event.roomId)
|
is RoomListEvent.MarkAsRead -> coroutineScope.markAsRead(event.roomId)
|
||||||
is RoomListEvents.MarkAsUnread -> coroutineScope.markAsUnread(event.roomId)
|
is RoomListEvent.MarkAsUnread -> coroutineScope.markAsUnread(event.roomId)
|
||||||
is RoomListEvents.AcceptInvite -> {
|
is RoomListEvent.AcceptInvite -> {
|
||||||
acceptDeclineInviteState.eventSink(
|
acceptDeclineInviteState.eventSink(
|
||||||
AcceptInvite(event.roomSummary.toInviteData())
|
AcceptInvite(event.roomSummary.toInviteData())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is RoomListEvents.DeclineInvite -> {
|
is RoomListEvent.DeclineInvite -> {
|
||||||
acceptDeclineInviteState.eventSink(
|
acceptDeclineInviteState.eventSink(
|
||||||
DeclineInvite(event.roomSummary.toInviteData(), blockUser = event.blockUser, shouldConfirm = false)
|
DeclineInvite(event.roomSummary.toInviteData(), blockUser = event.blockUser, shouldConfirm = false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is RoomListEvents.ShowDeclineInviteMenu -> declineInviteMenu.value = RoomListState.DeclineInviteMenu.Shown(event.roomSummary)
|
is RoomListEvent.ShowDeclineInviteMenu -> declineInviteMenu.value = RoomListState.DeclineInviteMenu.Shown(event.roomSummary)
|
||||||
RoomListEvents.HideDeclineInviteMenu -> declineInviteMenu.value = RoomListState.DeclineInviteMenu.Hidden
|
RoomListEvent.HideDeclineInviteMenu -> declineInviteMenu.value = RoomListState.DeclineInviteMenu.Hidden
|
||||||
is RoomListEvents.ClearCacheOfRoom -> coroutineScope.clearCacheOfRoom(event.roomId)
|
is RoomListEvent.ClearCacheOfRoom -> coroutineScope.clearCacheOfRoom(event.roomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(filtersState.filterSelectionStates, spaceFiltersState.selectedFilter()) {
|
||||||
|
val selectedFilters = filtersState.selectedFilters().map { filter -> filter.into() }
|
||||||
|
val selectedSpaceFilter = spaceFiltersState.selectedFilter().into()
|
||||||
|
val allFilters = RoomListFilter.All(selectedFilters + listOfNotNull(selectedSpaceFilter))
|
||||||
|
roomListDataSource.updateFilter(allFilters)
|
||||||
|
}
|
||||||
|
|
||||||
val contentState = roomListContentState(
|
val contentState = roomListContentState(
|
||||||
securityBannerDismissed,
|
securityBannerDismissed,
|
||||||
showNewNotificationSoundBanner,
|
showNewNotificationSoundBanner,
|
||||||
|
|
@ -168,6 +178,7 @@ class RoomListPresenter(
|
||||||
leaveRoomState = leaveRoomState,
|
leaveRoomState = leaveRoomState,
|
||||||
filtersState = filtersState,
|
filtersState = filtersState,
|
||||||
searchState = searchState,
|
searchState = searchState,
|
||||||
|
spaceFiltersState = spaceFiltersState,
|
||||||
contentState = contentState,
|
contentState = contentState,
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
hideInvitesAvatars = hideInvitesAvatar,
|
hideInvitesAvatars = hideInvitesAvatar,
|
||||||
|
|
@ -217,7 +228,7 @@ class RoomListPresenter(
|
||||||
showNewNotificationSoundBanner: Boolean,
|
showNewNotificationSoundBanner: Boolean,
|
||||||
): RoomListContentState {
|
): RoomListContentState {
|
||||||
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
|
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
|
||||||
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
|
roomListDataSource.roomSummariesFlow.collect { value = AsyncData.Success(it) }
|
||||||
}
|
}
|
||||||
val loadingState by roomListDataSource.loadingState.collectAsState()
|
val loadingState by roomListDataSource.loadingState.collectAsState()
|
||||||
val showEmpty by remember {
|
val showEmpty by remember {
|
||||||
|
|
@ -253,7 +264,7 @@ class RoomListPresenter(
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private fun CoroutineScope.showContextMenu(event: RoomListEvents.ShowContextMenu, contextMenuState: MutableState<RoomListState.ContextMenu>) = launch {
|
private fun CoroutineScope.showContextMenu(event: RoomListEvent.ShowContextMenu, contextMenuState: MutableState<RoomListState.ContextMenu>) = launch {
|
||||||
val initialState = RoomListState.ContextMenu.Shown(
|
val initialState = RoomListState.ContextMenu.Shown(
|
||||||
roomId = event.roomSummary.roomId,
|
roomId = event.roomSummary.roomId,
|
||||||
roomName = event.roomSummary.name,
|
roomName = event.roomSummary.name,
|
||||||
|
|
@ -322,23 +333,4 @@ class RoomListPresenter(
|
||||||
room.clearEventCacheStorage()
|
room.clearEventCacheStorage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentUpdateVisibleRangeJob: Job? = null
|
|
||||||
private fun CoroutineScope.updateVisibleRange(range: IntRange) {
|
|
||||||
currentUpdateVisibleRangeJob?.cancel()
|
|
||||||
currentUpdateVisibleRangeJob = launch {
|
|
||||||
// Debounce the subscription to avoid subscribing to too many rooms
|
|
||||||
delay(SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS)
|
|
||||||
|
|
||||||
if (range.isEmpty()) return@launch
|
|
||||||
val currentRoomList = roomListDataSource.allRooms.first()
|
|
||||||
// Use extended range to 'prefetch' the next rooms info
|
|
||||||
val midExtendedRangeSize = EXTENDED_RANGE_SIZE / 2
|
|
||||||
val extendedRange = range.first until range.last + midExtendedRangeSize
|
|
||||||
val roomIds = extendedRange.mapNotNull { index ->
|
|
||||||
currentRoomList.getOrNull(index)?.roomId
|
|
||||||
}
|
|
||||||
roomListDataSource.subscribeToVisibleRooms(roomIds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.home.impl.filters.RoomListFiltersState
|
import io.element.android.features.home.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.home.impl.search.RoomListSearchState
|
import io.element.android.features.home.impl.search.RoomListSearchState
|
||||||
|
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
|
||||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
|
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
|
||||||
|
|
@ -26,11 +27,12 @@ data class RoomListState(
|
||||||
val leaveRoomState: LeaveRoomState,
|
val leaveRoomState: LeaveRoomState,
|
||||||
val filtersState: RoomListFiltersState,
|
val filtersState: RoomListFiltersState,
|
||||||
val searchState: RoomListSearchState,
|
val searchState: RoomListSearchState,
|
||||||
|
val spaceFiltersState: SpaceFiltersState,
|
||||||
val contentState: RoomListContentState,
|
val contentState: RoomListContentState,
|
||||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||||
val hideInvitesAvatars: Boolean,
|
val hideInvitesAvatars: Boolean,
|
||||||
val canReportRoom: Boolean,
|
val canReportRoom: Boolean,
|
||||||
val eventSink: (RoomListEvents) -> Unit,
|
val eventSink: (RoomListEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
val displayFilters = contentState is RoomListContentState.Rooms
|
val displayFilters = contentState is RoomListContentState.Rooms
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ open class RoomListStateContextMenuShownProvider : PreviewParameterProvider<Room
|
||||||
override val values: Sequence<RoomListState.ContextMenu.Shown>
|
override val values: Sequence<RoomListState.ContextMenu.Shown>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aContextMenuShown(hasNewContent = true),
|
aContextMenuShown(hasNewContent = true),
|
||||||
aContextMenuShown(isDm = true),
|
aContextMenuShown(isDm = true, isFavorite = true),
|
||||||
aContextMenuShown(roomName = null)
|
aContextMenuShown(roomName = null)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import io.element.android.features.home.impl.model.aRoomListRoomSummary
|
||||||
import io.element.android.features.home.impl.model.anInviteSender
|
import io.element.android.features.home.impl.model.anInviteSender
|
||||||
import io.element.android.features.home.impl.search.RoomListSearchState
|
import io.element.android.features.home.impl.search.RoomListSearchState
|
||||||
import io.element.android.features.home.impl.search.aRoomListSearchState
|
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.anUnselectedSpaceFiltersState
|
||||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
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.acceptdecline.anAcceptDeclineInviteState
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||||
|
|
@ -52,17 +54,19 @@ internal fun aRoomListState(
|
||||||
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
||||||
searchState: RoomListSearchState = aRoomListSearchState(),
|
searchState: RoomListSearchState = aRoomListSearchState(),
|
||||||
filtersState: RoomListFiltersState = aRoomListFiltersState(),
|
filtersState: RoomListFiltersState = aRoomListFiltersState(),
|
||||||
|
spaceFiltersState: SpaceFiltersState = anUnselectedSpaceFiltersState(),
|
||||||
contentState: RoomListContentState = aRoomsContentState(),
|
contentState: RoomListContentState = aRoomsContentState(),
|
||||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||||
hideInvitesAvatars: Boolean = false,
|
hideInvitesAvatars: Boolean = false,
|
||||||
canReportRoom: Boolean = true,
|
canReportRoom: Boolean = true,
|
||||||
eventSink: (RoomListEvents) -> Unit = {}
|
eventSink: (RoomListEvent) -> Unit = {}
|
||||||
) = RoomListState(
|
) = RoomListState(
|
||||||
contextMenu = contextMenu,
|
contextMenu = contextMenu,
|
||||||
declineInviteMenu = declineInviteMenu,
|
declineInviteMenu = declineInviteMenu,
|
||||||
leaveRoomState = leaveRoomState,
|
leaveRoomState = leaveRoomState,
|
||||||
filtersState = filtersState,
|
filtersState = filtersState,
|
||||||
searchState = searchState,
|
searchState = searchState,
|
||||||
|
spaceFiltersState = spaceFiltersState,
|
||||||
contentState = contentState,
|
contentState = contentState,
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
hideInvitesAvatars = hideInvitesAvatars,
|
hideInvitesAvatars = hideInvitesAvatars,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
|
import io.element.android.libraries.matrix.api.roomlist.updateVisibleRange
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -42,12 +42,11 @@ class RoomListSearchDataSource(
|
||||||
|
|
||||||
private val roomList = roomListService.createRoomList(
|
private val roomList = roomListService.createRoomList(
|
||||||
pageSize = PAGE_SIZE,
|
pageSize = PAGE_SIZE,
|
||||||
initialFilter = RoomListFilter.None,
|
|
||||||
source = RoomList.Source.All,
|
source = RoomList.Source.All,
|
||||||
coroutineScope = coroutineScope
|
coroutineScope = coroutineScope
|
||||||
)
|
)
|
||||||
|
|
||||||
val roomSummaries: Flow<ImmutableList<RoomListRoomSummary>> = roomList.filteredSummaries
|
val roomSummaries: Flow<ImmutableList<RoomListRoomSummary>> = roomList.summaries
|
||||||
.map { roomSummaries ->
|
.map { roomSummaries ->
|
||||||
roomSummaries
|
roomSummaries
|
||||||
.map(roomSummaryFactory::create)
|
.map(roomSummaryFactory::create)
|
||||||
|
|
@ -55,12 +54,8 @@ class RoomListSearchDataSource(
|
||||||
}
|
}
|
||||||
.flowOn(coroutineDispatchers.computation)
|
.flowOn(coroutineDispatchers.computation)
|
||||||
|
|
||||||
suspend fun setIsActive(isActive: Boolean) = coroutineScope {
|
suspend fun updateVisibleRange(visibleRange: IntRange) {
|
||||||
if (isActive) {
|
roomList.updateVisibleRange(visibleRange)
|
||||||
roomList.loadAllIncrementally(this)
|
|
||||||
} else {
|
|
||||||
roomList.reset()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setSearchQuery(searchQuery: String) = coroutineScope {
|
suspend fun setSearchQuery(searchQuery: String) = coroutineScope {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
|
|
||||||
package io.element.android.features.home.impl.search
|
package io.element.android.features.home.impl.search
|
||||||
|
|
||||||
sealed interface RoomListSearchEvents {
|
sealed interface RoomListSearchEvent {
|
||||||
data object ToggleSearchVisibility : RoomListSearchEvents
|
data object ToggleSearchVisibility : RoomListSearchEvent
|
||||||
data object ClearQuery : RoomListSearchEvents
|
data object ClearQuery : RoomListSearchEvent
|
||||||
|
data class UpdateVisibleRange(val range: IntRange) : RoomListSearchEvent
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class RoomListSearchPresenter(
|
class RoomListSearchPresenter(
|
||||||
|
|
@ -37,23 +38,22 @@ class RoomListSearchPresenter(
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
|
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
|
||||||
|
|
||||||
LaunchedEffect(isSearchActive) {
|
|
||||||
dataSource.setIsActive(isSearchActive)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(searchQuery.text) {
|
LaunchedEffect(searchQuery.text) {
|
||||||
dataSource.setSearchQuery(searchQuery.text.toString())
|
dataSource.setSearchQuery(searchQuery.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvent(event: RoomListSearchEvents) {
|
fun handleEvent(event: RoomListSearchEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
RoomListSearchEvents.ClearQuery -> {
|
RoomListSearchEvent.ClearQuery -> {
|
||||||
searchQuery.clearText()
|
searchQuery.clearText()
|
||||||
}
|
}
|
||||||
RoomListSearchEvents.ToggleSearchVisibility -> {
|
RoomListSearchEvent.ToggleSearchVisibility -> {
|
||||||
isSearchActive = !isSearchActive
|
isSearchActive = !isSearchActive
|
||||||
searchQuery.clearText()
|
searchQuery.clearText()
|
||||||
}
|
}
|
||||||
|
is RoomListSearchEvent.UpdateVisibleRange -> coroutineScope.launch {
|
||||||
|
dataSource.updateVisibleRange(visibleRange = event.range)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,5 +16,5 @@ data class RoomListSearchState(
|
||||||
val isSearchActive: Boolean,
|
val isSearchActive: Boolean,
|
||||||
val query: TextFieldState,
|
val query: TextFieldState,
|
||||||
val results: ImmutableList<RoomListRoomSummary>,
|
val results: ImmutableList<RoomListRoomSummary>,
|
||||||
val eventSink: (RoomListSearchEvents) -> Unit
|
val eventSink: (RoomListSearchEvent) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ fun aRoomListSearchState(
|
||||||
isSearchActive: Boolean = false,
|
isSearchActive: Boolean = false,
|
||||||
query: String = "",
|
query: String = "",
|
||||||
results: ImmutableList<RoomListRoomSummary> = persistentListOf(),
|
results: ImmutableList<RoomListRoomSummary> = persistentListOf(),
|
||||||
eventSink: (RoomListSearchEvents) -> Unit = { },
|
eventSink: (RoomListSearchEvent) -> Unit = { },
|
||||||
) = RoomListSearchState(
|
) = RoomListSearchState(
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
query = TextFieldState(initialText = query),
|
query = TextFieldState(initialText = query),
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -38,7 +39,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.home.impl.components.RoomSummaryRow
|
import io.element.android.features.home.impl.components.RoomSummaryRow
|
||||||
import io.element.android.features.home.impl.contentType
|
import io.element.android.features.home.impl.contentType
|
||||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListEvents
|
import io.element.android.features.home.impl.roomlist.RoomListEvent
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -47,6 +48,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
|
@ -54,12 +56,12 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
internal fun RoomListSearchView(
|
internal fun RoomListSearchView(
|
||||||
state: RoomListSearchState,
|
state: RoomListSearchState,
|
||||||
hideInvitesAvatars: Boolean,
|
hideInvitesAvatars: Boolean,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvent) -> Unit,
|
||||||
onRoomClick: (RoomId) -> Unit,
|
onRoomClick: (RoomId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
BackHandler(enabled = state.isSearchActive) {
|
BackHandler(enabled = state.isSearchActive) {
|
||||||
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
state.eventSink(RoomListSearchEvent.ToggleSearchVisibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
|
|
@ -83,13 +85,13 @@ internal fun RoomListSearchView(
|
||||||
private fun RoomListSearchContent(
|
private fun RoomListSearchContent(
|
||||||
state: RoomListSearchState,
|
state: RoomListSearchState,
|
||||||
hideInvitesAvatars: Boolean,
|
hideInvitesAvatars: Boolean,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvent) -> Unit,
|
||||||
onRoomClick: (RoomId) -> Unit,
|
onRoomClick: (RoomId) -> Unit,
|
||||||
) {
|
) {
|
||||||
val borderColor = MaterialTheme.colorScheme.tertiary
|
val borderColor = MaterialTheme.colorScheme.tertiary
|
||||||
val strokeWidth = 1.dp
|
val strokeWidth = 1.dp
|
||||||
fun onBackButtonClick() {
|
fun onBackButtonClick() {
|
||||||
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
state.eventSink(RoomListSearchEvent.ToggleSearchVisibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRoomClick(room: RoomListRoomSummary) {
|
fun onRoomClick(room: RoomListRoomSummary) {
|
||||||
|
|
@ -127,7 +129,7 @@ private fun RoomListSearchContent(
|
||||||
),
|
),
|
||||||
trailingIcon = if (state.query.text.isNotEmpty()) {
|
trailingIcon = if (state.query.text.isNotEmpty()) {
|
||||||
@Composable {
|
@Composable {
|
||||||
IconButton(onClick = { state.eventSink(RoomListSearchEvents.ClearQuery) }) {
|
IconButton(onClick = { state.eventSink(RoomListSearchEvent.ClearQuery) }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.Close(),
|
imageVector = CompoundIcons.Close(),
|
||||||
contentDescription = stringResource(CommonStrings.action_cancel)
|
contentDescription = stringResource(CommonStrings.action_cancel)
|
||||||
|
|
@ -154,7 +156,12 @@ private fun RoomListSearchContent(
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||||
|
state.eventSink(RoomListSearchEvent.UpdateVisibleRange(visibleRange))
|
||||||
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.home.impl.spacefilters
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
|
||||||
|
|
||||||
|
sealed interface SpaceFiltersEvent {
|
||||||
|
// Only valid in Unselected state
|
||||||
|
sealed interface Unselected : SpaceFiltersEvent {
|
||||||
|
data object ShowFilters : Unselected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only valid in Selecting state
|
||||||
|
sealed interface Selecting : SpaceFiltersEvent {
|
||||||
|
data object Cancel : Selecting
|
||||||
|
data class SelectFilter(val spaceFilter: SpaceServiceFilter) : Selecting
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only valid in Selected state
|
||||||
|
sealed interface Selected : SpaceFiltersEvent {
|
||||||
|
data object ClearSelection : Selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.home.impl.spacefilters
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
class SpaceFiltersPresenter(
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
|
) : Presenter<SpaceFiltersState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): SpaceFiltersState {
|
||||||
|
val isFeatureEnabled by featureFlagService
|
||||||
|
.isFeatureEnabledFlow(FeatureFlags.RoomListSpaceFilters)
|
||||||
|
.collectAsState(initial = false)
|
||||||
|
|
||||||
|
val availableFilters by remember {
|
||||||
|
matrixClient.spaceService.spaceFiltersFlow.map { it.toImmutableList() }
|
||||||
|
}.collectAsState(initial = persistentListOf())
|
||||||
|
|
||||||
|
if (!isFeatureEnabled || availableFilters.isEmpty()) {
|
||||||
|
return SpaceFiltersState.Disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectionMode by remember { mutableStateOf<SelectionMode>(SelectionMode.Unselected) }
|
||||||
|
|
||||||
|
fun handleUnselectedEvent(event: SpaceFiltersEvent.Unselected) {
|
||||||
|
when (event) {
|
||||||
|
SpaceFiltersEvent.Unselected.ShowFilters -> {
|
||||||
|
selectionMode = SelectionMode.Selecting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleSelectingEvent(event: SpaceFiltersEvent.Selecting) {
|
||||||
|
when (event) {
|
||||||
|
SpaceFiltersEvent.Selecting.Cancel -> {
|
||||||
|
selectionMode = SelectionMode.Unselected
|
||||||
|
}
|
||||||
|
is SpaceFiltersEvent.Selecting.SelectFilter -> {
|
||||||
|
selectionMode = SelectionMode.Selected(event.spaceFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleSelectedEvent(event: SpaceFiltersEvent.Selected) {
|
||||||
|
when (event) {
|
||||||
|
SpaceFiltersEvent.Selected.ClearSelection -> {
|
||||||
|
selectionMode = SelectionMode.Unselected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return when (val mode = selectionMode) {
|
||||||
|
SelectionMode.Unselected -> SpaceFiltersState.Unselected(
|
||||||
|
eventSink = ::handleUnselectedEvent,
|
||||||
|
)
|
||||||
|
SelectionMode.Selecting -> {
|
||||||
|
val searchQuery = rememberTextFieldState()
|
||||||
|
SpaceFiltersState.Selecting(
|
||||||
|
availableFilters = availableFilters,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
eventSink = ::handleSelectingEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is SelectionMode.Selected -> {
|
||||||
|
var selectedFilter by remember { mutableStateOf(mode.filter) }
|
||||||
|
// Makes sure the selectedFilter stays in sync with the available filters
|
||||||
|
LaunchedEffect(availableFilters) {
|
||||||
|
val upToDateFilter = availableFilters
|
||||||
|
.firstOrNull { it.spaceRoom.roomId == mode.filter.spaceRoom.roomId }
|
||||||
|
if (upToDateFilter == null) {
|
||||||
|
selectionMode = SelectionMode.Unselected
|
||||||
|
} else {
|
||||||
|
selectedFilter = upToDateFilter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpaceFiltersState.Selected(
|
||||||
|
selectedFilter = selectedFilter,
|
||||||
|
eventSink = ::handleSelectedEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed interface SelectionMode {
|
||||||
|
data object Unselected : SelectionMode
|
||||||
|
data object Selecting : SelectionMode
|
||||||
|
data class Selected(val filter: SpaceServiceFilter) : SelectionMode
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.home.impl.spacefilters
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface SpaceFiltersState {
|
||||||
|
data object Disabled : SpaceFiltersState
|
||||||
|
|
||||||
|
data class Unselected(
|
||||||
|
val eventSink: (SpaceFiltersEvent.Unselected) -> Unit,
|
||||||
|
) : SpaceFiltersState
|
||||||
|
|
||||||
|
data class Selecting(
|
||||||
|
val availableFilters: ImmutableList<SpaceServiceFilter>,
|
||||||
|
val searchQuery: TextFieldState,
|
||||||
|
val eventSink: (SpaceFiltersEvent.Selecting) -> Unit,
|
||||||
|
) : SpaceFiltersState {
|
||||||
|
val visibleFilters: ImmutableList<SpaceServiceFilter>
|
||||||
|
get() {
|
||||||
|
val query = searchQuery.text.toString()
|
||||||
|
if (query.isBlank()) return availableFilters
|
||||||
|
return availableFilters.filter { filter ->
|
||||||
|
filter.spaceRoom.displayName.contains(query, ignoreCase = true) ||
|
||||||
|
(filter.spaceRoom.canonicalAlias?.value ?: "").contains(query, ignoreCase = true)
|
||||||
|
}.toImmutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Selected(
|
||||||
|
val selectedFilter: SpaceServiceFilter,
|
||||||
|
val eventSink: (SpaceFiltersEvent.Selected) -> Unit,
|
||||||
|
) : SpaceFiltersState
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SpaceFiltersState.selectedFilter(): SpaceServiceFilter? {
|
||||||
|
return when (this) {
|
||||||
|
is SpaceFiltersState.Selected -> this.selectedFilter
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SpaceServiceFilter?.into(): RoomListFilter? {
|
||||||
|
return this?.let { RoomListFilter.Identifiers(descendants) }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.home.impl.spacefilters
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
|
||||||
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
class SpaceFiltersStateProvider : PreviewParameterProvider<SpaceFiltersState> {
|
||||||
|
override val values: Sequence<SpaceFiltersState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aSelectingSpaceFiltersState(),
|
||||||
|
aSelectingSpaceFiltersState(searchQuery = "Pr")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aDisabledSpaceFiltersState() = SpaceFiltersState.Disabled
|
||||||
|
|
||||||
|
fun anUnselectedSpaceFiltersState(
|
||||||
|
eventSink: (SpaceFiltersEvent.Unselected) -> Unit = {},
|
||||||
|
) = SpaceFiltersState.Unselected(
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aSelectingSpaceFiltersState(
|
||||||
|
availableFilters: List<SpaceServiceFilter> = listOf(
|
||||||
|
aSpaceServiceFilter(
|
||||||
|
displayName = "Work",
|
||||||
|
canonicalAlias = RoomAlias("#work:example.com"),
|
||||||
|
),
|
||||||
|
aSpaceServiceFilter(
|
||||||
|
displayName = "Personal",
|
||||||
|
roomId = RoomId("!personal:example.com"),
|
||||||
|
),
|
||||||
|
aSpaceServiceFilter(
|
||||||
|
displayName = "Projects",
|
||||||
|
roomId = RoomId("!projects:example.com"),
|
||||||
|
canonicalAlias = RoomAlias("#projects:example.com"),
|
||||||
|
level = 1,
|
||||||
|
),
|
||||||
|
aSpaceServiceFilter(
|
||||||
|
displayName = "Gaming",
|
||||||
|
roomId = RoomId("!gaming:example.com"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
searchQuery: String = "",
|
||||||
|
eventSink: (SpaceFiltersEvent.Selecting) -> Unit = {},
|
||||||
|
) = SpaceFiltersState.Selecting(
|
||||||
|
availableFilters = availableFilters.toImmutableList(),
|
||||||
|
searchQuery = TextFieldState(searchQuery),
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aSelectedSpaceFiltersState(
|
||||||
|
selectedFilter: SpaceServiceFilter = aSpaceServiceFilter(displayName = "Work"),
|
||||||
|
eventSink: (SpaceFiltersEvent.Selected) -> Unit = {},
|
||||||
|
) = SpaceFiltersState.Selected(
|
||||||
|
selectedFilter = selectedFilter,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aSpaceServiceFilter(
|
||||||
|
displayName: String = "Space",
|
||||||
|
roomId: RoomId = RoomId("!space:example.com"),
|
||||||
|
canonicalAlias: RoomAlias? = null,
|
||||||
|
level: Int = 0,
|
||||||
|
descendants: List<RoomId> = emptyList(),
|
||||||
|
) = SpaceServiceFilter(
|
||||||
|
spaceRoom = aSpaceRoom(displayName = displayName, roomId = roomId, canonicalAlias = canonicalAlias),
|
||||||
|
level = level,
|
||||||
|
descendants = descendants,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.home.impl.spacefilters
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.SheetValue
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.features.home.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchField
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SpaceFiltersView(
|
||||||
|
state: SpaceFiltersState,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val isSelecting by rememberUpdatedState(state is SpaceFiltersState.Selecting)
|
||||||
|
val sheetState = rememberModalBottomSheetState(
|
||||||
|
skipPartiallyExpanded = true,
|
||||||
|
confirmValueChange = { sheetValueTarget ->
|
||||||
|
// This ensures the hide animation is not cancelled
|
||||||
|
when (sheetValueTarget) {
|
||||||
|
SheetValue.Expanded -> isSelecting
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
LaunchedEffect(isSelecting) {
|
||||||
|
if (!isSelecting) {
|
||||||
|
sheetState.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sheetState.isVisible || isSelecting) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
modifier = modifier
|
||||||
|
.systemBarsPadding()
|
||||||
|
.navigationBarsPadding(),
|
||||||
|
sheetState = sheetState,
|
||||||
|
onDismissRequest = {
|
||||||
|
if (state is SpaceFiltersState.Selecting) {
|
||||||
|
state.eventSink(SpaceFiltersEvent.Selecting.Cancel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(0.9f)
|
||||||
|
) {
|
||||||
|
if (state is SpaceFiltersState.Selecting) {
|
||||||
|
SpaceFiltersBottomSheetContent(
|
||||||
|
filters = state.visibleFilters,
|
||||||
|
searchQuery = state.searchQuery,
|
||||||
|
onFilterSelected = { filter ->
|
||||||
|
state.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(filter))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SpaceFiltersBottomSheetContent(
|
||||||
|
filters: List<SpaceServiceFilter>,
|
||||||
|
searchQuery: TextFieldState,
|
||||||
|
onFilterSelected: (SpaceServiceFilter) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_roomlist_your_spaces),
|
||||||
|
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
SearchField(
|
||||||
|
state = searchQuery,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
placeholder = stringResource(CommonStrings.action_search),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
LazyColumn {
|
||||||
|
items(filters) { filter ->
|
||||||
|
SpaceFilterItem(
|
||||||
|
filter = filter,
|
||||||
|
onClick = { onFilterSelected(filter) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SpaceFilterItem(
|
||||||
|
filter: SpaceServiceFilter,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val spaceRoom = filter.spaceRoom
|
||||||
|
val supportingText = spaceRoom.canonicalAlias?.value
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
// Level-based indentation
|
||||||
|
Spacer(modifier = Modifier.width((16 * filter.level).dp))
|
||||||
|
Avatar(
|
||||||
|
avatarData = spaceRoom.getAvatarData(AvatarSize.RoomSelectRoomListItem),
|
||||||
|
avatarType = AvatarType.Space(),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = spaceRoom.displayName,
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
if (supportingText != null) {
|
||||||
|
Text(
|
||||||
|
text = supportingText,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun SpaceFiltersViewPreview(@PreviewParameter(SpaceFiltersStateProvider::class) state: SpaceFiltersState) = ElementPreview {
|
||||||
|
SpaceFiltersView(state = state)
|
||||||
|
}
|
||||||
|
|
@ -36,7 +36,7 @@ class HomeSpacesPresenter(
|
||||||
val canCreateSpaces by featureFlagsService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
|
val canCreateSpaces by featureFlagsService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
|
||||||
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
|
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
|
||||||
val spaceRooms by remember {
|
val spaceRooms by remember {
|
||||||
client.spaceService.spaceRoomsFlow.map { it.toImmutableList() }
|
client.spaceService.topLevelSpacesFlow.map { it.toImmutableList() }
|
||||||
}.collectAsState(persistentListOf())
|
}.collectAsState(persistentListOf())
|
||||||
|
|
||||||
val seenSpaceInvites by remember {
|
val seenSpaceInvites by remember {
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ fun HomeSpacesView(
|
||||||
item {
|
item {
|
||||||
SpaceHeaderView(
|
SpaceHeaderView(
|
||||||
avatarData = space.spaceRoom.getAvatarData(AvatarSize.SpaceHeader),
|
avatarData = space.spaceRoom.getAvatarData(AvatarSize.SpaceHeader),
|
||||||
|
alias = space.spaceRoom.canonicalAlias,
|
||||||
name = space.spaceRoom.displayName,
|
name = space.spaceRoom.displayName,
|
||||||
topic = space.spaceRoom.topic,
|
topic = space.spaceRoom.topic,
|
||||||
visibility = space.spaceRoom.visibility,
|
visibility = space.spaceRoom.visibility,
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ Nemáte žádné nepřečtené zprávy!"</string>
|
||||||
<string name="screen_roomlist_mark_as_read">"Označit jako přečtené"</string>
|
<string name="screen_roomlist_mark_as_read">"Označit jako přečtené"</string>
|
||||||
<string name="screen_roomlist_mark_as_unread">"Označit jako nepřečtené"</string>
|
<string name="screen_roomlist_mark_as_unread">"Označit jako nepřečtené"</string>
|
||||||
<string name="screen_roomlist_tombstoned_room_description">"Tato místnost byla aktualizována"</string>
|
<string name="screen_roomlist_tombstoned_room_description">"Tato místnost byla aktualizována"</string>
|
||||||
|
<string name="screen_roomlist_your_spaces">"Vaše prostory"</string>
|
||||||
<string name="session_verification_banner_message">"Zdá se, že používáte nové zařízení. Ověřte přihlášení, abyste měli přístup k zašifrovaným zprávám."</string>
|
<string name="session_verification_banner_message">"Zdá se, že používáte nové zařízení. Ověřte přihlášení, abyste měli přístup k zašifrovaným zprávám."</string>
|
||||||
<string name="session_verification_banner_title">"Ověřte, že jste to vy"</string>
|
<string name="session_verification_banner_title">"Ověřte, že jste to vy"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ Sul pole ühtegi lugemata sõnumit!"</string>
|
||||||
<string name="screen_roomlist_mark_as_read">"Märgi loetuks"</string>
|
<string name="screen_roomlist_mark_as_read">"Märgi loetuks"</string>
|
||||||
<string name="screen_roomlist_mark_as_unread">"Märgi mitteloetuks"</string>
|
<string name="screen_roomlist_mark_as_unread">"Märgi mitteloetuks"</string>
|
||||||
<string name="screen_roomlist_tombstoned_room_description">"See jututuba on uuendatud"</string>
|
<string name="screen_roomlist_tombstoned_room_description">"See jututuba on uuendatud"</string>
|
||||||
|
<string name="screen_roomlist_your_spaces">"Sinu kogukonnad"</string>
|
||||||
<string name="session_verification_banner_message">"Tundub, et kasutad uut seadet. Oma krüptitud sõnumite lugemiseks verifitseeri ta mõne muu oma seadmega."</string>
|
<string name="session_verification_banner_message">"Tundub, et kasutad uut seadet. Oma krüptitud sõnumite lugemiseks verifitseeri ta mõne muu oma seadmega."</string>
|
||||||
<string name="session_verification_banner_title">"Verifitseeri, et see oled sina"</string>
|
<string name="session_verification_banner_title">"Verifitseeri, et see oled sina"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@
|
||||||
|
|
||||||
package io.element.android.features.home.impl
|
package io.element.android.features.home.impl
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.announcement.api.Announcement
|
import io.element.android.features.announcement.api.Announcement
|
||||||
import io.element.android.features.announcement.api.AnnouncementService
|
import io.element.android.features.announcement.api.AnnouncementService
|
||||||
|
|
@ -70,9 +67,7 @@ class HomePresenterTest {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(
|
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(
|
||||||
MatrixUser(A_USER_ID, null, null)
|
MatrixUser(A_USER_ID, null, null)
|
||||||
|
|
@ -96,9 +91,7 @@ class HomePresenterTest {
|
||||||
updateUserProfileResult = { _, _, _ -> },
|
updateUserProfileResult = { _, _, _ -> },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.canReportBug).isFalse()
|
assertThat(initialState.canReportBug).isFalse()
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
|
|
@ -115,9 +108,7 @@ class HomePresenterTest {
|
||||||
updateUserProfileResult = { _, _, _ -> },
|
updateUserProfileResult = { _, _, _ -> },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.showAvatarIndicator).isFalse()
|
assertThat(initialState.showAvatarIndicator).isFalse()
|
||||||
indicatorService.setShowRoomListTopBarIndicator(true)
|
indicatorService.setShowRoomListTopBarIndicator(true)
|
||||||
|
|
@ -139,9 +130,7 @@ class HomePresenterTest {
|
||||||
updateUserProfileResult = { _, _, _ -> },
|
updateUserProfileResult = { _, _, _ -> },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(MatrixUser(matrixClient.sessionId))
|
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(MatrixUser(matrixClient.sessionId))
|
||||||
// No new state is coming
|
// No new state is coming
|
||||||
|
|
@ -159,12 +148,10 @@ class HomePresenterTest {
|
||||||
showAnnouncementResult = showAnnouncementResult,
|
showAnnouncementResult = showAnnouncementResult,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
|
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
|
||||||
initialState.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
|
initialState.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
|
assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
|
||||||
showAnnouncementResult.assertions().isCalledOnce()
|
showAnnouncementResult.assertions().isCalledOnce()
|
||||||
|
|
@ -189,7 +176,7 @@ class HomePresenterTest {
|
||||||
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
|
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
|
||||||
assertThat(initialState.showNavigationBar).isTrue()
|
assertThat(initialState.showNavigationBar).isTrue()
|
||||||
// User navigate to Spaces
|
// User navigate to Spaces
|
||||||
initialState.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
|
initialState.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
|
||||||
val spaceState = awaitItem()
|
val spaceState = awaitItem()
|
||||||
assertThat(spaceState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
|
assertThat(spaceState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
|
||||||
// The last space is left
|
// The last space is left
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,11 @@ import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -27,9 +29,13 @@ import java.time.Instant
|
||||||
class RoomListDataSourceTest {
|
class RoomListDataSourceTest {
|
||||||
@Test
|
@Test
|
||||||
fun `when DateTimeObserver gets a date change, the room summaries are refreshed`() = runTest {
|
fun `when DateTimeObserver gets a date change, the room summaries are refreshed`() = runTest {
|
||||||
val roomListService = FakeRoomListService().apply {
|
val roomList = FakeDynamicRoomList().apply {
|
||||||
|
summaries.emit(listOf(aRoomSummary()))
|
||||||
|
}
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
).apply {
|
||||||
postState(RoomListService.State.Running)
|
postState(RoomListService.State.Running)
|
||||||
postAllRooms(listOf(aRoomSummary()))
|
|
||||||
}
|
}
|
||||||
val dateTimeObserver = FakeDateTimeObserver()
|
val dateTimeObserver = FakeDateTimeObserver()
|
||||||
var dateFormatterResult = "Today"
|
var dateFormatterResult = "Today"
|
||||||
|
|
@ -42,7 +48,7 @@ class RoomListDataSourceTest {
|
||||||
dateTimeObserver = dateTimeObserver,
|
dateTimeObserver = dateTimeObserver,
|
||||||
)
|
)
|
||||||
|
|
||||||
roomListDataSource.allRooms.test {
|
roomListDataSource.roomSummariesFlow.test {
|
||||||
// Observe room list items changes
|
// Observe room list items changes
|
||||||
roomListDataSource.launchIn(backgroundScope)
|
roomListDataSource.launchIn(backgroundScope)
|
||||||
// Get the initial room list
|
// Get the initial room list
|
||||||
|
|
@ -61,9 +67,11 @@ class RoomListDataSourceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when DateTimeObserver gets a time zone change, the room summaries are refreshed`() = runTest {
|
fun `when DateTimeObserver gets a time zone change, the room summaries are refreshed`() = runTest {
|
||||||
val roomListService = FakeRoomListService().apply {
|
val roomList = FakeDynamicRoomList(summaries = MutableStateFlow(listOf(aRoomSummary())))
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
).apply {
|
||||||
postState(RoomListService.State.Running)
|
postState(RoomListService.State.Running)
|
||||||
postAllRooms(listOf(aRoomSummary()))
|
|
||||||
}
|
}
|
||||||
val dateTimeObserver = FakeDateTimeObserver()
|
val dateTimeObserver = FakeDateTimeObserver()
|
||||||
var dateFormatterResult = "Today"
|
var dateFormatterResult = "Today"
|
||||||
|
|
@ -75,7 +83,7 @@ class RoomListDataSourceTest {
|
||||||
),
|
),
|
||||||
dateTimeObserver = dateTimeObserver,
|
dateTimeObserver = dateTimeObserver,
|
||||||
)
|
)
|
||||||
roomListDataSource.allRooms.test {
|
roomListDataSource.roomSummariesFlow.test {
|
||||||
// Observe room list items changes
|
// Observe room list items changes
|
||||||
roomListDataSource.launchIn(backgroundScope)
|
roomListDataSource.launchIn(backgroundScope)
|
||||||
// Get the initial room list
|
// Get the initial room list
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,14 @@ class RoomListFiltersEmptyStateResourcesTest {
|
||||||
@Test
|
@Test
|
||||||
fun `fromSelectedFilters should return null when selectedFilters is empty`() {
|
fun `fromSelectedFilters should return null when selectedFilters is empty`() {
|
||||||
val selectedFilters = emptyList<RoomListFilter>()
|
val selectedFilters = emptyList<RoomListFilter>()
|
||||||
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
|
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
|
||||||
assertThat(result).isNull()
|
assertThat(result).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only unread filter`() {
|
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only unread filter`() {
|
||||||
val selectedFilters = listOf(RoomListFilter.Unread)
|
val selectedFilters = listOf(RoomListFilter.Unread)
|
||||||
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
|
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
|
||||||
assertThat(result).isNotNull()
|
assertThat(result).isNotNull()
|
||||||
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_unreads_empty_state_title)
|
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_unreads_empty_state_title)
|
||||||
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
||||||
|
|
@ -32,7 +32,7 @@ class RoomListFiltersEmptyStateResourcesTest {
|
||||||
@Test
|
@Test
|
||||||
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only people filter`() {
|
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only people filter`() {
|
||||||
val selectedFilters = listOf(RoomListFilter.People)
|
val selectedFilters = listOf(RoomListFilter.People)
|
||||||
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
|
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
|
||||||
assertThat(result).isNotNull()
|
assertThat(result).isNotNull()
|
||||||
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_people_empty_state_title)
|
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_people_empty_state_title)
|
||||||
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
||||||
|
|
@ -41,7 +41,7 @@ class RoomListFiltersEmptyStateResourcesTest {
|
||||||
@Test
|
@Test
|
||||||
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only rooms filter`() {
|
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only rooms filter`() {
|
||||||
val selectedFilters = listOf(RoomListFilter.Rooms)
|
val selectedFilters = listOf(RoomListFilter.Rooms)
|
||||||
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
|
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
|
||||||
assertThat(result).isNotNull()
|
assertThat(result).isNotNull()
|
||||||
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_rooms_empty_state_title)
|
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_rooms_empty_state_title)
|
||||||
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
||||||
|
|
@ -50,7 +50,7 @@ class RoomListFiltersEmptyStateResourcesTest {
|
||||||
@Test
|
@Test
|
||||||
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only favourites filter`() {
|
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only favourites filter`() {
|
||||||
val selectedFilters = listOf(RoomListFilter.Favourites)
|
val selectedFilters = listOf(RoomListFilter.Favourites)
|
||||||
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
|
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
|
||||||
assertThat(result).isNotNull()
|
assertThat(result).isNotNull()
|
||||||
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_title)
|
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_title)
|
||||||
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_subtitle)
|
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_subtitle)
|
||||||
|
|
@ -59,7 +59,7 @@ class RoomListFiltersEmptyStateResourcesTest {
|
||||||
@Test
|
@Test
|
||||||
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only invites filter`() {
|
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only invites filter`() {
|
||||||
val selectedFilters = listOf(RoomListFilter.Invites)
|
val selectedFilters = listOf(RoomListFilter.Invites)
|
||||||
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
|
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
|
||||||
assertThat(result).isNotNull()
|
assertThat(result).isNotNull()
|
||||||
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_invites_empty_state_title)
|
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_invites_empty_state_title)
|
||||||
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
||||||
|
|
@ -68,7 +68,15 @@ class RoomListFiltersEmptyStateResourcesTest {
|
||||||
@Test
|
@Test
|
||||||
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has multiple filters`() {
|
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has multiple filters`() {
|
||||||
val selectedFilters = listOf(RoomListFilter.Unread, RoomListFilter.People, RoomListFilter.Rooms, RoomListFilter.Favourites)
|
val selectedFilters = listOf(RoomListFilter.Unread, RoomListFilter.People, RoomListFilter.Rooms, RoomListFilter.Favourites)
|
||||||
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
|
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
|
||||||
|
assertThat(result).isNotNull()
|
||||||
|
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_title)
|
||||||
|
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when isSpaceFilterSelected is true`() {
|
||||||
|
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(emptyList(), isSpaceFilterSelected = true)
|
||||||
assertThat(result).isNotNull()
|
assertThat(result).isNotNull()
|
||||||
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_title)
|
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_title)
|
||||||
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
||||||
|
|
|
||||||
|
|
@ -8,26 +8,22 @@
|
||||||
|
|
||||||
package io.element.android.features.home.impl.filters
|
package io.element.android.features.home.impl.filters
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.home.impl.filters.selection.DefaultFilterSelectionStrategy
|
import io.element.android.features.home.impl.filters.selection.DefaultFilterSelectionStrategy
|
||||||
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
|
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
|
|
||||||
|
|
||||||
class RoomListFiltersPresenterTest {
|
class RoomListFiltersPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = createRoomListFiltersPresenter()
|
val presenter = createRoomListFiltersPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.hasAnyFilterSelected).isFalse()
|
assertThat(state.hasAnyFilterSelected).isFalse()
|
||||||
assertThat(state.filterSelectionStates).containsExactly(
|
assertThat(state.filterSelectionStates).containsExactly(
|
||||||
|
|
@ -43,15 +39,12 @@ class RoomListFiltersPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun `present - toggle rooms filter`() = runTest {
|
fun `present - toggle rooms filter`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val presenter = createRoomListFiltersPresenter()
|
||||||
val presenter = createRoomListFiltersPresenter(roomListService)
|
presenter.test {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem().eventSink.invoke(RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms))
|
|
||||||
awaitLastSequentialItem().let { state ->
|
awaitLastSequentialItem().let { state ->
|
||||||
|
|
||||||
assertThat(state.hasAnyFilterSelected).isTrue()
|
assertThat(state.hasAnyFilterSelected).isTrue()
|
||||||
assertThat(state.filterSelectionStates).containsExactly(
|
assertThat(state.filterSelectionStates).containsExactly(
|
||||||
filterSelectionState(RoomListFilter.Rooms, true),
|
filterSelectionState(RoomListFilter.Rooms, true),
|
||||||
|
|
@ -62,12 +55,9 @@ class RoomListFiltersPresenterTest {
|
||||||
assertThat(state.selectedFilters()).containsExactly(
|
assertThat(state.selectedFilters()).containsExactly(
|
||||||
RoomListFilter.Rooms,
|
RoomListFilter.Rooms,
|
||||||
)
|
)
|
||||||
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
|
state.eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
|
||||||
assertThat(roomListCurrentFilter.filters).containsExactly(
|
|
||||||
MatrixRoomListFilter.Category.Group,
|
|
||||||
)
|
|
||||||
state.eventSink.invoke(RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms))
|
|
||||||
}
|
}
|
||||||
|
advanceUntilIdle()
|
||||||
awaitLastSequentialItem().let { state ->
|
awaitLastSequentialItem().let { state ->
|
||||||
assertThat(state.hasAnyFilterSelected).isFalse()
|
assertThat(state.hasAnyFilterSelected).isFalse()
|
||||||
assertThat(state.filterSelectionStates).containsExactly(
|
assertThat(state.filterSelectionStates).containsExactly(
|
||||||
|
|
@ -78,24 +68,21 @@ class RoomListFiltersPresenterTest {
|
||||||
filterSelectionState(RoomListFilter.Invites, false),
|
filterSelectionState(RoomListFilter.Invites, false),
|
||||||
).inOrder()
|
).inOrder()
|
||||||
assertThat(state.selectedFilters()).isEmpty()
|
assertThat(state.selectedFilters()).isEmpty()
|
||||||
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
|
|
||||||
assertThat(roomListCurrentFilter.filters).isEmpty()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun `present - clear filters event`() = runTest {
|
fun `present - clear filters event`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val presenter = createRoomListFiltersPresenter()
|
||||||
val presenter = createRoomListFiltersPresenter(roomListService)
|
presenter.test {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem().eventSink.invoke(RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms))
|
|
||||||
awaitLastSequentialItem().let { state ->
|
awaitLastSequentialItem().let { state ->
|
||||||
assertThat(state.hasAnyFilterSelected).isTrue()
|
assertThat(state.hasAnyFilterSelected).isTrue()
|
||||||
state.eventSink.invoke(RoomListFiltersEvents.ClearSelectedFilters)
|
state.eventSink.invoke(RoomListFiltersEvent.ClearSelectedFilters)
|
||||||
}
|
}
|
||||||
|
advanceUntilIdle()
|
||||||
awaitLastSequentialItem().let { state ->
|
awaitLastSequentialItem().let { state ->
|
||||||
assertThat(state.hasAnyFilterSelected).isFalse()
|
assertThat(state.hasAnyFilterSelected).isFalse()
|
||||||
}
|
}
|
||||||
|
|
@ -108,11 +95,8 @@ private fun filterSelectionState(filter: RoomListFilter, selected: Boolean) = Fi
|
||||||
isSelected = selected,
|
isSelected = selected,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createRoomListFiltersPresenter(
|
private fun TestScope.createRoomListFiltersPresenter(): RoomListFiltersPresenter {
|
||||||
roomListService: RoomListService = FakeRoomListService(),
|
|
||||||
): RoomListFiltersPresenter {
|
|
||||||
return RoomListFiltersPresenter(
|
return RoomListFiltersPresenter(
|
||||||
roomListService = roomListService,
|
|
||||||
filterSelectionStrategy = DefaultFilterSelectionStrategy(),
|
filterSelectionStrategy = DefaultFilterSelectionStrategy(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class RoomListFiltersViewTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on filters generates expected Event`() {
|
fun `clicking on filters generates expected Event`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListFiltersEvents>()
|
val eventsRecorder = EventsRecorder<RoomListFiltersEvent>()
|
||||||
rule.setContent {
|
rule.setContent {
|
||||||
RoomListFiltersView(
|
RoomListFiltersView(
|
||||||
state = aRoomListFiltersState(eventSink = eventsRecorder),
|
state = aRoomListFiltersState(eventSink = eventsRecorder),
|
||||||
|
|
@ -36,14 +36,14 @@ class RoomListFiltersViewTest {
|
||||||
rule.clickOn(R.string.screen_roomlist_filter_rooms)
|
rule.clickOn(R.string.screen_roomlist_filter_rooms)
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms),
|
RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on clear filters generates expected Event`() {
|
fun `clicking on clear filters generates expected Event`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListFiltersEvents>()
|
val eventsRecorder = EventsRecorder<RoomListFiltersEvent>()
|
||||||
rule.setContent {
|
rule.setContent {
|
||||||
RoomListFiltersView(
|
RoomListFiltersView(
|
||||||
state = aRoomListFiltersState(
|
state = aRoomListFiltersState(
|
||||||
|
|
@ -55,7 +55,7 @@ class RoomListFiltersViewTest {
|
||||||
rule.pressTag(TestTags.homeScreenClearFilters.value)
|
rule.pressTag(TestTags.homeScreenClearFilters.value)
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
RoomListFiltersEvents.ClearSelectedFilters,
|
RoomListFiltersEvent.ClearSelectedFilters,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class RoomListContextMenuTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on Mark as read generates expected Events`() {
|
fun `clicking on Mark as read generates expected Events`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
val contextMenu = aContextMenuShown(hasNewContent = true)
|
val contextMenu = aContextMenuShown(hasNewContent = true)
|
||||||
rule.setRoomListContextMenu(
|
rule.setRoomListContextMenu(
|
||||||
contextMenu = contextMenu,
|
contextMenu = contextMenu,
|
||||||
|
|
@ -39,15 +39,15 @@ class RoomListContextMenuTest {
|
||||||
rule.clickOn(R.string.screen_roomlist_mark_as_read)
|
rule.clickOn(R.string.screen_roomlist_mark_as_read)
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
RoomListEvents.HideContextMenu,
|
RoomListEvent.HideContextMenu,
|
||||||
RoomListEvents.MarkAsRead(contextMenu.roomId),
|
RoomListEvent.MarkAsRead(contextMenu.roomId),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on Mark as unread generates expected Events`() {
|
fun `clicking on Mark as unread generates expected Events`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
val contextMenu = aContextMenuShown(hasNewContent = false)
|
val contextMenu = aContextMenuShown(hasNewContent = false)
|
||||||
rule.setRoomListContextMenu(
|
rule.setRoomListContextMenu(
|
||||||
contextMenu = contextMenu,
|
contextMenu = contextMenu,
|
||||||
|
|
@ -56,15 +56,15 @@ class RoomListContextMenuTest {
|
||||||
rule.clickOn(R.string.screen_roomlist_mark_as_unread)
|
rule.clickOn(R.string.screen_roomlist_mark_as_unread)
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
RoomListEvents.HideContextMenu,
|
RoomListEvent.HideContextMenu,
|
||||||
RoomListEvents.MarkAsUnread(contextMenu.roomId),
|
RoomListEvent.MarkAsUnread(contextMenu.roomId),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on Leave room generates expected Events`() {
|
fun `clicking on Leave room generates expected Events`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
val contextMenu = aContextMenuShown(isDm = false)
|
val contextMenu = aContextMenuShown(isDm = false)
|
||||||
rule.setRoomListContextMenu(
|
rule.setRoomListContextMenu(
|
||||||
contextMenu = contextMenu,
|
contextMenu = contextMenu,
|
||||||
|
|
@ -73,15 +73,15 @@ class RoomListContextMenuTest {
|
||||||
rule.clickOn(CommonStrings.action_leave_room)
|
rule.clickOn(CommonStrings.action_leave_room)
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
RoomListEvents.HideContextMenu,
|
RoomListEvent.HideContextMenu,
|
||||||
RoomListEvents.LeaveRoom(contextMenu.roomId, needsConfirmation = true),
|
RoomListEvent.LeaveRoom(contextMenu.roomId, needsConfirmation = true),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on Report room invokes the expected callback and generates expected Event`() {
|
fun `clicking on Report room invokes the expected callback and generates expected Event`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
val contextMenu = aContextMenuShown()
|
val contextMenu = aContextMenuShown()
|
||||||
val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit)
|
val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit)
|
||||||
rule.setRoomListContextMenu(
|
rule.setRoomListContextMenu(
|
||||||
|
|
@ -92,13 +92,13 @@ class RoomListContextMenuTest {
|
||||||
onReportRoomClick = callback,
|
onReportRoomClick = callback,
|
||||||
)
|
)
|
||||||
rule.clickOn(CommonStrings.action_report_room)
|
rule.clickOn(CommonStrings.action_report_room)
|
||||||
eventsRecorder.assertSingle(RoomListEvents.HideContextMenu)
|
eventsRecorder.assertSingle(RoomListEvent.HideContextMenu)
|
||||||
callback.assertSuccess()
|
callback.assertSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on Settings invokes the expected callback and generates expected Event`() {
|
fun `clicking on Settings invokes the expected callback and generates expected Event`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
val contextMenu = aContextMenuShown()
|
val contextMenu = aContextMenuShown()
|
||||||
val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit)
|
val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit)
|
||||||
rule.setRoomListContextMenu(
|
rule.setRoomListContextMenu(
|
||||||
|
|
@ -107,13 +107,13 @@ class RoomListContextMenuTest {
|
||||||
onRoomSettingsClick = callback,
|
onRoomSettingsClick = callback,
|
||||||
)
|
)
|
||||||
rule.clickOn(CommonStrings.common_settings)
|
rule.clickOn(CommonStrings.common_settings)
|
||||||
eventsRecorder.assertSingle(RoomListEvents.HideContextMenu)
|
eventsRecorder.assertSingle(RoomListEvent.HideContextMenu)
|
||||||
callback.assertSuccess()
|
callback.assertSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on Favourites generates expected Event`() {
|
fun `clicking on Favourites generates expected Event`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
val contextMenu = aContextMenuShown(isDm = false, isFavorite = false)
|
val contextMenu = aContextMenuShown(isDm = false, isFavorite = false)
|
||||||
val callback = EnsureNeverCalledWithParam<RoomId>()
|
val callback = EnsureNeverCalledWithParam<RoomId>()
|
||||||
rule.setRoomListContextMenu(
|
rule.setRoomListContextMenu(
|
||||||
|
|
@ -124,7 +124,7 @@ class RoomListContextMenuTest {
|
||||||
rule.clickOn(CommonStrings.common_favourite)
|
rule.clickOn(CommonStrings.common_favourite)
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, true),
|
RoomListEvent.SetRoomIsFavorite(contextMenu.roomId, true),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +132,7 @@ class RoomListContextMenuTest {
|
||||||
private fun AndroidComposeTestRule<*, *>.setRoomListContextMenu(
|
private fun AndroidComposeTestRule<*, *>.setRoomListContextMenu(
|
||||||
contextMenu: RoomListState.ContextMenu.Shown,
|
contextMenu: RoomListState.ContextMenu.Shown,
|
||||||
canReportRoom: Boolean = false,
|
canReportRoom: Boolean = false,
|
||||||
eventSink: (RoomListEvents) -> Unit,
|
eventSink: (RoomListEvent) -> Unit,
|
||||||
onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onReportRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
onReportRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ class RoomListDeclineInviteMenuTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on decline emits the expected Events`() {
|
fun `clicking on decline emits the expected Events`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
|
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
|
||||||
rule.setSafeContent {
|
rule.setSafeContent {
|
||||||
RoomListDeclineInviteMenu(
|
RoomListDeclineInviteMenu(
|
||||||
|
|
@ -41,15 +41,15 @@ class RoomListDeclineInviteMenuTest {
|
||||||
rule.clickOn(CommonStrings.action_decline)
|
rule.clickOn(CommonStrings.action_decline)
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
RoomListEvents.HideDeclineInviteMenu,
|
RoomListEvent.HideDeclineInviteMenu,
|
||||||
RoomListEvents.DeclineInvite(menu.roomSummary, blockUser = false),
|
RoomListEvent.DeclineInvite(menu.roomSummary, blockUser = false),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on decline and block when canReportRoom=true, it emits the expected Events and callback`() {
|
fun `clicking on decline and block when canReportRoom=true, it emits the expected Events and callback`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
|
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
|
||||||
rule.setSafeContent {
|
rule.setSafeContent {
|
||||||
RoomListDeclineInviteMenu(
|
RoomListDeclineInviteMenu(
|
||||||
|
|
@ -60,13 +60,13 @@ class RoomListDeclineInviteMenuTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
rule.clickOn(CommonStrings.action_decline_and_block)
|
rule.clickOn(CommonStrings.action_decline_and_block)
|
||||||
val expectedEvents = listOf(RoomListEvents.HideDeclineInviteMenu)
|
val expectedEvents = listOf(RoomListEvent.HideDeclineInviteMenu)
|
||||||
eventsRecorder.assertList(expectedEvents)
|
eventsRecorder.assertList(expectedEvents)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on decline and block when canReportRoom=false, it emits the expected Events`() {
|
fun `clicking on decline and block when canReportRoom=false, it emits the expected Events`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
|
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
|
||||||
rule.setSafeContent {
|
rule.setSafeContent {
|
||||||
RoomListDeclineInviteMenu(
|
RoomListDeclineInviteMenu(
|
||||||
|
|
@ -78,15 +78,15 @@ class RoomListDeclineInviteMenuTest {
|
||||||
}
|
}
|
||||||
rule.clickOn(CommonStrings.action_decline_and_block)
|
rule.clickOn(CommonStrings.action_decline_and_block)
|
||||||
val expectedEvents = listOf(
|
val expectedEvents = listOf(
|
||||||
RoomListEvents.HideDeclineInviteMenu,
|
RoomListEvent.HideDeclineInviteMenu,
|
||||||
RoomListEvents.DeclineInvite(menu.roomSummary, blockUser = true),
|
RoomListEvent.DeclineInvite(menu.roomSummary, blockUser = true),
|
||||||
)
|
)
|
||||||
eventsRecorder.assertList(expectedEvents)
|
eventsRecorder.assertList(expectedEvents)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on cancel emits the expected Event`() {
|
fun `clicking on cancel emits the expected Event`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
|
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
|
||||||
rule.setSafeContent {
|
rule.setSafeContent {
|
||||||
RoomListDeclineInviteMenu(
|
RoomListDeclineInviteMenu(
|
||||||
|
|
@ -97,6 +97,6 @@ class RoomListDeclineInviteMenuTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
rule.clickOn(CommonStrings.action_cancel)
|
rule.clickOn(CommonStrings.action_cancel)
|
||||||
eventsRecorder.assertList(listOf(RoomListEvents.HideDeclineInviteMenu))
|
eventsRecorder.assertList(listOf(RoomListEvent.HideDeclineInviteMenu))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@
|
||||||
|
|
||||||
package io.element.android.features.home.impl.roomlist
|
package io.element.android.features.home.impl.roomlist
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.Interaction
|
import im.vector.app.features.analytics.plan.Interaction
|
||||||
import io.element.android.features.announcement.api.Announcement
|
import io.element.android.features.announcement.api.Announcement
|
||||||
|
|
@ -21,9 +18,11 @@ import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFact
|
||||||
import io.element.android.features.home.impl.filters.RoomListFiltersState
|
import io.element.android.features.home.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.home.impl.filters.aRoomListFiltersState
|
import io.element.android.features.home.impl.filters.aRoomListFiltersState
|
||||||
import io.element.android.features.home.impl.model.createRoomListRoomSummary
|
import io.element.android.features.home.impl.model.createRoomListRoomSummary
|
||||||
import io.element.android.features.home.impl.search.RoomListSearchEvents
|
import io.element.android.features.home.impl.search.RoomListSearchEvent
|
||||||
import io.element.android.features.home.impl.search.RoomListSearchState
|
import io.element.android.features.home.impl.search.RoomListSearchState
|
||||||
import io.element.android.features.home.impl.search.aRoomListSearchState
|
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.SeenInvitesStore
|
||||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
||||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
|
|
@ -58,6 +57,7 @@ import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
|
|
@ -80,6 +80,7 @@ import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.advanceTimeBy
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
|
|
@ -94,7 +95,10 @@ class RoomListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - load 1 room with success`() = runTest {
|
fun `present - load 1 room with success`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList()
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
roomListService = roomListService
|
roomListService = roomListService
|
||||||
)
|
)
|
||||||
|
|
@ -105,8 +109,8 @@ class RoomListPresenterTest {
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = consumeItemsUntilPredicate { state -> state.contentState is RoomListContentState.Skeleton }.last()
|
val initialState = consumeItemsUntilPredicate { state -> state.contentState is RoomListContentState.Skeleton }.last()
|
||||||
assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Skeleton::class.java)
|
assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Skeleton::class.java)
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
roomList.loadingState.emit(RoomList.LoadingState.Loaded(1))
|
||||||
roomListService.postAllRooms(
|
roomList.summaries.emit(
|
||||||
listOf(
|
listOf(
|
||||||
aRoomSummary(
|
aRoomSummary(
|
||||||
numUnreadMentions = 1,
|
numUnreadMentions = 1,
|
||||||
|
|
@ -131,9 +135,12 @@ class RoomListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
|
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
|
||||||
val roomListService = FakeRoomListService().apply {
|
val roomList = FakeDynamicRoomList(
|
||||||
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
|
||||||
}
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val encryptionService = FakeEncryptionService().apply {
|
val encryptionService = FakeEncryptionService().apply {
|
||||||
emitRecoveryState(RecoveryState.INCOMPLETE)
|
emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||||
}
|
}
|
||||||
|
|
@ -141,15 +148,13 @@ class RoomListPresenterTest {
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
client = FakeMatrixClient(roomListService = roomListService, encryptionService = encryptionService, syncService = syncService),
|
client = FakeMatrixClient(roomListService = roomListService, encryptionService = encryptionService, syncService = syncService),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val eventWithContentAsRooms = consumeItemsUntilPredicate {
|
val eventWithContentAsRooms = consumeItemsUntilPredicate {
|
||||||
it.contentState is RoomListContentState.Rooms
|
it.contentState is RoomListContentState.Rooms
|
||||||
}.last()
|
}.last()
|
||||||
val eventSink = eventWithContentAsRooms.eventSink
|
val eventSink = eventWithContentAsRooms.eventSink
|
||||||
assertThat(eventWithContentAsRooms.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)
|
assertThat(eventWithContentAsRooms.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)
|
||||||
eventSink(RoomListEvents.DismissRequestVerificationPrompt)
|
eventSink(RoomListEvent.DismissRequestVerificationPrompt)
|
||||||
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -159,9 +164,12 @@ class RoomListPresenterTest {
|
||||||
val encryptionService = FakeEncryptionService().apply {
|
val encryptionService = FakeEncryptionService().apply {
|
||||||
recoveryStateStateFlow.emit(RecoveryState.DISABLED)
|
recoveryStateStateFlow.emit(RecoveryState.DISABLED)
|
||||||
}
|
}
|
||||||
val roomListService = FakeRoomListService().apply {
|
val roomList = FakeDynamicRoomList(
|
||||||
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
|
||||||
}
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
encryptionService = encryptionService,
|
encryptionService = encryptionService,
|
||||||
|
|
@ -173,9 +181,7 @@ class RoomListPresenterTest {
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
client = matrixClient,
|
client = matrixClient,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = consumeItemsUntilPredicate {
|
val initialState = consumeItemsUntilPredicate {
|
||||||
it.contentState is RoomListContentState.Rooms
|
it.contentState is RoomListContentState.Rooms
|
||||||
}.last()
|
}.last()
|
||||||
|
|
@ -194,7 +200,7 @@ class RoomListPresenterTest {
|
||||||
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||||
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
|
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
|
||||||
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SetUpRecovery)
|
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SetUpRecovery)
|
||||||
nextState.eventSink(RoomListEvents.DismissBanner)
|
nextState.eventSink(RoomListEvent.DismissBanner)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
assertThat(finalState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
|
||||||
}
|
}
|
||||||
|
|
@ -207,12 +213,10 @@ class RoomListPresenterTest {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
}
|
}
|
||||||
val presenter = createRoomListPresenter(client = client)
|
val presenter = createRoomListPresenter(client = client)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val summary = createRoomListRoomSummary()
|
val summary = createRoomListRoomSummary()
|
||||||
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
initialState.eventSink(RoomListEvent.ShowContextMenu(summary))
|
||||||
|
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.contextMenu)
|
assertThat(state.contextMenu)
|
||||||
|
|
@ -257,7 +261,7 @@ class RoomListPresenterTest {
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val summary = createRoomListRoomSummary()
|
val summary = createRoomListRoomSummary()
|
||||||
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
initialState.eventSink(RoomListEvent.ShowContextMenu(summary))
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.contextMenu)
|
assertThat(state.contextMenu)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
|
|
@ -282,12 +286,10 @@ class RoomListPresenterTest {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
}
|
}
|
||||||
val presenter = createRoomListPresenter(client = client)
|
val presenter = createRoomListPresenter(client = client)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val summary = createRoomListRoomSummary()
|
val summary = createRoomListRoomSummary()
|
||||||
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
initialState.eventSink(RoomListEvent.ShowContextMenu(summary))
|
||||||
|
|
||||||
val shownState = awaitItem()
|
val shownState = awaitItem()
|
||||||
assertThat(shownState.contextMenu)
|
assertThat(shownState.contextMenu)
|
||||||
|
|
@ -302,7 +304,7 @@ class RoomListPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
shownState.eventSink(RoomListEvents.HideContextMenu)
|
shownState.eventSink(RoomListEvent.HideContextMenu)
|
||||||
|
|
||||||
val hiddenState = awaitItem()
|
val hiddenState = awaitItem()
|
||||||
assertThat(hiddenState.contextMenu).isEqualTo(RoomListState.ContextMenu.Hidden)
|
assertThat(hiddenState.contextMenu).isEqualTo(RoomListState.ContextMenu.Hidden)
|
||||||
|
|
@ -315,11 +317,9 @@ class RoomListPresenterTest {
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
leaveRoomState = aLeaveRoomState(eventSink = leaveRoomEventsRecorder),
|
leaveRoomState = aLeaveRoomState(eventSink = leaveRoomEventsRecorder),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
initialState.eventSink(RoomListEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
||||||
leaveRoomEventsRecorder.assertSingle(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
leaveRoomEventsRecorder.assertSingle(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
|
|
@ -327,7 +327,7 @@ class RoomListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - toggle search menu`() = runTest {
|
fun `present - toggle search menu`() = runTest {
|
||||||
val eventRecorder = EventsRecorder<RoomListSearchEvents>()
|
val eventRecorder = EventsRecorder<RoomListSearchEvent>()
|
||||||
val searchPresenter: Presenter<RoomListSearchState> = Presenter {
|
val searchPresenter: Presenter<RoomListSearchState> = Presenter {
|
||||||
aRoomListSearchState(
|
aRoomListSearchState(
|
||||||
eventSink = eventRecorder
|
eventSink = eventRecorder
|
||||||
|
|
@ -336,20 +336,18 @@ class RoomListPresenterTest {
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
searchPresenter = searchPresenter,
|
searchPresenter = searchPresenter,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
eventRecorder.assertEmpty()
|
eventRecorder.assertEmpty()
|
||||||
initialState.eventSink(RoomListEvents.ToggleSearchResults)
|
initialState.eventSink(RoomListEvent.ToggleSearchResults)
|
||||||
eventRecorder.assertSingle(
|
eventRecorder.assertSingle(
|
||||||
RoomListSearchEvents.ToggleSearchVisibility
|
RoomListSearchEvent.ToggleSearchVisibility
|
||||||
)
|
)
|
||||||
initialState.eventSink(RoomListEvents.ToggleSearchResults)
|
initialState.eventSink(RoomListEvent.ToggleSearchResults)
|
||||||
eventRecorder.assertList(
|
eventRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
RoomListSearchEvents.ToggleSearchVisibility,
|
RoomListSearchEvent.ToggleSearchVisibility,
|
||||||
RoomListSearchEvents.ToggleSearchVisibility
|
RoomListSearchEvent.ToggleSearchVisibility
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -359,17 +357,19 @@ class RoomListPresenterTest {
|
||||||
fun `present - change in notification settings updates the summary for decorations`() = runTest {
|
fun `present - change in notification settings updates the summary for decorations`() = runTest {
|
||||||
val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
||||||
val notificationSettingsService = FakeNotificationSettingsService()
|
val notificationSettingsService = FakeNotificationSettingsService()
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList(
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
summaries = MutableStateFlow(listOf(aRoomSummary(userDefinedNotificationMode = userDefinedMode))),
|
||||||
roomListService.postAllRooms(listOf(aRoomSummary(userDefinedNotificationMode = userDefinedMode)))
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
notificationSettingsService = notificationSettingsService
|
notificationSettingsService = notificationSettingsService
|
||||||
)
|
)
|
||||||
val presenter = createRoomListPresenter(client = matrixClient)
|
val presenter = createRoomListPresenter(client = matrixClient)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
notificationSettingsService.setRoomNotificationMode(A_ROOM_ID, userDefinedMode)
|
notificationSettingsService.setRoomNotificationMode(A_ROOM_ID, userDefinedMode)
|
||||||
val updatedState = consumeItemsUntilPredicate { state ->
|
val updatedState = consumeItemsUntilPredicate { state ->
|
||||||
(state.contentState as? RoomListContentState.Rooms)?.summaries.orEmpty().any { summary ->
|
(state.contentState as? RoomListContentState.Rooms)?.summaries.orEmpty().any { summary ->
|
||||||
|
|
@ -394,13 +394,11 @@ class RoomListPresenterTest {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
}
|
}
|
||||||
val presenter = createRoomListPresenter(client = client, analyticsService = analyticsService)
|
val presenter = createRoomListPresenter(client = client, analyticsService = analyticsService)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(RoomListEvents.SetRoomIsFavorite(A_ROOM_ID, true))
|
initialState.eventSink(RoomListEvent.SetRoomIsFavorite(A_ROOM_ID, true))
|
||||||
setIsFavoriteResult.assertions().isCalledOnce().with(value(true))
|
setIsFavoriteResult.assertions().isCalledOnce().with(value(true))
|
||||||
initialState.eventSink(RoomListEvents.SetRoomIsFavorite(A_ROOM_ID, false))
|
initialState.eventSink(RoomListEvent.SetRoomIsFavorite(A_ROOM_ID, false))
|
||||||
setIsFavoriteResult.assertions().isCalledExactly(2)
|
setIsFavoriteResult.assertions().isCalledExactly(2)
|
||||||
.withSequence(
|
.withSequence(
|
||||||
listOf(value(true)),
|
listOf(value(true)),
|
||||||
|
|
@ -416,17 +414,19 @@ class RoomListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - when room service returns no room, then contentState is Empty`() = runTest {
|
fun `present - when room service returns no room, then contentState is Empty`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList(
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(0))
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(0))
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
)
|
)
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
client = matrixClient,
|
client = matrixClient,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Empty::class.java)
|
assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Empty::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -463,23 +463,21 @@ class RoomListPresenterTest {
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
notificationCleaner = notificationCleaner,
|
notificationCleaner = notificationCleaner,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
allRooms.forEach {
|
allRooms.forEach {
|
||||||
assertThat(it.setUnreadFlagCalls).isEmpty()
|
assertThat(it.setUnreadFlagCalls).isEmpty()
|
||||||
}
|
}
|
||||||
initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID))
|
initialState.eventSink.invoke(RoomListEvent.MarkAsRead(A_ROOM_ID))
|
||||||
markAsReadResult.assertions().isCalledOnce().with(value(ReceiptType.READ))
|
markAsReadResult.assertions().isCalledOnce().with(value(ReceiptType.READ))
|
||||||
assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false))
|
assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false))
|
||||||
clearMessagesForRoomLambda.assertions().isCalledOnce()
|
clearMessagesForRoomLambda.assertions().isCalledOnce()
|
||||||
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
||||||
initialState.eventSink.invoke(RoomListEvents.MarkAsUnread(A_ROOM_ID_2))
|
initialState.eventSink.invoke(RoomListEvent.MarkAsUnread(A_ROOM_ID_2))
|
||||||
assertThat(room2.setUnreadFlagCalls).isEqualTo(listOf(true))
|
assertThat(room2.setUnreadFlagCalls).isEqualTo(listOf(true))
|
||||||
// Test again with private read receipts
|
// Test again with private read receipts
|
||||||
sessionPreferencesStore.setSendPublicReadReceipts(false)
|
sessionPreferencesStore.setSendPublicReadReceipts(false)
|
||||||
initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID_3))
|
initialState.eventSink.invoke(RoomListEvent.MarkAsRead(A_ROOM_ID_3))
|
||||||
markAsReadResult3.assertions().isCalledOnce().with(value(ReceiptType.READ_PRIVATE))
|
markAsReadResult3.assertions().isCalledOnce().with(value(ReceiptType.READ_PRIVATE))
|
||||||
assertThat(room3.setUnreadFlagCalls).isEqualTo(listOf(false))
|
assertThat(room3.setUnreadFlagCalls).isEqualTo(listOf(false))
|
||||||
clearMessagesForRoomLambda.assertions().isCalledExactly(2)
|
clearMessagesForRoomLambda.assertions().isCalledExactly(2)
|
||||||
|
|
@ -502,16 +500,21 @@ class RoomListPresenterTest {
|
||||||
val acceptDeclinePresenter = Presenter {
|
val acceptDeclinePresenter = Presenter {
|
||||||
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
|
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
|
||||||
}
|
}
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
val matrixClient = FakeMatrixClient(
|
|
||||||
roomListService = roomListService,
|
|
||||||
)
|
|
||||||
val roomSummary = aRoomSummary(
|
val roomSummary = aRoomSummary(
|
||||||
currentUserMembership = CurrentUserMembership.INVITED,
|
currentUserMembership = CurrentUserMembership.INVITED,
|
||||||
inviter = aRoomMember(),
|
inviter = aRoomMember(),
|
||||||
)
|
)
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
val roomList = FakeDynamicRoomList(
|
||||||
roomListService.postAllRooms(listOf(roomSummary))
|
summaries = MutableStateFlow(listOf(roomSummary)),
|
||||||
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
roomListService = roomListService,
|
||||||
|
)
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
client = matrixClient,
|
client = matrixClient,
|
||||||
acceptDeclineInvitePresenter = acceptDeclinePresenter
|
acceptDeclineInvitePresenter = acceptDeclinePresenter
|
||||||
|
|
@ -525,8 +528,8 @@ class RoomListPresenterTest {
|
||||||
it.id == roomSummary.roomId.value
|
it.id == roomSummary.roomId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
state.eventSink(RoomListEvents.AcceptInvite(roomListRoomSummary))
|
state.eventSink(RoomListEvent.AcceptInvite(roomListRoomSummary))
|
||||||
state.eventSink(RoomListEvents.DeclineInvite(roomListRoomSummary, blockUser = false))
|
state.eventSink(RoomListEvent.DeclineInvite(roomListRoomSummary, blockUser = false))
|
||||||
|
|
||||||
val inviteData = roomListRoomSummary.toInviteData()
|
val inviteData = roomListRoomSummary.toInviteData()
|
||||||
assert(eventSinkRecorder)
|
assert(eventSinkRecorder)
|
||||||
|
|
@ -542,15 +545,20 @@ class RoomListPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - UpdateVisibleRange will cancel the previous subscription if called too soon`() = runTest {
|
fun `present - UpdateVisibleRange will cancel the previous subscription if called too soon`() = runTest {
|
||||||
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
|
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
|
||||||
val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
|
|
||||||
val matrixClient = FakeMatrixClient(
|
|
||||||
roomListService = roomListService,
|
|
||||||
)
|
|
||||||
val roomSummary = aRoomSummary(
|
val roomSummary = aRoomSummary(
|
||||||
currentUserMembership = CurrentUserMembership.INVITED
|
currentUserMembership = CurrentUserMembership.INVITED
|
||||||
)
|
)
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
val roomList = FakeDynamicRoomList(
|
||||||
roomListService.postAllRooms(listOf(roomSummary))
|
summaries = MutableStateFlow(listOf(roomSummary)),
|
||||||
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList },
|
||||||
|
subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda,
|
||||||
|
)
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
roomListService = roomListService,
|
||||||
|
)
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
client = matrixClient,
|
client = matrixClient,
|
||||||
)
|
)
|
||||||
|
|
@ -559,9 +567,9 @@ class RoomListPresenterTest {
|
||||||
it.contentState is RoomListContentState.Rooms
|
it.contentState is RoomListContentState.Rooms
|
||||||
}.last()
|
}.last()
|
||||||
|
|
||||||
state.eventSink(RoomListEvents.UpdateVisibleRange(IntRange(0, 10)))
|
state.eventSink(RoomListEvent.UpdateVisibleRange(IntRange(0, 10)))
|
||||||
// If called again, it will cancel the current one, which should not result in a test failure
|
// If called again, it will cancel the current one, which should not result in a test failure
|
||||||
state.eventSink(RoomListEvents.UpdateVisibleRange(IntRange(0, 11)))
|
state.eventSink(RoomListEvent.UpdateVisibleRange(IntRange(0, 11)))
|
||||||
advanceTimeBy(1.seconds)
|
advanceTimeBy(1.seconds)
|
||||||
subscribeToVisibleRoomsLambda.assertions().isCalledOnce()
|
subscribeToVisibleRoomsLambda.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
@ -571,15 +579,20 @@ class RoomListPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - UpdateVisibleRange subscribes to rooms in visible range`() = runTest {
|
fun `present - UpdateVisibleRange subscribes to rooms in visible range`() = runTest {
|
||||||
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
|
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
|
||||||
val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
|
|
||||||
val matrixClient = FakeMatrixClient(
|
|
||||||
roomListService = roomListService,
|
|
||||||
)
|
|
||||||
val roomSummary = aRoomSummary(
|
val roomSummary = aRoomSummary(
|
||||||
currentUserMembership = CurrentUserMembership.INVITED
|
currentUserMembership = CurrentUserMembership.INVITED
|
||||||
)
|
)
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
val roomList = FakeDynamicRoomList(
|
||||||
roomListService.postAllRooms(listOf(roomSummary))
|
summaries = MutableStateFlow(listOf(roomSummary)),
|
||||||
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList },
|
||||||
|
subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda,
|
||||||
|
)
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
roomListService = roomListService,
|
||||||
|
)
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
client = matrixClient,
|
client = matrixClient,
|
||||||
)
|
)
|
||||||
|
|
@ -588,12 +601,12 @@ class RoomListPresenterTest {
|
||||||
it.contentState is RoomListContentState.Rooms
|
it.contentState is RoomListContentState.Rooms
|
||||||
}.last()
|
}.last()
|
||||||
|
|
||||||
state.eventSink(RoomListEvents.UpdateVisibleRange(IntRange(0, 10)))
|
state.eventSink(RoomListEvent.UpdateVisibleRange(IntRange(0, 10)))
|
||||||
advanceTimeBy(1.seconds)
|
advanceTimeBy(1.seconds)
|
||||||
subscribeToVisibleRoomsLambda.assertions().isCalledOnce()
|
subscribeToVisibleRoomsLambda.assertions().isCalledOnce()
|
||||||
|
|
||||||
// If called again, it will subscribe to the next items
|
// If called again, it will subscribe to the next items
|
||||||
state.eventSink(RoomListEvents.UpdateVisibleRange(IntRange(0, 11)))
|
state.eventSink(RoomListEvent.UpdateVisibleRange(IntRange(0, 11)))
|
||||||
advanceTimeBy(1.seconds)
|
advanceTimeBy(1.seconds)
|
||||||
subscribeToVisibleRoomsLambda.assertions().isCalledExactly(2)
|
subscribeToVisibleRoomsLambda.assertions().isCalledExactly(2)
|
||||||
}
|
}
|
||||||
|
|
@ -602,15 +615,20 @@ class RoomListPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - notification sound banner`() = runTest {
|
fun `present - notification sound banner`() = runTest {
|
||||||
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
|
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
|
||||||
val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
|
|
||||||
val matrixClient = FakeMatrixClient(
|
|
||||||
roomListService = roomListService,
|
|
||||||
)
|
|
||||||
val roomSummary = aRoomSummary(
|
val roomSummary = aRoomSummary(
|
||||||
currentUserMembership = CurrentUserMembership.INVITED
|
currentUserMembership = CurrentUserMembership.INVITED
|
||||||
)
|
)
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
val roomList = FakeDynamicRoomList(
|
||||||
roomListService.postAllRooms(listOf(roomSummary))
|
summaries = MutableStateFlow(listOf(roomSummary)),
|
||||||
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList },
|
||||||
|
subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda,
|
||||||
|
)
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
roomListService = roomListService,
|
||||||
|
)
|
||||||
val onAnnouncementDismissedResult = lambdaRecorder<Announcement, Unit> { }
|
val onAnnouncementDismissedResult = lambdaRecorder<Announcement, Unit> { }
|
||||||
val announcementService = FakeAnnouncementService(
|
val announcementService = FakeAnnouncementService(
|
||||||
onAnnouncementDismissedResult = onAnnouncementDismissedResult,
|
onAnnouncementDismissedResult = onAnnouncementDismissedResult,
|
||||||
|
|
@ -626,7 +644,7 @@ class RoomListPresenterTest {
|
||||||
assertThat(state.contentAsRooms().showNewNotificationSoundBanner).isFalse()
|
assertThat(state.contentAsRooms().showNewNotificationSoundBanner).isFalse()
|
||||||
announcementService.emitAnnouncementsToShow(listOf(Announcement.NewNotificationSound))
|
announcementService.emitAnnouncementsToShow(listOf(Announcement.NewNotificationSound))
|
||||||
assertThat(awaitItem().contentAsRooms().showNewNotificationSoundBanner).isTrue()
|
assertThat(awaitItem().contentAsRooms().showNewNotificationSoundBanner).isTrue()
|
||||||
state.eventSink(RoomListEvents.DismissNewNotificationSoundBanner)
|
state.eventSink(RoomListEvent.DismissNewNotificationSoundBanner)
|
||||||
onAnnouncementDismissedResult.assertions().isCalledOnce()
|
onAnnouncementDismissedResult.assertions().isCalledOnce()
|
||||||
.with(value(Announcement.NewNotificationSound))
|
.with(value(Announcement.NewNotificationSound))
|
||||||
// Simulate service updating the value
|
// Simulate service updating the value
|
||||||
|
|
@ -644,6 +662,7 @@ class RoomListPresenterTest {
|
||||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
|
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
|
||||||
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
|
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
|
||||||
|
spaceFiltersPresenter: Presenter<SpaceFiltersState> = Presenter { aDisabledSpaceFiltersState() },
|
||||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
||||||
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
|
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
|
||||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
|
|
@ -667,6 +686,7 @@ class RoomListPresenterTest {
|
||||||
searchPresenter = searchPresenter,
|
searchPresenter = searchPresenter,
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
filtersPresenter = filtersPresenter,
|
filtersPresenter = filtersPresenter,
|
||||||
|
spaceFiltersPresenter = spaceFiltersPresenter,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||||
fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },
|
fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class RoomListViewTest {
|
||||||
@Config(qualifiers = "h1024dp")
|
@Config(qualifiers = "h1024dp")
|
||||||
@Test
|
@Test
|
||||||
fun `displaying the view automatically sends a couple of UpdateVisibleRangeEvents`() {
|
fun `displaying the view automatically sends a couple of UpdateVisibleRangeEvents`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
rule.setRoomListView(
|
rule.setRoomListView(
|
||||||
state = aRoomListState(
|
state = aRoomListState(
|
||||||
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
|
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
|
||||||
|
|
@ -56,15 +56,15 @@ class RoomListViewTest {
|
||||||
|
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
RoomListEvents.UpdateVisibleRange(IntRange.EMPTY),
|
RoomListEvent.UpdateVisibleRange(IntRange.EMPTY),
|
||||||
RoomListEvents.UpdateVisibleRange(0..5),
|
RoomListEvent.UpdateVisibleRange(0..5),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on close recovery key banner emits the expected Event`() {
|
fun `clicking on close recovery key banner emits the expected Event`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
rule.setRoomListView(
|
rule.setRoomListView(
|
||||||
state = aRoomListState(
|
state = aRoomListState(
|
||||||
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
|
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
|
||||||
|
|
@ -77,12 +77,12 @@ class RoomListViewTest {
|
||||||
|
|
||||||
val close = rule.activity.getString(CommonStrings.action_close)
|
val close = rule.activity.getString(CommonStrings.action_close)
|
||||||
rule.onNodeWithContentDescription(close).performClick()
|
rule.onNodeWithContentDescription(close).performClick()
|
||||||
eventsRecorder.assertSingle(RoomListEvents.DismissBanner)
|
eventsRecorder.assertSingle(RoomListEvent.DismissBanner)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on close setup key banner emits the expected Event`() {
|
fun `clicking on close setup key banner emits the expected Event`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
rule.setRoomListView(
|
rule.setRoomListView(
|
||||||
state = aRoomListState(
|
state = aRoomListState(
|
||||||
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery),
|
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery),
|
||||||
|
|
@ -95,12 +95,12 @@ class RoomListViewTest {
|
||||||
|
|
||||||
val close = rule.activity.getString(CommonStrings.action_close)
|
val close = rule.activity.getString(CommonStrings.action_close)
|
||||||
rule.onNodeWithContentDescription(close).performClick()
|
rule.onNodeWithContentDescription(close).performClick()
|
||||||
eventsRecorder.assertSingle(RoomListEvents.DismissBanner)
|
eventsRecorder.assertSingle(RoomListEvent.DismissBanner)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on continue recovery key banner invokes the expected callback`() {
|
fun `clicking on continue recovery key banner invokes the expected callback`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setRoomListView(
|
rule.setRoomListView(
|
||||||
state = aRoomListState(
|
state = aRoomListState(
|
||||||
|
|
@ -121,7 +121,7 @@ class RoomListViewTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on continue setup key banner invokes the expected callback`() {
|
fun `clicking on continue setup key banner invokes the expected callback`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setRoomListView(
|
rule.setRoomListView(
|
||||||
state = aRoomListState(
|
state = aRoomListState(
|
||||||
|
|
@ -139,7 +139,7 @@ class RoomListViewTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on start chat when the session has no room invokes the expected callback`() {
|
fun `clicking on start chat when the session has no room invokes the expected callback`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>(expectEvents = false)
|
val eventsRecorder = EventsRecorder<RoomListEvent>(expectEvents = false)
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setRoomListView(
|
rule.setRoomListView(
|
||||||
state = aRoomListState(
|
state = aRoomListState(
|
||||||
|
|
@ -154,7 +154,7 @@ class RoomListViewTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on a room invokes the expected callback`() {
|
fun `clicking on a room invokes the expected callback`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
val state = aRoomListState(
|
val state = aRoomListState(
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
|
|
@ -178,7 +178,7 @@ class RoomListViewTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on a room twice invokes the expected callback only once`() {
|
fun `clicking on a room twice invokes the expected callback only once`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
val state = aRoomListState(
|
val state = aRoomListState(
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
|
|
@ -201,7 +201,7 @@ class RoomListViewTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `long clicking on a room emits the expected Event`() {
|
fun `long clicking on a room emits the expected Event`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
val state = aRoomListState(
|
val state = aRoomListState(
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
|
|
@ -215,12 +215,12 @@ class RoomListViewTest {
|
||||||
eventsRecorder.clear()
|
eventsRecorder.clear()
|
||||||
|
|
||||||
rule.onNodeWithText(room0.latestEvent.content().toString()).performTouchInput { longClick() }
|
rule.onNodeWithText(room0.latestEvent.content().toString()).performTouchInput { longClick() }
|
||||||
eventsRecorder.assertSingle(RoomListEvents.ShowContextMenu(room0))
|
eventsRecorder.assertSingle(RoomListEvent.ShowContextMenu(room0))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on a room setting invokes the expected callback and emits expected Event`() {
|
fun `clicking on a room setting invokes the expected callback and emits expected Event`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
val state = aRoomListState(
|
val state = aRoomListState(
|
||||||
contextMenu = aContextMenuShown(),
|
contextMenu = aContextMenuShown(),
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
|
|
@ -238,12 +238,12 @@ class RoomListViewTest {
|
||||||
rule.clickOn(CommonStrings.common_settings)
|
rule.clickOn(CommonStrings.common_settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
eventsRecorder.assertSingle(RoomListEvents.HideContextMenu)
|
eventsRecorder.assertSingle(RoomListEvent.HideContextMenu)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on accept and decline invite emits the expected Events`() {
|
fun `clicking on accept and decline invite emits the expected Events`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
val eventsRecorder = EventsRecorder<RoomListEvent>()
|
||||||
val state = aRoomListState(
|
val state = aRoomListState(
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
|
|
@ -259,8 +259,8 @@ class RoomListViewTest {
|
||||||
rule.clickOn(CommonStrings.action_decline)
|
rule.clickOn(CommonStrings.action_decline)
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
RoomListEvents.AcceptInvite(invitedRoom),
|
RoomListEvent.AcceptInvite(invitedRoom),
|
||||||
RoomListEvents.ShowDeclineInviteMenu(invitedRoom),
|
RoomListEvent.ShowDeclineInviteMenu(invitedRoom),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@
|
||||||
|
|
||||||
package io.element.android.features.home.impl.search
|
package io.element.android.features.home.impl.search
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory
|
import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory
|
||||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
|
|
@ -18,7 +15,11 @@ import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventForma
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
@ -29,9 +30,7 @@ class RoomListSearchPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = createRoomListSearchPresenter()
|
val presenter = createRoomListSearchPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.isSearchActive).isFalse()
|
assertThat(state.isSearchActive).isFalse()
|
||||||
assertThat(state.query.text.toString()).isEmpty()
|
assertThat(state.query.text.toString()).isEmpty()
|
||||||
|
|
@ -43,16 +42,14 @@ class RoomListSearchPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - toggle search visibility`() = runTest {
|
fun `present - toggle search visibility`() = runTest {
|
||||||
val presenter = createRoomListSearchPresenter()
|
val presenter = createRoomListSearchPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.isSearchActive).isFalse()
|
assertThat(state.isSearchActive).isFalse()
|
||||||
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
state.eventSink(RoomListSearchEvent.ToggleSearchVisibility)
|
||||||
}
|
}
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.isSearchActive).isTrue()
|
assertThat(state.isSearchActive).isTrue()
|
||||||
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
state.eventSink(RoomListSearchEvent.ToggleSearchVisibility)
|
||||||
}
|
}
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.isSearchActive).isFalse()
|
assertThat(state.isSearchActive).isFalse()
|
||||||
|
|
@ -62,14 +59,15 @@ class RoomListSearchPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - query search changes`() = runTest {
|
fun `present - query search changes`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList()
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val presenter = createRoomListSearchPresenter(roomListService)
|
val presenter = createRoomListSearchPresenter(roomListService)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(
|
assertThat(
|
||||||
roomListService.allRooms.currentFilter.value
|
roomList.currentFilter.value
|
||||||
).isEqualTo(
|
).isEqualTo(
|
||||||
RoomListFilter.None
|
RoomListFilter.None
|
||||||
)
|
)
|
||||||
|
|
@ -78,16 +76,16 @@ class RoomListSearchPresenterTest {
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.query.text).isEqualTo("Search")
|
assertThat(state.query.text).isEqualTo("Search")
|
||||||
assertThat(
|
assertThat(
|
||||||
roomListService.allRooms.currentFilter.value
|
roomList.currentFilter.value
|
||||||
).isEqualTo(
|
).isEqualTo(
|
||||||
RoomListFilter.NormalizedMatchRoomName("Search")
|
RoomListFilter.NormalizedMatchRoomName("Search")
|
||||||
)
|
)
|
||||||
state.eventSink(RoomListSearchEvents.ClearQuery)
|
state.eventSink(RoomListSearchEvent.ClearQuery)
|
||||||
}
|
}
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.query.text.toString()).isEmpty()
|
assertThat(state.query.text.toString()).isEmpty()
|
||||||
assertThat(
|
assertThat(
|
||||||
roomListService.allRooms.currentFilter.value
|
roomList.currentFilter.value
|
||||||
).isEqualTo(
|
).isEqualTo(
|
||||||
RoomListFilter.None
|
RoomListFilter.None
|
||||||
)
|
)
|
||||||
|
|
@ -97,26 +95,51 @@ class RoomListSearchPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - room list changes`() = runTest {
|
fun `present - room list changes`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList()
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val presenter = createRoomListSearchPresenter(roomListService)
|
val presenter = createRoomListSearchPresenter(roomListService)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.results).isEmpty()
|
assertThat(state.results).isEmpty()
|
||||||
}
|
}
|
||||||
roomListService.postAllRooms(
|
roomList.summaries.emit(
|
||||||
listOf(aRoomSummary())
|
listOf(aRoomSummary())
|
||||||
)
|
)
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.results).hasSize(1)
|
assertThat(state.results).hasSize(1)
|
||||||
}
|
}
|
||||||
roomListService.postAllRooms(emptyList())
|
roomList.summaries.emit(emptyList())
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.results).isEmpty()
|
assertThat(state.results).isEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - UpdateVisibleRange triggers pagination when near end`() = runTest {
|
||||||
|
val loadMoreLambda = lambdaRecorder<Unit> { }
|
||||||
|
val roomList = FakeDynamicRoomList(loadMoreLambda = loadMoreLambda)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
|
val presenter = createRoomListSearchPresenter(roomListService)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
// Post some rooms to simulate loaded content
|
||||||
|
val rooms = (1..10).map { aRoomSummary() }
|
||||||
|
roomList.summaries.emit(rooms)
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
|
// UpdateVisibleRange near end should trigger loadMore
|
||||||
|
initialState.eventSink(RoomListSearchEvent.UpdateVisibleRange(IntRange(0, 9)))
|
||||||
|
// Give time for the coroutine to complete
|
||||||
|
testScheduler.advanceUntilIdle()
|
||||||
|
|
||||||
|
assert(loadMoreLambda).isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TestScope.createRoomListSearchPresenter(
|
fun TestScope.createRoomListSearchPresenter(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,313 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.home.impl.spacefilters
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||||
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class SpaceFiltersPresenterTest {
|
||||||
|
@Test
|
||||||
|
fun `present - when feature flag is disabled returns Disabled state`() = runTest {
|
||||||
|
val presenter = createSpaceFiltersPresenter(
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val state = awaitItem()
|
||||||
|
assertThat(state).isEqualTo(SpaceFiltersState.Disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when available filters is empty returns Disabled state`() = runTest {
|
||||||
|
val presenter = createSpaceFiltersPresenter(
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val state = awaitLastSequentialItem()
|
||||||
|
assertThat(state).isEqualTo(SpaceFiltersState.Disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when feature flag is enabled and filters exist returns Unselected state`() = runTest {
|
||||||
|
val spaceFilter = aSpaceServiceFilter(displayName = "Test Space")
|
||||||
|
val spaceService = FakeSpaceService()
|
||||||
|
val matrixClient = FakeMatrixClient(spaceService = spaceService)
|
||||||
|
|
||||||
|
val presenter = createSpaceFiltersPresenter(
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
|
||||||
|
),
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
// Emit filters
|
||||||
|
spaceService.emitSpaceFilters(listOf(spaceFilter))
|
||||||
|
|
||||||
|
val state = awaitLastSequentialItem()
|
||||||
|
assertThat(state).isInstanceOf(SpaceFiltersState.Unselected::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - ShowFilters event transitions from Unselected to Selecting`() = runTest {
|
||||||
|
val spaceFilter = aSpaceServiceFilter(displayName = "Test Space")
|
||||||
|
val spaceService = FakeSpaceService()
|
||||||
|
val matrixClient = FakeMatrixClient(spaceService = spaceService)
|
||||||
|
|
||||||
|
val presenter = createSpaceFiltersPresenter(
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
|
||||||
|
),
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
// Emit filters first
|
||||||
|
spaceService.emitSpaceFilters(listOf(spaceFilter))
|
||||||
|
|
||||||
|
val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected
|
||||||
|
unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
|
||||||
|
|
||||||
|
val selectingState = awaitLastSequentialItem()
|
||||||
|
assertThat(selectingState).isInstanceOf(SpaceFiltersState.Selecting::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - Cancel event in Selecting state transitions back to Unselected`() = runTest {
|
||||||
|
val spaceFilter = aSpaceServiceFilter(displayName = "Test Space")
|
||||||
|
val spaceService = FakeSpaceService()
|
||||||
|
val matrixClient = FakeMatrixClient(spaceService = spaceService)
|
||||||
|
|
||||||
|
val presenter = createSpaceFiltersPresenter(
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
|
||||||
|
),
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
// Emit filters first
|
||||||
|
spaceService.emitSpaceFilters(listOf(spaceFilter))
|
||||||
|
|
||||||
|
// Start in Unselected
|
||||||
|
val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected
|
||||||
|
unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
|
||||||
|
|
||||||
|
// Now in Selecting
|
||||||
|
val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting
|
||||||
|
selectingState.eventSink(SpaceFiltersEvent.Selecting.Cancel)
|
||||||
|
|
||||||
|
// Back to Unselected
|
||||||
|
val finalState = awaitLastSequentialItem()
|
||||||
|
assertThat(finalState).isInstanceOf(SpaceFiltersState.Unselected::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - SelectFilter event in Selecting state transitions to Selected`() = runTest {
|
||||||
|
val spaceFilter = aSpaceServiceFilter(displayName = "Test Space")
|
||||||
|
val spaceService = FakeSpaceService()
|
||||||
|
val matrixClient = FakeMatrixClient(spaceService = spaceService)
|
||||||
|
|
||||||
|
val presenter = createSpaceFiltersPresenter(
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
|
||||||
|
),
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
// Emit filters first
|
||||||
|
spaceService.emitSpaceFilters(listOf(spaceFilter))
|
||||||
|
|
||||||
|
// Start in Unselected
|
||||||
|
val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected
|
||||||
|
unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
|
||||||
|
|
||||||
|
// Now in Selecting
|
||||||
|
val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting
|
||||||
|
selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(spaceFilter))
|
||||||
|
|
||||||
|
// Now in Selected
|
||||||
|
val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected
|
||||||
|
assertThat(selectedState.selectedFilter).isEqualTo(spaceFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - ClearSelection event in Selected state transitions back to Unselected`() = runTest {
|
||||||
|
val spaceFilter = aSpaceServiceFilter(displayName = "Test Space")
|
||||||
|
val spaceService = FakeSpaceService()
|
||||||
|
val matrixClient = FakeMatrixClient(spaceService = spaceService)
|
||||||
|
|
||||||
|
val presenter = createSpaceFiltersPresenter(
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
|
||||||
|
),
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
// Emit filters first
|
||||||
|
spaceService.emitSpaceFilters(listOf(spaceFilter))
|
||||||
|
|
||||||
|
// Start in Unselected
|
||||||
|
val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected
|
||||||
|
unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
|
||||||
|
|
||||||
|
// Now in Selecting
|
||||||
|
val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting
|
||||||
|
selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(spaceFilter))
|
||||||
|
|
||||||
|
// Now in Selected
|
||||||
|
val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected
|
||||||
|
selectedState.eventSink(SpaceFiltersEvent.Selected.ClearSelection)
|
||||||
|
|
||||||
|
// Back to Unselected
|
||||||
|
val finalState = awaitLastSequentialItem()
|
||||||
|
assertThat(finalState).isInstanceOf(SpaceFiltersState.Unselected::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - available filters are passed from SpaceService`() = runTest {
|
||||||
|
val spaceFilter1 = aSpaceServiceFilter(displayName = "Work", roomId = RoomId("!work:example.com"))
|
||||||
|
val spaceFilter2 = aSpaceServiceFilter(displayName = "Personal", roomId = RoomId("!personal:example.com"))
|
||||||
|
val spaceFilters = listOf(spaceFilter1, spaceFilter2)
|
||||||
|
|
||||||
|
val spaceService = FakeSpaceService()
|
||||||
|
val matrixClient = FakeMatrixClient(spaceService = spaceService)
|
||||||
|
|
||||||
|
val presenter = createSpaceFiltersPresenter(
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
|
||||||
|
),
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
// Emit space filters
|
||||||
|
spaceService.emitSpaceFilters(spaceFilters)
|
||||||
|
|
||||||
|
// Start in Unselected
|
||||||
|
val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected
|
||||||
|
unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
|
||||||
|
|
||||||
|
// Now in Selecting with available filters
|
||||||
|
val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting
|
||||||
|
assertThat(selectingState.availableFilters).containsExactly(spaceFilter1, spaceFilter2).inOrder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - selected filter is cleared when space is removed from available filters`() = runTest {
|
||||||
|
val spaceFilter = aSpaceServiceFilter(displayName = "Work", roomId = RoomId("!work:example.com"))
|
||||||
|
val otherSpaceFilter = aSpaceServiceFilter(displayName = "Personal", roomId = RoomId("!personal:example.com"))
|
||||||
|
|
||||||
|
val spaceService = FakeSpaceService()
|
||||||
|
val matrixClient = FakeMatrixClient(spaceService = spaceService)
|
||||||
|
|
||||||
|
val presenter = createSpaceFiltersPresenter(
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
|
||||||
|
),
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
// Emit filters first
|
||||||
|
spaceService.emitSpaceFilters(listOf(spaceFilter, otherSpaceFilter))
|
||||||
|
|
||||||
|
// Go to Selecting
|
||||||
|
val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected
|
||||||
|
unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
|
||||||
|
|
||||||
|
// Select the filter
|
||||||
|
val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting
|
||||||
|
selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(spaceFilter))
|
||||||
|
|
||||||
|
// Verify in Selected state
|
||||||
|
val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected
|
||||||
|
assertThat(selectedState.selectedFilter).isEqualTo(spaceFilter)
|
||||||
|
|
||||||
|
// Remove the selected space from available filters (but keep other spaces)
|
||||||
|
spaceService.emitSpaceFilters(listOf(otherSpaceFilter))
|
||||||
|
|
||||||
|
// Should auto-transition to Unselected
|
||||||
|
val finalState = awaitLastSequentialItem()
|
||||||
|
assertThat(finalState).isInstanceOf(SpaceFiltersState.Unselected::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - selected filter stays in sync when available filters update`() = runTest {
|
||||||
|
val originalFilter = aSpaceServiceFilter(
|
||||||
|
displayName = "Work",
|
||||||
|
roomId = RoomId("!work:example.com"),
|
||||||
|
descendants = listOf(RoomId("!room1:example.com"))
|
||||||
|
)
|
||||||
|
val updatedFilter = aSpaceServiceFilter(
|
||||||
|
displayName = "Work",
|
||||||
|
roomId = RoomId("!work:example.com"),
|
||||||
|
descendants = listOf(RoomId("!room1:example.com"), RoomId("!room2:example.com"))
|
||||||
|
)
|
||||||
|
|
||||||
|
val spaceService = FakeSpaceService()
|
||||||
|
val matrixClient = FakeMatrixClient(spaceService = spaceService)
|
||||||
|
|
||||||
|
val presenter = createSpaceFiltersPresenter(
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
|
||||||
|
),
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
// Emit initial space filters
|
||||||
|
spaceService.emitSpaceFilters(listOf(originalFilter))
|
||||||
|
|
||||||
|
// Start in Unselected
|
||||||
|
val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected
|
||||||
|
unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
|
||||||
|
|
||||||
|
// Now in Selecting
|
||||||
|
val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting
|
||||||
|
selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(originalFilter))
|
||||||
|
|
||||||
|
// Now in Selected
|
||||||
|
val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected
|
||||||
|
assertThat(selectedState.selectedFilter.descendants).hasSize(1)
|
||||||
|
|
||||||
|
// Emit updated space filters
|
||||||
|
spaceService.emitSpaceFilters(listOf(updatedFilter))
|
||||||
|
|
||||||
|
// Selected filter should be updated
|
||||||
|
val updatedSelectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected
|
||||||
|
assertThat(updatedSelectedState.selectedFilter.descendants).hasSize(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSpaceFiltersPresenter(
|
||||||
|
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
|
||||||
|
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
||||||
|
): SpaceFiltersPresenter {
|
||||||
|
return SpaceFiltersPresenter(
|
||||||
|
featureFlagService = featureFlagService,
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.home.impl.spacefilters
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class SpaceFiltersViewTest {
|
||||||
|
@get:Rule
|
||||||
|
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on a filter with alias shows display name and alias`() {
|
||||||
|
val filter = aSpaceServiceFilter(
|
||||||
|
displayName = "Test Space",
|
||||||
|
canonicalAlias = A_ROOM_ALIAS,
|
||||||
|
)
|
||||||
|
val eventsRecorder = EventsRecorder<SpaceFiltersEvent.Selecting>()
|
||||||
|
rule.setSpaceFiltersView(
|
||||||
|
state = aSelectingSpaceFiltersState(
|
||||||
|
availableFilters = listOf(filter),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Both display name and alias should be visible
|
||||||
|
rule.onNodeWithText(filter.spaceRoom.displayName).assertExists()
|
||||||
|
rule.onNodeWithText(A_ROOM_ALIAS.value).assertExists()
|
||||||
|
|
||||||
|
rule.onNodeWithText(filter.spaceRoom.displayName).performClick()
|
||||||
|
|
||||||
|
eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `multiple filters are displayed and clickable`() {
|
||||||
|
val filter1 = aSpaceServiceFilter(displayName = "Space One")
|
||||||
|
val filter2 = aSpaceServiceFilter(displayName = "Space Two")
|
||||||
|
val eventsRecorder = EventsRecorder<SpaceFiltersEvent.Selecting>()
|
||||||
|
rule.setSpaceFiltersView(
|
||||||
|
state = aSelectingSpaceFiltersState(
|
||||||
|
availableFilters = listOf(filter1, filter2),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Both filters should be visible
|
||||||
|
rule.onNodeWithText(filter1.spaceRoom.displayName).assertExists()
|
||||||
|
rule.onNodeWithText(filter2.spaceRoom.displayName).assertExists()
|
||||||
|
|
||||||
|
// Click on second filter
|
||||||
|
rule.onNodeWithText(filter2.spaceRoom.displayName).performClick()
|
||||||
|
|
||||||
|
eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceFiltersView(
|
||||||
|
state: SpaceFiltersState,
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
SpaceFiltersView(state = state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -376,10 +376,9 @@ private fun JoinRule?.toJoinAuthorisationStatus(): JoinAuthorisationStatus {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
JoinRule.Knock,
|
JoinRule.Knock,
|
||||||
is JoinRule.KnockRestricted -> JoinAuthorisationStatus.CanKnock
|
is JoinRule.KnockRestricted -> JoinAuthorisationStatus.CanKnock
|
||||||
JoinRule.Invite,
|
JoinRule.Invite -> JoinAuthorisationStatus.NeedInvite
|
||||||
JoinRule.Private -> JoinAuthorisationStatus.NeedInvite
|
|
||||||
is JoinRule.Restricted -> JoinAuthorisationStatus.Restricted
|
is JoinRule.Restricted -> JoinAuthorisationStatus.Restricted
|
||||||
JoinRule.Public -> JoinAuthorisationStatus.CanJoin
|
JoinRule.Public -> JoinAuthorisationStatus.CanJoin
|
||||||
else -> JoinAuthorisationStatus.Unknown
|
is JoinRule.Custom, null -> JoinAuthorisationStatus.Unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@ import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.invite.api.InviteData
|
import io.element.android.features.invite.api.InviteData
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewAliasAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom
|
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
|
||||||
|
|
@ -514,7 +514,7 @@ private fun IncompleteContent(
|
||||||
title = {
|
title = {
|
||||||
when (roomIdOrAlias) {
|
when (roomIdOrAlias) {
|
||||||
is RoomIdOrAlias.Alias -> {
|
is RoomIdOrAlias.Alias -> {
|
||||||
RoomPreviewSubtitleAtom(roomIdOrAlias.identifier)
|
RoomPreviewAliasAtom(roomIdOrAlias.identifier)
|
||||||
}
|
}
|
||||||
is RoomIdOrAlias.Id -> {
|
is RoomIdOrAlias.Id -> {
|
||||||
PlaceholderAtom(width = 200.dp, height = 22.dp)
|
PlaceholderAtom(width = 200.dp, height = 22.dp)
|
||||||
|
|
@ -566,13 +566,12 @@ private fun DefaultLoadedContent(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
subtitle = {
|
subtitle = {
|
||||||
when {
|
if (contentState.alias != null) {
|
||||||
contentState.details is LoadedDetails.Space -> {
|
RoomPreviewAliasAtom(contentState.alias.value)
|
||||||
SpaceInfoRow(visibility = SpaceRoomVisibility.fromJoinRule(contentState.joinRule))
|
}
|
||||||
}
|
if (contentState.details is LoadedDetails.Space) {
|
||||||
contentState.alias != null -> {
|
Spacer(Modifier.height(8.dp))
|
||||||
RoomPreviewSubtitleAtom(contentState.alias.value)
|
SpaceInfoRow(visibility = SpaceRoomVisibility.fromJoinRule(contentState.joinRule))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
description = {
|
description = {
|
||||||
|
|
|
||||||
|
|
@ -1035,34 +1035,6 @@ class JoinRoomPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - when room is not known RoomPreview is loaded as Private`() = runTest {
|
|
||||||
val client = FakeMatrixClient(
|
|
||||||
getNotJoinedRoomResult = { _, _ ->
|
|
||||||
Result.success(
|
|
||||||
aRoomPreview(
|
|
||||||
info = aRoomPreviewInfo(joinRule = JoinRule.Private),
|
|
||||||
roomMembershipDetails = {
|
|
||||||
Result.success(aRoomMembershipDetails())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
spaceService = FakeSpaceService(
|
|
||||||
spaceRoomListResult = { FakeSpaceRoomList() },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
val presenter = createJoinRoomPresenter(
|
|
||||||
matrixClient = client
|
|
||||||
)
|
|
||||||
presenter.test {
|
|
||||||
skipItems(1)
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.NeedInvite)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - when room is not known RoomPreview is loaded as Custom`() = runTest {
|
fun `present - when room is not known RoomPreview is loaded as Custom`() = runTest {
|
||||||
val client = FakeMatrixClient(
|
val client = FakeMatrixClient(
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,8 @@ class LinkNewDeviceFlowNode(
|
||||||
is ErrorType.Unknown -> ErrorScreenType.UnknownError
|
is ErrorType.Unknown -> ErrorScreenType.UnknownError
|
||||||
is ErrorType.UnsupportedProtocol -> ErrorScreenType.UnknownError
|
is ErrorType.UnsupportedProtocol -> ErrorScreenType.UnknownError
|
||||||
}
|
}
|
||||||
// It is OK to push on backstack, since when user leaves the error screen, a new root will be set
|
// It is OK to push on backstack, since when user leaves the error screen, a new root will be set,
|
||||||
|
// or the whole flow will be popped.
|
||||||
backstack.push(NavTarget.Error(error))
|
backstack.push(NavTarget.Error(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -263,6 +264,12 @@ class LinkNewDeviceFlowNode(
|
||||||
linkNewDesktopHandler.reset()
|
linkNewDesktopHandler.reset()
|
||||||
backstack.newRoot(NavTarget.Root)
|
backstack.newRoot(NavTarget.Root)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCancel() {
|
||||||
|
linkNewMobileHandler.reset()
|
||||||
|
linkNewDesktopHandler.reset()
|
||||||
|
callback.onDone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createNode<ErrorNode>(buildContext, listOf(callback, navTarget.errorScreenType))
|
createNode<ErrorNode>(buildContext, listOf(callback, navTarget.errorScreenType))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ class ErrorNode(
|
||||||
) : Node(buildContext = buildContext, plugins = plugins) {
|
) : Node(buildContext = buildContext, plugins = plugins) {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onRetry()
|
fun onRetry()
|
||||||
|
fun onCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback: Callback = callback()
|
private val callback: Callback = callback()
|
||||||
|
|
@ -38,6 +39,7 @@ class ErrorNode(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
errorScreenType = errorScreenType,
|
errorScreenType = errorScreenType,
|
||||||
onRetry = callback::onRetry,
|
onRetry = callback::onRetry,
|
||||||
|
onCancel = callback::onCancel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
|
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -41,17 +42,23 @@ import kotlinx.collections.immutable.persistentListOf
|
||||||
fun ErrorView(
|
fun ErrorView(
|
||||||
errorScreenType: ErrorScreenType,
|
errorScreenType: ErrorScreenType,
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
|
onCancel: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val appName = LocalBuildMeta.current.applicationName
|
val appName = LocalBuildMeta.current.applicationName
|
||||||
BackHandler(onBack = onRetry)
|
BackHandler(onBack = onCancel)
|
||||||
FlowStepPage(
|
FlowStepPage(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
iconStyle = BigIcon.Style.AlertSolid,
|
iconStyle = BigIcon.Style.AlertSolid,
|
||||||
title = titleText(errorScreenType, appName),
|
title = titleText(errorScreenType, appName),
|
||||||
subTitle = subtitleText(errorScreenType, appName),
|
subTitle = subtitleText(errorScreenType, appName),
|
||||||
content = { Content(errorScreenType) },
|
content = { Content(errorScreenType) },
|
||||||
buttons = { Buttons(onRetry) },
|
buttons = {
|
||||||
|
Buttons(
|
||||||
|
onRetry = onRetry,
|
||||||
|
onCancel = onCancel,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,11 +125,19 @@ private fun Content(errorScreenType: ErrorScreenType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Buttons(onRetry: () -> Unit) {
|
private fun Buttons(
|
||||||
|
onRetry: () -> Unit,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
) {
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(CommonStrings.action_start_over),
|
text = stringResource(CommonStrings.action_try_again),
|
||||||
onClick = onRetry
|
onClick = onRetry,
|
||||||
|
)
|
||||||
|
OutlinedButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(CommonStrings.action_cancel),
|
||||||
|
onClick = onCancel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,6 +148,7 @@ internal fun ErrorViewPreview(@PreviewParameter(ErrorScreenTypeProvider::class)
|
||||||
ErrorView(
|
ErrorView(
|
||||||
errorScreenType = errorScreenType,
|
errorScreenType = errorScreenType,
|
||||||
onRetry = {},
|
onRetry = {},
|
||||||
|
onCancel = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,33 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_link_new_device_desktop_scanning_title">"Skann QR-koden"</string>
|
<string name="screen_link_new_device_desktop_scanning_title">"Skann QR-koden"</string>
|
||||||
|
<string name="screen_link_new_device_desktop_step1">"Åpne %1$s på en bærbar eller stasjonær datamaskin"</string>
|
||||||
<string name="screen_link_new_device_desktop_step3">"Skann QR-koden med denne enheten"</string>
|
<string name="screen_link_new_device_desktop_step3">"Skann QR-koden med denne enheten"</string>
|
||||||
<string name="screen_link_new_device_desktop_submit">"Klar til å skanne"</string>
|
<string name="screen_link_new_device_desktop_submit">"Klar til å skanne"</string>
|
||||||
|
<string name="screen_link_new_device_desktop_title">"Åpne %1$s på en datamaskin for å få QR-koden"</string>
|
||||||
|
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Tallene stemmer ikke overens"</string>
|
||||||
|
<string name="screen_link_new_device_enter_number_notice">"Skriv inn 2-sifret kode"</string>
|
||||||
|
<string name="screen_link_new_device_enter_number_subtitle">"Dette vil bekrefte at forbindelsen til den andre enheten din er sikker."</string>
|
||||||
|
<string name="screen_link_new_device_enter_number_title">"Skriv inn nummeret som vises på den andre enheten din"</string>
|
||||||
<string name="screen_link_new_device_error_app_not_supported_subtitle">"Kontotilbyderen din støtter ikke %1$s."</string>
|
<string name="screen_link_new_device_error_app_not_supported_subtitle">"Kontotilbyderen din støtter ikke %1$s."</string>
|
||||||
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s støttes ikke"</string>
|
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s støttes ikke"</string>
|
||||||
|
<string name="screen_link_new_device_error_not_supported_subtitle">"Din kontoleverandør støtter ikke pålogging på en ny enhet med QR-kode."</string>
|
||||||
<string name="screen_link_new_device_error_not_supported_title">"QR-kode støttes ikke"</string>
|
<string name="screen_link_new_device_error_not_supported_title">"QR-kode støttes ikke"</string>
|
||||||
<string name="screen_link_new_device_error_request_cancelled_subtitle">"Påloggingen ble kansellert på den andre enheten."</string>
|
<string name="screen_link_new_device_error_request_cancelled_subtitle">"Påloggingen ble kansellert på den andre enheten."</string>
|
||||||
<string name="screen_link_new_device_error_request_cancelled_title">"Påloggingsforespørsel kansellert"</string>
|
<string name="screen_link_new_device_error_request_cancelled_title">"Påloggingsforespørsel kansellert"</string>
|
||||||
<string name="screen_link_new_device_error_request_timeout_subtitle">"Påloggingen er utløpt. Vennligst prøv igjen."</string>
|
<string name="screen_link_new_device_error_request_timeout_subtitle">"Påloggingen er utløpt. Vennligst prøv igjen."</string>
|
||||||
<string name="screen_link_new_device_error_request_timeout_title">"Påloggingen ble ikke fullført i tide"</string>
|
<string name="screen_link_new_device_error_request_timeout_title">"Påloggingen ble ikke fullført i tide"</string>
|
||||||
|
<string name="screen_link_new_device_mobile_step1">"Åpne %1$s på den andre enheten"</string>
|
||||||
<string name="screen_link_new_device_mobile_step2">"Velg %1$s"</string>
|
<string name="screen_link_new_device_mobile_step2">"Velg %1$s"</string>
|
||||||
|
<string name="screen_link_new_device_mobile_step2_action">"Logg inn med QR-kode"</string>
|
||||||
|
<string name="screen_link_new_device_mobile_step3">"Skann QR-koden som vises her med den andre enheten"</string>
|
||||||
|
<string name="screen_link_new_device_mobile_title">"Åpne %1$s på den andre enheten"</string>
|
||||||
|
<string name="screen_link_new_device_root_desktop_computer">"Datamaskin"</string>
|
||||||
|
<string name="screen_link_new_device_root_loading_qr_code">"Laster QR-kode…"</string>
|
||||||
|
<string name="screen_link_new_device_root_mobile_device">"Mobil enhet"</string>
|
||||||
|
<string name="screen_link_new_device_root_title">"Hvilken type enhet ønsker du å koble til?"</string>
|
||||||
|
<string name="screen_link_new_device_wrong_number_subtitle">"Prøv igjen og påse at du har tastet inn den tosifrede koden riktig. Hvis tallene fortsatt ikke stemmer, må du kontakte kontoleverandøren din."</string>
|
||||||
|
<string name="screen_link_new_device_wrong_number_title">"Tallene stemmer ikke overens"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"En sikker tilkobling kunne ikke opprettes til den nye enheten. Dine eksisterende enheter er fortsatt trygge, og du trenger ikke å bekymre deg for dem."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"En sikker tilkobling kunne ikke opprettes til den nye enheten. Dine eksisterende enheter er fortsatt trygge, og du trenger ikke å bekymre deg for dem."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Hva nå?"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Hva nå?"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Prøv å logge på igjen med en QR-kode i tilfelle dette var et nettverksproblem"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Prøv å logge på igjen med en QR-kode i tilfelle dette var et nettverksproblem"</string>
|
||||||
|
|
@ -21,6 +38,8 @@
|
||||||
<string name="screen_qr_code_login_error_cancelled_title">"Påloggingsforespørsel kansellert"</string>
|
<string name="screen_qr_code_login_error_cancelled_title">"Påloggingsforespørsel kansellert"</string>
|
||||||
<string name="screen_qr_code_login_error_declined_subtitle">"Påloggingen ble avvist på den andre enheten."</string>
|
<string name="screen_qr_code_login_error_declined_subtitle">"Påloggingen ble avvist på den andre enheten."</string>
|
||||||
<string name="screen_qr_code_login_error_declined_title">"Pålogging avslått"</string>
|
<string name="screen_qr_code_login_error_declined_title">"Pålogging avslått"</string>
|
||||||
|
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Du trenger ikke å gjøre noe annet."</string>
|
||||||
|
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Din andre enhet er allerede logget inn"</string>
|
||||||
<string name="screen_qr_code_login_error_expired_subtitle">"Påloggingen er utløpt. Vennligst prøv igjen."</string>
|
<string name="screen_qr_code_login_error_expired_subtitle">"Påloggingen er utløpt. Vennligst prøv igjen."</string>
|
||||||
<string name="screen_qr_code_login_error_expired_title">"Påloggingen ble ikke fullført i tide"</string>
|
<string name="screen_qr_code_login_error_expired_title">"Påloggingen ble ikke fullført i tide"</string>
|
||||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Den andre enheten din støtter ikke pålogging på %s med en QR-kode.
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Den andre enheten din støtter ikke pålogging på %s med en QR-kode.
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
import io.element.android.tests.testutils.clickOn
|
import io.element.android.tests.testutils.clickOn
|
||||||
import io.element.android.tests.testutils.ensureCalledOnce
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
import io.element.android.tests.testutils.pressBackKey
|
import io.element.android.tests.testutils.pressBackKey
|
||||||
|
|
@ -26,33 +27,45 @@ class ErrorViewTest {
|
||||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on back pressed - calls the onRetry callback`() {
|
fun `on back pressed - calls the onCancel callback`() {
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setErrorView(
|
rule.setErrorView(
|
||||||
onRetry = callback
|
onCancel = callback,
|
||||||
)
|
)
|
||||||
rule.pressBackKey()
|
rule.pressBackKey()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on start over button clicked - calls the expected callback`() {
|
fun `on try again button clicked - calls the expected callback`() {
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setErrorView(
|
rule.setErrorView(
|
||||||
onRetry = callback
|
onRetry = callback
|
||||||
)
|
)
|
||||||
rule.clickOn(CommonStrings.action_start_over)
|
rule.clickOn(CommonStrings.action_try_again)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on cancel button clicked - calls the expected callback`() {
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setErrorView(
|
||||||
|
onCancel = callback
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_cancel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setErrorView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setErrorView(
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onCancel: () -> Unit = EnsureNeverCalled(),
|
||||||
errorScreenType: ErrorScreenType = ErrorScreenType.UnknownError,
|
errorScreenType: ErrorScreenType = ErrorScreenType.UnknownError,
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
ErrorView(
|
ErrorView(
|
||||||
errorScreenType = errorScreenType,
|
errorScreenType = errorScreenType,
|
||||||
onRetry = onRetry,
|
onRetry = onRetry,
|
||||||
|
onCancel = onCancel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
|
|
@ -196,7 +197,12 @@ class LoginFlowNode(
|
||||||
createNode<ChooseAccountProviderNode>(buildContext, listOf(callback))
|
createNode<ChooseAccountProviderNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
NavTarget.QrCode -> {
|
NavTarget.QrCode -> {
|
||||||
createNode<QrCodeLoginFlowNode>(buildContext)
|
val callback = object : QrCodeLoginFlowNode.Callback {
|
||||||
|
override fun navigateBack() {
|
||||||
|
backstack.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<QrCodeLoginFlowNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
is NavTarget.ConfirmAccountProvider -> {
|
is NavTarget.ConfirmAccountProvider -> {
|
||||||
val inputs = ConfirmAccountProviderNode.Inputs(
|
val inputs = ConfirmAccountProviderNode.Inputs(
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
|
import io.element.android.libraries.architecture.callback
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||||
|
|
@ -64,6 +65,12 @@ class QrCodeLoginFlowNode(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
), DependencyInjectionGraphOwner {
|
), DependencyInjectionGraphOwner {
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val callback: Callback = callback()
|
||||||
|
|
||||||
private var authenticationJob: Job? = null
|
private var authenticationJob: Job? = null
|
||||||
|
|
||||||
override val graph = qrCodeLoginGraphFactory.create()
|
override val graph = qrCodeLoginGraphFactory.create()
|
||||||
|
|
@ -85,7 +92,6 @@ class QrCodeLoginFlowNode(
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
|
|
||||||
observeLoginStep()
|
observeLoginStep()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,7 +119,8 @@ class QrCodeLoginFlowNode(
|
||||||
is QrLoginException.Cancelled -> {
|
is QrLoginException.Cancelled -> {
|
||||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.Cancelled))
|
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.Cancelled))
|
||||||
}
|
}
|
||||||
is QrLoginException.Expired -> {
|
is QrLoginException.Expired,
|
||||||
|
is QrLoginException.NotFound -> {
|
||||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.Expired))
|
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.Expired))
|
||||||
}
|
}
|
||||||
is QrLoginException.Declined -> {
|
is QrLoginException.Declined -> {
|
||||||
|
|
@ -132,7 +139,9 @@ class QrCodeLoginFlowNode(
|
||||||
Timber.e(error, "OIDC metadata is invalid")
|
Timber.e(error, "OIDC metadata is invalid")
|
||||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError))
|
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError))
|
||||||
}
|
}
|
||||||
else -> {
|
QrLoginException.CheckCodeAlreadySent,
|
||||||
|
QrLoginException.CheckCodeCannotBeSent,
|
||||||
|
QrLoginException.Unknown -> {
|
||||||
Timber.e(error, "Unknown error found")
|
Timber.e(error, "Unknown error found")
|
||||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError))
|
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError))
|
||||||
}
|
}
|
||||||
|
|
@ -178,7 +187,13 @@ class QrCodeLoginFlowNode(
|
||||||
}
|
}
|
||||||
is NavTarget.Error -> {
|
is NavTarget.Error -> {
|
||||||
val callback = object : QrCodeErrorNode.Callback {
|
val callback = object : QrCodeErrorNode.Callback {
|
||||||
override fun onRetry() = reset()
|
override fun onRetry() {
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancel() {
|
||||||
|
callback.navigateBack()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createNode<QrCodeErrorNode>(buildContext, plugins = listOf(navTarget.errorType, callback))
|
createNode<QrCodeErrorNode>(buildContext, plugins = listOf(navTarget.errorType, callback))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ class QrCodeErrorNode(
|
||||||
) : Node(buildContext = buildContext, plugins = plugins) {
|
) : Node(buildContext = buildContext, plugins = plugins) {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onRetry()
|
fun onRetry()
|
||||||
|
fun onCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback: Callback = callback()
|
private val callback: Callback = callback()
|
||||||
|
|
@ -43,6 +44,7 @@ class QrCodeErrorNode(
|
||||||
errorScreenType = qrCodeErrorScreenType,
|
errorScreenType = qrCodeErrorScreenType,
|
||||||
appName = buildMeta.productionApplicationName,
|
appName = buildMeta.productionApplicationName,
|
||||||
onRetry = callback::onRetry,
|
onRetry = callback::onRetry,
|
||||||
|
onCancel = callback::onCancel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -44,16 +45,22 @@ fun QrCodeErrorView(
|
||||||
errorScreenType: QrCodeErrorScreenType,
|
errorScreenType: QrCodeErrorScreenType,
|
||||||
appName: String,
|
appName: String,
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
|
onCancel: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
BackHandler(onBack = onRetry)
|
BackHandler(onBack = onCancel)
|
||||||
FlowStepPage(
|
FlowStepPage(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
iconStyle = BigIcon.Style.AlertSolid,
|
iconStyle = BigIcon.Style.AlertSolid,
|
||||||
title = titleText(errorScreenType, appName),
|
title = titleText(errorScreenType, appName),
|
||||||
subTitle = subtitleText(errorScreenType, appName),
|
subTitle = subtitleText(errorScreenType, appName),
|
||||||
content = { Content(errorScreenType) },
|
content = { Content(errorScreenType) },
|
||||||
buttons = { Buttons(onRetry) },
|
buttons = {
|
||||||
|
Buttons(
|
||||||
|
onRetry = onRetry,
|
||||||
|
onCancel = onCancel,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,11 +125,19 @@ private fun Content(errorScreenType: QrCodeErrorScreenType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Buttons(onRetry: () -> Unit) {
|
private fun Buttons(
|
||||||
|
onRetry: () -> Unit,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
) {
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(R.string.screen_qr_code_login_start_over_button),
|
text = stringResource(CommonStrings.action_try_again),
|
||||||
onClick = onRetry
|
onClick = onRetry,
|
||||||
|
)
|
||||||
|
OutlinedButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(CommonStrings.action_cancel),
|
||||||
|
onClick = onCancel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,7 +148,8 @@ internal fun QrCodeErrorViewPreview(@PreviewParameter(QrCodeErrorScreenTypeProvi
|
||||||
QrCodeErrorView(
|
QrCodeErrorView(
|
||||||
errorScreenType = errorScreenType,
|
errorScreenType = errorScreenType,
|
||||||
appName = "Element X",
|
appName = "Element X",
|
||||||
onRetry = {}
|
onRetry = {},
|
||||||
|
onCancel = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@
|
||||||
<string name="screen_qr_code_login_error_cancelled_title">"Påloggingsforespørsel kansellert"</string>
|
<string name="screen_qr_code_login_error_cancelled_title">"Påloggingsforespørsel kansellert"</string>
|
||||||
<string name="screen_qr_code_login_error_declined_subtitle">"Påloggingen ble avvist på den andre enheten."</string>
|
<string name="screen_qr_code_login_error_declined_subtitle">"Påloggingen ble avvist på den andre enheten."</string>
|
||||||
<string name="screen_qr_code_login_error_declined_title">"Pålogging avslått"</string>
|
<string name="screen_qr_code_login_error_declined_title">"Pålogging avslått"</string>
|
||||||
|
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Du trenger ikke å gjøre noe annet."</string>
|
||||||
|
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Din andre enhet er allerede logget inn"</string>
|
||||||
<string name="screen_qr_code_login_error_expired_subtitle">"Påloggingen er utløpt. Vennligst prøv igjen."</string>
|
<string name="screen_qr_code_login_error_expired_subtitle">"Påloggingen er utløpt. Vennligst prøv igjen."</string>
|
||||||
<string name="screen_qr_code_login_error_expired_title">"Påloggingen ble ikke fullført i tide"</string>
|
<string name="screen_qr_code_login_error_expired_title">"Påloggingen ble ikke fullført i tide"</string>
|
||||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Den andre enheten din støtter ikke pålogging på %s med en QR-kode.
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Den andre enheten din støtter ikke pålogging på %s med en QR-kode.
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData
|
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
@ -60,6 +61,9 @@ class QrCodeLoginFlowNodeTest {
|
||||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Expired)
|
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Expired)
|
||||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.Expired))
|
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.Expired))
|
||||||
|
|
||||||
|
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.NotFound)
|
||||||
|
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.Expired))
|
||||||
|
|
||||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Declined)
|
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Declined)
|
||||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.Declined))
|
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.Declined))
|
||||||
|
|
||||||
|
|
@ -183,7 +187,11 @@ class QrCodeLoginFlowNodeTest {
|
||||||
)
|
)
|
||||||
return QrCodeLoginFlowNode(
|
return QrCodeLoginFlowNode(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = emptyList(),
|
plugins = listOf(
|
||||||
|
object : QrCodeLoginFlowNode.Callback {
|
||||||
|
override fun navigateBack() = lambdaError()
|
||||||
|
}
|
||||||
|
),
|
||||||
qrCodeLoginGraphFactory = FakeQrCodeLoginGraph.Builder(qrCodeLoginManager),
|
qrCodeLoginGraphFactory = FakeQrCodeLoginGraph.Builder(qrCodeLoginManager),
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,9 @@ import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.features.login.impl.R
|
|
||||||
import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType
|
import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
import io.element.android.tests.testutils.clickOn
|
import io.element.android.tests.testutils.clickOn
|
||||||
import io.element.android.tests.testutils.ensureCalledOnce
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
import io.element.android.tests.testutils.pressBackKey
|
import io.element.android.tests.testutils.pressBackKey
|
||||||
|
|
@ -28,10 +29,10 @@ class QrCodeErrorViewTest {
|
||||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on back pressed - calls the onRetry callback`() {
|
fun `on back pressed - calls the onCancel callback`() {
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setQrCodeErrorView(
|
rule.setQrCodeErrorView(
|
||||||
onRetry = callback
|
onCancel = callback,
|
||||||
)
|
)
|
||||||
rule.pressBackKey()
|
rule.pressBackKey()
|
||||||
}
|
}
|
||||||
|
|
@ -41,14 +42,25 @@ class QrCodeErrorViewTest {
|
||||||
fun `on try again button clicked - calls the expected callback`() {
|
fun `on try again button clicked - calls the expected callback`() {
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setQrCodeErrorView(
|
rule.setQrCodeErrorView(
|
||||||
onRetry = callback
|
onRetry = callback,
|
||||||
)
|
)
|
||||||
rule.clickOn(R.string.screen_qr_code_login_start_over_button)
|
rule.clickOn(CommonStrings.action_try_again)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on cancel button clicked - calls the expected callback`() {
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setQrCodeErrorView(
|
||||||
|
onCancel = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_cancel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setQrCodeErrorView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setQrCodeErrorView(
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onCancel: () -> Unit = EnsureNeverCalled(),
|
||||||
errorScreenType: QrCodeErrorScreenType = QrCodeErrorScreenType.UnknownError,
|
errorScreenType: QrCodeErrorScreenType = QrCodeErrorScreenType.UnknownError,
|
||||||
appName: String = "Element X",
|
appName: String = "Element X",
|
||||||
) {
|
) {
|
||||||
|
|
@ -56,7 +68,8 @@ class QrCodeErrorViewTest {
|
||||||
QrCodeErrorView(
|
QrCodeErrorView(
|
||||||
errorScreenType = errorScreenType,
|
errorScreenType = errorScreenType,
|
||||||
appName = appName,
|
appName = appName,
|
||||||
onRetry = onRetry
|
onRetry = onRetry,
|
||||||
|
onCancel = onCancel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -210,10 +210,7 @@ class MessagesPresenter(
|
||||||
// * History sharing is enabled,
|
// * History sharing is enabled,
|
||||||
// * The room is encrypted, and:
|
// * The room is encrypted, and:
|
||||||
// * The room's history_visibility allows future users to see content.
|
// * The room's history_visibility allows future users to see content.
|
||||||
val showSharedHistoryIcon = isKeyShareOnInviteEnabled &&
|
val topBarSharedHistoryIcon = if (isKeyShareOnInviteEnabled) roomInfo.sharedHistoryIcon() else SharedHistoryIcon.NONE
|
||||||
roomInfo.isEncrypted == true &&
|
|
||||||
(roomInfo.historyVisibility == RoomHistoryVisibility.Shared ||
|
|
||||||
roomInfo.historyVisibility == RoomHistoryVisibility.WorldReadable)
|
|
||||||
|
|
||||||
LifecycleResumeEffect(dmRoomMember, roomInfo.isEncrypted) {
|
LifecycleResumeEffect(dmRoomMember, roomInfo.isEncrypted) {
|
||||||
if (roomInfo.isEncrypted == true) {
|
if (roomInfo.isEncrypted == true) {
|
||||||
|
|
@ -297,12 +294,24 @@ class MessagesPresenter(
|
||||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||||
dmUserVerificationState = dmUserVerificationState,
|
dmUserVerificationState = dmUserVerificationState,
|
||||||
roomMemberModerationState = roomMemberModerationState,
|
roomMemberModerationState = roomMemberModerationState,
|
||||||
showSharedHistoryIcon = showSharedHistoryIcon,
|
topBarSharedHistoryIcon = topBarSharedHistoryIcon,
|
||||||
successorRoom = roomInfo.successorRoom,
|
successorRoom = roomInfo.successorRoom,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun RoomInfo.sharedHistoryIcon(): SharedHistoryIcon {
|
||||||
|
if (isEncrypted == true) {
|
||||||
|
if (historyVisibility == RoomHistoryVisibility.Shared) {
|
||||||
|
return SharedHistoryIcon.SHARED
|
||||||
|
} else if (historyVisibility == RoomHistoryVisibility.WorldReadable) {
|
||||||
|
return SharedHistoryIcon.WORLD_READABLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SharedHistoryIcon.NONE
|
||||||
|
}
|
||||||
|
|
||||||
private fun RoomInfo.avatarData(): AvatarData {
|
private fun RoomInfo.avatarData(): AvatarData {
|
||||||
return AvatarData(
|
return AvatarData(
|
||||||
id = id.value,
|
id = id.value,
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue