Merge branch 'develop' into feature/fga/room_list_filters
This commit is contained in:
commit
c2e969c592
94 changed files with 1437 additions and 451 deletions
2
.github/workflows/maestro.yml
vendored
2
.github/workflows/maestro.yml
vendored
|
|
@ -47,7 +47,7 @@ jobs:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.8.0
|
- uses: mobile-dev-inc/action-maestro-cloud@v1.8.1
|
||||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||||
with:
|
with:
|
||||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||||
|
|
|
||||||
1
changelog.d/+improve-accessibility-in-timeline.bugfix
Normal file
1
changelog.d/+improve-accessibility-in-timeline.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Improve how Talkback works with the timeline. Sadly, it's still not 100% working, but there is some issue with the `LazyColumn` using `reverseLayout` that only Google can fix.
|
||||||
3
changelog.d/2256.feature
Normal file
3
changelog.d/2256.feature
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
Add moderation to rooms:
|
||||||
|
|
||||||
|
- Sort member in room member list by powerlevel, display their roles.
|
||||||
|
|
@ -27,7 +27,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
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.room.RoomMember
|
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||||
|
|
@ -38,6 +37,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
|
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||||
|
|
@ -431,7 +431,7 @@ class InviteListPresenterTests {
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
isDirect = false,
|
isDirect = false,
|
||||||
lastMessage = null,
|
lastMessage = null,
|
||||||
inviter = RoomMember(
|
inviter = aRoomMember(
|
||||||
userId = A_USER_ID,
|
userId = A_USER_ID,
|
||||||
displayName = A_USER_NAME,
|
displayName = A_USER_NAME,
|
||||||
avatarUrl = AN_AVATAR_URL,
|
avatarUrl = AN_AVATAR_URL,
|
||||||
|
|
@ -458,7 +458,7 @@ class InviteListPresenterTests {
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
isDirect = true,
|
isDirect = true,
|
||||||
lastMessage = null,
|
lastMessage = null,
|
||||||
inviter = RoomMember(
|
inviter = aRoomMember(
|
||||||
userId = A_USER_ID,
|
userId = A_USER_ID,
|
||||||
displayName = A_USER_NAME,
|
displayName = A_USER_NAME,
|
||||||
avatarUrl = AN_AVATAR_URL,
|
avatarUrl = AN_AVATAR_URL,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.location.impl"
|
namespace = "io.element.android.features.location.impl"
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
anvil {
|
anvil {
|
||||||
|
|
@ -51,11 +56,14 @@ dependencies {
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(libs.molecule.runtime)
|
testImplementation(libs.molecule.runtime)
|
||||||
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.testtags)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.features.messages.test)
|
testImplementation(projects.features.messages.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,78 +24,47 @@ private const val APP_NAME = "ApplicationName"
|
||||||
class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
||||||
override val values: Sequence<ShowLocationState>
|
override val values: Sequence<ShowLocationState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
ShowLocationState(
|
aShowLocationState(),
|
||||||
ShowLocationState.Dialog.None,
|
aShowLocationState(
|
||||||
Location(1.23, 2.34, 4f),
|
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
|
||||||
description = null,
|
|
||||||
hasLocationPermission = false,
|
|
||||||
isTrackMyLocation = false,
|
|
||||||
appName = APP_NAME,
|
|
||||||
eventSink = {},
|
|
||||||
),
|
),
|
||||||
ShowLocationState(
|
aShowLocationState(
|
||||||
ShowLocationState.Dialog.PermissionDenied,
|
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
|
||||||
Location(1.23, 2.34, 4f),
|
|
||||||
description = null,
|
|
||||||
hasLocationPermission = false,
|
|
||||||
isTrackMyLocation = false,
|
|
||||||
appName = APP_NAME,
|
|
||||||
eventSink = {},
|
|
||||||
),
|
),
|
||||||
ShowLocationState(
|
aShowLocationState(
|
||||||
ShowLocationState.Dialog.PermissionRationale,
|
|
||||||
Location(1.23, 2.34, 4f),
|
|
||||||
description = null,
|
|
||||||
hasLocationPermission = false,
|
|
||||||
isTrackMyLocation = false,
|
|
||||||
appName = APP_NAME,
|
|
||||||
eventSink = {},
|
|
||||||
),
|
|
||||||
ShowLocationState(
|
|
||||||
ShowLocationState.Dialog.None,
|
|
||||||
Location(1.23, 2.34, 4f),
|
|
||||||
description = null,
|
|
||||||
hasLocationPermission = true,
|
hasLocationPermission = true,
|
||||||
isTrackMyLocation = false,
|
|
||||||
appName = APP_NAME,
|
|
||||||
eventSink = {},
|
|
||||||
),
|
),
|
||||||
ShowLocationState(
|
aShowLocationState(
|
||||||
ShowLocationState.Dialog.None,
|
|
||||||
Location(1.23, 2.34, 4f),
|
|
||||||
description = null,
|
|
||||||
hasLocationPermission = true,
|
hasLocationPermission = true,
|
||||||
isTrackMyLocation = true,
|
isTrackMyLocation = true,
|
||||||
appName = APP_NAME,
|
|
||||||
eventSink = {},
|
|
||||||
),
|
),
|
||||||
ShowLocationState(
|
aShowLocationState(
|
||||||
ShowLocationState.Dialog.None,
|
|
||||||
Location(1.23, 2.34, 4f),
|
|
||||||
description = "My favourite place!",
|
description = "My favourite place!",
|
||||||
hasLocationPermission = false,
|
|
||||||
isTrackMyLocation = false,
|
|
||||||
appName = APP_NAME,
|
|
||||||
eventSink = {},
|
|
||||||
),
|
),
|
||||||
ShowLocationState(
|
aShowLocationState(
|
||||||
ShowLocationState.Dialog.None,
|
|
||||||
Location(1.23, 2.34, 4f),
|
|
||||||
description = "For some reason I decided to to write a small essay that wraps at just two lines!",
|
description = "For some reason I decided to to write a small essay that wraps at just two lines!",
|
||||||
hasLocationPermission = false,
|
|
||||||
isTrackMyLocation = false,
|
|
||||||
appName = APP_NAME,
|
|
||||||
eventSink = {},
|
|
||||||
),
|
),
|
||||||
ShowLocationState(
|
aShowLocationState(
|
||||||
ShowLocationState.Dialog.None,
|
|
||||||
Location(1.23, 2.34, 4f),
|
|
||||||
description = "For some reason I decided to write a small essay in the location description. " +
|
description = "For some reason I decided to write a small essay in the location description. " +
|
||||||
"It is so long that it will wrap onto more than two lines!",
|
"It is so long that it will wrap onto more than two lines!",
|
||||||
hasLocationPermission = false,
|
|
||||||
isTrackMyLocation = false,
|
|
||||||
appName = APP_NAME,
|
|
||||||
eventSink = {},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun aShowLocationState(
|
||||||
|
permissionDialog: ShowLocationState.Dialog = ShowLocationState.Dialog.None,
|
||||||
|
location: Location = Location(1.23, 2.34, 4f),
|
||||||
|
description: String? = null,
|
||||||
|
hasLocationPermission: Boolean = false,
|
||||||
|
isTrackMyLocation: Boolean = false,
|
||||||
|
appName: String = APP_NAME,
|
||||||
|
eventSink: (ShowLocationEvents) -> Unit = {},
|
||||||
|
) = ShowLocationState(
|
||||||
|
permissionDialog = permissionDialog,
|
||||||
|
location = location,
|
||||||
|
description = description,
|
||||||
|
hasLocationPermission = hasLocationPermission,
|
||||||
|
isTrackMyLocation = isTrackMyLocation,
|
||||||
|
appName = appName,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -120,10 +120,14 @@ fun ShowLocationView(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
BackButton(onClick = onBackPressed)
|
BackButton(
|
||||||
|
onClick = onBackPressed,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = { state.eventSink(ShowLocationEvents.Share) }) {
|
IconButton(
|
||||||
|
onClick = { state.eventSink(ShowLocationEvents.Share) }
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.ShareAndroid(),
|
imageVector = CompoundIcons.ShareAndroid(),
|
||||||
contentDescription = stringResource(CommonStrings.action_share),
|
contentDescription = stringResource(CommonStrings.action_share),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.location.impl.show
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.libraries.testtags.TestTags
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.pressBack
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ShowLocationViewTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test back action`() {
|
||||||
|
val eventsRecorder = EventsRecorder<ShowLocationEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setShowLocationView(
|
||||||
|
state = aShowLocationState(
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onBackPressed = callback,
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test share action`() {
|
||||||
|
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
||||||
|
rule.setShowLocationView(
|
||||||
|
aShowLocationState(
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onBackPressed = EnsureNeverCalled(),
|
||||||
|
)
|
||||||
|
val shareContentDescription = rule.activity.getString(CommonStrings.action_share)
|
||||||
|
rule.onNodeWithContentDescription(shareContentDescription).performClick()
|
||||||
|
eventsRecorder.assertSingle(ShowLocationEvents.Share)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test fab click`() {
|
||||||
|
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
||||||
|
rule.setShowLocationView(
|
||||||
|
aShowLocationState(
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onBackPressed = EnsureNeverCalled(),
|
||||||
|
)
|
||||||
|
val shareContentDescription = rule.activity.getString(CommonStrings.action_share)
|
||||||
|
rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick()
|
||||||
|
eventsRecorder.assertSingle(ShowLocationEvents.TrackMyLocation(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when permission denied is displayed user can open the settings`() {
|
||||||
|
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
||||||
|
rule.setShowLocationView(
|
||||||
|
aShowLocationState(
|
||||||
|
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onBackPressed = EnsureNeverCalled(),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_continue)
|
||||||
|
eventsRecorder.assertSingle(ShowLocationEvents.OpenAppSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when permission denied is displayed user can close the dialog`() {
|
||||||
|
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
||||||
|
rule.setShowLocationView(
|
||||||
|
aShowLocationState(
|
||||||
|
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onBackPressed = EnsureNeverCalled(),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_cancel)
|
||||||
|
eventsRecorder.assertSingle(ShowLocationEvents.DismissDialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when permission rationale is displayed user can request permissions`() {
|
||||||
|
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
||||||
|
rule.setShowLocationView(
|
||||||
|
aShowLocationState(
|
||||||
|
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onBackPressed = EnsureNeverCalled(),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_continue)
|
||||||
|
eventsRecorder.assertSingle(ShowLocationEvents.RequestPermissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when permission rationale is displayed user can close the dialog`() {
|
||||||
|
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
||||||
|
rule.setShowLocationView(
|
||||||
|
aShowLocationState(
|
||||||
|
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onBackPressed = EnsureNeverCalled(),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_cancel)
|
||||||
|
eventsRecorder.assertSingle(ShowLocationEvents.DismissDialog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setShowLocationView(
|
||||||
|
state: ShowLocationState,
|
||||||
|
onBackPressed: () -> Unit = EnsureNeverCalled(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
// Simulate a LocalInspectionMode for MapboxMap
|
||||||
|
CompositionLocalProvider(LocalInspectionMode provides true) {
|
||||||
|
ShowLocationView(
|
||||||
|
state = state,
|
||||||
|
onBackPressed = onBackPressed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -152,6 +152,7 @@ internal fun MentionSuggestionsPickerView_Preview() {
|
||||||
powerLevel = 0L,
|
powerLevel = 0L,
|
||||||
normalizedPowerLevel = 0L,
|
normalizedPowerLevel = 0L,
|
||||||
isIgnored = false,
|
isIgnored = false,
|
||||||
|
role = RoomMember.Role.USER,
|
||||||
)
|
)
|
||||||
MentionSuggestionsPickerView(
|
MentionSuggestionsPickerView(
|
||||||
roomId = RoomId("!room:matrix.org"),
|
roomId = RoomId("!room:matrix.org"),
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,11 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline
|
package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
|
import android.view.accessibility.AccessibilityManager
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -45,8 +47,8 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
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.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -64,7 +66,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationView
|
import io.element.android.features.messages.impl.typing.TypingNotificationView
|
||||||
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
||||||
import io.element.android.libraries.designsystem.animation.alphaAnimation
|
|
||||||
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.FloatingActionButton
|
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||||
|
|
@ -99,7 +100,13 @@ fun TimelineView(
|
||||||
state.eventSink(TimelineEvents.OnScrollFinished(firstVisibleIndex))
|
state.eventSink(TimelineEvents.OnScrollFinished(firstVisibleIndex))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
|
// Disable reverse layout when TalkBack is enabled to avoid incorrect ordering issues seen in the current Compose UI version
|
||||||
|
val useReverseLayout = remember {
|
||||||
|
val accessibilityManager = context.getSystemService(AccessibilityManager::class.java)
|
||||||
|
accessibilityManager.isTouchExplorationEnabled.not()
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun inReplyToClicked(eventId: EventId) {
|
fun inReplyToClicked(eventId: EventId) {
|
||||||
|
|
@ -107,67 +114,67 @@ fun TimelineView(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms
|
// Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms
|
||||||
val alpha by alphaAnimation(label = "alpha for timeline")
|
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||||
|
Box(modifier) {
|
||||||
Box(modifier = modifier.alpha(alpha)) {
|
LazyColumn(
|
||||||
LazyColumn(
|
modifier = Modifier.fillMaxSize(),
|
||||||
modifier = Modifier.fillMaxSize(),
|
state = lazyListState,
|
||||||
state = lazyListState,
|
reverseLayout = useReverseLayout,
|
||||||
reverseLayout = true,
|
contentPadding = PaddingValues(vertical = 8.dp),
|
||||||
contentPadding = PaddingValues(vertical = 8.dp),
|
) {
|
||||||
) {
|
item {
|
||||||
item {
|
TypingNotificationView(state = typingNotificationState)
|
||||||
TypingNotificationView(state = typingNotificationState)
|
}
|
||||||
}
|
items(
|
||||||
items(
|
items = state.timelineItems,
|
||||||
items = state.timelineItems,
|
contentType = { timelineItem -> timelineItem.contentType() },
|
||||||
contentType = { timelineItem -> timelineItem.contentType() },
|
key = { timelineItem -> timelineItem.identifier() },
|
||||||
key = { timelineItem -> timelineItem.identifier() },
|
) { timelineItem ->
|
||||||
) { timelineItem ->
|
TimelineItemRow(
|
||||||
TimelineItemRow(
|
timelineItem = timelineItem,
|
||||||
timelineItem = timelineItem,
|
timelineRoomInfo = state.timelineRoomInfo,
|
||||||
timelineRoomInfo = state.timelineRoomInfo,
|
renderReadReceipts = state.renderReadReceipts,
|
||||||
renderReadReceipts = state.renderReadReceipts,
|
isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true &&
|
||||||
isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true &&
|
state.timelineItems.first().identifier() == timelineItem.identifier(),
|
||||||
state.timelineItems.first().identifier() == timelineItem.identifier(),
|
highlightedItem = state.highlightedEventId?.value,
|
||||||
highlightedItem = state.highlightedEventId?.value,
|
onClick = onMessageClicked,
|
||||||
onClick = onMessageClicked,
|
onLongClick = onMessageLongClicked,
|
||||||
onLongClick = onMessageLongClicked,
|
onUserDataClick = onUserDataClicked,
|
||||||
onUserDataClick = onUserDataClicked,
|
inReplyToClick = ::inReplyToClicked,
|
||||||
inReplyToClick = ::inReplyToClicked,
|
onReactionClick = onReactionClicked,
|
||||||
onReactionClick = onReactionClicked,
|
onReactionLongClick = onReactionLongClicked,
|
||||||
onReactionLongClick = onReactionLongClicked,
|
onMoreReactionsClick = onMoreReactionsClicked,
|
||||||
onMoreReactionsClick = onMoreReactionsClicked,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
onTimestampClicked = onTimestampClicked,
|
||||||
onTimestampClicked = onTimestampClicked,
|
sessionState = state.sessionState,
|
||||||
sessionState = state.sessionState,
|
eventSink = state.eventSink,
|
||||||
eventSink = state.eventSink,
|
onSwipeToReply = onSwipeToReply,
|
||||||
onSwipeToReply = onSwipeToReply,
|
)
|
||||||
)
|
}
|
||||||
}
|
if (state.paginationState.hasMoreToLoadBackwards) {
|
||||||
if (state.paginationState.hasMoreToLoadBackwards) {
|
// Do not use key parameter to avoid wrong positioning
|
||||||
// Do not use key parameter to avoid wrong positioning
|
item(contentType = "TimelineLoadingMoreIndicator") {
|
||||||
item(contentType = "TimelineLoadingMoreIndicator") {
|
TimelineLoadingMoreIndicator()
|
||||||
TimelineLoadingMoreIndicator()
|
LaunchedEffect(Unit) {
|
||||||
LaunchedEffect(Unit) {
|
onReachedLoadMore()
|
||||||
onReachedLoadMore()
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.paginationState.beginningOfRoomReached && !state.timelineRoomInfo.isDirect) {
|
||||||
|
item(contentType = "BeginningOfRoomReached") {
|
||||||
|
TimelineItemRoomBeginningView(roomName = roomName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state.paginationState.beginningOfRoomReached && !state.timelineRoomInfo.isDirect) {
|
|
||||||
item(contentType = "BeginningOfRoomReached") {
|
|
||||||
TimelineItemRoomBeginningView(roomName = roomName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TimelineScrollHelper(
|
TimelineScrollHelper(
|
||||||
isTimelineEmpty = state.timelineItems.isEmpty(),
|
isTimelineEmpty = state.timelineItems.isEmpty(),
|
||||||
lazyListState = lazyListState,
|
lazyListState = lazyListState,
|
||||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||||
newEventState = state.newEventState,
|
newEventState = state.newEventState,
|
||||||
onScrollFinishedAt = ::onScrollFinishedAt
|
onScrollFinishedAt = ::onScrollFinishedAt
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
|
@ -53,6 +54,11 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||||
import androidx.compose.ui.platform.ViewConfiguration
|
import androidx.compose.ui.platform.ViewConfiguration
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.invisibleToUser
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.semantics.testTag
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
|
@ -107,6 +113,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
import io.element.android.libraries.matrix.api.room.Mention
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
||||||
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
@ -256,6 +263,7 @@ private fun SwipeSensitivity(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun TimelineItemEventRowContent(
|
private fun TimelineItemEventRowContent(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
|
|
@ -305,6 +313,11 @@ private fun TimelineItemEventRowContent(
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.zIndex(1f)
|
.zIndex(1f)
|
||||||
.clickable(onClick = onUserDataClicked)
|
.clickable(onClick = onUserDataClicked)
|
||||||
|
// This is redundant when using talkback
|
||||||
|
.clearAndSetSemantics {
|
||||||
|
invisibleToUser()
|
||||||
|
testTag = TestTags.timelineItemSenderInfo.value
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -413,6 +426,7 @@ private fun MessageSenderInformation(
|
||||||
private fun MessageEventBubbleContent(
|
private fun MessageEventBubbleContent(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
onMessageLongClick: () -> Unit,
|
onMessageLongClick: () -> Unit,
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
inReplyToClick: () -> Unit,
|
inReplyToClick: () -> Unit,
|
||||||
onTimestampClicked: () -> Unit,
|
onTimestampClicked: () -> Unit,
|
||||||
onMentionClicked: (Mention) -> Unit,
|
onMentionClicked: (Mention) -> Unit,
|
||||||
|
|
@ -445,6 +459,7 @@ private fun MessageEventBubbleContent(
|
||||||
text = stringResource(CommonStrings.common_thread),
|
text = stringResource(CommonStrings.common_thread),
|
||||||
style = ElementTheme.typography.fontBodyXsRegular,
|
style = ElementTheme.typography.fontBodyXsRegular,
|
||||||
color = ElementTheme.colors.textPrimary,
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
modifier = Modifier.clearAndSetSemantics { }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -580,7 +595,8 @@ private fun MessageEventBubbleContent(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = topPadding, start = 8.dp, end = 8.dp)
|
.padding(top = topPadding, start = 8.dp, end = 8.dp)
|
||||||
.clip(RoundedCornerShape(6.dp))
|
.clip(RoundedCornerShape(6.dp))
|
||||||
.clickable(enabled = true, onClick = inReplyToClick),
|
// FIXME when a node is clickable, its contents won't be added to the semantics tree of its parent
|
||||||
|
// .clickable(enabled = true, onClick = inReplyToClick)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (inReplyToDetails != null) {
|
if (inReplyToDetails != null) {
|
||||||
|
|
@ -611,7 +627,9 @@ private fun MessageEventBubbleContent(
|
||||||
timestampPosition = timestampPosition,
|
timestampPosition = timestampPosition,
|
||||||
inReplyToDetails = event.inReplyTo,
|
inReplyToDetails = event.inReplyTo,
|
||||||
canShrinkContent = event.content is TimelineItemVoiceContent,
|
canShrinkContent = event.content is TimelineItemVoiceContent,
|
||||||
modifier = bubbleModifier
|
modifier = bubbleModifier.semantics(mergeDescendants = true) {
|
||||||
|
contentDescription = event.safeSenderName
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -641,8 +659,12 @@ private fun ReplyToContent(
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
|
val a11InReplyToText = stringResource(CommonStrings.common_in_reply_to, senderName)
|
||||||
Column(verticalArrangement = Arrangement.SpaceBetween) {
|
Column(verticalArrangement = Arrangement.SpaceBetween) {
|
||||||
Text(
|
Text(
|
||||||
|
modifier = Modifier.semantics {
|
||||||
|
contentDescription = a11InReplyToText
|
||||||
|
},
|
||||||
text = senderName,
|
text = senderName,
|
||||||
style = ElementTheme.typography.fontBodySmMedium,
|
style = ElementTheme.typography.fontBodySmMedium,
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ package io.element.android.features.messages.impl.timeline.components.event
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider
|
||||||
|
|
@ -25,15 +28,17 @@ import io.element.android.libraries.designsystem.components.BlurHashAsyncImage
|
||||||
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.matrix.ui.media.MediaRequestData
|
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemImageView(
|
fun TimelineItemImageView(
|
||||||
content: TimelineItemImageContent,
|
content: TimelineItemImageContent,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val description = stringResource(CommonStrings.common_image)
|
||||||
TimelineItemAspectRatioBox(
|
TimelineItemAspectRatioBox(
|
||||||
aspectRatio = content.aspectRatio,
|
aspectRatio = content.aspectRatio,
|
||||||
modifier = modifier,
|
modifier = modifier.semantics { contentDescription = description },
|
||||||
) {
|
) {
|
||||||
BlurHashAsyncImage(
|
BlurHashAsyncImage(
|
||||||
model = MediaRequestData(content.preferredMediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)),
|
model = MediaRequestData(content.preferredMediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)),
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
|
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
|
||||||
|
|
@ -48,7 +50,7 @@ fun TimelineItemTextView(
|
||||||
val formattedBody = content.formattedBody
|
val formattedBody = content.formattedBody
|
||||||
val body = SpannableString(formattedBody ?: content.body)
|
val body = SpannableString(formattedBody ?: content.body)
|
||||||
|
|
||||||
Box(modifier) {
|
Box(modifier.semantics { contentDescription = body.toString() }) {
|
||||||
EditorStyledText(
|
EditorStyledText(
|
||||||
text = body,
|
text = body,
|
||||||
onLinkClickedListener = onLinkClicked,
|
onLinkClickedListener = onLinkClicked,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider
|
||||||
|
|
@ -42,9 +44,10 @@ fun TimelineItemVideoView(
|
||||||
content: TimelineItemVideoContent,
|
content: TimelineItemVideoContent,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val description = stringResource(CommonStrings.common_image)
|
||||||
TimelineItemAspectRatioBox(
|
TimelineItemAspectRatioBox(
|
||||||
aspectRatio = content.aspectRatio,
|
aspectRatio = content.aspectRatio,
|
||||||
modifier = modifier,
|
modifier = modifier.semantics { contentDescription = description },
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
BlurHashAsyncImage(
|
BlurHashAsyncImage(
|
||||||
|
|
|
||||||
|
|
@ -103,5 +103,6 @@ private fun createDefaultRoomMemberForTyping(userId: UserId): RoomMember {
|
||||||
powerLevel = 0,
|
powerLevel = 0,
|
||||||
normalizedPowerLevel = 0,
|
normalizedPowerLevel = 0,
|
||||||
isIgnored = false,
|
isIgnored = false,
|
||||||
|
role = RoomMember.Role.USER,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,5 +98,6 @@ internal fun aTypingRoomMember(
|
||||||
powerLevel = 0,
|
powerLevel = 0,
|
||||||
normalizedPowerLevel = 0,
|
normalizedPowerLevel = 0,
|
||||||
isIgnored = false,
|
isIgnored = false,
|
||||||
|
role = RoomMember.Role.USER,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ import io.element.android.features.messages.impl.voicemessages.timeline.FakeReda
|
||||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||||
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
|
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
|
import io.element.android.features.poll.api.actions.EndPollAction
|
||||||
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
||||||
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
||||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||||
|
|
@ -98,6 +99,7 @@ import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runCurrent
|
import kotlinx.coroutines.test.runCurrent
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -372,6 +374,22 @@ class MessagesPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle action end poll`() = runTest {
|
||||||
|
val endPollAction = FakeEndPollAction()
|
||||||
|
val presenter = createMessagesPresenter(endPollAction = endPollAction)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitFirstItem()
|
||||||
|
endPollAction.verifyExecutionCount(0)
|
||||||
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent())))
|
||||||
|
delay(1)
|
||||||
|
endPollAction.verifyExecutionCount(1)
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle action redact`() = runTest {
|
fun `present - handle action redact`() = runTest {
|
||||||
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||||
|
|
@ -683,6 +701,7 @@ class MessagesPresenterTest {
|
||||||
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
||||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||||
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
||||||
|
endPollAction: EndPollAction = FakeEndPollAction(),
|
||||||
): MessagesPresenter {
|
): MessagesPresenter {
|
||||||
val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom)
|
val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom)
|
||||||
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
|
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
|
||||||
|
|
@ -721,7 +740,7 @@ class MessagesPresenterTest {
|
||||||
encryptionService = FakeEncryptionService(),
|
encryptionService = FakeEncryptionService(),
|
||||||
verificationService = FakeSessionVerificationService(),
|
verificationService = FakeSessionVerificationService(),
|
||||||
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
||||||
endPollAction = FakeEndPollAction(),
|
endPollAction = endPollAction,
|
||||||
sendPollResponseAction = FakeSendPollResponseAction(),
|
sendPollResponseAction = FakeSendPollResponseAction(),
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -326,8 +326,7 @@ class MessagesViewTest {
|
||||||
state = state,
|
state = state,
|
||||||
onUserDataClicked = callback,
|
onUserDataClicked = callback,
|
||||||
)
|
)
|
||||||
val senderName = (timelineItem as? TimelineItem.Event)?.senderDisplayName.orEmpty()
|
rule.onNodeWithTag(TestTags.timelineItemSenderInfo.value).performClick()
|
||||||
rule.onNodeWithText(senderName).performClick()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,10 +87,13 @@ import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
import uniffi.wysiwyg_composer.MentionsState
|
import uniffi.wysiwyg_composer.MentionsState
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@Suppress("LargeClass")
|
@Suppress("LargeClass")
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class MessageComposerPresenterTest {
|
class MessageComposerPresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
@ -875,6 +878,19 @@ class MessageComposerPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - send uri`() = runTest {
|
||||||
|
val presenter = createPresenter(this)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
val state = presenter.present()
|
||||||
|
remember(state, state.richTextEditorState.messageHtml) { state }
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitFirstItem()
|
||||||
|
initialState.eventSink.invoke(MessageComposerEvents.SendUri(Uri.parse("content://uri")))
|
||||||
|
waitForPredicate { mediaPreProcessor.processCallCount == 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle typing notice event`() = runTest {
|
fun `present - handle typing notice event`() = runTest {
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,6 @@ import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesS
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||||
|
|
@ -178,7 +176,6 @@ class TypingNotificationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - reserveSpace becomes true once we get the first typing notification with room members`() = runTest {
|
fun `present - reserveSpace becomes true once we get the first typing notification with room members`() = runTest {
|
||||||
val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2)
|
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = createPresenter(matrixRoom = room)
|
val presenter = createPresenter(matrixRoom = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -216,27 +213,17 @@ class TypingNotificationPresenterTest {
|
||||||
|
|
||||||
private fun createDefaultRoomMember(
|
private fun createDefaultRoomMember(
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
) = RoomMember(
|
) = aTypingRoomMember(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
displayName = null,
|
displayName = null,
|
||||||
avatarUrl = null,
|
|
||||||
membership = RoomMembershipState.JOIN,
|
|
||||||
isNameAmbiguous = false,
|
isNameAmbiguous = false,
|
||||||
powerLevel = 0,
|
|
||||||
normalizedPowerLevel = 0,
|
|
||||||
isIgnored = false,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createKnownRoomMember(
|
private fun createKnownRoomMember(
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
) = RoomMember(
|
) = aTypingRoomMember(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
displayName = "Alice Doe",
|
displayName = "Alice Doe",
|
||||||
avatarUrl = "an_avatar_url",
|
|
||||||
membership = RoomMembershipState.JOIN,
|
|
||||||
isNameAmbiguous = true,
|
isNameAmbiguous = true,
|
||||||
powerLevel = 0,
|
|
||||||
normalizedPowerLevel = 0,
|
|
||||||
isIgnored = false,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.roomdetails.impl"
|
namespace = "io.element.android.features.roomdetails.impl"
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
anvil {
|
anvil {
|
||||||
|
|
@ -61,6 +66,7 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.test.mockk)
|
testImplementation(libs.test.mockk)
|
||||||
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.mediaupload.test)
|
testImplementation(projects.libraries.mediaupload.test)
|
||||||
testImplementation(projects.libraries.mediapickers.test)
|
testImplementation(projects.libraries.mediapickers.test)
|
||||||
|
|
@ -70,6 +76,8 @@ dependencies {
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(projects.features.leaveroom.test)
|
testImplementation(projects.features.leaveroom.test)
|
||||||
testImplementation(projects.features.createroom.test)
|
testImplementation(projects.features.createroom.test)
|
||||||
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ fun aDmRoomMember(
|
||||||
powerLevel: Long = 0,
|
powerLevel: Long = 0,
|
||||||
normalizedPowerLevel: Long = powerLevel,
|
normalizedPowerLevel: Long = powerLevel,
|
||||||
isIgnored: Boolean = false,
|
isIgnored: Boolean = false,
|
||||||
|
role: RoomMember.Role = RoomMember.Role.USER,
|
||||||
) = RoomMember(
|
) = RoomMember(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
|
|
@ -64,6 +65,7 @@ fun aDmRoomMember(
|
||||||
powerLevel = powerLevel,
|
powerLevel = powerLevel,
|
||||||
normalizedPowerLevel = normalizedPowerLevel,
|
normalizedPowerLevel = normalizedPowerLevel,
|
||||||
isIgnored = isIgnored,
|
isIgnored = isIgnored,
|
||||||
|
role = role,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRoomDetailsState() = RoomDetailsState(
|
fun aRoomDetailsState() = RoomDetailsState(
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,10 @@ fun BlockUserDialogs(state: RoomMemberDetailsState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BlockConfirmationDialog(onBlockAction: () -> Unit, onDismiss: () -> Unit) {
|
private fun BlockConfirmationDialog(
|
||||||
|
onBlockAction: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
ConfirmationDialog(
|
ConfirmationDialog(
|
||||||
title = stringResource(R.string.screen_dm_details_block_user),
|
title = stringResource(R.string.screen_dm_details_block_user),
|
||||||
content = stringResource(R.string.screen_dm_details_block_alert_description),
|
content = stringResource(R.string.screen_dm_details_block_alert_description),
|
||||||
|
|
@ -66,7 +69,10 @@ private fun BlockConfirmationDialog(onBlockAction: () -> Unit, onDismiss: () ->
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun UnblockConfirmationDialog(onUnblockAction: () -> Unit, onDismiss: () -> Unit) {
|
private fun UnblockConfirmationDialog(
|
||||||
|
onUnblockAction: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
ConfirmationDialog(
|
ConfirmationDialog(
|
||||||
title = stringResource(R.string.screen_dm_details_unblock_user),
|
title = stringResource(R.string.screen_dm_details_unblock_user),
|
||||||
content = stringResource(R.string.screen_dm_details_unblock_alert_description),
|
content = stringResource(R.string.screen_dm_details_unblock_alert_description),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
import io.element.android.libraries.matrix.ui.room.sortingName
|
||||||
|
import java.text.Collator
|
||||||
|
|
||||||
|
// Comparator used to sort room members by power level (descending) and then by name (ascending)
|
||||||
|
internal class PowerLevelRoomMemberComparator : Comparator<RoomMember> {
|
||||||
|
// Used to simplify and compare unicode and ASCII chars (á == a)
|
||||||
|
private val collator = Collator.getInstance().apply {
|
||||||
|
decomposition = Collator.CANONICAL_DECOMPOSITION
|
||||||
|
}
|
||||||
|
override fun compare(o1: RoomMember, o2: RoomMember): Int {
|
||||||
|
return when {
|
||||||
|
o1.powerLevel > o2.powerLevel -> return -1
|
||||||
|
o1.powerLevel < o2.powerLevel -> return 1
|
||||||
|
else -> {
|
||||||
|
collator.compare(o1.sortingName(), o2.sortingName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -66,7 +66,9 @@ class RoomMemberListPresenter @Inject constructor(
|
||||||
roomMembers = AsyncData.Success(
|
roomMembers = AsyncData.Success(
|
||||||
RoomMembers(
|
RoomMembers(
|
||||||
invited = members.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),
|
invited = members.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),
|
||||||
joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList()).toImmutableList(),
|
joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList())
|
||||||
|
.sortedWith(PowerLevelRoomMemberComparator())
|
||||||
|
.toImmutableList(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -84,7 +86,9 @@ class RoomMemberListPresenter @Inject constructor(
|
||||||
SearchBarResultState.Results(
|
SearchBarResultState.Results(
|
||||||
RoomMembers(
|
RoomMembers(
|
||||||
invited = results.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),
|
invited = results.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),
|
||||||
joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList()).toImmutableList(),
|
joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList())
|
||||||
|
.sortedWith(PowerLevelRoomMemberComparator())
|
||||||
|
.toImmutableList(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
|
||||||
roomMembers = AsyncData.Success(
|
roomMembers = AsyncData.Success(
|
||||||
RoomMembers(
|
RoomMembers(
|
||||||
invited = persistentListOf(aVictor(), aWalter()),
|
invited = persistentListOf(aVictor(), aWalter()),
|
||||||
joined = persistentListOf(anAlice(), aBob()),
|
joined = persistentListOf(anAlice(), aBob(), aWalter()),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -79,6 +79,7 @@ fun aRoomMember(
|
||||||
powerLevel: Long = 0L,
|
powerLevel: Long = 0L,
|
||||||
normalizedPowerLevel: Long = 0L,
|
normalizedPowerLevel: Long = 0L,
|
||||||
isIgnored: Boolean = false,
|
isIgnored: Boolean = false,
|
||||||
|
role: RoomMember.Role = RoomMember.Role.USER,
|
||||||
) = RoomMember(
|
) = RoomMember(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
|
|
@ -88,6 +89,7 @@ fun aRoomMember(
|
||||||
powerLevel = powerLevel,
|
powerLevel = powerLevel,
|
||||||
normalizedPowerLevel = normalizedPowerLevel,
|
normalizedPowerLevel = normalizedPowerLevel,
|
||||||
isIgnored = isIgnored,
|
isIgnored = isIgnored,
|
||||||
|
role = role,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRoomMemberList() = persistentListOf(
|
fun aRoomMemberList() = persistentListOf(
|
||||||
|
|
@ -103,8 +105,8 @@ fun aRoomMemberList() = persistentListOf(
|
||||||
aWalter(),
|
aWalter(),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice")
|
fun anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice", role = RoomMember.Role.ADMIN)
|
||||||
fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob")
|
fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob", role = RoomMember.Role.MODERATOR)
|
||||||
|
|
||||||
fun aVictor() = aRoomMember(UserId("@victor:server.org"), "Victor", membership = RoomMembershipState.INVITE)
|
fun aVictor() = aRoomMember(UserId("@victor:server.org"), "Victor", membership = RoomMembershipState.INVITE)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -177,14 +177,28 @@ private fun RoomMemberListItem(
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val roleText = when (roomMember.role) {
|
||||||
|
RoomMember.Role.ADMIN -> stringResource(R.string.screen_room_member_list_role_administrator)
|
||||||
|
RoomMember.Role.MODERATOR -> stringResource(R.string.screen_room_member_list_role_moderator)
|
||||||
|
RoomMember.Role.USER -> null
|
||||||
|
}
|
||||||
MatrixUserRow(
|
MatrixUserRow(
|
||||||
modifier = modifier.clickable(onClick = onClick),
|
modifier = modifier.clickable(onClick = onClick),
|
||||||
matrixUser = MatrixUser(
|
matrixUser = MatrixUser(
|
||||||
userId = roomMember.userId,
|
userId = roomMember.userId,
|
||||||
displayName = roomMember.displayName,
|
displayName = roomMember.displayName,
|
||||||
avatarUrl = roomMember.avatarUrl
|
avatarUrl = roomMember.avatarUrl,
|
||||||
),
|
),
|
||||||
avatarSize = AvatarSize.UserListItem,
|
avatarSize = AvatarSize.UserListItem,
|
||||||
|
trailingContent = roleText?.let {
|
||||||
|
@Composable {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,28 +19,38 @@ package io.element.android.features.roomdetails.impl.members.details
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
open class RoomMemberDetailsStateProvider : PreviewParameterProvider<RoomMemberDetailsState> {
|
open class RoomMemberDetailsStateProvider : PreviewParameterProvider<RoomMemberDetailsState> {
|
||||||
override val values: Sequence<RoomMemberDetailsState>
|
override val values: Sequence<RoomMemberDetailsState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aRoomMemberDetailsState(),
|
aRoomMemberDetailsState(),
|
||||||
aRoomMemberDetailsState().copy(userName = null),
|
aRoomMemberDetailsState(userName = null),
|
||||||
aRoomMemberDetailsState().copy(isBlocked = AsyncData.Success(true)),
|
aRoomMemberDetailsState(isBlocked = AsyncData.Success(true)),
|
||||||
aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block),
|
aRoomMemberDetailsState(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block),
|
||||||
aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock),
|
aRoomMemberDetailsState(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock),
|
||||||
aRoomMemberDetailsState().copy(isBlocked = AsyncData.Loading(true)),
|
aRoomMemberDetailsState(isBlocked = AsyncData.Loading(true)),
|
||||||
aRoomMemberDetailsState().copy(startDmActionState = AsyncAction.Loading),
|
aRoomMemberDetailsState(startDmActionState = AsyncAction.Loading),
|
||||||
// Add other states here
|
// Add other states here
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aRoomMemberDetailsState() = RoomMemberDetailsState(
|
fun aRoomMemberDetailsState(
|
||||||
userId = "@daniel:domain.com",
|
userId: String = "@daniel:domain.com",
|
||||||
userName = "Daniel",
|
userName: String? = "Daniel",
|
||||||
avatarUrl = null,
|
avatarUrl: String? = null,
|
||||||
isBlocked = AsyncData.Success(false),
|
isBlocked: AsyncData<Boolean> = AsyncData.Success(false),
|
||||||
startDmActionState = AsyncAction.Uninitialized,
|
startDmActionState: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||||
displayConfirmationDialog = null,
|
displayConfirmationDialog: RoomMemberDetailsState.ConfirmationDialog? = null,
|
||||||
isCurrentUser = false,
|
isCurrentUser: Boolean = false,
|
||||||
eventSink = {},
|
eventSink: (RoomMemberDetailsEvents) -> Unit = {},
|
||||||
|
) = RoomMemberDetailsState(
|
||||||
|
userId = userId,
|
||||||
|
userName = userName,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
|
isBlocked = isBlocked,
|
||||||
|
startDmActionState = startDmActionState,
|
||||||
|
displayConfirmationDialog = displayConfirmationDialog,
|
||||||
|
isCurrentUser = isCurrentUser,
|
||||||
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomdetails.impl.blockuser
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.features.roomdetails.impl.R
|
||||||
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents
|
||||||
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
||||||
|
import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class BlockUserDialogsTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `confirm block user emit expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>()
|
||||||
|
rule.setContent {
|
||||||
|
BlockUserDialogs(
|
||||||
|
state = aRoomMemberDetailsState(
|
||||||
|
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rule.clickOn(R.string.screen_dm_details_block_alert_action)
|
||||||
|
eventsRecorder.assertSingle(RoomMemberDetailsEvents.BlockUser(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cancel block user emit expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>()
|
||||||
|
rule.setContent {
|
||||||
|
BlockUserDialogs(
|
||||||
|
state = aRoomMemberDetailsState(
|
||||||
|
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rule.clickOn(CommonStrings.action_cancel)
|
||||||
|
eventsRecorder.assertSingle(RoomMemberDetailsEvents.ClearConfirmationDialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `confirm unblock user emit expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>()
|
||||||
|
rule.setContent {
|
||||||
|
BlockUserDialogs(
|
||||||
|
state = aRoomMemberDetailsState(
|
||||||
|
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rule.clickOn(R.string.screen_dm_details_unblock_alert_action)
|
||||||
|
eventsRecorder.assertSingle(RoomMemberDetailsEvents.UnblockUser(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cancel unblock user emit expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>()
|
||||||
|
rule.setContent {
|
||||||
|
BlockUserDialogs(
|
||||||
|
state = aRoomMemberDetailsState(
|
||||||
|
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rule.clickOn(CommonStrings.action_cancel)
|
||||||
|
eventsRecorder.assertSingle(RoomMemberDetailsEvents.ClearConfirmationDialog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomdetails.members
|
||||||
|
|
||||||
|
import io.element.android.features.roomdetails.impl.members.PowerLevelRoomMemberComparator
|
||||||
|
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_4
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_5
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class PowerLevelRoomMemberComparatorTest {
|
||||||
|
@Test
|
||||||
|
fun `order is Admin, then Moderator, then User`() {
|
||||||
|
val memberList = listOf(
|
||||||
|
aRoomMember(userId = UserId("@admin:example.com"), powerLevel = 100),
|
||||||
|
aRoomMember(userId = UserId("@moderator:example.com"), powerLevel = 50),
|
||||||
|
aRoomMember(userId = UserId("@user:example.com"), powerLevel = 0),
|
||||||
|
).shuffled()
|
||||||
|
|
||||||
|
val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator())
|
||||||
|
assert(ordered[0].userId == UserId("@admin:example.com"))
|
||||||
|
assert(ordered[1].userId == UserId("@moderator:example.com"))
|
||||||
|
assert(ordered[2].userId == UserId("@user:example.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `with the same power level, alphabetical ascending order for name is used`() {
|
||||||
|
val memberList = listOf(
|
||||||
|
aRoomMember(userId = A_USER_ID, displayName = "First - admin", powerLevel = 100),
|
||||||
|
aRoomMember(userId = A_USER_ID_2, displayName = "Second - admin", powerLevel = 100),
|
||||||
|
aRoomMember(userId = A_USER_ID_3, displayName = "Third - admin", powerLevel = 100),
|
||||||
|
aRoomMember(userId = A_USER_ID_4, displayName = "First - user", powerLevel = 0),
|
||||||
|
aRoomMember(userId = A_USER_ID_5, displayName = "Second - user", powerLevel = 0),
|
||||||
|
).shuffled()
|
||||||
|
|
||||||
|
val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator())
|
||||||
|
assert(ordered[0].userId == A_USER_ID)
|
||||||
|
assert(ordered[1].userId == A_USER_ID_2)
|
||||||
|
assert(ordered[2].userId == A_USER_ID_3)
|
||||||
|
assert(ordered[3].userId == A_USER_ID_4)
|
||||||
|
assert(ordered[4].userId == A_USER_ID_5)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when no names are provided, alphabetical order uses user id`() {
|
||||||
|
val memberList = listOf(
|
||||||
|
aRoomMember(userId = A_USER_ID, displayName = "Z - LAST!", powerLevel = 100),
|
||||||
|
aRoomMember(userId = A_USER_ID_2, powerLevel = 100),
|
||||||
|
aRoomMember(userId = A_USER_ID_3, powerLevel = 100),
|
||||||
|
).shuffled()
|
||||||
|
|
||||||
|
val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator())
|
||||||
|
assert(ordered[0].userId == A_USER_ID_2)
|
||||||
|
assert(ordered[1].userId == A_USER_ID_3)
|
||||||
|
assert(ordered[2].userId == A_USER_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unicode characters are simplified and compared, order ignores case`() {
|
||||||
|
val memberList = listOf(
|
||||||
|
aRoomMember(userId = A_USER_ID, displayName = "First", powerLevel = 100),
|
||||||
|
aRoomMember(userId = A_USER_ID_2, displayName = "Șecond", powerLevel = 100),
|
||||||
|
aRoomMember(userId = A_USER_ID_3, displayName = "third", powerLevel = 100),
|
||||||
|
).shuffled()
|
||||||
|
|
||||||
|
val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator())
|
||||||
|
assert(ordered[0].userId == A_USER_ID)
|
||||||
|
assert(ordered[1].userId == A_USER_ID_2)
|
||||||
|
assert(ordered[2].userId == A_USER_ID_3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,6 @@ import io.element.android.features.roomlist.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 RoomListEvents {
|
||||||
data class UpdateFilter(val newFilter: String) : RoomListEvents
|
|
||||||
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
|
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
|
||||||
data object DismissRequestVerificationPrompt : RoomListEvents
|
data object DismissRequestVerificationPrompt : RoomListEvents
|
||||||
data object DismissRecoveryKeyPrompt : RoomListEvents
|
data object DismissRecoveryKeyPrompt : RoomListEvents
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ import io.element.android.features.roomlist.impl.datasource.InviteStateDataSourc
|
||||||
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
|
||||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
|
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
|
||||||
|
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
||||||
|
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
|
|
@ -74,13 +76,15 @@ class RoomListPresenter @Inject constructor(
|
||||||
private val inviteStateDataSource: InviteStateDataSource,
|
private val inviteStateDataSource: InviteStateDataSource,
|
||||||
private val leaveRoomPresenter: LeaveRoomPresenter,
|
private val leaveRoomPresenter: LeaveRoomPresenter,
|
||||||
private val roomListDataSource: RoomListDataSource,
|
private val roomListDataSource: RoomListDataSource,
|
||||||
private val encryptionService: EncryptionService,
|
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val indicatorService: IndicatorService,
|
private val indicatorService: IndicatorService,
|
||||||
private val filtersPresenter: RoomListFiltersPresenter,
|
private val filtersPresenter: RoomListFiltersPresenter,
|
||||||
|
private val searchPresenter: Presenter<RoomListSearchState>,
|
||||||
private val migrationScreenPresenter: MigrationScreenPresenter,
|
private val migrationScreenPresenter: MigrationScreenPresenter,
|
||||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
) : Presenter<RoomListState> {
|
) : Presenter<RoomListState> {
|
||||||
|
private val encryptionService: EncryptionService = client.encryptionService()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomListState {
|
override fun present(): RoomListState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
@ -91,10 +95,10 @@ class RoomListPresenter @Inject constructor(
|
||||||
val roomList by produceState(initialValue = AsyncData.Loading()) {
|
val roomList by produceState(initialValue = AsyncData.Loading()) {
|
||||||
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
|
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
|
||||||
}
|
}
|
||||||
val filteredRoomList by roomListDataSource.filteredRooms.collectAsState()
|
|
||||||
val filter by roomListDataSource.filter.collectAsState()
|
|
||||||
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
|
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
|
||||||
|
|
||||||
val filtersState = filtersPresenter.present()
|
val filtersState = filtersPresenter.present()
|
||||||
|
val searchState = searchPresenter.present()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
roomListDataSource.launchIn(this)
|
roomListDataSource.launchIn(this)
|
||||||
|
|
@ -125,21 +129,14 @@ class RoomListPresenter @Inject constructor(
|
||||||
// Avatar indicator
|
// Avatar indicator
|
||||||
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
|
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
|
||||||
|
|
||||||
var displaySearchResults by rememberSaveable { mutableStateOf(false) }
|
|
||||||
val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
|
val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
|
||||||
|
|
||||||
fun handleEvents(event: RoomListEvents) {
|
fun handleEvents(event: RoomListEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is RoomListEvents.UpdateFilter -> roomListDataSource.updateFilter(event.newFilter)
|
|
||||||
is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range)
|
is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range)
|
||||||
RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true
|
RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true
|
||||||
RoomListEvents.DismissRecoveryKeyPrompt -> recoveryKeyPromptDismissed = true
|
RoomListEvents.DismissRecoveryKeyPrompt -> recoveryKeyPromptDismissed = true
|
||||||
RoomListEvents.ToggleSearchResults -> {
|
RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
||||||
if (displaySearchResults) {
|
|
||||||
roomListDataSource.updateFilter("")
|
|
||||||
}
|
|
||||||
displaySearchResults = !displaySearchResults
|
|
||||||
}
|
|
||||||
is RoomListEvents.ShowContextMenu -> {
|
is RoomListEvents.ShowContextMenu -> {
|
||||||
coroutineScope.showContextMenu(event, contextMenu)
|
coroutineScope.showContextMenu(event, contextMenu)
|
||||||
}
|
}
|
||||||
|
|
@ -147,7 +144,6 @@ class RoomListPresenter @Inject constructor(
|
||||||
contextMenu.value = RoomListState.ContextMenu.Hidden
|
contextMenu.value = RoomListState.ContextMenu.Hidden
|
||||||
}
|
}
|
||||||
is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId))
|
is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId))
|
||||||
|
|
||||||
is RoomListEvents.SetRoomIsFavorite -> coroutineScope.launch {
|
is RoomListEvents.SetRoomIsFavorite -> coroutineScope.launch {
|
||||||
client.getRoom(event.roomId)?.use { room ->
|
client.getRoom(event.roomId)?.use { room ->
|
||||||
room.setIsFavorite(event.isFavorite)
|
room.setIsFavorite(event.isFavorite)
|
||||||
|
|
@ -178,17 +174,15 @@ class RoomListPresenter @Inject constructor(
|
||||||
matrixUser = matrixUser.value,
|
matrixUser = matrixUser.value,
|
||||||
showAvatarIndicator = showAvatarIndicator,
|
showAvatarIndicator = showAvatarIndicator,
|
||||||
roomList = roomList,
|
roomList = roomList,
|
||||||
filter = filter,
|
|
||||||
filteredRoomList = filteredRoomList,
|
|
||||||
displayVerificationPrompt = displayVerificationPrompt,
|
displayVerificationPrompt = displayVerificationPrompt,
|
||||||
displayRecoveryKeyPrompt = displayRecoveryKeyPrompt,
|
displayRecoveryKeyPrompt = displayRecoveryKeyPrompt,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||||
invitesState = inviteStateDataSource.inviteState(),
|
invitesState = inviteStateDataSource.inviteState(),
|
||||||
displaySearchResults = displaySearchResults,
|
|
||||||
contextMenu = contextMenu.value,
|
contextMenu = contextMenu.value,
|
||||||
leaveRoomState = leaveRoomState,
|
leaveRoomState = leaveRoomState,
|
||||||
filtersState = filtersState,
|
filtersState = filtersState,
|
||||||
|
searchState = searchState,
|
||||||
displayMigrationStatus = isMigrating,
|
displayMigrationStatus = isMigrating,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
|
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -31,17 +32,15 @@ data class RoomListState(
|
||||||
val matrixUser: MatrixUser?,
|
val matrixUser: MatrixUser?,
|
||||||
val showAvatarIndicator: Boolean,
|
val showAvatarIndicator: Boolean,
|
||||||
val roomList: AsyncData<ImmutableList<RoomListRoomSummary>>,
|
val roomList: AsyncData<ImmutableList<RoomListRoomSummary>>,
|
||||||
val filter: String?,
|
|
||||||
val filteredRoomList: ImmutableList<RoomListRoomSummary>,
|
|
||||||
val displayVerificationPrompt: Boolean,
|
val displayVerificationPrompt: Boolean,
|
||||||
val displayRecoveryKeyPrompt: Boolean,
|
val displayRecoveryKeyPrompt: Boolean,
|
||||||
val hasNetworkConnection: Boolean,
|
val hasNetworkConnection: Boolean,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val invitesState: InvitesState,
|
val invitesState: InvitesState,
|
||||||
val displaySearchResults: Boolean,
|
|
||||||
val contextMenu: ContextMenu,
|
val contextMenu: ContextMenu,
|
||||||
val leaveRoomState: LeaveRoomState,
|
val leaveRoomState: LeaveRoomState,
|
||||||
val filtersState: RoomListFiltersState,
|
val filtersState: RoomListFiltersState,
|
||||||
|
val searchState: RoomListSearchState,
|
||||||
val displayMigrationStatus: Boolean,
|
val displayMigrationStatus: Boolean,
|
||||||
val eventSink: (RoomListEvents) -> Unit,
|
val eventSink: (RoomListEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryF
|
||||||
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary
|
||||||
|
import io.element.android.features.roomlist.impl.search.aRoomListSearchState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -42,14 +43,13 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||||
aRoomListState().copy(hasNetworkConnection = false),
|
aRoomListState().copy(hasNetworkConnection = false),
|
||||||
aRoomListState().copy(invitesState = InvitesState.SeenInvites),
|
aRoomListState().copy(invitesState = InvitesState.SeenInvites),
|
||||||
aRoomListState().copy(invitesState = InvitesState.NewInvites),
|
aRoomListState().copy(invitesState = InvitesState.NewInvites),
|
||||||
aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()),
|
|
||||||
aRoomListState().copy(displaySearchResults = true),
|
|
||||||
aRoomListState().copy(contextMenu = aContextMenuShown(roomName = "A nice room name")),
|
aRoomListState().copy(contextMenu = aContextMenuShown(roomName = "A nice room name")),
|
||||||
aRoomListState().copy(contextMenu = aContextMenuShown(isFavorite = true)),
|
aRoomListState().copy(contextMenu = aContextMenuShown(isFavorite = true)),
|
||||||
aRoomListState().copy(displayRecoveryKeyPrompt = true),
|
aRoomListState().copy(displayRecoveryKeyPrompt = true),
|
||||||
aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())),
|
aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())),
|
||||||
aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())),
|
aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())),
|
||||||
aRoomListState().copy(matrixUser = null, displayMigrationStatus = true),
|
aRoomListState().copy(matrixUser = null, displayMigrationStatus = true),
|
||||||
|
aRoomListState().copy(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,17 +57,15 @@ internal fun aRoomListState() = RoomListState(
|
||||||
matrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
|
matrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
|
||||||
showAvatarIndicator = false,
|
showAvatarIndicator = false,
|
||||||
roomList = AsyncData.Success(aRoomListRoomSummaryList()),
|
roomList = AsyncData.Success(aRoomListRoomSummaryList()),
|
||||||
filter = "filter",
|
|
||||||
filteredRoomList = aRoomListRoomSummaryList(),
|
|
||||||
hasNetworkConnection = true,
|
hasNetworkConnection = true,
|
||||||
snackbarMessage = null,
|
snackbarMessage = null,
|
||||||
displayVerificationPrompt = false,
|
displayVerificationPrompt = false,
|
||||||
displayRecoveryKeyPrompt = false,
|
displayRecoveryKeyPrompt = false,
|
||||||
invitesState = InvitesState.NoInvites,
|
invitesState = InvitesState.NoInvites,
|
||||||
displaySearchResults = false,
|
|
||||||
contextMenu = RoomListState.ContextMenu.Hidden,
|
contextMenu = RoomListState.ContextMenu.Hidden,
|
||||||
leaveRoomState = aLeaveRoomState(),
|
leaveRoomState = aLeaveRoomState(),
|
||||||
filtersState = aRoomListFiltersState(),
|
filtersState = aRoomListFiltersState(),
|
||||||
|
searchState = aRoomListSearchState(),
|
||||||
displayMigrationStatus = false,
|
displayMigrationStatus = false,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersView
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersView
|
||||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenView
|
import io.element.android.features.roomlist.impl.migration.MigrationScreenView
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchResultView
|
import io.element.android.features.roomlist.impl.search.RoomListSearchView
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
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
|
||||||
|
|
@ -119,8 +119,8 @@ fun RoomListView(
|
||||||
onMenuActionClicked = onMenuActionClicked,
|
onMenuActionClicked = onMenuActionClicked,
|
||||||
)
|
)
|
||||||
// This overlaid view will only be visible when state.displaySearchResults is true
|
// This overlaid view will only be visible when state.displaySearchResults is true
|
||||||
RoomListSearchResultView(
|
RoomListSearchView(
|
||||||
state = state,
|
state = state.searchState,
|
||||||
onRoomClicked = onRoomClicked,
|
onRoomClicked = onRoomClicked,
|
||||||
onRoomLongClicked = { onRoomLongClicked(it) },
|
onRoomLongClicked = { onRoomLongClicked(it) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -209,8 +209,7 @@ private fun RoomListContent(
|
||||||
RoomListTopBar(
|
RoomListTopBar(
|
||||||
matrixUser = state.matrixUser,
|
matrixUser = state.matrixUser,
|
||||||
showAvatarIndicator = state.showAvatarIndicator,
|
showAvatarIndicator = state.showAvatarIndicator,
|
||||||
areSearchResultsDisplayed = state.displaySearchResults,
|
areSearchResultsDisplayed = state.searchState.isSearchActive,
|
||||||
onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
|
|
||||||
onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) },
|
onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) },
|
||||||
onMenuActionClicked = onMenuActionClicked,
|
onMenuActionClicked = onMenuActionClicked,
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.roomlist.impl.components
|
package io.element.android.features.roomlist.impl.components
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
|
@ -87,7 +86,6 @@ fun RoomListTopBar(
|
||||||
matrixUser: MatrixUser?,
|
matrixUser: MatrixUser?,
|
||||||
showAvatarIndicator: Boolean,
|
showAvatarIndicator: Boolean,
|
||||||
areSearchResultsDisplayed: Boolean,
|
areSearchResultsDisplayed: Boolean,
|
||||||
onFilterChanged: (String) -> Unit,
|
|
||||||
onToggleSearch: () -> Unit,
|
onToggleSearch: () -> Unit,
|
||||||
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
|
|
@ -95,15 +93,6 @@ fun RoomListTopBar(
|
||||||
displayMenuItems: Boolean,
|
displayMenuItems: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun closeFilter() {
|
|
||||||
onFilterChanged("")
|
|
||||||
}
|
|
||||||
|
|
||||||
BackHandler(enabled = areSearchResultsDisplayed) {
|
|
||||||
closeFilter()
|
|
||||||
onToggleSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
DefaultRoomListTopBar(
|
DefaultRoomListTopBar(
|
||||||
matrixUser = matrixUser,
|
matrixUser = matrixUser,
|
||||||
showAvatarIndicator = showAvatarIndicator,
|
showAvatarIndicator = showAvatarIndicator,
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,11 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
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 kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
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.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
@ -54,9 +50,7 @@ class RoomListDataSource @Inject constructor(
|
||||||
observeNotificationSettings()
|
observeNotificationSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _filter = MutableStateFlow("")
|
|
||||||
private val _allRooms = MutableSharedFlow<ImmutableList<RoomListRoomSummary>>(replay = 1)
|
private val _allRooms = MutableSharedFlow<ImmutableList<RoomListRoomSummary>>(replay = 1)
|
||||||
private val _filteredRooms = MutableStateFlow<ImmutableList<RoomListRoomSummary>>(persistentListOf())
|
|
||||||
|
|
||||||
private val lock = Mutex()
|
private val lock = Mutex()
|
||||||
private val diffCache = MutableListDiffCache<RoomListRoomSummary>()
|
private val diffCache = MutableListDiffCache<RoomListRoomSummary>()
|
||||||
|
|
@ -72,29 +66,9 @@ class RoomListDataSource @Inject constructor(
|
||||||
replaceWith(roomSummaries)
|
replaceWith(roomSummaries)
|
||||||
}
|
}
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
|
|
||||||
combine(
|
|
||||||
_filter,
|
|
||||||
_allRooms
|
|
||||||
) { filterValue, allRoomsValue ->
|
|
||||||
when {
|
|
||||||
filterValue.isEmpty() -> emptyList()
|
|
||||||
else -> allRoomsValue.filter { it.name.contains(filterValue, ignoreCase = true) }
|
|
||||||
}.toImmutableList()
|
|
||||||
}
|
|
||||||
.onEach {
|
|
||||||
_filteredRooms.value = it
|
|
||||||
}
|
|
||||||
.launchIn(coroutineScope)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFilter(filterValue: String) {
|
|
||||||
_filter.value = filterValue
|
|
||||||
}
|
|
||||||
|
|
||||||
val filter: StateFlow<String> = _filter
|
|
||||||
val allRooms: SharedFlow<ImmutableList<RoomListRoomSummary>> = _allRooms
|
val allRooms: SharedFlow<ImmutableList<RoomListRoomSummary>> = _allRooms
|
||||||
val filteredRooms: StateFlow<ImmutableList<RoomListRoomSummary>> = _filteredRooms
|
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
private fun observeNotificationSettings() {
|
private fun observeNotificationSettings() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomlist.impl.di
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
|
||||||
|
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
||||||
|
@ContributesTo(SessionScope::class)
|
||||||
|
@Module
|
||||||
|
interface RoomListModule {
|
||||||
|
@Binds
|
||||||
|
fun bindSearchPresenter(presenter: RoomListSearchPresenter): Presenter<RoomListSearchState>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomlist.impl.search
|
||||||
|
|
||||||
|
sealed interface RoomListSearchEvents {
|
||||||
|
data object ToggleSearchVisibility : RoomListSearchEvents
|
||||||
|
data class QueryChanged(val query: String) : RoomListSearchEvents
|
||||||
|
data object ClearQuery : RoomListSearchEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomlist.impl.search
|
||||||
|
|
||||||
|
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.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
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.RoomListFilter
|
||||||
|
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.loadAllIncrementally
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val PAGE_SIZE = 50
|
||||||
|
|
||||||
|
class RoomListSearchPresenter @Inject constructor(
|
||||||
|
private val roomListService: RoomListService,
|
||||||
|
private val roomSummaryFactory: RoomListRoomSummaryFactory,
|
||||||
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
) : Presenter<RoomListSearchState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): RoomListSearchState {
|
||||||
|
var isSearchActive by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
var searchQuery by rememberSaveable {
|
||||||
|
mutableStateOf("")
|
||||||
|
}
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val roomList = remember {
|
||||||
|
roomListService.createRoomList(
|
||||||
|
coroutineScope = coroutineScope,
|
||||||
|
pageSize = PAGE_SIZE,
|
||||||
|
initialFilter = RoomListFilter.all(RoomListFilter.None),
|
||||||
|
source = RoomList.Source.All,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
roomList.loadAllIncrementally(this)
|
||||||
|
}
|
||||||
|
LaunchedEffect(key1 = searchQuery) {
|
||||||
|
val filter = if (searchQuery.isBlank()) {
|
||||||
|
RoomListFilter.all(RoomListFilter.None)
|
||||||
|
} else {
|
||||||
|
RoomListFilter.all(RoomListFilter.NonLeft, RoomListFilter.NormalizedMatchRoomName(searchQuery))
|
||||||
|
}
|
||||||
|
roomList.updateFilter(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleEvents(event: RoomListSearchEvents) {
|
||||||
|
when (event) {
|
||||||
|
RoomListSearchEvents.ClearQuery -> {
|
||||||
|
searchQuery = ""
|
||||||
|
}
|
||||||
|
is RoomListSearchEvents.QueryChanged -> {
|
||||||
|
searchQuery = event.query
|
||||||
|
}
|
||||||
|
RoomListSearchEvents.ToggleSearchVisibility -> {
|
||||||
|
isSearchActive = !isSearchActive
|
||||||
|
searchQuery = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchResults by roomList
|
||||||
|
.rememberMappedSummaries()
|
||||||
|
.collectAsState(initial = persistentListOf())
|
||||||
|
|
||||||
|
return RoomListSearchState(
|
||||||
|
isSearchActive = isSearchActive,
|
||||||
|
query = searchQuery,
|
||||||
|
results = searchResults,
|
||||||
|
eventSink = ::handleEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomList.rememberMappedSummaries() = remember {
|
||||||
|
summaries
|
||||||
|
.map { roomSummaries ->
|
||||||
|
roomSummaries
|
||||||
|
.filterIsInstance<RoomSummary.Filled>()
|
||||||
|
.map(roomSummaryFactory::create)
|
||||||
|
.toPersistentList()
|
||||||
|
}
|
||||||
|
.flowOn(coroutineDispatchers.computation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomlist.impl.search
|
||||||
|
|
||||||
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
data class RoomListSearchState(
|
||||||
|
val isSearchActive: Boolean,
|
||||||
|
val query: String,
|
||||||
|
val results: ImmutableList<RoomListRoomSummary>,
|
||||||
|
val eventSink: (RoomListSearchEvents) -> Unit
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomlist.impl.search
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.roomlist.impl.aRoomListRoomSummaryList
|
||||||
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
class RoomListSearchStateProvider : PreviewParameterProvider<RoomListSearchState> {
|
||||||
|
override val values: Sequence<RoomListSearchState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aRoomListSearchState(),
|
||||||
|
aRoomListSearchState(
|
||||||
|
isSearchActive = true,
|
||||||
|
query = "Test",
|
||||||
|
results = aRoomListRoomSummaryList()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aRoomListSearchState(
|
||||||
|
isSearchActive: Boolean = false,
|
||||||
|
query: String = "",
|
||||||
|
results: ImmutableList<RoomListRoomSummary> = persistentListOf(),
|
||||||
|
eventSink: (RoomListSearchEvents) -> Unit = { },
|
||||||
|
) = RoomListSearchState(
|
||||||
|
isSearchActive = isSearchActive,
|
||||||
|
query = query,
|
||||||
|
results = results,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.roomlist.impl.search
|
package io.element.android.features.roomlist.impl.search
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
|
@ -25,32 +26,23 @@ 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.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Velocity
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.roomlist.impl.RoomListEvents
|
|
||||||
import io.element.android.features.roomlist.impl.RoomListState
|
|
||||||
import io.element.android.features.roomlist.impl.aRoomListState
|
|
||||||
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
||||||
import io.element.android.features.roomlist.impl.contentType
|
import io.element.android.features.roomlist.impl.contentType
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
|
|
@ -68,26 +60,30 @@ 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
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RoomListSearchResultView(
|
internal fun RoomListSearchView(
|
||||||
state: RoomListState,
|
state: RoomListSearchState,
|
||||||
onRoomClicked: (RoomId) -> Unit,
|
onRoomClicked: (RoomId) -> Unit,
|
||||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
BackHandler(enabled = state.isSearchActive) {
|
||||||
|
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = state.displaySearchResults,
|
visible = state.isSearchActive,
|
||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.applyIf(state.displaySearchResults, ifTrue = {
|
.applyIf(state.isSearchActive, ifTrue = {
|
||||||
// Disable input interaction to underlying views
|
// Disable input interaction to underlying views
|
||||||
pointerInput(Unit) {}
|
pointerInput(Unit) {}
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
if (state.displaySearchResults) {
|
if (state.isSearchActive) {
|
||||||
RoomListSearchResultContent(
|
RoomListSearchContent(
|
||||||
state = state,
|
state = state,
|
||||||
onRoomClicked = onRoomClicked,
|
onRoomClicked = onRoomClicked,
|
||||||
onRoomLongClicked = onRoomLongClicked,
|
onRoomLongClicked = onRoomLongClicked,
|
||||||
|
|
@ -99,15 +95,15 @@ internal fun RoomListSearchResultView(
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomListSearchResultContent(
|
private fun RoomListSearchContent(
|
||||||
state: RoomListState,
|
state: RoomListSearchState,
|
||||||
onRoomClicked: (RoomId) -> Unit,
|
onRoomClicked: (RoomId) -> Unit,
|
||||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||||
) {
|
) {
|
||||||
val borderColor = MaterialTheme.colorScheme.tertiary
|
val borderColor = MaterialTheme.colorScheme.tertiary
|
||||||
val strokeWidth = 1.dp
|
val strokeWidth = 1.dp
|
||||||
fun onBackButtonPressed() {
|
fun onBackButtonPressed() {
|
||||||
state.eventSink(RoomListEvents.ToggleSearchResults)
|
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRoomClicked(room: RoomListRoomSummary) {
|
fun onRoomClicked(room: RoomListRoomSummary) {
|
||||||
|
|
@ -126,7 +122,7 @@ private fun RoomListSearchResultContent(
|
||||||
},
|
},
|
||||||
navigationIcon = { BackButton(onClick = ::onBackButtonPressed) },
|
navigationIcon = { BackButton(onClick = ::onBackButtonPressed) },
|
||||||
title = {
|
title = {
|
||||||
val filter = state.filter.orEmpty()
|
val filter = state.query
|
||||||
val focusRequester = FocusRequester()
|
val focusRequester = FocusRequester()
|
||||||
TextField(
|
TextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -134,7 +130,7 @@ private fun RoomListSearchResultContent(
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
value = filter,
|
value = filter,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
onValueChange = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
|
onValueChange = { state.eventSink(RoomListSearchEvents.QueryChanged(it)) },
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
focusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
unfocusedContainerColor = Color.Transparent,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
|
@ -147,7 +143,7 @@ private fun RoomListSearchResultContent(
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (filter.isNotEmpty()) {
|
if (filter.isNotEmpty()) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
state.eventSink(RoomListEvents.UpdateFilter(""))
|
state.eventSink(RoomListSearchEvents.ClearQuery)
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.Close(),
|
imageVector = CompoundIcons.Close(),
|
||||||
|
|
@ -158,8 +154,8 @@ private fun RoomListSearchResultContent(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
LaunchedEffect(state.displaySearchResults) {
|
LaunchedEffect(state.isSearchActive) {
|
||||||
if (state.displaySearchResults) {
|
if (state.isSearchActive) {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -168,39 +164,16 @@ private fun RoomListSearchResultContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
val visibleRange by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
val layoutInfo = lazyListState.layoutInfo
|
|
||||||
val firstItemIndex = layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0
|
|
||||||
val size = layoutInfo.visibleItemsInfo.size
|
|
||||||
firstItemIndex until firstItemIndex + size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val nestedScrollConnection = remember {
|
|
||||||
object : NestedScrollConnection {
|
|
||||||
override suspend fun onPostFling(
|
|
||||||
consumed: Velocity,
|
|
||||||
available: Velocity
|
|
||||||
): Velocity {
|
|
||||||
state.eventSink(RoomListEvents.UpdateVisibleRange(visibleRange))
|
|
||||||
return super.onPostFling(consumed, available)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier.weight(1f),
|
||||||
.weight(1f)
|
|
||||||
.nestedScroll(nestedScrollConnection),
|
|
||||||
state = lazyListState,
|
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = state.filteredRoomList,
|
items = state.results,
|
||||||
contentType = { room -> room.contentType() },
|
contentType = { room -> room.contentType() },
|
||||||
) { room ->
|
) { room ->
|
||||||
RoomSummaryRow(
|
RoomSummaryRow(
|
||||||
|
|
@ -216,9 +189,9 @@ private fun RoomListSearchResultContent(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RoomListSearchResultContentPreview() = ElementPreview {
|
internal fun RoomListSearchResultContentPreview(@PreviewParameter(RoomListSearchStateProvider::class) state: RoomListSearchState) = ElementPreview {
|
||||||
RoomListSearchResultContent(
|
RoomListSearchContent(
|
||||||
state = aRoomListState(),
|
state = state,
|
||||||
onRoomClicked = {},
|
onRoomClicked = {},
|
||||||
onRoomLongClicked = {}
|
onRoomLongClicked = {}
|
||||||
)
|
)
|
||||||
|
|
@ -34,19 +34,24 @@ import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresente
|
||||||
import io.element.android.features.roomlist.impl.migration.InMemoryMigrationScreenStore
|
import io.element.android.features.roomlist.impl.migration.InMemoryMigrationScreenStore
|
||||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
|
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
|
||||||
import io.element.android.features.roomlist.impl.model.createRoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.createRoomListRoomSummary
|
||||||
|
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
||||||
|
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||||
|
import io.element.android.features.roomlist.impl.search.aRoomListSearchState
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||||
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
|
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
|
||||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||||
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
|
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
|
||||||
import io.element.android.libraries.indicator.impl.DefaultIndicatorService
|
import io.element.android.libraries.indicator.impl.DefaultIndicatorService
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
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.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
|
|
@ -55,7 +60,6 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
|
||||||
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.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
|
|
@ -67,6 +71,7 @@ import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
||||||
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.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
|
@ -78,6 +83,7 @@ import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class RoomListPresenterTests {
|
class RoomListPresenterTests {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
|
|
@ -106,9 +112,12 @@ class RoomListPresenterTests {
|
||||||
fun `present - show avatar indicator`() = runTest {
|
fun `present - show avatar indicator`() = runTest {
|
||||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
val encryptionService = FakeEncryptionService()
|
val encryptionService = FakeEncryptionService()
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
encryptionService = encryptionService,
|
||||||
|
)
|
||||||
val sessionVerificationService = FakeSessionVerificationService()
|
val sessionVerificationService = FakeSessionVerificationService()
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
encryptionService = encryptionService,
|
client = matrixClient,
|
||||||
sessionVerificationService = sessionVerificationService,
|
sessionVerificationService = sessionVerificationService,
|
||||||
coroutineScope = scope
|
coroutineScope = scope
|
||||||
)
|
)
|
||||||
|
|
@ -146,24 +155,6 @@ class RoomListPresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - should filter room with success`() = runTest {
|
|
||||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
|
||||||
val presenter = createRoomListPresenter(coroutineScope = scope)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
|
||||||
val withUserState = awaitItem()
|
|
||||||
assertThat(withUserState.filter).isEqualTo("")
|
|
||||||
withUserState.eventSink.invoke(RoomListEvents.UpdateFilter("t"))
|
|
||||||
val withFilterState = awaitItem()
|
|
||||||
assertThat(withFilterState.filter).isEqualTo("t")
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - load 1 room with success`() = runTest {
|
fun `present - load 1 room with success`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomListService = FakeRoomListService()
|
||||||
|
|
@ -175,7 +166,7 @@ class RoomListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = consumeItemsUntilPredicate { state -> state.roomList.dataOrNull()?.size == 16 }.last()
|
val initialState = consumeItemsUntilPredicate(timeout = 3.seconds) { state -> state.roomList.dataOrNull()?.size == 16 }.last()
|
||||||
// Room list is loaded with 16 placeholders
|
// Room list is loaded with 16 placeholders
|
||||||
val initialItems = initialState.roomList.dataOrNull().orEmpty()
|
val initialItems = initialState.roomList.dataOrNull().orEmpty()
|
||||||
assertThat(initialItems.size).isEqualTo(16)
|
assertThat(initialItems.size).isEqualTo(16)
|
||||||
|
|
@ -197,51 +188,7 @@ class RoomListPresenterTests {
|
||||||
numberOfUnreadMessages = 2,
|
numberOfUnreadMessages = 2,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
scope.cancel()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - load 1 room with success and filter rooms`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
val matrixClient = FakeMatrixClient(
|
|
||||||
roomListService = roomListService
|
|
||||||
)
|
|
||||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
|
||||||
val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
roomListService.postAllRooms(
|
|
||||||
listOf(
|
|
||||||
aRoomSummaryFilled(
|
|
||||||
numUnreadMentions = 1,
|
|
||||||
numUnreadMessages = 2,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
skipItems(3)
|
|
||||||
val loadedState = awaitItem()
|
|
||||||
// Test filtering with result
|
|
||||||
assertThat(loadedState.roomList.dataOrNull().orEmpty().size).isEqualTo(1)
|
|
||||||
loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3)))
|
|
||||||
skipItems(1)
|
|
||||||
val withFilteredRoomState = awaitItem()
|
|
||||||
assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1)
|
|
||||||
assertThat(withFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3))
|
|
||||||
assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1)
|
|
||||||
assertThat(withFilteredRoomState.filteredRoomList.first()).isEqualTo(
|
|
||||||
createRoomListRoomSummary(
|
|
||||||
numberOfUnreadMentions = 1,
|
|
||||||
numberOfUnreadMessages = 2,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// Test filtering without result
|
|
||||||
withFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada"))
|
|
||||||
skipItems(1)
|
|
||||||
val withNotFilteredRoomState = awaitItem()
|
|
||||||
assertThat(withNotFilteredRoomState.filter).isEqualTo("tada")
|
|
||||||
assertThat(withNotFilteredRoomState.filteredRoomList).isEmpty()
|
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -315,6 +262,33 @@ class RoomListPresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle DismissRecoveryKeyPrompt`() = runTest {
|
||||||
|
val encryptionService = FakeEncryptionService()
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
encryptionService = encryptionService,
|
||||||
|
)
|
||||||
|
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
|
||||||
|
val presenter = createRoomListPresenter(
|
||||||
|
client = matrixClient,
|
||||||
|
coroutineScope = scope,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.displayRecoveryKeyPrompt).isFalse()
|
||||||
|
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||||
|
val nextState = awaitItem()
|
||||||
|
assertThat(nextState.displayRecoveryKeyPrompt).isTrue()
|
||||||
|
nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt)
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.displayRecoveryKeyPrompt).isFalse()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - sets invite state`() = runTest {
|
fun `present - sets invite state`() = runTest {
|
||||||
val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites)
|
val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites)
|
||||||
|
|
@ -443,6 +417,40 @@ class RoomListPresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - toggle search menu`() = runTest {
|
||||||
|
val eventRecorder = EventsRecorder<RoomListSearchEvents>()
|
||||||
|
val searchPresenter: Presenter<RoomListSearchState> = Presenter {
|
||||||
|
aRoomListSearchState(
|
||||||
|
eventSink = eventRecorder
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
val presenter = createRoomListPresenter(
|
||||||
|
coroutineScope = scope,
|
||||||
|
searchPresenter = searchPresenter,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
eventRecorder.assertEmpty()
|
||||||
|
initialState.eventSink(RoomListEvents.ToggleSearchResults)
|
||||||
|
eventRecorder.assertSingle(
|
||||||
|
RoomListSearchEvents.ToggleSearchVisibility
|
||||||
|
)
|
||||||
|
initialState.eventSink(RoomListEvents.ToggleSearchResults)
|
||||||
|
eventRecorder.assertList(
|
||||||
|
listOf(
|
||||||
|
RoomListSearchEvents.ToggleSearchVisibility,
|
||||||
|
RoomListSearchEvents.ToggleSearchVisibility
|
||||||
|
)
|
||||||
|
)
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
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
|
||||||
|
|
@ -516,7 +524,6 @@ class RoomListPresenterTests {
|
||||||
|
|
||||||
// The migration screen is not shown anymore
|
// The migration screen is not shown anymore
|
||||||
assertThat(awaitItem().displayMigrationStatus).isFalse()
|
assertThat(awaitItem().displayMigrationStatus).isFalse()
|
||||||
cancelAndIgnoreRemainingEvents()
|
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -567,14 +574,15 @@ class RoomListPresenterTests {
|
||||||
givenFormat(A_FORMATTED_DATE)
|
givenFormat(A_FORMATTED_DATE)
|
||||||
},
|
},
|
||||||
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
||||||
encryptionService: EncryptionService = FakeEncryptionService(),
|
|
||||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
migrationScreenPresenter: MigrationScreenPresenter = MigrationScreenPresenter(
|
migrationScreenPresenter: MigrationScreenPresenter = MigrationScreenPresenter(
|
||||||
matrixClient = client,
|
matrixClient = client,
|
||||||
migrationScreenStore = InMemoryMigrationScreenStore(),
|
migrationScreenStore = InMemoryMigrationScreenStore(),
|
||||||
),
|
),
|
||||||
filtersPresenter: RoomListFiltersPresenter = RoomListFiltersPresenter(),
|
filtersPresenter: RoomListFiltersPresenter = RoomListFiltersPresenter(),
|
||||||
|
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
|
||||||
) = RoomListPresenter(
|
) = RoomListPresenter(
|
||||||
client = client,
|
client = client,
|
||||||
sessionVerificationService = sessionVerificationService,
|
sessionVerificationService = sessionVerificationService,
|
||||||
|
|
@ -592,14 +600,14 @@ class RoomListPresenterTests {
|
||||||
notificationSettingsService = client.notificationSettingsService(),
|
notificationSettingsService = client.notificationSettingsService(),
|
||||||
appScope = coroutineScope
|
appScope = coroutineScope
|
||||||
),
|
),
|
||||||
encryptionService = encryptionService,
|
featureFlagService = featureFlagService,
|
||||||
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
|
|
||||||
indicatorService = DefaultIndicatorService(
|
indicatorService = DefaultIndicatorService(
|
||||||
sessionVerificationService = sessionVerificationService,
|
sessionVerificationService = sessionVerificationService,
|
||||||
encryptionService = encryptionService,
|
encryptionService = client.encryptionService(),
|
||||||
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
|
featureFlagService = featureFlagService,
|
||||||
),
|
),
|
||||||
migrationScreenPresenter = migrationScreenPresenter,
|
migrationScreenPresenter = migrationScreenPresenter,
|
||||||
|
searchPresenter = searchPresenter,
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
filtersPresenter = filtersPresenter,
|
filtersPresenter = filtersPresenter,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomlist.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 io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||||
|
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||||
|
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
||||||
|
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.RoomSummary
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class RoomListSearchPresenterTests {
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val presenter = createRoomListSearchPresenter()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(state.isSearchActive).isFalse()
|
||||||
|
assertThat(state.query).isEmpty()
|
||||||
|
assertThat(state.results).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - toggle search visibility`() = runTest {
|
||||||
|
val presenter = createRoomListSearchPresenter()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(state.isSearchActive).isFalse()
|
||||||
|
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
||||||
|
}
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(state.isSearchActive).isTrue()
|
||||||
|
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
|
||||||
|
}
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(state.isSearchActive).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - query search changes`() = runTest {
|
||||||
|
val roomListService = FakeRoomListService()
|
||||||
|
val presenter = createRoomListSearchPresenter(roomListService)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(
|
||||||
|
roomListService.allRooms.currentFilter.value
|
||||||
|
).isEqualTo(
|
||||||
|
RoomListFilter.all(
|
||||||
|
RoomListFilter.None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
state.eventSink(RoomListSearchEvents.QueryChanged("Search"))
|
||||||
|
}
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(state.query).isEqualTo("Search")
|
||||||
|
assertThat(
|
||||||
|
roomListService.allRooms.currentFilter.value
|
||||||
|
).isEqualTo(
|
||||||
|
RoomListFilter.all(
|
||||||
|
RoomListFilter.NonLeft,
|
||||||
|
RoomListFilter.NormalizedMatchRoomName("Search")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
state.eventSink(RoomListSearchEvents.ClearQuery)
|
||||||
|
}
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(state.query).isEmpty()
|
||||||
|
assertThat(
|
||||||
|
roomListService.allRooms.currentFilter.value
|
||||||
|
).isEqualTo(
|
||||||
|
RoomListFilter.all(
|
||||||
|
RoomListFilter.None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - room list changes`() = runTest {
|
||||||
|
val roomListService = FakeRoomListService()
|
||||||
|
val presenter = createRoomListSearchPresenter(roomListService)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(state.results).isEmpty()
|
||||||
|
}
|
||||||
|
roomListService.postAllRooms(
|
||||||
|
listOf(
|
||||||
|
RoomSummary.Empty("1"),
|
||||||
|
aRoomSummaryFilled()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(state.results).hasSize(1)
|
||||||
|
}
|
||||||
|
roomListService.postAllRooms(emptyList())
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(state.results).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TestScope.createRoomListSearchPresenter(
|
||||||
|
roomListService: RoomListService = FakeRoomListService(),
|
||||||
|
): RoomListSearchPresenter {
|
||||||
|
return RoomListSearchPresenter(
|
||||||
|
roomListService = roomListService,
|
||||||
|
roomSummaryFactory = RoomListRoomSummaryFactory(
|
||||||
|
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(),
|
||||||
|
roomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
||||||
|
),
|
||||||
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,7 @@ compose_bom = "2024.02.00"
|
||||||
composecompiler = "1.5.9"
|
composecompiler = "1.5.9"
|
||||||
|
|
||||||
# Coroutines
|
# Coroutines
|
||||||
coroutines = "1.7.3"
|
coroutines = "1.8.0"
|
||||||
|
|
||||||
# Accompanist
|
# Accompanist
|
||||||
accompanist = "0.34.0"
|
accompanist = "0.34.0"
|
||||||
|
|
@ -34,7 +34,7 @@ test_core = "1.5.0"
|
||||||
coil = "2.5.0"
|
coil = "2.5.0"
|
||||||
datetime = "0.5.0"
|
datetime = "0.5.0"
|
||||||
dependencyAnalysis = "1.30.0"
|
dependencyAnalysis = "1.30.0"
|
||||||
serialization_json = "1.6.2"
|
serialization_json = "1.6.3"
|
||||||
showkase = "1.0.2"
|
showkase = "1.0.2"
|
||||||
appyx = "1.4.0"
|
appyx = "1.4.0"
|
||||||
sqldelight = "2.0.1"
|
sqldelight = "2.0.1"
|
||||||
|
|
@ -51,7 +51,7 @@ autoservice = "1.1.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
androidx-test-ext-junit = "1.1.5"
|
androidx-test-ext-junit = "1.1.5"
|
||||||
espresso-core = "3.5.1"
|
espresso-core = "3.5.1"
|
||||||
kover = "0.7.5"
|
kover = "0.7.6"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# Project
|
# Project
|
||||||
|
|
@ -132,7 +132,7 @@ test_junitext = "androidx.test.ext:junit:1.1.5"
|
||||||
test_mockk = "io.mockk:mockk:1.13.9"
|
test_mockk = "io.mockk:mockk:1.13.9"
|
||||||
test_konsist = "com.lemonappdev:konsist:0.13.0"
|
test_konsist = "com.lemonappdev:konsist:0.13.0"
|
||||||
test_turbine = "app.cash.turbine:turbine:1.0.0"
|
test_turbine = "app.cash.turbine:turbine:1.0.0"
|
||||||
test_truth = "com.google.truth:truth:1.4.0"
|
test_truth = "com.google.truth:truth:1.4.1"
|
||||||
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.15"
|
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.15"
|
||||||
test_robolectric = "org.robolectric:robolectric:4.11.1"
|
test_robolectric = "org.robolectric:robolectric:4.11.1"
|
||||||
test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" }
|
test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" }
|
||||||
|
|
@ -163,7 +163,7 @@ sqlite = "androidx.sqlite:sqlite-ktx:2.4.0"
|
||||||
unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
|
unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
|
||||||
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5"
|
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5"
|
||||||
vanniktech_blurhash = "com.vanniktech:blurhash:0.2.0"
|
vanniktech_blurhash = "com.vanniktech:blurhash:0.2.0"
|
||||||
telephoto_zoomableimage = "me.saket.telephoto:zoomable-image-coil:0.7.1"
|
telephoto_zoomableimage = "me.saket.telephoto:zoomable-image-coil:0.8.0"
|
||||||
statemachine = "com.freeletics.flowredux:compose:1.2.1"
|
statemachine = "com.freeletics.flowredux:compose:1.2.1"
|
||||||
maplibre = "org.maplibre.gl:android-sdk:10.2.0"
|
maplibre = "org.maplibre.gl:android-sdk:10.2.0"
|
||||||
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.2"
|
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.2"
|
||||||
|
|
@ -172,7 +172,7 @@ opusencoder = "io.element.android:opusencoder:1.1.0"
|
||||||
kotlinpoet = "com.squareup:kotlinpoet:1.16.0"
|
kotlinpoet = "com.squareup:kotlinpoet:1.16.0"
|
||||||
|
|
||||||
# Analytics
|
# Analytics
|
||||||
posthog = "com.posthog:posthog-android:3.1.7"
|
posthog = "com.posthog:posthog-android:3.1.8"
|
||||||
sentry = "io.sentry:sentry-android:7.3.0"
|
sentry = "io.sentry:sentry-android:7.3.0"
|
||||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:aa14cbcdf81af2746d20a71779ec751f971e1d7f"
|
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:aa14cbcdf81af2746d20a71779ec751f971e1d7f"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
|
import io.element.android.libraries.testtags.TestTags
|
||||||
|
import io.element.android.libraries.testtags.testTag
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FloatingActionButton(
|
fun FloatingActionButton(
|
||||||
|
|
@ -48,7 +50,7 @@ fun FloatingActionButton(
|
||||||
) {
|
) {
|
||||||
androidx.compose.material3.FloatingActionButton(
|
androidx.compose.material3.FloatingActionButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier.testTag(TestTags.floatingActionButton),
|
||||||
shape = shape,
|
shape = shape,
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
contentColor = contentColor,
|
contentColor = contentColor,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,17 @@ data class RoomMember(
|
||||||
val powerLevel: Long,
|
val powerLevel: Long,
|
||||||
val normalizedPowerLevel: Long,
|
val normalizedPowerLevel: Long,
|
||||||
val isIgnored: Boolean,
|
val isIgnored: Boolean,
|
||||||
|
val role: Role,
|
||||||
) {
|
) {
|
||||||
|
/**
|
||||||
|
* Role of the RoomMember, based on its [powerLevel].
|
||||||
|
*/
|
||||||
|
enum class Role {
|
||||||
|
ADMIN,
|
||||||
|
MODERATOR,
|
||||||
|
USER
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disambiguated display name for the RoomMember.
|
* Disambiguated display name for the RoomMember.
|
||||||
* If the display name is null, the user ID is returned.
|
* If the display name is null, the user ID is returned.
|
||||||
|
|
@ -49,6 +59,10 @@ enum class RoomMembershipState {
|
||||||
LEAVE
|
LEAVE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the best name value to display for the RoomMember.
|
||||||
|
* If the [RoomMember.displayName] is present and not empty it'll be used, otherwise the [RoomMember.userId] will be used.
|
||||||
|
*/
|
||||||
fun RoomMember.getBestName(): String {
|
fun RoomMember.getBestName(): String {
|
||||||
return displayName?.takeIf { it.isNotEmpty() } ?: userId.value
|
return displayName?.takeIf { it.isNotEmpty() } ?: userId.value
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,26 @@ import kotlin.time.Duration
|
||||||
* Can be retrieved from [RoomListService] methods.
|
* Can be retrieved from [RoomListService] methods.
|
||||||
*/
|
*/
|
||||||
interface RoomList {
|
interface RoomList {
|
||||||
|
/**
|
||||||
|
* The loading state of the room list.
|
||||||
|
*/
|
||||||
sealed interface LoadingState {
|
sealed interface LoadingState {
|
||||||
data object NotLoaded : LoadingState
|
data object NotLoaded : LoadingState
|
||||||
data class Loaded(val numberOfRooms: Int) : LoadingState
|
data class Loaded(val numberOfRooms: Int) : LoadingState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The source of the room list data.
|
||||||
|
* All: all rooms except invites.
|
||||||
|
* Invites: only invites.
|
||||||
|
*
|
||||||
|
* To apply some dynamic filtering on top of that, use [DynamicRoomList].
|
||||||
|
*/
|
||||||
|
enum class Source {
|
||||||
|
All,
|
||||||
|
Invites,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of room summaries as a flow.
|
* The list of room summaries as a flow.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -18,38 +18,68 @@ package io.element.android.libraries.matrix.api.roomlist
|
||||||
|
|
||||||
sealed interface RoomListFilter {
|
sealed interface RoomListFilter {
|
||||||
companion object {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Create a filter that matches all the given filters.
|
||||||
|
*/
|
||||||
fun all(vararg filters: RoomListFilter): RoomListFilter {
|
fun all(vararg filters: RoomListFilter): RoomListFilter {
|
||||||
return All(filters.toList())
|
return All(filters.toList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a filter that matches any of the given filters.
|
||||||
|
*/
|
||||||
fun any(vararg filters: RoomListFilter): RoomListFilter {
|
fun any(vararg filters: RoomListFilter): RoomListFilter {
|
||||||
return Any(filters.toList())
|
return Any(filters.toList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter that matches all the given filters.
|
||||||
|
*/
|
||||||
data class All(
|
data class All(
|
||||||
val filters: List<RoomListFilter>
|
val filters: List<RoomListFilter>
|
||||||
) : RoomListFilter
|
) : RoomListFilter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter that matches any of the given filters.
|
||||||
|
*/
|
||||||
data class Any(
|
data class Any(
|
||||||
val filters: List<RoomListFilter>
|
val filters: List<RoomListFilter>
|
||||||
) : RoomListFilter
|
) : RoomListFilter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter that matches rooms that are not left.
|
||||||
|
*/
|
||||||
data object NonLeft : RoomListFilter
|
data object NonLeft : RoomListFilter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter that matches rooms that are unread.
|
||||||
|
*/
|
||||||
data object Unread : RoomListFilter
|
data object Unread : RoomListFilter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter that matches either Group or People rooms.
|
||||||
|
*/
|
||||||
sealed interface Category : RoomListFilter {
|
sealed interface Category : RoomListFilter {
|
||||||
data object Group : Category
|
data object Group : Category
|
||||||
data object People : Category
|
data object People : Category
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter that matches no room.
|
||||||
|
*/
|
||||||
data object None : RoomListFilter
|
data object None : RoomListFilter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter that matches rooms with a name using a normalized match.
|
||||||
|
*/
|
||||||
data class NormalizedMatchRoomName(
|
data class NormalizedMatchRoomName(
|
||||||
val pattern: String
|
val pattern: String
|
||||||
) : RoomListFilter
|
) : RoomListFilter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter that matches rooms with a name using a fuzzy match.
|
||||||
|
*/
|
||||||
data class FuzzyMatchRoomName(
|
data class FuzzyMatchRoomName(
|
||||||
val pattern: String
|
val pattern: String
|
||||||
) : RoomListFilter
|
) : RoomListFilter
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.libraries.matrix.api.roomlist
|
package io.element.android.libraries.matrix.api.roomlist
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -39,6 +40,20 @@ interface RoomListService {
|
||||||
data object Hide : SyncIndicator
|
data object Hide : SyncIndicator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a room list that can be used to load more rooms and filter them dynamically.
|
||||||
|
* @param coroutineScope the scope to use for the room list. When the scope will be closed, the room list will be closed too.
|
||||||
|
* @param pageSize the number of rooms to load at once.
|
||||||
|
* @param initialFilter the initial filter to apply to the rooms.
|
||||||
|
* @param source the source of the rooms, either all rooms or invites.
|
||||||
|
*/
|
||||||
|
fun createRoomList(
|
||||||
|
coroutineScope: CoroutineScope,
|
||||||
|
pageSize: Int,
|
||||||
|
initialFilter: RoomListFilter,
|
||||||
|
source: RoomList.Source,
|
||||||
|
): DynamicRoomList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns a [DynamicRoomList] object of all rooms we want to display.
|
* returns a [DynamicRoomList] object of all rooms we want to display.
|
||||||
* This will exclude some rooms like the invites, or spaces.
|
* This will exclude some rooms like the invites, or spaces.
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,5 @@ import kotlinx.parcelize.Parcelize
|
||||||
data class MatrixUser(
|
data class MatrixUser(
|
||||||
val userId: UserId,
|
val userId: UserId,
|
||||||
val displayName: String? = null,
|
val displayName: String? = null,
|
||||||
val avatarUrl: String? = null
|
val avatarUrl: String? = null,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
|
||||||
|
|
@ -197,10 +197,10 @@ class RustMatrixClient(
|
||||||
RustRoomListService(
|
RustRoomListService(
|
||||||
innerRoomListService = innerRoomListService,
|
innerRoomListService = innerRoomListService,
|
||||||
sessionCoroutineScope = sessionCoroutineScope,
|
sessionCoroutineScope = sessionCoroutineScope,
|
||||||
|
sessionDispatcher = sessionDispatcher,
|
||||||
roomListFactory = RoomListFactory(
|
roomListFactory = RoomListFactory(
|
||||||
innerRoomListService = innerRoomListService,
|
innerRoomListService = innerRoomListService,
|
||||||
coroutineScope = sessionCoroutineScope,
|
sessionCoroutineScope = sessionCoroutineScope,
|
||||||
dispatcher = sessionDispatcher,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.impl.room.member
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
|
import uniffi.matrix_sdk.RoomMemberRole
|
||||||
import org.matrix.rustcomponents.sdk.MembershipState as RustMembershipState
|
import org.matrix.rustcomponents.sdk.MembershipState as RustMembershipState
|
||||||
import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember
|
import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember
|
||||||
|
|
||||||
|
|
@ -33,9 +34,17 @@ object RoomMemberMapper {
|
||||||
it.powerLevel(),
|
it.powerLevel(),
|
||||||
it.normalizedPowerLevel(),
|
it.normalizedPowerLevel(),
|
||||||
it.isIgnored(),
|
it.isIgnored(),
|
||||||
|
mapRole(it.suggestedRoleForPowerLevel())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun mapRole(role: RoomMemberRole): RoomMember.Role =
|
||||||
|
when (role) {
|
||||||
|
RoomMemberRole.ADMINISTRATOR -> RoomMember.Role.ADMIN
|
||||||
|
RoomMemberRole.MODERATOR -> RoomMember.Role.MODERATOR
|
||||||
|
RoomMemberRole.USER -> RoomMember.Role.USER
|
||||||
|
}
|
||||||
|
|
||||||
fun mapMembership(membershipState: RustMembershipState): RoomMembershipState =
|
fun mapMembership(membershipState: RustMembershipState): RoomMembershipState =
|
||||||
when (membershipState) {
|
when (membershipState) {
|
||||||
RustMembershipState.BAN -> RoomMembershipState.BAN
|
RustMembershipState.BAN -> RoomMembershipState.BAN
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
|
||||||
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.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -30,13 +29,14 @@ import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListService
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
|
import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
|
||||||
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
|
|
||||||
|
|
||||||
internal class RoomListFactory(
|
internal class RoomListFactory(
|
||||||
private val innerRoomListService: InnerRoomListService,
|
private val innerRoomListService: RoomListService,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val sessionCoroutineScope: CoroutineScope,
|
||||||
private val dispatcher: CoroutineDispatcher,
|
|
||||||
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
|
@ -44,18 +44,21 @@ internal class RoomListFactory(
|
||||||
*/
|
*/
|
||||||
fun createRoomList(
|
fun createRoomList(
|
||||||
pageSize: Int,
|
pageSize: Int,
|
||||||
|
coroutineScope: CoroutineScope = sessionCoroutineScope,
|
||||||
|
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
||||||
initialFilter: RoomListFilter = RoomListFilter.all(),
|
initialFilter: RoomListFilter = RoomListFilter.all(),
|
||||||
innerProvider: suspend () -> InnerRoomList
|
innerProvider: suspend () -> InnerRoomList
|
||||||
): DynamicRoomList {
|
): DynamicRoomList {
|
||||||
val loadingStateFlow: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
|
val loadingStateFlow: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
|
||||||
val summariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList())
|
val summariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList())
|
||||||
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, dispatcher, roomSummaryDetailsFactory)
|
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryDetailsFactory)
|
||||||
// Makes sure we don't miss any events
|
// Makes sure we don't miss any events
|
||||||
val dynamicEvents = MutableSharedFlow<RoomListDynamicEvents>(replay = 100)
|
val dynamicEvents = MutableSharedFlow<RoomListDynamicEvents>(replay = 100)
|
||||||
val currentFilter = MutableStateFlow(initialFilter)
|
val currentFilter = MutableStateFlow(initialFilter)
|
||||||
val loadedPages = MutableStateFlow(1)
|
val loadedPages = MutableStateFlow(1)
|
||||||
var innerRoomList: InnerRoomList? = null
|
var innerRoomList: InnerRoomList? = null
|
||||||
coroutineScope.launch(dispatcher) {
|
|
||||||
|
coroutineScope.launch(coroutineContext) {
|
||||||
innerRoomList = innerProvider()
|
innerRoomList = innerProvider()
|
||||||
innerRoomList?.let { innerRoomList ->
|
innerRoomList?.let { innerRoomList ->
|
||||||
innerRoomList.entriesFlow(
|
innerRoomList.entriesFlow(
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package io.element.android.libraries.matrix.impl.roomlist
|
package io.element.android.libraries.matrix.impl.roomlist
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
@ -28,11 +27,12 @@ import org.matrix.rustcomponents.sdk.RoomListServiceInterface
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.use
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
class RoomSummaryListProcessor(
|
class RoomSummaryListProcessor(
|
||||||
private val roomSummaries: MutableStateFlow<List<RoomSummary>>,
|
private val roomSummaries: MutableStateFlow<List<RoomSummary>>,
|
||||||
private val roomListService: RoomListServiceInterface,
|
private val roomListService: RoomListServiceInterface,
|
||||||
private val dispatcher: CoroutineDispatcher,
|
private val coroutineContext: CoroutineContext,
|
||||||
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||||
) {
|
) {
|
||||||
private val roomSummariesByIdentifier = HashMap<String, RoomSummary>()
|
private val roomSummariesByIdentifier = HashMap<String, RoomSummary>()
|
||||||
|
|
@ -130,7 +130,7 @@ class RoomSummaryListProcessor(
|
||||||
return builtRoomSummary
|
return builtRoomSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(dispatcher) {
|
private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(coroutineContext) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val mutableRoomSummaries = roomSummaries.value.toMutableList()
|
val mutableRoomSummaries = roomSummaries.value.toMutableList()
|
||||||
block(mutableRoomSummaries)
|
block(mutableRoomSummaries)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ 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.loadAllIncrementally
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
@ -42,10 +43,31 @@ private const val DEFAULT_PAGE_SIZE = 20
|
||||||
internal class RustRoomListService(
|
internal class RustRoomListService(
|
||||||
private val innerRoomListService: InnerRustRoomListService,
|
private val innerRoomListService: InnerRustRoomListService,
|
||||||
private val sessionCoroutineScope: CoroutineScope,
|
private val sessionCoroutineScope: CoroutineScope,
|
||||||
roomListFactory: RoomListFactory,
|
private val sessionDispatcher: CoroutineDispatcher,
|
||||||
|
private val roomListFactory: RoomListFactory,
|
||||||
) : RoomListService {
|
) : RoomListService {
|
||||||
|
override fun createRoomList(
|
||||||
|
coroutineScope: CoroutineScope,
|
||||||
|
pageSize: Int,
|
||||||
|
initialFilter: RoomListFilter,
|
||||||
|
source: RoomList.Source
|
||||||
|
): DynamicRoomList {
|
||||||
|
return roomListFactory.createRoomList(
|
||||||
|
pageSize = pageSize,
|
||||||
|
initialFilter = initialFilter,
|
||||||
|
coroutineScope = coroutineScope,
|
||||||
|
coroutineContext = sessionDispatcher,
|
||||||
|
) {
|
||||||
|
when (source) {
|
||||||
|
RoomList.Source.All -> innerRoomListService.allRooms()
|
||||||
|
RoomList.Source.Invites -> innerRoomListService.invites()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override val allRooms: DynamicRoomList = roomListFactory.createRoomList(
|
override val allRooms: DynamicRoomList = roomListFactory.createRoomList(
|
||||||
pageSize = DEFAULT_PAGE_SIZE,
|
pageSize = DEFAULT_PAGE_SIZE,
|
||||||
|
coroutineContext = sessionDispatcher,
|
||||||
initialFilter = RoomListFilter.all(RoomListFilter.NonLeft),
|
initialFilter = RoomListFilter.all(RoomListFilter.NonLeft),
|
||||||
) {
|
) {
|
||||||
innerRoomListService.allRooms()
|
innerRoomListService.allRooms()
|
||||||
|
|
@ -53,6 +75,7 @@ internal class RustRoomListService(
|
||||||
|
|
||||||
override val invites: RoomList = roomListFactory.createRoomList(
|
override val invites: RoomList = roomListFactory.createRoomList(
|
||||||
pageSize = Int.MAX_VALUE,
|
pageSize = Int.MAX_VALUE,
|
||||||
|
coroutineContext = sessionDispatcher,
|
||||||
) {
|
) {
|
||||||
innerRoomListService.invites()
|
innerRoomListService.invites()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import org.matrix.rustcomponents.sdk.NoPointer
|
||||||
import org.matrix.rustcomponents.sdk.Room
|
import org.matrix.rustcomponents.sdk.Room
|
||||||
import org.matrix.rustcomponents.sdk.RoomMember
|
import org.matrix.rustcomponents.sdk.RoomMember
|
||||||
import org.matrix.rustcomponents.sdk.RoomMembersIterator
|
import org.matrix.rustcomponents.sdk.RoomMembersIterator
|
||||||
|
import uniffi.matrix_sdk.RoomMemberRole
|
||||||
|
|
||||||
class RoomMemberListFetcherTest {
|
class RoomMemberListFetcherTest {
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -268,6 +269,7 @@ class FakeRustRoomMember(
|
||||||
private val membership: MembershipState = MembershipState.JOIN,
|
private val membership: MembershipState = MembershipState.JOIN,
|
||||||
private val isNameAmbiguous: Boolean = false,
|
private val isNameAmbiguous: Boolean = false,
|
||||||
private val powerLevel: Long = 0L,
|
private val powerLevel: Long = 0L,
|
||||||
|
private val role: RoomMemberRole = RoomMemberRole.USER,
|
||||||
) : RoomMember(NoPointer) {
|
) : RoomMember(NoPointer) {
|
||||||
override fun userId(): String {
|
override fun userId(): String {
|
||||||
return userId.value
|
return userId.value
|
||||||
|
|
@ -300,4 +302,8 @@ class FakeRustRoomMember(
|
||||||
override fun isIgnored(): Boolean {
|
override fun isIgnored(): Boolean {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun suggestedRoleForPowerLevel(): RoomMemberRole {
|
||||||
|
return role
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ class RoomSummaryListProcessorTests {
|
||||||
private fun TestScope.createProcessor() = RoomSummaryListProcessor(
|
private fun TestScope.createProcessor() = RoomSummaryListProcessor(
|
||||||
summaries,
|
summaries,
|
||||||
fakeRoomListService,
|
fakeRoomListService,
|
||||||
dispatcher = StandardTestDispatcher(testScheduler),
|
coroutineContext = StandardTestDispatcher(testScheduler),
|
||||||
roomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
roomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,10 @@ class FakeEncryptionService : EncryptionService {
|
||||||
backupStateStateFlow.emit(state)
|
backupStateStateFlow.emit(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun emitRecoveryState(state: RecoveryState) {
|
||||||
|
recoveryStateStateFlow.emit(state)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun emitEnableRecoveryProgress(state: EnableRecoveryProgress) {
|
suspend fun emitEnableRecoveryProgress(state: EnableRecoveryProgress) {
|
||||||
enableRecoveryProgressStateFlow.emit(state)
|
enableRecoveryProgressStateFlow.emit(state)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ fun aRoomMember(
|
||||||
powerLevel: Long = 0L,
|
powerLevel: Long = 0L,
|
||||||
normalizedPowerLevel: Long = 0L,
|
normalizedPowerLevel: Long = 0L,
|
||||||
isIgnored: Boolean = false,
|
isIgnored: Boolean = false,
|
||||||
|
role: RoomMember.Role = RoomMember.Role.USER,
|
||||||
) = RoomMember(
|
) = RoomMember(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
|
|
@ -38,4 +39,5 @@ fun aRoomMember(
|
||||||
powerLevel = powerLevel,
|
powerLevel = powerLevel,
|
||||||
normalizedPowerLevel = normalizedPowerLevel,
|
normalizedPowerLevel = normalizedPowerLevel,
|
||||||
isIgnored = isIgnored,
|
isIgnored = isIgnored,
|
||||||
|
role = role,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,10 @@ fun aRoomSummaryFilled(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun aRoomSummaryFilled(
|
||||||
|
details: RoomSummaryDetails = aRoomSummaryDetails(),
|
||||||
|
) = RoomSummary.Filled(details)
|
||||||
|
|
||||||
fun aRoomSummaryDetails(
|
fun aRoomSummaryDetails(
|
||||||
roomId: RoomId = A_ROOM_ID,
|
roomId: RoomId = A_ROOM_ID,
|
||||||
name: String = A_ROOM_NAME,
|
name: String = A_ROOM_NAME,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ 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.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
|
@ -59,13 +60,20 @@ class FakeRoomListService : RoomListService {
|
||||||
var latestSlidingSyncRange: IntRange? = null
|
var latestSlidingSyncRange: IntRange? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
override val allRooms: DynamicRoomList = SimplePagedRoomList(
|
override fun createRoomList(coroutineScope: CoroutineScope, pageSize: Int, initialFilter: RoomListFilter, source: RoomList.Source): DynamicRoomList {
|
||||||
|
return when (source) {
|
||||||
|
RoomList.Source.All -> allRooms
|
||||||
|
RoomList.Source.Invites -> invites
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val allRooms = SimplePagedRoomList(
|
||||||
allRoomSummariesFlow,
|
allRoomSummariesFlow,
|
||||||
allRoomsLoadingStateFlow,
|
allRoomsLoadingStateFlow,
|
||||||
MutableStateFlow(RoomListFilter.all())
|
MutableStateFlow(RoomListFilter.all())
|
||||||
)
|
)
|
||||||
|
|
||||||
override val invites: RoomList = SimplePagedRoomList(
|
override val invites = SimplePagedRoomList(
|
||||||
inviteRoomSummariesFlow,
|
inviteRoomSummariesFlow,
|
||||||
inviteRoomsLoadingStateFlow,
|
inviteRoomsLoadingStateFlow,
|
||||||
MutableStateFlow(RoomListFilter.all())
|
MutableStateFlow(RoomListFilter.all())
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
|
|
||||||
data class SimplePagedRoomList(
|
data class SimplePagedRoomList(
|
||||||
override val summaries: StateFlow<List<RoomSummary>>,
|
override val summaries: MutableStateFlow<List<RoomSummary>>,
|
||||||
override val loadingState: StateFlow<RoomList.LoadingState>,
|
override val loadingState: StateFlow<RoomList.LoadingState>,
|
||||||
override val currentFilter: MutableStateFlow<RoomListFilter>
|
override val currentFilter: MutableStateFlow<RoomListFilter>
|
||||||
) : DynamicRoomList {
|
) : DynamicRoomList {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.ui.room
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name value to use when sorting room members.
|
||||||
|
*
|
||||||
|
* If the display name is not null and not empty, it is returned.
|
||||||
|
* Otherwise, the user ID is returned without the initial "@".
|
||||||
|
*/
|
||||||
|
fun RoomMember.sortingName(): String {
|
||||||
|
return displayName?.takeIf { it.isNotEmpty() } ?: userId.value.drop(1)
|
||||||
|
}
|
||||||
|
|
@ -74,4 +74,14 @@ object TestTags {
|
||||||
val dialogPositive = TestTag("dialog-positive")
|
val dialogPositive = TestTag("dialog-positive")
|
||||||
val dialogNegative = TestTag("dialog-negative")
|
val dialogNegative = TestTag("dialog-negative")
|
||||||
val dialogNeutral = TestTag("dialog-neutral")
|
val dialogNeutral = TestTag("dialog-neutral")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Floating Action Button.
|
||||||
|
*/
|
||||||
|
val floatingActionButton = TestTag("floating-action-button")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeline item.
|
||||||
|
*/
|
||||||
|
val timelineItemSenderInfo = TestTag("timeline_item-sender_info")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,8 @@ fun Project.setupKover() {
|
||||||
"*Presenter\$present\$*",
|
"*Presenter\$present\$*",
|
||||||
// Forked from compose
|
// Forked from compose
|
||||||
"io.element.android.libraries.designsystem.theme.components.bottomsheet.*",
|
"io.element.android.libraries.designsystem.theme.components.bottomsheet.*",
|
||||||
|
// Test presenter
|
||||||
|
"io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter",
|
||||||
)
|
)
|
||||||
annotatedBy(
|
annotatedBy(
|
||||||
"androidx.compose.ui.tooling.preview.Preview",
|
"androidx.compose.ui.tooling.preview.Preview",
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
|
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
|
||||||
import io.element.android.features.roomlist.impl.migration.SharedPrefsMigrationScreenStore
|
import io.element.android.features.roomlist.impl.migration.SharedPrefsMigrationScreenStore
|
||||||
|
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.dateformatter.impl.DateFormatters
|
import io.element.android.libraries.dateformatter.impl.DateFormatters
|
||||||
import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter
|
||||||
|
|
@ -71,6 +72,21 @@ class RoomListScreen(
|
||||||
private val featureFlagService = DefaultFeatureFlagService(
|
private val featureFlagService = DefaultFeatureFlagService(
|
||||||
providers = setOf(StaticFeatureFlagProvider())
|
providers = setOf(StaticFeatureFlagProvider())
|
||||||
)
|
)
|
||||||
|
private val roomListRoomSummaryFactory = RoomListRoomSummaryFactory(
|
||||||
|
lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter(
|
||||||
|
localDateTimeProvider = dateTimeProvider,
|
||||||
|
dateFormatters = dateFormatters
|
||||||
|
),
|
||||||
|
roomLastMessageFormatter = DefaultRoomLastMessageFormatter(
|
||||||
|
sp = stringProvider,
|
||||||
|
roomMembershipContentFormatter = RoomMembershipContentFormatter(
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
sp = stringProvider
|
||||||
|
),
|
||||||
|
profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider),
|
||||||
|
stateContentFormatter = StateContentFormatter(stringProvider),
|
||||||
|
),
|
||||||
|
)
|
||||||
private val presenter = RoomListPresenter(
|
private val presenter = RoomListPresenter(
|
||||||
client = matrixClient,
|
client = matrixClient,
|
||||||
sessionVerificationService = sessionVerificationService,
|
sessionVerificationService = sessionVerificationService,
|
||||||
|
|
@ -80,26 +96,11 @@ class RoomListScreen(
|
||||||
leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver(), coroutineDispatchers),
|
leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver(), coroutineDispatchers),
|
||||||
roomListDataSource = RoomListDataSource(
|
roomListDataSource = RoomListDataSource(
|
||||||
roomListService = matrixClient.roomListService,
|
roomListService = matrixClient.roomListService,
|
||||||
roomListRoomSummaryFactory = RoomListRoomSummaryFactory(
|
roomListRoomSummaryFactory = roomListRoomSummaryFactory,
|
||||||
lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter(
|
|
||||||
localDateTimeProvider = dateTimeProvider,
|
|
||||||
dateFormatters = dateFormatters
|
|
||||||
),
|
|
||||||
roomLastMessageFormatter = DefaultRoomLastMessageFormatter(
|
|
||||||
sp = stringProvider,
|
|
||||||
roomMembershipContentFormatter = RoomMembershipContentFormatter(
|
|
||||||
matrixClient = matrixClient,
|
|
||||||
sp = stringProvider
|
|
||||||
),
|
|
||||||
profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider),
|
|
||||||
stateContentFormatter = StateContentFormatter(stringProvider),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
notificationSettingsService = matrixClient.notificationSettingsService(),
|
notificationSettingsService = matrixClient.notificationSettingsService(),
|
||||||
appScope = Singleton.appScope
|
appScope = Singleton.appScope
|
||||||
),
|
),
|
||||||
encryptionService = encryptionService,
|
|
||||||
indicatorService = DefaultIndicatorService(
|
indicatorService = DefaultIndicatorService(
|
||||||
sessionVerificationService = sessionVerificationService,
|
sessionVerificationService = sessionVerificationService,
|
||||||
encryptionService = encryptionService,
|
encryptionService = encryptionService,
|
||||||
|
|
@ -110,6 +111,11 @@ class RoomListScreen(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
migrationScreenStore = SharedPrefsMigrationScreenStore(context.getSharedPreferences("migration", Context.MODE_PRIVATE))
|
migrationScreenStore = SharedPrefsMigrationScreenStore(context.getSharedPreferences("migration", Context.MODE_PRIVATE))
|
||||||
),
|
),
|
||||||
|
searchPresenter = RoomListSearchPresenter(
|
||||||
|
roomListService = matrixClient.roomListService,
|
||||||
|
roomSummaryFactory = roomListRoomSummaryFactory,
|
||||||
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
|
),
|
||||||
sessionPreferencesStore = DefaultSessionPreferencesStore(
|
sessionPreferencesStore = DefaultSessionPreferencesStore(
|
||||||
context = context,
|
context = context,
|
||||||
sessionId = matrixClient.sessionId,
|
sessionId = matrixClient.sessionId,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:eba9e3f6b232813ccb2c42eb9ede3b594769619e4ce8f13b9d43c53688da070b
|
oid sha256:76bd5b2b4e4277cd9f87c8d39ec7490cde4d9de0f1f99cde8cd9061c19a9f34f
|
||||||
size 38958
|
size 47105
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:839119e003cdbccea31c8a19ee50248890a3bc042a0f820163ce5fdd7411f193
|
oid sha256:da396a8bdd3b9124c0b896e0dd79e7835091041b0544041421bf25b7f2edcaef
|
||||||
size 25282
|
size 25960
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:372179764b951cb9074a0e573241402ec2137045f26d9ffea1d567b93377c624
|
oid sha256:02874e8bf08b040dc154b81e579c602e023abd99b2af8732a9e322921042ac8d
|
||||||
size 38233
|
size 46597
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6ac113aa7a641c20d9c725078509de96e6878adf95e7ff456d77c309f263a16f
|
oid sha256:3f764f7ca578de87f854d4bd090bc642a851f4ce85deee91c0ca77e3d4a5f582
|
||||||
size 24936
|
size 25653
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:b2dc223ad7ca556a74aaff1e0ec6a94c10b951457c7b9614804a4c477c4e6e8e
|
|
||||||
size 29974
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7f313e09baa93bb4ff9eda9a3126b2cf49867fac3c1d53f11ec027b4dad7fbff
|
||||||
|
size 4909
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1625ac34428f660c235d1d64f5de867baa6c0ca296f0a93d81588d633d6a74bf
|
||||||
|
size 30082
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:199f1cf052e17b08810d026921b53ee35b1a942f9b661beb2286e6891843898c
|
|
||||||
size 29867
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6e5a9aba28b5a7dfa41b8ca47e74ebe629f717d9bd51836d71c31adb436083af
|
||||||
|
size 4861
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:264d9373767b6b59d0cea4bfa4d148a453058be70e7805581fe96d7448ef5232
|
||||||
|
size 29978
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5f8802121f779f48dd556a9a3a08e6ac1a63e86d5695f6f8c133fbb346d76ef6
|
oid sha256:13dadbd502163a9bfe81cb1e67f9aa0933c4b7f4fd3c8f731e930a28709485d2
|
||||||
size 89779
|
size 51948
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2523a28a889fd1739fe1e0c94edce5d86a5ebcbe6c3973a3c49fbb8fb8a19f79
|
oid sha256:64c4eb481f40871925405ae317cb80927caf31cb552a8aa9549bfb5658ca91e4
|
||||||
size 55617
|
size 137589
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:13dadbd502163a9bfe81cb1e67f9aa0933c4b7f4fd3c8f731e930a28709485d2
|
oid sha256:af42c9891a6670cdde07cf054e139ce1f83e877bb68f60463fcad6dd28d8e049
|
||||||
size 51948
|
size 6867
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:64c4eb481f40871925405ae317cb80927caf31cb552a8aa9549bfb5658ca91e4
|
|
||||||
size 137589
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7f313e09baa93bb4ff9eda9a3126b2cf49867fac3c1d53f11ec027b4dad7fbff
|
oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0
|
||||||
size 4909
|
size 4462
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b2dc223ad7ca556a74aaff1e0ec6a94c10b951457c7b9614804a4c477c4e6e8e
|
oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0
|
||||||
size 29974
|
size 4462
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0
|
oid sha256:5f8802121f779f48dd556a9a3a08e6ac1a63e86d5695f6f8c133fbb346d76ef6
|
||||||
size 4462
|
size 89779
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0
|
oid sha256:2523a28a889fd1739fe1e0c94edce5d86a5ebcbe6c3973a3c49fbb8fb8a19f79
|
||||||
size 4462
|
size 55617
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7cdd93c157565f7f8d3fa23a43c30e02531dab40578a315c5d0d10a99e74f259
|
oid sha256:0a00391ef16a762a14dd7e348ce97b500ca1b42a32ee3ff926a10865f46cd06c
|
||||||
size 91394
|
size 53590
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2b25e023153dd094299a0c794d5e488bdc6fc0c478abbfbce7337e52b233715d
|
oid sha256:a7bf47b0a25c455b108d7b3585a42849f03c6652af73a175fe2147cb1ad62a66
|
||||||
size 57463
|
size 161125
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0a00391ef16a762a14dd7e348ce97b500ca1b42a32ee3ff926a10865f46cd06c
|
oid sha256:6e95dac6e75d3f615ccf4d8e98f1780d8aeace1cddf0b33bba8b485860d3216b
|
||||||
size 53590
|
size 6688
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:a7bf47b0a25c455b108d7b3585a42849f03c6652af73a175fe2147cb1ad62a66
|
|
||||||
size 161125
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6e5a9aba28b5a7dfa41b8ca47e74ebe629f717d9bd51836d71c31adb436083af
|
oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0
|
||||||
size 4861
|
size 4462
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:199f1cf052e17b08810d026921b53ee35b1a942f9b661beb2286e6891843898c
|
oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0
|
||||||
size 29867
|
size 4462
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0
|
oid sha256:7cdd93c157565f7f8d3fa23a43c30e02531dab40578a315c5d0d10a99e74f259
|
||||||
size 4462
|
size 91394
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0
|
oid sha256:2b25e023153dd094299a0c794d5e488bdc6fc0c478abbfbce7337e52b233715d
|
||||||
size 4462
|
size 57463
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue