Merge branch 'develop' into fix/start-voice-recording-when-permission-is-granted
This commit is contained in:
commit
10bc15d647
215 changed files with 2436 additions and 2080 deletions
|
|
@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
|
||||||
|
|
@ -54,7 +55,8 @@ class LoadingBaseRoomStateFlowFactoryTest {
|
||||||
@Test
|
@Test
|
||||||
fun `flow should emit Loading and then Loaded when there is a room in cache after SS is loaded`() = runTest {
|
fun `flow should emit Loading and then Loaded when there is a room in cache after SS is loaded`() = runTest {
|
||||||
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID))
|
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID))
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList()
|
||||||
|
val roomListService = FakeRoomListService(allRooms = roomList)
|
||||||
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
|
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
|
||||||
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
|
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
|
||||||
flowFactory
|
flowFactory
|
||||||
|
|
@ -62,21 +64,22 @@ class LoadingBaseRoomStateFlowFactoryTest {
|
||||||
.test {
|
.test {
|
||||||
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
|
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
|
||||||
matrixClient.givenGetRoomResult(A_ROOM_ID, room)
|
matrixClient.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
roomList.loadingState.emit(RoomList.LoadingState.Loaded(1))
|
||||||
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
|
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `flow should emit Loading and then Error when there is no room in cache after SS is loaded`() = runTest {
|
fun `flow should emit Loading and then Error when there is no room in cache after SS is loaded`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList()
|
||||||
|
val roomListService = FakeRoomListService(allRooms = roomList)
|
||||||
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
|
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
|
||||||
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
|
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
|
||||||
flowFactory
|
flowFactory
|
||||||
.create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null)
|
.create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null)
|
||||||
.test {
|
.test {
|
||||||
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
|
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
roomList.loadingState.emit(RoomList.LoadingState.Loaded(1))
|
||||||
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Error)
|
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,15 +149,18 @@ class CallScreenPresenter(
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
if (callType is CallType.RoomCall) {
|
||||||
// Wait for the call to be joined, if it takes too long, we display an error
|
// Note: For external calls isWidgetLoaded will always be false
|
||||||
delay(10.seconds)
|
LaunchedEffect(Unit) {
|
||||||
|
// Wait for the call to be joined, if it takes too long, we display an error
|
||||||
|
delay(10.seconds)
|
||||||
|
|
||||||
if (!isWidgetLoaded) {
|
if (!isWidgetLoaded) {
|
||||||
Timber.w("The call took too long to load. Displaying an error before exiting.")
|
Timber.w("The call took too long to load. Displaying an error before exiting.")
|
||||||
|
|
||||||
// This will display a simple 'Sorry, an error occurred' dialog and force the user to exit the call
|
// This will display a simple 'Sorry, an error occurred' dialog and force the user to exit the call
|
||||||
webViewError = ""
|
webViewError = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ Du kannst dies jederzeit in den Einstellungen des Chats ändern."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
|
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">" Sichtbarkeit des Chats"</string>
|
<string name="screen_create_room_room_visibility_section_title">" Sichtbarkeit des Chats"</string>
|
||||||
<string name="screen_create_room_space_selection_no_space_description">"(kein Space)"</string>
|
<string name="screen_create_room_space_selection_no_space_description">"(kein Space)"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_title">"Home"</string>
|
||||||
<string name="screen_create_room_space_selection_sheet_title">"Space hinzufügen"</string>
|
<string name="screen_create_room_space_selection_sheet_title">"Space hinzufügen"</string>
|
||||||
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
|
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
|
||||||
<string name="screen_create_room_topic_placeholder">"Beschreibung hinzufügen…"</string>
|
<string name="screen_create_room_topic_placeholder">"Beschreibung hinzufügen…"</string>
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,22 @@
|
||||||
<string name="screen_create_room_action_create_room">"Uus jututuba"</string>
|
<string name="screen_create_room_action_create_room">"Uus jututuba"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Kutsu osalejaid"</string>
|
<string name="screen_create_room_add_people_title">"Kutsu osalejaid"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"Jututoa loomisel tekkis viga"</string>
|
<string name="screen_create_room_error_creating_room">"Jututoa loomisel tekkis viga"</string>
|
||||||
<string name="screen_create_room_private_option_description">"Ligipääs siia jututuppa on vaid kutse alusel. Kõik sõnumid siin jututoas on läbivalt krüptitud."</string>
|
<string name="screen_create_room_name_placeholder">"Sisesta nimi…"</string>
|
||||||
|
<string name="screen_create_room_new_room_title">"Uus jututuba"</string>
|
||||||
|
<string name="screen_create_room_new_space_title">"Uus kogukond"</string>
|
||||||
|
<string name="screen_create_room_private_option_description">"Ligipääs siia jututuppa on vaid kutse alusel."</string>
|
||||||
<string name="screen_create_room_public_option_description">"Kõik saavad seda jututuba leida.
|
<string name="screen_create_room_public_option_description">"Kõik saavad seda jututuba leida.
|
||||||
Sa võid seda jututoa seadistustest alati muuta."</string>
|
Sa võid seda jututoa seadistustest alati muuta."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Avalik jututuba"</string>
|
<string name="screen_create_room_public_option_title">"Avalik jututuba"</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Kõik võivad paluda selle jututoaga liitumist, kuid peakasutaja või moderaator peavad selle kinnitama"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Kõik võivad paluda selle jututoaga liitumist, kuid peakasutaja või moderaator peavad selle kinnitama."</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Küsi võimalust liitumiseks"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Luba küsida liitumisvõimalust"</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_description">"Kõik võivad selle jututoaga liituda"</string>
|
<string name="screen_create_room_room_access_section_public_option_description">"Kõik võivad selle jututoaga liituda."</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_title">"Kõik kasutajad"</string>
|
<string name="screen_create_room_room_access_section_public_option_title">"Kõik kasutajad"</string>
|
||||||
<string name="screen_create_room_room_address_section_footer">"Selleks, et see jututuba oleks nähtav jututubade avalikus kataloogis, sa vajad jututoa aadressi."</string>
|
<string name="screen_create_room_room_address_section_footer">"Selleks, et jututuba oleks nähtav jututubade avalikus kataloogis, vajab ta aadressi."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Jututoa aadress"</string>
|
<string name="screen_create_room_room_address_section_title">"Aadress"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"Jututoa nähtavus"</string>
|
<string name="screen_create_room_room_visibility_section_title">"Jututoa nähtavus"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_title">"Avaleht"</string>
|
||||||
|
<string name="screen_create_room_space_selection_sheet_title">"Lisa kogukonda"</string>
|
||||||
<string name="screen_create_room_topic_label">"Teema (kui soovid lisada)"</string>
|
<string name="screen_create_room_topic_label">"Teema (kui soovid lisada)"</string>
|
||||||
|
<string name="screen_create_room_topic_placeholder">"Lisa kirjeldus…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
<string name="screen_create_room_action_create_room">"Nytt rum"</string>
|
<string name="screen_create_room_action_create_room">"Nytt rum"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Bjud in personer"</string>
|
<string name="screen_create_room_add_people_title">"Bjud in personer"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"Ett fel uppstod när rummet skapades"</string>
|
<string name="screen_create_room_error_creating_room">"Ett fel uppstod när rummet skapades"</string>
|
||||||
<string name="screen_create_room_private_option_description">"Endast inbjudna personer har tillgång till detta rum. Alla meddelanden är totalsträckskrypterade."</string>
|
<string name="screen_create_room_private_option_description">"Endast inbjudna personer kan gå med."</string>
|
||||||
<string name="screen_create_room_public_option_description">"Vem som helst kan hitta det här rummet.
|
<string name="screen_create_room_public_option_description">"Vem som helst kan hitta det här rummet.
|
||||||
Du kan ändra detta när som helst i rumsinställningarna."</string>
|
Du kan ändra detta när som helst i rumsinställningarna."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Offentligt rum"</string>
|
<string name="screen_create_room_public_option_title">"Offentligt rum"</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Vem som helst kan be om att gå med i rummet men en administratör eller en moderator måste acceptera begäran"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Vem som helst kan be om att gå med i rummet men en administratör eller en moderator måste acceptera begäran"</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Be om att gå med"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Tillåt att be om att gå med"</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_description">"Vem som helst kan gå med i det här rummet"</string>
|
<string name="screen_create_room_room_access_section_public_option_description">"Vem som helst kan gå med i det här rummet"</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_title">"Vem som helst"</string>
|
<string name="screen_create_room_room_access_section_public_option_title">"Offentligt"</string>
|
||||||
<string name="screen_create_room_room_address_section_footer">"För att detta rum ska vara synligt i den allmänna rumskatalogen behöver du en rumsadress."</string>
|
<string name="screen_create_room_room_address_section_footer">"För att detta rum ska vara synligt i den allmänna rumskatalogen behöver du en rumsadress."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Rumsadress"</string>
|
<string name="screen_create_room_room_address_section_title">"Rumsadress"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"Rumssynlighet"</string>
|
<string name="screen_create_room_room_visibility_section_title">"Rumssynlighet"</string>
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@
|
||||||
<string name="screen_create_room_name_placeholder">"Add name…"</string>
|
<string name="screen_create_room_name_placeholder">"Add name…"</string>
|
||||||
<string name="screen_create_room_new_room_title">"New room"</string>
|
<string name="screen_create_room_new_room_title">"New room"</string>
|
||||||
<string name="screen_create_room_new_space_title">"New space"</string>
|
<string name="screen_create_room_new_space_title">"New space"</string>
|
||||||
<string name="screen_create_room_parent_space_home_description">"(no space)"</string>
|
|
||||||
<string name="screen_create_room_parent_space_home_title">"Home"</string>
|
|
||||||
<string name="screen_create_room_private_option_description">"Only people invited can join."</string>
|
<string name="screen_create_room_private_option_description">"Only people invited can join."</string>
|
||||||
<string name="screen_create_room_private_option_title">"Private"</string>
|
<string name="screen_create_room_private_option_title">"Private"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Anyone can find this room.
|
<string name="screen_create_room_public_option_description">"Anyone can find this room.
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,7 @@ import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -55,6 +51,7 @@ import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
|
@ -215,17 +212,8 @@ private fun RoomsViewList(
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val visibleRange by remember {
|
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||||
derivedStateOf {
|
eventSink(RoomListEvent.UpdateVisibleRange(visibleRange))
|
||||||
val layoutInfo = lazyListState.layoutInfo
|
|
||||||
val firstItemIndex = layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0
|
|
||||||
val size = layoutInfo.visibleItemsInfo.size
|
|
||||||
firstItemIndex until firstItemIndex + size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val updatedEventSink by rememberUpdatedState(newValue = eventSink)
|
|
||||||
LaunchedEffect(visibleRange) {
|
|
||||||
updatedEventSink(RoomListEvent.UpdateVisibleRange(visibleRange))
|
|
||||||
}
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
|
|
@ -237,7 +225,7 @@ private fun RoomsViewList(
|
||||||
item {
|
item {
|
||||||
SetUpRecoveryKeyBanner(
|
SetUpRecoveryKeyBanner(
|
||||||
onContinueClick = onSetUpRecoveryClick,
|
onContinueClick = onSetUpRecoveryClick,
|
||||||
onDismissClick = { updatedEventSink(RoomListEvent.DismissBanner) },
|
onDismissClick = { eventSink(RoomListEvent.DismissBanner) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +233,7 @@ private fun RoomsViewList(
|
||||||
item {
|
item {
|
||||||
ConfirmRecoveryKeyBanner(
|
ConfirmRecoveryKeyBanner(
|
||||||
onContinueClick = onConfirmRecoveryKeyClick,
|
onContinueClick = onConfirmRecoveryKeyClick,
|
||||||
onDismissClick = { updatedEventSink(RoomListEvent.DismissBanner) },
|
onDismissClick = { eventSink(RoomListEvent.DismissBanner) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -260,7 +248,7 @@ private fun RoomsViewList(
|
||||||
} else if (state.showNewNotificationSoundBanner) {
|
} else if (state.showNewNotificationSoundBanner) {
|
||||||
item {
|
item {
|
||||||
NewNotificationSoundBanner(
|
NewNotificationSoundBanner(
|
||||||
onDismissClick = { updatedEventSink(RoomListEvent.DismissNewNotificationSoundBanner) },
|
onDismissClick = { eventSink(RoomListEvent.DismissNewNotificationSoundBanner) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,33 +9,48 @@
|
||||||
package io.element.android.features.home.impl.datasource
|
package io.element.android.features.home.impl.datasource
|
||||||
|
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
|
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
|
||||||
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
|
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
|
||||||
import io.element.android.libraries.androidutils.system.DateTimeObserver
|
import io.element.android.libraries.androidutils.system.DateTimeObserver
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.updateVisibleRange
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.lang.IllegalStateException
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
private const val PAGE_SIZE = 20
|
||||||
|
private const val EXTENDED_VISIBILITY_RANGE_SIZE = 40
|
||||||
|
private const val SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS = 300L
|
||||||
|
private const val PAGINATION_THRESHOLD = 3 * PAGE_SIZE
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
@SingleIn(SessionScope::class)
|
||||||
class RoomListDataSource(
|
class RoomListDataSource(
|
||||||
private val roomListService: RoomListService,
|
private val roomListService: RoomListService,
|
||||||
private val roomListRoomSummaryFactory: RoomListRoomSummaryFactory,
|
private val roomListRoomSummaryFactory: RoomListRoomSummaryFactory,
|
||||||
|
|
@ -51,7 +66,12 @@ class RoomListDataSource(
|
||||||
observeDateTimeChanges()
|
observeDateTimeChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _allRooms = MutableSharedFlow<ImmutableList<RoomListRoomSummary>>(replay = 1)
|
private val roomList = roomListService.createRoomList(
|
||||||
|
pageSize = PAGE_SIZE,
|
||||||
|
source = RoomList.Source.All,
|
||||||
|
coroutineScope = sessionCoroutineScope
|
||||||
|
)
|
||||||
|
private val _roomSummariesFlow = MutableSharedFlow<ImmutableList<RoomListRoomSummary>>(replay = 1)
|
||||||
|
|
||||||
private val lock = Mutex()
|
private val lock = Mutex()
|
||||||
private val diffCache = MutableListDiffCache<RoomListRoomSummary>()
|
private val diffCache = MutableListDiffCache<RoomListRoomSummary>()
|
||||||
|
|
@ -59,22 +79,49 @@ class RoomListDataSource(
|
||||||
old?.roomId == new?.roomId
|
old?.roomId == new?.roomId
|
||||||
}
|
}
|
||||||
|
|
||||||
val allRooms: Flow<ImmutableList<RoomListRoomSummary>> = _allRooms
|
val roomSummariesFlow: Flow<ImmutableList<RoomListRoomSummary>> = _roomSummariesFlow
|
||||||
|
|
||||||
val loadingState = roomListService.allRooms.loadingState
|
val loadingState = roomList.loadingState
|
||||||
|
|
||||||
fun launchIn(coroutineScope: CoroutineScope) {
|
fun launchIn(coroutineScope: CoroutineScope) {
|
||||||
roomListService
|
roomList
|
||||||
.allRooms
|
.summaries
|
||||||
.filteredSummaries
|
|
||||||
.onEach { roomSummaries ->
|
.onEach { roomSummaries ->
|
||||||
replaceWith(roomSummaries)
|
replaceWith(roomSummaries)
|
||||||
}
|
}
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>) {
|
suspend fun updateFilter(filter: RoomListFilter) {
|
||||||
roomListService.subscribeToVisibleRooms(roomIds)
|
roomList.updateFilter(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateVisibleRange(visibleRange: IntRange) = coroutineScope {
|
||||||
|
launch {
|
||||||
|
roomList.updateVisibleRange(visibleRange, PAGINATION_THRESHOLD)
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
subscribeToVisibleRoomsIfNeeded(visibleRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentSubscribeToVisibleRoomsJob: Job? = null
|
||||||
|
private fun CoroutineScope.subscribeToVisibleRoomsIfNeeded(range: IntRange) {
|
||||||
|
currentSubscribeToVisibleRoomsJob?.cancel()
|
||||||
|
currentSubscribeToVisibleRoomsJob = launch {
|
||||||
|
// Debounce the subscription to avoid subscribing to too many rooms
|
||||||
|
delay(SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS)
|
||||||
|
|
||||||
|
if (range.isEmpty()) return@launch
|
||||||
|
val currentRoomList = roomSummariesFlow.first()
|
||||||
|
// Use extended range to 'prefetch' the next rooms info
|
||||||
|
val midExtendedRangeSize = EXTENDED_VISIBILITY_RANGE_SIZE / 2
|
||||||
|
val extendedRange = range.first until range.last + midExtendedRangeSize
|
||||||
|
val roomIds = extendedRange.mapNotNull { index ->
|
||||||
|
currentRoomList.getOrNull(index)?.roomId
|
||||||
|
}
|
||||||
|
roomListService.subscribeToVisibleRooms(roomIds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
|
|
@ -82,7 +129,7 @@ class RoomListDataSource(
|
||||||
notificationSettingsService.notificationSettingsChangeFlow
|
notificationSettingsService.notificationSettingsChangeFlow
|
||||||
.debounce(0.5.seconds)
|
.debounce(0.5.seconds)
|
||||||
.onEach {
|
.onEach {
|
||||||
roomListService.allRooms.rebuildSummaries()
|
roomList.rebuildSummaries()
|
||||||
}
|
}
|
||||||
.launchIn(sessionCoroutineScope)
|
.launchIn(sessionCoroutineScope)
|
||||||
}
|
}
|
||||||
|
|
@ -108,6 +155,7 @@ class RoomListDataSource(
|
||||||
private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>, useCache: Boolean = true) {
|
private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>, useCache: Boolean = true) {
|
||||||
// Used to detect duplicates in the room list summaries - see comment below
|
// Used to detect duplicates in the room list summaries - see comment below
|
||||||
data class CacheResult(val index: Int, val fromCache: Boolean)
|
data class CacheResult(val index: Int, val fromCache: Boolean)
|
||||||
|
|
||||||
val cachingResults = mutableMapOf<RoomId, MutableList<CacheResult>>()
|
val cachingResults = mutableMapOf<RoomId, MutableList<CacheResult>>()
|
||||||
|
|
||||||
val roomListRoomSummaries = diffCache.indices().mapNotNull { index ->
|
val roomListRoomSummaries = diffCache.indices().mapNotNull { index ->
|
||||||
|
|
@ -144,14 +192,14 @@ class RoomListDataSource(
|
||||||
analyticsService.trackError(
|
analyticsService.trackError(
|
||||||
IllegalStateException(
|
IllegalStateException(
|
||||||
"Found duplicates in room summaries after a local UI update: $duplicates. " +
|
"Found duplicates in room summaries after a local UI update: $duplicates. " +
|
||||||
"This could be a race condition/caching issue of some kind"
|
"This could be a race condition/caching issue of some kind"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Remove duplicates before emitting the new values
|
// Remove duplicates before emitting the new values
|
||||||
_allRooms.emit(roomListRoomSummaries.distinctBy { it.roomId }.toImmutableList())
|
_roomSummariesFlow.emit(roomListRoomSummaries.distinctBy { it.roomId }.toImmutableList())
|
||||||
} else {
|
} else {
|
||||||
_allRooms.emit(roomListRoomSummaries.toImmutableList())
|
_roomSummariesFlow.emit(roomListRoomSummaries.toImmutableList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,7 +211,7 @@ class RoomListDataSource(
|
||||||
|
|
||||||
private suspend fun rebuildAllRoomSummaries() {
|
private suspend fun rebuildAllRoomSummaries() {
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
roomListService.allRooms.filteredSummaries.replayCache.firstOrNull()?.let { roomSummaries ->
|
roomList.summaries.replayCache.firstOrNull()?.let { roomSummaries ->
|
||||||
buildAndEmitAllRooms(roomSummaries, useCache = false)
|
buildAndEmitAllRooms(roomSummaries, useCache = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,17 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.features.home.impl.datasource.RoomListDataSource
|
||||||
import io.element.android.features.home.impl.filters.selection.FilterSelectionStrategy
|
import io.element.android.features.home.impl.filters.selection.FilterSelectionStrategy
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class RoomListFiltersPresenter(
|
class RoomListFiltersPresenter(
|
||||||
private val roomListService: RoomListService,
|
private val roomListDataSource: RoomListDataSource,
|
||||||
private val filterSelectionStrategy: FilterSelectionStrategy,
|
private val filterSelectionStrategy: FilterSelectionStrategy,
|
||||||
) : Presenter<RoomListFiltersState> {
|
) : Presenter<RoomListFiltersState> {
|
||||||
private val initialFilters = filterSelectionStrategy.filterSelectionStates.value.toImmutableList()
|
private val initialFilters = filterSelectionStrategy.filterSelectionStates.value.toImmutableList()
|
||||||
|
|
@ -56,9 +57,9 @@ class RoomListFiltersPresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.collect { filters ->
|
.collectLatest { filters ->
|
||||||
val result = MatrixRoomListFilter.All(filters)
|
val result = MatrixRoomListFilter.All(filters)
|
||||||
roomListService.allRooms.updateFilter(result)
|
roomListDataSource.updateFilter(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,6 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -68,9 +66,6 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.takeWhile
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
private const val EXTENDED_RANGE_SIZE = 40
|
|
||||||
private const val SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS = 300L
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class RoomListPresenter(
|
class RoomListPresenter(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
|
|
@ -119,7 +114,7 @@ class RoomListPresenter(
|
||||||
fun handleEvent(event: RoomListEvent) {
|
fun handleEvent(event: RoomListEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is RoomListEvent.UpdateVisibleRange -> coroutineScope.launch {
|
is RoomListEvent.UpdateVisibleRange -> coroutineScope.launch {
|
||||||
updateVisibleRange(event.range)
|
roomListDataSource.updateVisibleRange(event.range)
|
||||||
}
|
}
|
||||||
RoomListEvent.DismissRequestVerificationPrompt -> securityBannerDismissed = true
|
RoomListEvent.DismissRequestVerificationPrompt -> securityBannerDismissed = true
|
||||||
RoomListEvent.DismissBanner -> securityBannerDismissed = true
|
RoomListEvent.DismissBanner -> securityBannerDismissed = true
|
||||||
|
|
@ -217,7 +212,7 @@ class RoomListPresenter(
|
||||||
showNewNotificationSoundBanner: Boolean,
|
showNewNotificationSoundBanner: Boolean,
|
||||||
): RoomListContentState {
|
): RoomListContentState {
|
||||||
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
|
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
|
||||||
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
|
roomListDataSource.roomSummariesFlow.collect { value = AsyncData.Success(it) }
|
||||||
}
|
}
|
||||||
val loadingState by roomListDataSource.loadingState.collectAsState()
|
val loadingState by roomListDataSource.loadingState.collectAsState()
|
||||||
val showEmpty by remember {
|
val showEmpty by remember {
|
||||||
|
|
@ -322,23 +317,4 @@ class RoomListPresenter(
|
||||||
room.clearEventCacheStorage()
|
room.clearEventCacheStorage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentUpdateVisibleRangeJob: Job? = null
|
|
||||||
private fun CoroutineScope.updateVisibleRange(range: IntRange) {
|
|
||||||
currentUpdateVisibleRangeJob?.cancel()
|
|
||||||
currentUpdateVisibleRangeJob = launch {
|
|
||||||
// Debounce the subscription to avoid subscribing to too many rooms
|
|
||||||
delay(SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS)
|
|
||||||
|
|
||||||
if (range.isEmpty()) return@launch
|
|
||||||
val currentRoomList = roomListDataSource.allRooms.first()
|
|
||||||
// Use extended range to 'prefetch' the next rooms info
|
|
||||||
val midExtendedRangeSize = EXTENDED_RANGE_SIZE / 2
|
|
||||||
val extendedRange = range.first until range.last + midExtendedRangeSize
|
|
||||||
val roomIds = extendedRange.mapNotNull { index ->
|
|
||||||
currentRoomList.getOrNull(index)?.roomId
|
|
||||||
}
|
|
||||||
roomListDataSource.subscribeToVisibleRooms(roomIds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
|
import io.element.android.libraries.matrix.api.roomlist.updateVisibleRange
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -42,12 +42,11 @@ class RoomListSearchDataSource(
|
||||||
|
|
||||||
private val roomList = roomListService.createRoomList(
|
private val roomList = roomListService.createRoomList(
|
||||||
pageSize = PAGE_SIZE,
|
pageSize = PAGE_SIZE,
|
||||||
initialFilter = RoomListFilter.None,
|
|
||||||
source = RoomList.Source.All,
|
source = RoomList.Source.All,
|
||||||
coroutineScope = coroutineScope
|
coroutineScope = coroutineScope
|
||||||
)
|
)
|
||||||
|
|
||||||
val roomSummaries: Flow<ImmutableList<RoomListRoomSummary>> = roomList.filteredSummaries
|
val roomSummaries: Flow<ImmutableList<RoomListRoomSummary>> = roomList.summaries
|
||||||
.map { roomSummaries ->
|
.map { roomSummaries ->
|
||||||
roomSummaries
|
roomSummaries
|
||||||
.map(roomSummaryFactory::create)
|
.map(roomSummaryFactory::create)
|
||||||
|
|
@ -55,12 +54,8 @@ class RoomListSearchDataSource(
|
||||||
}
|
}
|
||||||
.flowOn(coroutineDispatchers.computation)
|
.flowOn(coroutineDispatchers.computation)
|
||||||
|
|
||||||
suspend fun setIsActive(isActive: Boolean) = coroutineScope {
|
suspend fun updateVisibleRange(visibleRange: IntRange) {
|
||||||
if (isActive) {
|
roomList.updateVisibleRange(visibleRange)
|
||||||
roomList.loadAllIncrementally(this)
|
|
||||||
} else {
|
|
||||||
roomList.reset()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setSearchQuery(searchQuery: String) = coroutineScope {
|
suspend fun setSearchQuery(searchQuery: String) = coroutineScope {
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,5 @@ package io.element.android.features.home.impl.search
|
||||||
sealed interface RoomListSearchEvent {
|
sealed interface RoomListSearchEvent {
|
||||||
data object ToggleSearchVisibility : RoomListSearchEvent
|
data object ToggleSearchVisibility : RoomListSearchEvent
|
||||||
data object ClearQuery : RoomListSearchEvent
|
data object ClearQuery : RoomListSearchEvent
|
||||||
|
data class UpdateVisibleRange(val range: IntRange) : RoomListSearchEvent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class RoomListSearchPresenter(
|
class RoomListSearchPresenter(
|
||||||
|
|
@ -37,10 +38,6 @@ class RoomListSearchPresenter(
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
|
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
|
||||||
|
|
||||||
LaunchedEffect(isSearchActive) {
|
|
||||||
dataSource.setIsActive(isSearchActive)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(searchQuery.text) {
|
LaunchedEffect(searchQuery.text) {
|
||||||
dataSource.setSearchQuery(searchQuery.text.toString())
|
dataSource.setSearchQuery(searchQuery.text.toString())
|
||||||
}
|
}
|
||||||
|
|
@ -54,6 +51,9 @@ class RoomListSearchPresenter(
|
||||||
isSearchActive = !isSearchActive
|
isSearchActive = !isSearchActive
|
||||||
searchQuery.clearText()
|
searchQuery.clearText()
|
||||||
}
|
}
|
||||||
|
is RoomListSearchEvent.UpdateVisibleRange -> coroutineScope.launch {
|
||||||
|
dataSource.updateVisibleRange(visibleRange = event.range)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -47,6 +48,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
|
@ -154,7 +156,12 @@ private fun RoomListSearchContent(
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||||
|
state.eventSink(RoomListSearchEvent.UpdateVisibleRange(visibleRange))
|
||||||
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ Sul pole ühtegi lugemata sõnumit!"</string>
|
||||||
<string name="screen_roomlist_mark_as_read">"Märgi loetuks"</string>
|
<string name="screen_roomlist_mark_as_read">"Märgi loetuks"</string>
|
||||||
<string name="screen_roomlist_mark_as_unread">"Märgi mitteloetuks"</string>
|
<string name="screen_roomlist_mark_as_unread">"Märgi mitteloetuks"</string>
|
||||||
<string name="screen_roomlist_tombstoned_room_description">"See jututuba on uuendatud"</string>
|
<string name="screen_roomlist_tombstoned_room_description">"See jututuba on uuendatud"</string>
|
||||||
|
<string name="screen_roomlist_your_spaces">"Sinu kogukonnad"</string>
|
||||||
<string name="session_verification_banner_message">"Tundub, et kasutad uut seadet. Oma krüptitud sõnumite lugemiseks verifitseeri ta mõne muu oma seadmega."</string>
|
<string name="session_verification_banner_message">"Tundub, et kasutad uut seadet. Oma krüptitud sõnumite lugemiseks verifitseeri ta mõne muu oma seadmega."</string>
|
||||||
<string name="session_verification_banner_title">"Verifitseeri, et see oled sina"</string>
|
<string name="session_verification_banner_title">"Verifitseeri, et see oled sina"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,11 @@ import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -27,9 +29,13 @@ import java.time.Instant
|
||||||
class RoomListDataSourceTest {
|
class RoomListDataSourceTest {
|
||||||
@Test
|
@Test
|
||||||
fun `when DateTimeObserver gets a date change, the room summaries are refreshed`() = runTest {
|
fun `when DateTimeObserver gets a date change, the room summaries are refreshed`() = runTest {
|
||||||
val roomListService = FakeRoomListService().apply {
|
val roomList = FakeDynamicRoomList().apply {
|
||||||
|
summaries.emit(listOf(aRoomSummary()))
|
||||||
|
}
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
).apply {
|
||||||
postState(RoomListService.State.Running)
|
postState(RoomListService.State.Running)
|
||||||
postAllRooms(listOf(aRoomSummary()))
|
|
||||||
}
|
}
|
||||||
val dateTimeObserver = FakeDateTimeObserver()
|
val dateTimeObserver = FakeDateTimeObserver()
|
||||||
var dateFormatterResult = "Today"
|
var dateFormatterResult = "Today"
|
||||||
|
|
@ -42,7 +48,7 @@ class RoomListDataSourceTest {
|
||||||
dateTimeObserver = dateTimeObserver,
|
dateTimeObserver = dateTimeObserver,
|
||||||
)
|
)
|
||||||
|
|
||||||
roomListDataSource.allRooms.test {
|
roomListDataSource.roomSummariesFlow.test {
|
||||||
// Observe room list items changes
|
// Observe room list items changes
|
||||||
roomListDataSource.launchIn(backgroundScope)
|
roomListDataSource.launchIn(backgroundScope)
|
||||||
// Get the initial room list
|
// Get the initial room list
|
||||||
|
|
@ -61,9 +67,11 @@ class RoomListDataSourceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when DateTimeObserver gets a time zone change, the room summaries are refreshed`() = runTest {
|
fun `when DateTimeObserver gets a time zone change, the room summaries are refreshed`() = runTest {
|
||||||
val roomListService = FakeRoomListService().apply {
|
val roomList = FakeDynamicRoomList(summaries = MutableStateFlow(listOf(aRoomSummary())))
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
).apply {
|
||||||
postState(RoomListService.State.Running)
|
postState(RoomListService.State.Running)
|
||||||
postAllRooms(listOf(aRoomSummary()))
|
|
||||||
}
|
}
|
||||||
val dateTimeObserver = FakeDateTimeObserver()
|
val dateTimeObserver = FakeDateTimeObserver()
|
||||||
var dateFormatterResult = "Today"
|
var dateFormatterResult = "Today"
|
||||||
|
|
@ -75,7 +83,7 @@ class RoomListDataSourceTest {
|
||||||
),
|
),
|
||||||
dateTimeObserver = dateTimeObserver,
|
dateTimeObserver = dateTimeObserver,
|
||||||
)
|
)
|
||||||
roomListDataSource.allRooms.test {
|
roomListDataSource.roomSummariesFlow.test {
|
||||||
// Observe room list items changes
|
// Observe room list items changes
|
||||||
roomListDataSource.launchIn(backgroundScope)
|
roomListDataSource.launchIn(backgroundScope)
|
||||||
// Get the initial room list
|
// Get the initial room list
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,28 @@
|
||||||
package io.element.android.features.home.impl.filters
|
package io.element.android.features.home.impl.filters
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.home.impl.FakeDateTimeObserver
|
||||||
|
import io.element.android.features.home.impl.datasource.RoomListDataSource
|
||||||
|
import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory
|
||||||
import io.element.android.features.home.impl.filters.selection.DefaultFilterSelectionStrategy
|
import io.element.android.features.home.impl.filters.selection.DefaultFilterSelectionStrategy
|
||||||
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
|
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
|
||||||
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
|
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
|
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
|
||||||
|
import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter
|
||||||
|
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
|
|
||||||
|
|
||||||
class RoomListFiltersPresenterTest {
|
class RoomListFiltersPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -39,13 +52,13 @@ class RoomListFiltersPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun `present - toggle rooms filter`() = runTest {
|
fun `present - toggle rooms filter`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomListService = FakeRoomListService()
|
||||||
val presenter = createRoomListFiltersPresenter(roomListService)
|
val presenter = createRoomListFiltersPresenter(roomListService)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
|
awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
|
||||||
awaitLastSequentialItem().let { state ->
|
awaitLastSequentialItem().let { state ->
|
||||||
|
|
||||||
assertThat(state.hasAnyFilterSelected).isTrue()
|
assertThat(state.hasAnyFilterSelected).isTrue()
|
||||||
assertThat(state.filterSelectionStates).containsExactly(
|
assertThat(state.filterSelectionStates).containsExactly(
|
||||||
filterSelectionState(RoomListFilter.Rooms, true),
|
filterSelectionState(RoomListFilter.Rooms, true),
|
||||||
|
|
@ -56,12 +69,9 @@ class RoomListFiltersPresenterTest {
|
||||||
assertThat(state.selectedFilters()).containsExactly(
|
assertThat(state.selectedFilters()).containsExactly(
|
||||||
RoomListFilter.Rooms,
|
RoomListFilter.Rooms,
|
||||||
)
|
)
|
||||||
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
|
|
||||||
assertThat(roomListCurrentFilter.filters).containsExactly(
|
|
||||||
MatrixRoomListFilter.Category.Group,
|
|
||||||
)
|
|
||||||
state.eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
|
state.eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
|
||||||
}
|
}
|
||||||
|
advanceUntilIdle()
|
||||||
awaitLastSequentialItem().let { state ->
|
awaitLastSequentialItem().let { state ->
|
||||||
assertThat(state.hasAnyFilterSelected).isFalse()
|
assertThat(state.hasAnyFilterSelected).isFalse()
|
||||||
assertThat(state.filterSelectionStates).containsExactly(
|
assertThat(state.filterSelectionStates).containsExactly(
|
||||||
|
|
@ -72,13 +82,12 @@ class RoomListFiltersPresenterTest {
|
||||||
filterSelectionState(RoomListFilter.Invites, false),
|
filterSelectionState(RoomListFilter.Invites, false),
|
||||||
).inOrder()
|
).inOrder()
|
||||||
assertThat(state.selectedFilters()).isEmpty()
|
assertThat(state.selectedFilters()).isEmpty()
|
||||||
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
|
|
||||||
assertThat(roomListCurrentFilter.filters).isEmpty()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun `present - clear filters event`() = runTest {
|
fun `present - clear filters event`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomListService = FakeRoomListService()
|
||||||
val presenter = createRoomListFiltersPresenter(roomListService)
|
val presenter = createRoomListFiltersPresenter(roomListService)
|
||||||
|
|
@ -88,6 +97,7 @@ class RoomListFiltersPresenterTest {
|
||||||
assertThat(state.hasAnyFilterSelected).isTrue()
|
assertThat(state.hasAnyFilterSelected).isTrue()
|
||||||
state.eventSink.invoke(RoomListFiltersEvent.ClearSelectedFilters)
|
state.eventSink.invoke(RoomListFiltersEvent.ClearSelectedFilters)
|
||||||
}
|
}
|
||||||
|
advanceUntilIdle()
|
||||||
awaitLastSequentialItem().let { state ->
|
awaitLastSequentialItem().let { state ->
|
||||||
assertThat(state.hasAnyFilterSelected).isFalse()
|
assertThat(state.hasAnyFilterSelected).isFalse()
|
||||||
}
|
}
|
||||||
|
|
@ -100,11 +110,25 @@ private fun filterSelectionState(filter: RoomListFilter, selected: Boolean) = Fi
|
||||||
isSelected = selected,
|
isSelected = selected,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createRoomListFiltersPresenter(
|
private fun TestScope.createRoomListFiltersPresenter(
|
||||||
roomListService: RoomListService = FakeRoomListService(),
|
roomListService: RoomListService = FakeRoomListService(),
|
||||||
|
notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
|
||||||
|
dateFormatter: DateFormatter = FakeDateFormatter(),
|
||||||
|
roomLatestEventFormatter: RoomLatestEventFormatter = FakeRoomLatestEventFormatter(),
|
||||||
): RoomListFiltersPresenter {
|
): RoomListFiltersPresenter {
|
||||||
return RoomListFiltersPresenter(
|
return RoomListFiltersPresenter(
|
||||||
roomListService = roomListService,
|
roomListDataSource = RoomListDataSource(
|
||||||
|
roomListService = roomListService,
|
||||||
|
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
|
||||||
|
dateFormatter = dateFormatter,
|
||||||
|
roomLatestEventFormatter = roomLatestEventFormatter,
|
||||||
|
),
|
||||||
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
notificationSettingsService = notificationSettingsService,
|
||||||
|
sessionCoroutineScope = backgroundScope,
|
||||||
|
dateTimeObserver = FakeDateTimeObserver(),
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
),
|
||||||
filterSelectionStrategy = DefaultFilterSelectionStrategy(),
|
filterSelectionStrategy = DefaultFilterSelectionStrategy(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
|
|
@ -77,6 +78,7 @@ import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.advanceTimeBy
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
|
|
@ -91,7 +93,10 @@ class RoomListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - load 1 room with success`() = runTest {
|
fun `present - load 1 room with success`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList()
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
roomListService = roomListService
|
roomListService = roomListService
|
||||||
)
|
)
|
||||||
|
|
@ -102,8 +107,8 @@ class RoomListPresenterTest {
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = consumeItemsUntilPredicate { state -> state.contentState is RoomListContentState.Skeleton }.last()
|
val initialState = consumeItemsUntilPredicate { state -> state.contentState is RoomListContentState.Skeleton }.last()
|
||||||
assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Skeleton::class.java)
|
assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Skeleton::class.java)
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
roomList.loadingState.emit(RoomList.LoadingState.Loaded(1))
|
||||||
roomListService.postAllRooms(
|
roomList.summaries.emit(
|
||||||
listOf(
|
listOf(
|
||||||
aRoomSummary(
|
aRoomSummary(
|
||||||
numUnreadMentions = 1,
|
numUnreadMentions = 1,
|
||||||
|
|
@ -128,9 +133,12 @@ class RoomListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
|
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
|
||||||
val roomListService = FakeRoomListService().apply {
|
val roomList = FakeDynamicRoomList(
|
||||||
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
|
||||||
}
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val encryptionService = FakeEncryptionService().apply {
|
val encryptionService = FakeEncryptionService().apply {
|
||||||
emitRecoveryState(RecoveryState.INCOMPLETE)
|
emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||||
}
|
}
|
||||||
|
|
@ -154,9 +162,12 @@ class RoomListPresenterTest {
|
||||||
val encryptionService = FakeEncryptionService().apply {
|
val encryptionService = FakeEncryptionService().apply {
|
||||||
recoveryStateStateFlow.emit(RecoveryState.DISABLED)
|
recoveryStateStateFlow.emit(RecoveryState.DISABLED)
|
||||||
}
|
}
|
||||||
val roomListService = FakeRoomListService().apply {
|
val roomList = FakeDynamicRoomList(
|
||||||
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
|
||||||
}
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
encryptionService = encryptionService,
|
encryptionService = encryptionService,
|
||||||
|
|
@ -344,9 +355,13 @@ class RoomListPresenterTest {
|
||||||
fun `present - change in notification settings updates the summary for decorations`() = runTest {
|
fun `present - change in notification settings updates the summary for decorations`() = runTest {
|
||||||
val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
||||||
val notificationSettingsService = FakeNotificationSettingsService()
|
val notificationSettingsService = FakeNotificationSettingsService()
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList(
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
summaries = MutableStateFlow(listOf(aRoomSummary(userDefinedNotificationMode = userDefinedMode))),
|
||||||
roomListService.postAllRooms(listOf(aRoomSummary(userDefinedNotificationMode = userDefinedMode)))
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
notificationSettingsService = notificationSettingsService
|
notificationSettingsService = notificationSettingsService
|
||||||
|
|
@ -397,8 +412,12 @@ class RoomListPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - when room service returns no room, then contentState is Empty`() = runTest {
|
fun `present - when room service returns no room, then contentState is Empty`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList(
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(0))
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(0))
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
)
|
)
|
||||||
|
|
@ -479,16 +498,21 @@ class RoomListPresenterTest {
|
||||||
val acceptDeclinePresenter = Presenter {
|
val acceptDeclinePresenter = Presenter {
|
||||||
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
|
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
|
||||||
}
|
}
|
||||||
val roomListService = FakeRoomListService()
|
|
||||||
val matrixClient = FakeMatrixClient(
|
|
||||||
roomListService = roomListService,
|
|
||||||
)
|
|
||||||
val roomSummary = aRoomSummary(
|
val roomSummary = aRoomSummary(
|
||||||
currentUserMembership = CurrentUserMembership.INVITED,
|
currentUserMembership = CurrentUserMembership.INVITED,
|
||||||
inviter = aRoomMember(),
|
inviter = aRoomMember(),
|
||||||
)
|
)
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
val roomList = FakeDynamicRoomList(
|
||||||
roomListService.postAllRooms(listOf(roomSummary))
|
summaries = MutableStateFlow(listOf(roomSummary)),
|
||||||
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
roomListService = roomListService,
|
||||||
|
)
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
client = matrixClient,
|
client = matrixClient,
|
||||||
acceptDeclineInvitePresenter = acceptDeclinePresenter
|
acceptDeclineInvitePresenter = acceptDeclinePresenter
|
||||||
|
|
@ -519,15 +543,20 @@ class RoomListPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - UpdateVisibleRange will cancel the previous subscription if called too soon`() = runTest {
|
fun `present - UpdateVisibleRange will cancel the previous subscription if called too soon`() = runTest {
|
||||||
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
|
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
|
||||||
val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
|
|
||||||
val matrixClient = FakeMatrixClient(
|
|
||||||
roomListService = roomListService,
|
|
||||||
)
|
|
||||||
val roomSummary = aRoomSummary(
|
val roomSummary = aRoomSummary(
|
||||||
currentUserMembership = CurrentUserMembership.INVITED
|
currentUserMembership = CurrentUserMembership.INVITED
|
||||||
)
|
)
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
val roomList = FakeDynamicRoomList(
|
||||||
roomListService.postAllRooms(listOf(roomSummary))
|
summaries = MutableStateFlow(listOf(roomSummary)),
|
||||||
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList },
|
||||||
|
subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda,
|
||||||
|
)
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
roomListService = roomListService,
|
||||||
|
)
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
client = matrixClient,
|
client = matrixClient,
|
||||||
)
|
)
|
||||||
|
|
@ -548,15 +577,20 @@ class RoomListPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - UpdateVisibleRange subscribes to rooms in visible range`() = runTest {
|
fun `present - UpdateVisibleRange subscribes to rooms in visible range`() = runTest {
|
||||||
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
|
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
|
||||||
val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
|
|
||||||
val matrixClient = FakeMatrixClient(
|
|
||||||
roomListService = roomListService,
|
|
||||||
)
|
|
||||||
val roomSummary = aRoomSummary(
|
val roomSummary = aRoomSummary(
|
||||||
currentUserMembership = CurrentUserMembership.INVITED
|
currentUserMembership = CurrentUserMembership.INVITED
|
||||||
)
|
)
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
val roomList = FakeDynamicRoomList(
|
||||||
roomListService.postAllRooms(listOf(roomSummary))
|
summaries = MutableStateFlow(listOf(roomSummary)),
|
||||||
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList },
|
||||||
|
subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda,
|
||||||
|
)
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
roomListService = roomListService,
|
||||||
|
)
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
client = matrixClient,
|
client = matrixClient,
|
||||||
)
|
)
|
||||||
|
|
@ -579,15 +613,20 @@ class RoomListPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - notification sound banner`() = runTest {
|
fun `present - notification sound banner`() = runTest {
|
||||||
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
|
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
|
||||||
val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
|
|
||||||
val matrixClient = FakeMatrixClient(
|
|
||||||
roomListService = roomListService,
|
|
||||||
)
|
|
||||||
val roomSummary = aRoomSummary(
|
val roomSummary = aRoomSummary(
|
||||||
currentUserMembership = CurrentUserMembership.INVITED
|
currentUserMembership = CurrentUserMembership.INVITED
|
||||||
)
|
)
|
||||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
val roomList = FakeDynamicRoomList(
|
||||||
roomListService.postAllRooms(listOf(roomSummary))
|
summaries = MutableStateFlow(listOf(roomSummary)),
|
||||||
|
loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1))
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList },
|
||||||
|
subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda,
|
||||||
|
)
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
roomListService = roomListService,
|
||||||
|
)
|
||||||
val onAnnouncementDismissedResult = lambdaRecorder<Announcement, Unit> { }
|
val onAnnouncementDismissedResult = lambdaRecorder<Announcement, Unit> { }
|
||||||
val announcementService = FakeAnnouncementService(
|
val announcementService = FakeAnnouncementService(
|
||||||
onAnnouncementDismissedResult = onAnnouncementDismissedResult,
|
onAnnouncementDismissedResult = onAnnouncementDismissedResult,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,10 @@ import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventForma
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -56,12 +59,15 @@ class RoomListSearchPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - query search changes`() = runTest {
|
fun `present - query search changes`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList()
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val presenter = createRoomListSearchPresenter(roomListService)
|
val presenter = createRoomListSearchPresenter(roomListService)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(
|
assertThat(
|
||||||
roomListService.allRooms.currentFilter.value
|
roomList.currentFilter.value
|
||||||
).isEqualTo(
|
).isEqualTo(
|
||||||
RoomListFilter.None
|
RoomListFilter.None
|
||||||
)
|
)
|
||||||
|
|
@ -70,7 +76,7 @@ class RoomListSearchPresenterTest {
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.query.text).isEqualTo("Search")
|
assertThat(state.query.text).isEqualTo("Search")
|
||||||
assertThat(
|
assertThat(
|
||||||
roomListService.allRooms.currentFilter.value
|
roomList.currentFilter.value
|
||||||
).isEqualTo(
|
).isEqualTo(
|
||||||
RoomListFilter.NormalizedMatchRoomName("Search")
|
RoomListFilter.NormalizedMatchRoomName("Search")
|
||||||
)
|
)
|
||||||
|
|
@ -79,7 +85,7 @@ class RoomListSearchPresenterTest {
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.query.text.toString()).isEmpty()
|
assertThat(state.query.text.toString()).isEmpty()
|
||||||
assertThat(
|
assertThat(
|
||||||
roomListService.allRooms.currentFilter.value
|
roomList.currentFilter.value
|
||||||
).isEqualTo(
|
).isEqualTo(
|
||||||
RoomListFilter.None
|
RoomListFilter.None
|
||||||
)
|
)
|
||||||
|
|
@ -89,24 +95,51 @@ class RoomListSearchPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - room list changes`() = runTest {
|
fun `present - room list changes`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList()
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val presenter = createRoomListSearchPresenter(roomListService)
|
val presenter = createRoomListSearchPresenter(roomListService)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.results).isEmpty()
|
assertThat(state.results).isEmpty()
|
||||||
}
|
}
|
||||||
roomListService.postAllRooms(
|
roomList.summaries.emit(
|
||||||
listOf(aRoomSummary())
|
listOf(aRoomSummary())
|
||||||
)
|
)
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.results).hasSize(1)
|
assertThat(state.results).hasSize(1)
|
||||||
}
|
}
|
||||||
roomListService.postAllRooms(emptyList())
|
roomList.summaries.emit(emptyList())
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.results).isEmpty()
|
assertThat(state.results).isEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - UpdateVisibleRange triggers pagination when near end`() = runTest {
|
||||||
|
val loadMoreLambda = lambdaRecorder<Unit> { }
|
||||||
|
val roomList = FakeDynamicRoomList(loadMoreLambda = loadMoreLambda)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
|
val presenter = createRoomListSearchPresenter(roomListService)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
// Post some rooms to simulate loaded content
|
||||||
|
val rooms = (1..10).map { aRoomSummary() }
|
||||||
|
roomList.summaries.emit(rooms)
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
|
// UpdateVisibleRange near end should trigger loadMore
|
||||||
|
initialState.eventSink(RoomListSearchEvent.UpdateVisibleRange(IntRange(0, 9)))
|
||||||
|
// Give time for the coroutine to complete
|
||||||
|
testScheduler.advanceUntilIdle()
|
||||||
|
|
||||||
|
assert(loadMoreLambda).isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TestScope.createRoomListSearchPresenter(
|
fun TestScope.createRoomListSearchPresenter(
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,8 @@ class LinkNewDeviceFlowNode(
|
||||||
is ErrorType.Unknown -> ErrorScreenType.UnknownError
|
is ErrorType.Unknown -> ErrorScreenType.UnknownError
|
||||||
is ErrorType.UnsupportedProtocol -> ErrorScreenType.UnknownError
|
is ErrorType.UnsupportedProtocol -> ErrorScreenType.UnknownError
|
||||||
}
|
}
|
||||||
// It is OK to push on backstack, since when user leaves the error screen, a new root will be set
|
// It is OK to push on backstack, since when user leaves the error screen, a new root will be set,
|
||||||
|
// or the whole flow will be popped.
|
||||||
backstack.push(NavTarget.Error(error))
|
backstack.push(NavTarget.Error(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -263,6 +264,12 @@ class LinkNewDeviceFlowNode(
|
||||||
linkNewDesktopHandler.reset()
|
linkNewDesktopHandler.reset()
|
||||||
backstack.newRoot(NavTarget.Root)
|
backstack.newRoot(NavTarget.Root)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCancel() {
|
||||||
|
linkNewMobileHandler.reset()
|
||||||
|
linkNewDesktopHandler.reset()
|
||||||
|
callback.onDone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createNode<ErrorNode>(buildContext, listOf(callback, navTarget.errorScreenType))
|
createNode<ErrorNode>(buildContext, listOf(callback, navTarget.errorScreenType))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ class ErrorNode(
|
||||||
) : Node(buildContext = buildContext, plugins = plugins) {
|
) : Node(buildContext = buildContext, plugins = plugins) {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onRetry()
|
fun onRetry()
|
||||||
|
fun onCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback: Callback = callback()
|
private val callback: Callback = callback()
|
||||||
|
|
@ -38,6 +39,7 @@ class ErrorNode(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
errorScreenType = errorScreenType,
|
errorScreenType = errorScreenType,
|
||||||
onRetry = callback::onRetry,
|
onRetry = callback::onRetry,
|
||||||
|
onCancel = callback::onCancel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
|
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -41,17 +42,23 @@ import kotlinx.collections.immutable.persistentListOf
|
||||||
fun ErrorView(
|
fun ErrorView(
|
||||||
errorScreenType: ErrorScreenType,
|
errorScreenType: ErrorScreenType,
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
|
onCancel: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val appName = LocalBuildMeta.current.applicationName
|
val appName = LocalBuildMeta.current.applicationName
|
||||||
BackHandler(onBack = onRetry)
|
BackHandler(onBack = onCancel)
|
||||||
FlowStepPage(
|
FlowStepPage(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
iconStyle = BigIcon.Style.AlertSolid,
|
iconStyle = BigIcon.Style.AlertSolid,
|
||||||
title = titleText(errorScreenType, appName),
|
title = titleText(errorScreenType, appName),
|
||||||
subTitle = subtitleText(errorScreenType, appName),
|
subTitle = subtitleText(errorScreenType, appName),
|
||||||
content = { Content(errorScreenType) },
|
content = { Content(errorScreenType) },
|
||||||
buttons = { Buttons(onRetry) },
|
buttons = {
|
||||||
|
Buttons(
|
||||||
|
onRetry = onRetry,
|
||||||
|
onCancel = onCancel,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,11 +125,19 @@ private fun Content(errorScreenType: ErrorScreenType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Buttons(onRetry: () -> Unit) {
|
private fun Buttons(
|
||||||
|
onRetry: () -> Unit,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
) {
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(CommonStrings.action_start_over),
|
text = stringResource(CommonStrings.action_try_again),
|
||||||
onClick = onRetry
|
onClick = onRetry,
|
||||||
|
)
|
||||||
|
OutlinedButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(CommonStrings.action_cancel),
|
||||||
|
onClick = onCancel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,6 +148,7 @@ internal fun ErrorViewPreview(@PreviewParameter(ErrorScreenTypeProvider::class)
|
||||||
ErrorView(
|
ErrorView(
|
||||||
errorScreenType = errorScreenType,
|
errorScreenType = errorScreenType,
|
||||||
onRetry = {},
|
onRetry = {},
|
||||||
|
onCancel = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
import io.element.android.tests.testutils.clickOn
|
import io.element.android.tests.testutils.clickOn
|
||||||
import io.element.android.tests.testutils.ensureCalledOnce
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
import io.element.android.tests.testutils.pressBackKey
|
import io.element.android.tests.testutils.pressBackKey
|
||||||
|
|
@ -26,33 +27,45 @@ class ErrorViewTest {
|
||||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on back pressed - calls the onRetry callback`() {
|
fun `on back pressed - calls the onCancel callback`() {
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setErrorView(
|
rule.setErrorView(
|
||||||
onRetry = callback
|
onCancel = callback,
|
||||||
)
|
)
|
||||||
rule.pressBackKey()
|
rule.pressBackKey()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on start over button clicked - calls the expected callback`() {
|
fun `on try again button clicked - calls the expected callback`() {
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setErrorView(
|
rule.setErrorView(
|
||||||
onRetry = callback
|
onRetry = callback
|
||||||
)
|
)
|
||||||
rule.clickOn(CommonStrings.action_start_over)
|
rule.clickOn(CommonStrings.action_try_again)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on cancel button clicked - calls the expected callback`() {
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setErrorView(
|
||||||
|
onCancel = callback
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_cancel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setErrorView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setErrorView(
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onCancel: () -> Unit = EnsureNeverCalled(),
|
||||||
errorScreenType: ErrorScreenType = ErrorScreenType.UnknownError,
|
errorScreenType: ErrorScreenType = ErrorScreenType.UnknownError,
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
ErrorView(
|
ErrorView(
|
||||||
errorScreenType = errorScreenType,
|
errorScreenType = errorScreenType,
|
||||||
onRetry = onRetry,
|
onRetry = onRetry,
|
||||||
|
onCancel = onCancel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
|
|
@ -196,7 +197,12 @@ class LoginFlowNode(
|
||||||
createNode<ChooseAccountProviderNode>(buildContext, listOf(callback))
|
createNode<ChooseAccountProviderNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
NavTarget.QrCode -> {
|
NavTarget.QrCode -> {
|
||||||
createNode<QrCodeLoginFlowNode>(buildContext)
|
val callback = object : QrCodeLoginFlowNode.Callback {
|
||||||
|
override fun navigateBack() {
|
||||||
|
backstack.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<QrCodeLoginFlowNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
is NavTarget.ConfirmAccountProvider -> {
|
is NavTarget.ConfirmAccountProvider -> {
|
||||||
val inputs = ConfirmAccountProviderNode.Inputs(
|
val inputs = ConfirmAccountProviderNode.Inputs(
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
|
import io.element.android.libraries.architecture.callback
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||||
|
|
@ -64,6 +65,12 @@ class QrCodeLoginFlowNode(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
), DependencyInjectionGraphOwner {
|
), DependencyInjectionGraphOwner {
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val callback: Callback = callback()
|
||||||
|
|
||||||
private var authenticationJob: Job? = null
|
private var authenticationJob: Job? = null
|
||||||
|
|
||||||
override val graph = qrCodeLoginGraphFactory.create()
|
override val graph = qrCodeLoginGraphFactory.create()
|
||||||
|
|
@ -85,7 +92,6 @@ class QrCodeLoginFlowNode(
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
|
|
||||||
observeLoginStep()
|
observeLoginStep()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,7 +184,13 @@ class QrCodeLoginFlowNode(
|
||||||
}
|
}
|
||||||
is NavTarget.Error -> {
|
is NavTarget.Error -> {
|
||||||
val callback = object : QrCodeErrorNode.Callback {
|
val callback = object : QrCodeErrorNode.Callback {
|
||||||
override fun onRetry() = reset()
|
override fun onRetry() {
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancel() {
|
||||||
|
callback.navigateBack()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createNode<QrCodeErrorNode>(buildContext, plugins = listOf(navTarget.errorType, callback))
|
createNode<QrCodeErrorNode>(buildContext, plugins = listOf(navTarget.errorType, callback))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ class QrCodeErrorNode(
|
||||||
) : Node(buildContext = buildContext, plugins = plugins) {
|
) : Node(buildContext = buildContext, plugins = plugins) {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onRetry()
|
fun onRetry()
|
||||||
|
fun onCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback: Callback = callback()
|
private val callback: Callback = callback()
|
||||||
|
|
@ -43,6 +44,7 @@ class QrCodeErrorNode(
|
||||||
errorScreenType = qrCodeErrorScreenType,
|
errorScreenType = qrCodeErrorScreenType,
|
||||||
appName = buildMeta.productionApplicationName,
|
appName = buildMeta.productionApplicationName,
|
||||||
onRetry = callback::onRetry,
|
onRetry = callback::onRetry,
|
||||||
|
onCancel = callback::onCancel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -44,16 +45,22 @@ fun QrCodeErrorView(
|
||||||
errorScreenType: QrCodeErrorScreenType,
|
errorScreenType: QrCodeErrorScreenType,
|
||||||
appName: String,
|
appName: String,
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
|
onCancel: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
BackHandler(onBack = onRetry)
|
BackHandler(onBack = onCancel)
|
||||||
FlowStepPage(
|
FlowStepPage(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
iconStyle = BigIcon.Style.AlertSolid,
|
iconStyle = BigIcon.Style.AlertSolid,
|
||||||
title = titleText(errorScreenType, appName),
|
title = titleText(errorScreenType, appName),
|
||||||
subTitle = subtitleText(errorScreenType, appName),
|
subTitle = subtitleText(errorScreenType, appName),
|
||||||
content = { Content(errorScreenType) },
|
content = { Content(errorScreenType) },
|
||||||
buttons = { Buttons(onRetry) },
|
buttons = {
|
||||||
|
Buttons(
|
||||||
|
onRetry = onRetry,
|
||||||
|
onCancel = onCancel,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,11 +125,19 @@ private fun Content(errorScreenType: QrCodeErrorScreenType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Buttons(onRetry: () -> Unit) {
|
private fun Buttons(
|
||||||
|
onRetry: () -> Unit,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
) {
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(R.string.screen_qr_code_login_start_over_button),
|
text = stringResource(CommonStrings.action_try_again),
|
||||||
onClick = onRetry
|
onClick = onRetry,
|
||||||
|
)
|
||||||
|
OutlinedButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(CommonStrings.action_cancel),
|
||||||
|
onClick = onCancel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,7 +148,8 @@ internal fun QrCodeErrorViewPreview(@PreviewParameter(QrCodeErrorScreenTypeProvi
|
||||||
QrCodeErrorView(
|
QrCodeErrorView(
|
||||||
errorScreenType = errorScreenType,
|
errorScreenType = errorScreenType,
|
||||||
appName = "Element X",
|
appName = "Element X",
|
||||||
onRetry = {}
|
onRetry = {},
|
||||||
|
onCancel = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData
|
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
@ -183,7 +184,11 @@ class QrCodeLoginFlowNodeTest {
|
||||||
)
|
)
|
||||||
return QrCodeLoginFlowNode(
|
return QrCodeLoginFlowNode(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = emptyList(),
|
plugins = listOf(
|
||||||
|
object : QrCodeLoginFlowNode.Callback {
|
||||||
|
override fun navigateBack() = lambdaError()
|
||||||
|
}
|
||||||
|
),
|
||||||
qrCodeLoginGraphFactory = FakeQrCodeLoginGraph.Builder(qrCodeLoginManager),
|
qrCodeLoginGraphFactory = FakeQrCodeLoginGraph.Builder(qrCodeLoginManager),
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,9 @@ import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.features.login.impl.R
|
|
||||||
import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType
|
import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
import io.element.android.tests.testutils.clickOn
|
import io.element.android.tests.testutils.clickOn
|
||||||
import io.element.android.tests.testutils.ensureCalledOnce
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
import io.element.android.tests.testutils.pressBackKey
|
import io.element.android.tests.testutils.pressBackKey
|
||||||
|
|
@ -28,10 +29,10 @@ class QrCodeErrorViewTest {
|
||||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on back pressed - calls the onRetry callback`() {
|
fun `on back pressed - calls the onCancel callback`() {
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setQrCodeErrorView(
|
rule.setQrCodeErrorView(
|
||||||
onRetry = callback
|
onCancel = callback,
|
||||||
)
|
)
|
||||||
rule.pressBackKey()
|
rule.pressBackKey()
|
||||||
}
|
}
|
||||||
|
|
@ -41,14 +42,25 @@ class QrCodeErrorViewTest {
|
||||||
fun `on try again button clicked - calls the expected callback`() {
|
fun `on try again button clicked - calls the expected callback`() {
|
||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
rule.setQrCodeErrorView(
|
rule.setQrCodeErrorView(
|
||||||
onRetry = callback
|
onRetry = callback,
|
||||||
)
|
)
|
||||||
rule.clickOn(R.string.screen_qr_code_login_start_over_button)
|
rule.clickOn(CommonStrings.action_try_again)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on cancel button clicked - calls the expected callback`() {
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setQrCodeErrorView(
|
||||||
|
onCancel = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_cancel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setQrCodeErrorView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setQrCodeErrorView(
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onCancel: () -> Unit = EnsureNeverCalled(),
|
||||||
errorScreenType: QrCodeErrorScreenType = QrCodeErrorScreenType.UnknownError,
|
errorScreenType: QrCodeErrorScreenType = QrCodeErrorScreenType.UnknownError,
|
||||||
appName: String = "Element X",
|
appName: String = "Element X",
|
||||||
) {
|
) {
|
||||||
|
|
@ -56,7 +68,8 @@ class QrCodeErrorViewTest {
|
||||||
QrCodeErrorView(
|
QrCodeErrorView(
|
||||||
errorScreenType = errorScreenType,
|
errorScreenType = errorScreenType,
|
||||||
appName = appName,
|
appName = appName,
|
||||||
onRetry = onRetry
|
onRetry = onRetry,
|
||||||
|
onCancel = onCancel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -210,10 +210,7 @@ class MessagesPresenter(
|
||||||
// * History sharing is enabled,
|
// * History sharing is enabled,
|
||||||
// * The room is encrypted, and:
|
// * The room is encrypted, and:
|
||||||
// * The room's history_visibility allows future users to see content.
|
// * The room's history_visibility allows future users to see content.
|
||||||
val showSharedHistoryIcon = isKeyShareOnInviteEnabled &&
|
val topBarSharedHistoryIcon = if (isKeyShareOnInviteEnabled) roomInfo.sharedHistoryIcon() else SharedHistoryIcon.NONE
|
||||||
roomInfo.isEncrypted == true &&
|
|
||||||
(roomInfo.historyVisibility == RoomHistoryVisibility.Shared ||
|
|
||||||
roomInfo.historyVisibility == RoomHistoryVisibility.WorldReadable)
|
|
||||||
|
|
||||||
LifecycleResumeEffect(dmRoomMember, roomInfo.isEncrypted) {
|
LifecycleResumeEffect(dmRoomMember, roomInfo.isEncrypted) {
|
||||||
if (roomInfo.isEncrypted == true) {
|
if (roomInfo.isEncrypted == true) {
|
||||||
|
|
@ -297,12 +294,24 @@ class MessagesPresenter(
|
||||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||||
dmUserVerificationState = dmUserVerificationState,
|
dmUserVerificationState = dmUserVerificationState,
|
||||||
roomMemberModerationState = roomMemberModerationState,
|
roomMemberModerationState = roomMemberModerationState,
|
||||||
showSharedHistoryIcon = showSharedHistoryIcon,
|
topBarSharedHistoryIcon = topBarSharedHistoryIcon,
|
||||||
successorRoom = roomInfo.successorRoom,
|
successorRoom = roomInfo.successorRoom,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun RoomInfo.sharedHistoryIcon(): SharedHistoryIcon {
|
||||||
|
if (isEncrypted == true) {
|
||||||
|
if (historyVisibility == RoomHistoryVisibility.Shared) {
|
||||||
|
return SharedHistoryIcon.SHARED
|
||||||
|
} else if (historyVisibility == RoomHistoryVisibility.WorldReadable) {
|
||||||
|
return SharedHistoryIcon.WORLD_READABLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SharedHistoryIcon.NONE
|
||||||
|
}
|
||||||
|
|
||||||
private fun RoomInfo.avatarData(): AvatarData {
|
private fun RoomInfo.avatarData(): AvatarData {
|
||||||
return AvatarData(
|
return AvatarData(
|
||||||
id = id.value,
|
id = id.value,
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,22 @@ data class MessagesState(
|
||||||
val pinnedMessagesBannerState: PinnedMessagesBannerState,
|
val pinnedMessagesBannerState: PinnedMessagesBannerState,
|
||||||
val dmUserVerificationState: IdentityState?,
|
val dmUserVerificationState: IdentityState?,
|
||||||
val roomMemberModerationState: RoomMemberModerationState,
|
val roomMemberModerationState: RoomMemberModerationState,
|
||||||
/** Should the top bar include the "history" icon? */
|
/** Type of "shared history" icon to show in the top bar. */
|
||||||
val showSharedHistoryIcon: Boolean,
|
val topBarSharedHistoryIcon: SharedHistoryIcon,
|
||||||
val successorRoom: SuccessorRoom?,
|
val successorRoom: SuccessorRoom?,
|
||||||
val eventSink: (MessagesEvent) -> Unit
|
val eventSink: (MessagesEvent) -> Unit
|
||||||
) {
|
) {
|
||||||
val isTombstoned = successorRoom != null
|
val isTombstoned = successorRoom != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Type of "shared history" icon to show in the top bar. */
|
||||||
|
enum class SharedHistoryIcon {
|
||||||
|
/** Show no icon at all. */
|
||||||
|
NONE,
|
||||||
|
|
||||||
|
/** history_visibility: shared. */
|
||||||
|
SHARED,
|
||||||
|
|
||||||
|
/** history_visibility: world_readable. */
|
||||||
|
WORLD_READABLE
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ fun aMessagesState(
|
||||||
pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(),
|
pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(),
|
||||||
dmUserVerificationState: IdentityState? = null,
|
dmUserVerificationState: IdentityState? = null,
|
||||||
roomMemberModerationState: RoomMemberModerationState = aRoomMemberModerationState(),
|
roomMemberModerationState: RoomMemberModerationState = aRoomMemberModerationState(),
|
||||||
showSharedHistoryIcon: Boolean = false,
|
topBarSharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE,
|
||||||
successorRoom: SuccessorRoom? = null,
|
successorRoom: SuccessorRoom? = null,
|
||||||
eventSink: (MessagesEvent) -> Unit = {},
|
eventSink: (MessagesEvent) -> Unit = {},
|
||||||
) = MessagesState(
|
) = MessagesState(
|
||||||
|
|
@ -148,7 +148,7 @@ fun aMessagesState(
|
||||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||||
dmUserVerificationState = dmUserVerificationState,
|
dmUserVerificationState = dmUserVerificationState,
|
||||||
roomMemberModerationState = roomMemberModerationState,
|
roomMemberModerationState = roomMemberModerationState,
|
||||||
showSharedHistoryIcon = showSharedHistoryIcon,
|
topBarSharedHistoryIcon = topBarSharedHistoryIcon,
|
||||||
successorRoom = successorRoom,
|
successorRoom = successorRoom,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,7 @@ fun MessagesView(
|
||||||
heroes = state.heroes,
|
heroes = state.heroes,
|
||||||
roomCallState = state.roomCallState,
|
roomCallState = state.roomCallState,
|
||||||
dmUserIdentityState = state.dmUserVerificationState,
|
dmUserIdentityState = state.dmUserVerificationState,
|
||||||
showSharedHistoryIcon = state.showSharedHistoryIcon,
|
sharedHistoryIcon = state.topBarSharedHistoryIcon,
|
||||||
onBackClick = { hidingKeyboard { onBackClick() } },
|
onBackClick = { hidingKeyboard { onBackClick() } },
|
||||||
onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
|
onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
|
||||||
onJoinCallClick = onJoinCallClick,
|
onJoinCallClick = onJoinCallClick,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.lifecycle.subscribe
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
|
|
@ -29,6 +30,7 @@ import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.messages.impl.MessagesNavigator
|
import io.element.android.features.messages.impl.MessagesNavigator
|
||||||
import io.element.android.features.messages.impl.MessagesPresenter
|
import io.element.android.features.messages.impl.MessagesPresenter
|
||||||
|
import io.element.android.features.messages.impl.MessagesState
|
||||||
import io.element.android.features.messages.impl.MessagesView
|
import io.element.android.features.messages.impl.MessagesView
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
|
||||||
|
|
@ -44,11 +46,11 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||||
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.callback
|
import io.element.android.libraries.architecture.callback
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
|
||||||
import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom
|
import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -67,22 +69,19 @@ import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class ThreadedMessagesNode(
|
class ThreadedMessagesNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
|
||||||
private val room: JoinedRoom,
|
private val room: JoinedRoom,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
messageComposerPresenterFactory: MessageComposerPresenter.Factory,
|
private val messageComposerPresenterFactory: MessageComposerPresenter.Factory,
|
||||||
timelinePresenterFactory: TimelinePresenter.Factory,
|
private val timelinePresenterFactory: TimelinePresenter.Factory,
|
||||||
presenterFactory: MessagesPresenter.Factory,
|
private val presenterFactory: MessagesPresenter.Factory,
|
||||||
actionListPresenterFactory: ActionListPresenter.Factory,
|
private val actionListPresenterFactory: ActionListPresenter.Factory,
|
||||||
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
||||||
private val mediaPlayer: MediaPlayer,
|
private val mediaPlayer: MediaPlayer,
|
||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
|
|
@ -96,20 +95,29 @@ class ThreadedMessagesNode(
|
||||||
private val inputs = inputs<Inputs>()
|
private val inputs = inputs<Inputs>()
|
||||||
private val callback: Callback = callback()
|
private val callback: Callback = callback()
|
||||||
|
|
||||||
// TODO use a loading state node to preload this instead of using `runBlocking`
|
private var timelineController: TimelineController? by mutableStateOf(null)
|
||||||
private val threadedTimeline = runBlocking { room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = inputs.threadRootEventId)).getOrThrow() }
|
private var presenter: Presenter<MessagesState>? by mutableStateOf(null)
|
||||||
private val timelineController = TimelineController(room, threadedTimeline)
|
|
||||||
private val presenter = presenterFactory.create(
|
/**
|
||||||
navigator = this,
|
* This should be fast to load, but not faster than several UI frames, which will cause ANRs.
|
||||||
composerPresenter = messageComposerPresenterFactory.create(timelineController, this),
|
* We'll load the [presenter] in an async way to prevent this.
|
||||||
timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this),
|
*/
|
||||||
// TODO add special processor for threaded timeline
|
private suspend fun createPresenter(): Presenter<MessagesState> {
|
||||||
actionListPresenter = actionListPresenterFactory.create(
|
val threadedTimeline = room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = inputs.threadRootEventId)).getOrThrow()
|
||||||
postProcessor = TimelineItemActionPostProcessor.Default,
|
val timelineController = TimelineController(room, threadedTimeline)
|
||||||
timelineMode = timelineController.mainTimelineMode(),
|
this.timelineController = timelineController
|
||||||
),
|
return presenterFactory.create(
|
||||||
timelineController = timelineController,
|
navigator = this,
|
||||||
)
|
composerPresenter = messageComposerPresenterFactory.create(timelineController, this),
|
||||||
|
timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this),
|
||||||
|
// TODO add special processor for threaded timeline
|
||||||
|
actionListPresenter = actionListPresenterFactory.create(
|
||||||
|
postProcessor = TimelineItemActionPostProcessor.Default,
|
||||||
|
timelineMode = timelineController.mainTimelineMode(),
|
||||||
|
),
|
||||||
|
timelineController = timelineController,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean
|
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean
|
||||||
|
|
@ -130,7 +138,10 @@ class ThreadedMessagesNode(
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
onCreate = {
|
onCreate = {
|
||||||
sessionCoroutineScope.launch { analyticsService.capture(room.toAnalyticsViewRoom()) }
|
analyticsService.capture(room.toAnalyticsViewRoom())
|
||||||
|
lifecycleScope.launch {
|
||||||
|
presenter = createPresenter()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onStart = {
|
onStart = {
|
||||||
appNavigationStateService.onNavigateToThread(id, inputs.threadRootEventId)
|
appNavigationStateService.onNavigateToThread(id, inputs.threadRootEventId)
|
||||||
|
|
@ -231,56 +242,61 @@ class ThreadedMessagesNode(
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
||||||
) {
|
) {
|
||||||
val state = presenter.present()
|
// Only display the actual UI and lifecycle logic if the presenter is loaded
|
||||||
OnLifecycleEvent { _, event ->
|
presenter?.present()?.let { state ->
|
||||||
when (event) {
|
OnLifecycleEvent { _, event ->
|
||||||
Lifecycle.Event.ON_PAUSE -> state.composerState.eventSink(MessageComposerEvent.SaveDraft)
|
when (event) {
|
||||||
else -> Unit
|
Lifecycle.Event.ON_PAUSE -> state.composerState.eventSink(MessageComposerEvent.SaveDraft)
|
||||||
}
|
else -> Unit
|
||||||
}
|
|
||||||
MessagesView(
|
|
||||||
state = state,
|
|
||||||
onBackClick = this::navigateUp,
|
|
||||||
onRoomDetailsClick = {},
|
|
||||||
onEventContentClick = { isLive, event ->
|
|
||||||
if (isLive) {
|
|
||||||
callback.handleEventClick(timelineController.mainTimelineMode(), event)
|
|
||||||
} else {
|
|
||||||
val detachedTimelineMode = timelineController.detachedTimelineMode()
|
|
||||||
if (detachedTimelineMode != null) {
|
|
||||||
callback.handleEventClick(detachedTimelineMode, event)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onUserDataClick = callback::navigateToRoomMemberDetails,
|
|
||||||
onLinkClick = { url, customTab ->
|
|
||||||
onLinkClick(
|
|
||||||
activity = activity,
|
|
||||||
darkTheme = isDark,
|
|
||||||
url = url,
|
|
||||||
eventSink = state.timelineState.eventSink,
|
|
||||||
customTab = customTab,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onSendLocationClick = callback::navigateToSendLocation,
|
|
||||||
onCreatePollClick = callback::navigateToCreatePoll,
|
|
||||||
onJoinCallClick = { callback.navigateToRoomCall(room.roomId) },
|
|
||||||
onViewAllPinnedMessagesClick = {},
|
|
||||||
modifier = modifier,
|
|
||||||
knockRequestsBannerView = {},
|
|
||||||
)
|
|
||||||
|
|
||||||
var focusedEventId by rememberSaveable {
|
|
||||||
mutableStateOf(inputs.focusedEventId)
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
focusedEventId?.also { eventId ->
|
|
||||||
state.timelineState.eventSink(TimelineEvent.FocusOnEvent(eventId))
|
|
||||||
}
|
}
|
||||||
// Reset the focused event id to null to avoid refocusing when restoring node.
|
|
||||||
focusedEventId = null
|
MessagesView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = this::navigateUp,
|
||||||
|
onRoomDetailsClick = {},
|
||||||
|
onEventContentClick = { isLive, event ->
|
||||||
|
timelineController?.let { controller ->
|
||||||
|
if (isLive) {
|
||||||
|
callback.handleEventClick(controller.mainTimelineMode(), event)
|
||||||
|
} else {
|
||||||
|
val detachedTimelineMode = controller.detachedTimelineMode()
|
||||||
|
if (detachedTimelineMode != null) {
|
||||||
|
callback.handleEventClick(detachedTimelineMode, event)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} == true
|
||||||
|
},
|
||||||
|
onUserDataClick = callback::navigateToRoomMemberDetails,
|
||||||
|
onLinkClick = { url, customTab ->
|
||||||
|
onLinkClick(
|
||||||
|
activity = activity,
|
||||||
|
darkTheme = isDark,
|
||||||
|
url = url,
|
||||||
|
eventSink = state.timelineState.eventSink,
|
||||||
|
customTab = customTab,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSendLocationClick = callback::navigateToSendLocation,
|
||||||
|
onCreatePollClick = callback::navigateToCreatePoll,
|
||||||
|
onJoinCallClick = { callback.navigateToRoomCall(room.roomId) },
|
||||||
|
onViewAllPinnedMessagesClick = {},
|
||||||
|
modifier = modifier,
|
||||||
|
knockRequestsBannerView = {},
|
||||||
|
)
|
||||||
|
|
||||||
|
var focusedEventId by rememberSaveable {
|
||||||
|
mutableStateOf(inputs.focusedEventId)
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusedEventId?.also { eventId ->
|
||||||
|
state.timelineState.eventSink(TimelineEvent.FocusOnEvent(eventId))
|
||||||
|
}
|
||||||
|
// Reset the focused event id to null to avoid refocusing when restoring node.
|
||||||
|
focusedEventId = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.messages.impl.SharedHistoryIcon
|
||||||
import io.element.android.features.messages.impl.timeline.components.CallMenuItem
|
import io.element.android.features.messages.impl.timeline.components.CallMenuItem
|
||||||
import io.element.android.features.roomcall.api.RoomCallState
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
import io.element.android.features.roomcall.api.aStandByCallState
|
import io.element.android.features.roomcall.api.aStandByCallState
|
||||||
|
|
@ -63,7 +64,7 @@ internal fun MessagesViewTopBar(
|
||||||
heroes: ImmutableList<AvatarData>,
|
heroes: ImmutableList<AvatarData>,
|
||||||
roomCallState: RoomCallState,
|
roomCallState: RoomCallState,
|
||||||
dmUserIdentityState: IdentityState?,
|
dmUserIdentityState: IdentityState?,
|
||||||
showSharedHistoryIcon: Boolean,
|
sharedHistoryIcon: SharedHistoryIcon,
|
||||||
onRoomDetailsClick: () -> Unit,
|
onRoomDetailsClick: () -> Unit,
|
||||||
onJoinCallClick: () -> Unit,
|
onJoinCallClick: () -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
|
|
@ -110,12 +111,18 @@ internal fun MessagesViewTopBar(
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showSharedHistoryIcon) {
|
when (sharedHistoryIcon) {
|
||||||
Icon(
|
SharedHistoryIcon.NONE -> Unit
|
||||||
|
SharedHistoryIcon.SHARED -> Icon(
|
||||||
imageVector = CompoundIcons.History(),
|
imageVector = CompoundIcons.History(),
|
||||||
tint = ElementTheme.colors.iconInfoPrimary,
|
tint = ElementTheme.colors.iconInfoPrimary,
|
||||||
contentDescription = stringResource(CommonStrings.common_shared_history),
|
contentDescription = stringResource(CommonStrings.common_shared_history),
|
||||||
)
|
)
|
||||||
|
SharedHistoryIcon.WORLD_READABLE -> Icon(
|
||||||
|
imageVector = CompoundIcons.UserProfileSolid(),
|
||||||
|
tint = ElementTheme.colors.iconInfoPrimary,
|
||||||
|
contentDescription = stringResource(CommonStrings.common_world_readable_history),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -178,7 +185,7 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
|
||||||
heroes: ImmutableList<AvatarData> = persistentListOf(),
|
heroes: ImmutableList<AvatarData> = persistentListOf(),
|
||||||
roomCallState: RoomCallState = RoomCallState.Unavailable,
|
roomCallState: RoomCallState = RoomCallState.Unavailable,
|
||||||
dmUserIdentityState: IdentityState? = null,
|
dmUserIdentityState: IdentityState? = null,
|
||||||
showSharedHistoryIcon: Boolean = false,
|
sharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE,
|
||||||
) = MessagesViewTopBar(
|
) = MessagesViewTopBar(
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
roomAvatar = roomAvatar,
|
roomAvatar = roomAvatar,
|
||||||
|
|
@ -186,7 +193,7 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
|
||||||
heroes = heroes,
|
heroes = heroes,
|
||||||
roomCallState = roomCallState,
|
roomCallState = roomCallState,
|
||||||
dmUserIdentityState = dmUserIdentityState,
|
dmUserIdentityState = dmUserIdentityState,
|
||||||
showSharedHistoryIcon = showSharedHistoryIcon,
|
sharedHistoryIcon = sharedHistoryIcon,
|
||||||
onRoomDetailsClick = {},
|
onRoomDetailsClick = {},
|
||||||
onJoinCallClick = {},
|
onJoinCallClick = {},
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
|
|
@ -223,7 +230,12 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
|
||||||
AMessagesViewTopBar(
|
AMessagesViewTopBar(
|
||||||
roomName = "A DM with shared history",
|
roomName = "A DM with shared history",
|
||||||
dmUserIdentityState = IdentityState.Verified,
|
dmUserIdentityState = IdentityState.Verified,
|
||||||
showSharedHistoryIcon = true,
|
sharedHistoryIcon = SharedHistoryIcon.SHARED,
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
AMessagesViewTopBar(
|
||||||
|
roomName = "A room with world_readable history",
|
||||||
|
sharedHistoryIcon = SharedHistoryIcon.WORLD_READABLE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
<string name="screen_report_content_explanation">"Teade selle sõnumi kohta edastatakse sinu koduserveri haldajale. Haldajal ei ole võimalik lugeda krüptitud sõnumite sisu."</string>
|
<string name="screen_report_content_explanation">"Teade selle sõnumi kohta edastatakse sinu koduserveri haldajale. Haldajal ei ole võimalik lugeda krüptitud sõnumite sisu."</string>
|
||||||
<string name="screen_report_content_hint">"Sellest sisust teatamise põhjus"</string>
|
<string name="screen_report_content_hint">"Sellest sisust teatamise põhjus"</string>
|
||||||
<string name="screen_room_attachment_source_camera">"Kaamera"</string>
|
<string name="screen_room_attachment_source_camera">"Kaamera"</string>
|
||||||
<string name="screen_room_attachment_source_camera_photo">"Tee pilt"</string>
|
<string name="screen_room_attachment_source_camera_photo">"Pildista"</string>
|
||||||
<string name="screen_room_attachment_source_camera_video">"Salvesta video"</string>
|
<string name="screen_room_attachment_source_camera_video">"Salvesta video"</string>
|
||||||
<string name="screen_room_attachment_source_files">"Manus"</string>
|
<string name="screen_room_attachment_source_files">"Manus"</string>
|
||||||
<string name="screen_room_attachment_source_gallery">"Fotode ja videote galerii"</string>
|
<string name="screen_room_attachment_source_gallery">"Fotode ja videote galerii"</string>
|
||||||
|
|
|
||||||
|
|
@ -1217,7 +1217,7 @@ class MessagesPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - shows a "history" icon if the room is encrypted and history is shared`() = runTest {
|
fun `present - shows a history icon if the room is encrypted and history is shared`() = runTest {
|
||||||
val presenter = createMessagesPresenter(
|
val presenter = createMessagesPresenter(
|
||||||
joinedRoom = FakeJoinedRoom(
|
joinedRoom = FakeJoinedRoom(
|
||||||
baseRoom = FakeBaseRoom(
|
baseRoom = FakeBaseRoom(
|
||||||
|
|
@ -1233,7 +1233,28 @@ class MessagesPresenterTest {
|
||||||
awaitItem()
|
awaitItem()
|
||||||
runCurrent()
|
runCurrent()
|
||||||
val state = awaitItem()
|
val state = awaitItem()
|
||||||
assertThat(state.showSharedHistoryIcon).isTrue()
|
assertThat(state.topBarSharedHistoryIcon).isEqualTo(SharedHistoryIcon.SHARED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - shows a "world_readable" icon if the room is encrypted and history is world_readable`() = runTest {
|
||||||
|
val presenter = createMessagesPresenter(
|
||||||
|
joinedRoom = FakeJoinedRoom(
|
||||||
|
baseRoom = FakeBaseRoom(
|
||||||
|
roomPermissions = roomPermissions(),
|
||||||
|
initialRoomInfo = aRoomInfo(isEncrypted = true, historyVisibility = RoomHistoryVisibility.WorldReadable),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.EnableKeyShareOnInvite.key to true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
presenter.testWithLifecycleOwner {
|
||||||
|
awaitItem()
|
||||||
|
runCurrent()
|
||||||
|
val state = awaitItem()
|
||||||
|
assertThat(state.topBarSharedHistoryIcon).isEqualTo(SharedHistoryIcon.WORLD_READABLE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import io.element.android.features.messages.impl.messagecomposer.suggestions.Roo
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
|
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -22,7 +23,10 @@ import org.junit.Test
|
||||||
class DefaultRoomAliasSuggestionsDataSourceTest {
|
class DefaultRoomAliasSuggestionsDataSourceTest {
|
||||||
@Test
|
@Test
|
||||||
fun `getAllRoomAliasSuggestions must emit a list of room alias suggestions`() = runTest {
|
fun `getAllRoomAliasSuggestions must emit a list of room alias suggestions`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList()
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val sut = DefaultRoomAliasSuggestionsDataSource(
|
val sut = DefaultRoomAliasSuggestionsDataSource(
|
||||||
roomListService
|
roomListService
|
||||||
)
|
)
|
||||||
|
|
@ -31,7 +35,7 @@ class DefaultRoomAliasSuggestionsDataSourceTest {
|
||||||
)
|
)
|
||||||
sut.getAllRoomAliasSuggestions().test {
|
sut.getAllRoomAliasSuggestions().test {
|
||||||
assertThat(awaitItem()).isEmpty()
|
assertThat(awaitItem()).isEmpty()
|
||||||
roomListService.postAllRooms(
|
roomList.summaries.emit(
|
||||||
listOf(
|
listOf(
|
||||||
aRoomSummary(roomId = A_ROOM_ID_2, canonicalAlias = null),
|
aRoomSummary(roomId = A_ROOM_ID_2, canonicalAlias = null),
|
||||||
aRoomSummaryWithAnAlias,
|
aRoomSummaryWithAnAlias,
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,12 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -53,10 +55,14 @@ class EditDefaultNotificationSettingsPresenterTest {
|
||||||
initialRoomModeIsDefault = false,
|
initialRoomModeIsDefault = false,
|
||||||
getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID)) },
|
getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID)) },
|
||||||
)
|
)
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList(
|
||||||
|
summaries = MutableStateFlow(listOf(aRoomSummary(userDefinedNotificationMode = RoomNotificationMode.ALL_MESSAGES)))
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
|
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
roomListService.postAllRooms(listOf(aRoomSummary(userDefinedNotificationMode = RoomNotificationMode.ALL_MESSAGES)))
|
|
||||||
val loadedState = consumeItemsUntilPredicate { state ->
|
val loadedState = consumeItemsUntilPredicate { state ->
|
||||||
state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.ALL_MESSAGES }
|
state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.ALL_MESSAGES }
|
||||||
}.last()
|
}.last()
|
||||||
|
|
@ -71,10 +77,8 @@ class EditDefaultNotificationSettingsPresenterTest {
|
||||||
initialRoomModeIsDefault = false,
|
initialRoomModeIsDefault = false,
|
||||||
getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID, A_ROOM_ID_2)) },
|
getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID, A_ROOM_ID_2)) },
|
||||||
)
|
)
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList(
|
||||||
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
|
summaries = MutableStateFlow(
|
||||||
presenter.test {
|
|
||||||
roomListService.postAllRooms(
|
|
||||||
listOf(
|
listOf(
|
||||||
aRoomSummary(
|
aRoomSummary(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -86,8 +90,14 @@ class EditDefaultNotificationSettingsPresenterTest {
|
||||||
name = "A",
|
name = "A",
|
||||||
userDefinedNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
userDefinedNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
|
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
|
||||||
|
presenter.test {
|
||||||
val loadedState = consumeItemsUntilPredicate { state ->
|
val loadedState = consumeItemsUntilPredicate { state ->
|
||||||
state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY }
|
state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY }
|
||||||
}.last()
|
}.last()
|
||||||
|
|
@ -103,10 +113,8 @@ class EditDefaultNotificationSettingsPresenterTest {
|
||||||
initialRoomModeIsDefault = false,
|
initialRoomModeIsDefault = false,
|
||||||
getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID, A_ROOM_ID_2)) },
|
getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID, A_ROOM_ID_2)) },
|
||||||
)
|
)
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList(
|
||||||
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
|
summaries = MutableStateFlow(
|
||||||
presenter.test {
|
|
||||||
roomListService.postAllRooms(
|
|
||||||
listOf(
|
listOf(
|
||||||
aRoomSummary(
|
aRoomSummary(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -118,8 +126,14 @@ class EditDefaultNotificationSettingsPresenterTest {
|
||||||
name = null,
|
name = null,
|
||||||
userDefinedNotificationMode = RoomNotificationMode.MUTE,
|
userDefinedNotificationMode = RoomNotificationMode.MUTE,
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
|
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
|
||||||
|
presenter.test {
|
||||||
val loadedState = consumeItemsUntilPredicate { state ->
|
val loadedState = consumeItemsUntilPredicate { state ->
|
||||||
state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.MUTE }
|
state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.MUTE }
|
||||||
}.last()
|
}.last()
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_room_change_permissions_administrators">"Endast administratörer"</string>
|
<string name="screen_room_change_permissions_administrators">"Admin"</string>
|
||||||
<string name="screen_room_change_permissions_ban_people">"Banna personer"</string>
|
<string name="screen_room_change_permissions_ban_people">"Banna personer"</string>
|
||||||
<string name="screen_room_change_permissions_delete_messages">"Ta bort meddelanden"</string>
|
<string name="screen_room_change_permissions_delete_messages">"Ta bort meddelanden"</string>
|
||||||
<string name="screen_room_change_permissions_invite_people">"Bjuda in personer och acceptera förfrågningar om att gå med"</string>
|
<string name="screen_room_change_permissions_everyone">"Medlem"</string>
|
||||||
|
<string name="screen_room_change_permissions_invite_people">"Bjud in personer"</string>
|
||||||
|
<string name="screen_room_change_permissions_member_moderation">"Hantera medlemmar"</string>
|
||||||
<string name="screen_room_change_permissions_messages_and_content">"Meddelanden och innehåll"</string>
|
<string name="screen_room_change_permissions_messages_and_content">"Meddelanden och innehåll"</string>
|
||||||
<string name="screen_room_change_permissions_moderators">"Administratörer och moderatorer"</string>
|
<string name="screen_room_change_permissions_moderators">"Moderator"</string>
|
||||||
<string name="screen_room_change_permissions_remove_people">"Ta bort personer och avslå förfrågningar om att gå med"</string>
|
<string name="screen_room_change_permissions_remove_people">"Ta bort personer"</string>
|
||||||
<string name="screen_room_change_permissions_room_avatar">"Byt rumsavatar"</string>
|
<string name="screen_room_change_permissions_room_avatar">"Byt rumsavatar"</string>
|
||||||
<string name="screen_room_change_permissions_room_details">"Redigera rummet"</string>
|
<string name="screen_room_change_permissions_room_details">"Redigera detaljer"</string>
|
||||||
<string name="screen_room_change_permissions_room_name">"Byt rumsnamn"</string>
|
<string name="screen_room_change_permissions_room_name">"Byt rumsnamn"</string>
|
||||||
<string name="screen_room_change_permissions_room_topic">"Byt rumsämne"</string>
|
<string name="screen_room_change_permissions_room_topic">"Byt rumsämne"</string>
|
||||||
<string name="screen_room_change_permissions_send_messages">"Skicka meddelanden"</string>
|
<string name="screen_room_change_permissions_send_messages">"Skicka meddelanden"</string>
|
||||||
|
|
@ -31,10 +33,10 @@
|
||||||
<string name="screen_room_change_role_section_users">"Medlemmar"</string>
|
<string name="screen_room_change_role_section_users">"Medlemmar"</string>
|
||||||
<string name="screen_room_change_role_unsaved_changes_description">"Du har osparade ändringar."</string>
|
<string name="screen_room_change_role_unsaved_changes_description">"Du har osparade ändringar."</string>
|
||||||
<string name="screen_room_change_role_unsaved_changes_title">"Spara ändringar?"</string>
|
<string name="screen_room_change_role_unsaved_changes_title">"Spara ändringar?"</string>
|
||||||
<string name="screen_room_member_list_banned_empty">"Det finns inga bannade användare i det här rummet."</string>
|
<string name="screen_room_member_list_banned_empty">"Det finns inga bannade användare."</string>
|
||||||
<plurals name="screen_room_member_list_header_title">
|
<plurals name="screen_room_member_list_header_title">
|
||||||
<item quantity="one">"%1$d person"</item>
|
<item quantity="one">"%1$d Person"</item>
|
||||||
<item quantity="other">"%1$d personer"</item>
|
<item quantity="other">"%1$d Personer"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Ta bort och banna medlem"</string>
|
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Ta bort och banna medlem"</string>
|
||||||
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Ta bara bort medlem"</string>
|
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Ta bara bort medlem"</string>
|
||||||
|
|
@ -43,8 +45,8 @@
|
||||||
<string name="screen_room_member_list_manage_member_unban_title">"Avbanna från rummet"</string>
|
<string name="screen_room_member_list_manage_member_unban_title">"Avbanna från rummet"</string>
|
||||||
<string name="screen_room_member_list_mode_banned">"Bannade"</string>
|
<string name="screen_room_member_list_mode_banned">"Bannade"</string>
|
||||||
<string name="screen_room_member_list_mode_members">"Medlemmar"</string>
|
<string name="screen_room_member_list_mode_members">"Medlemmar"</string>
|
||||||
<string name="screen_room_member_list_role_administrator">"Endast administratörer"</string>
|
<string name="screen_room_member_list_role_administrator">"Admin"</string>
|
||||||
<string name="screen_room_member_list_role_moderator">"Administratörer och moderatorer"</string>
|
<string name="screen_room_member_list_role_moderator">"Moderator"</string>
|
||||||
<string name="screen_room_member_list_role_owner">"Ägare"</string>
|
<string name="screen_room_member_list_role_owner">"Ägare"</string>
|
||||||
<string name="screen_room_member_list_room_members_header_title">"Rumsmedlemmar"</string>
|
<string name="screen_room_member_list_room_members_header_title">"Rumsmedlemmar"</string>
|
||||||
<string name="screen_room_member_list_unbanning_user">"Avbannar %1$s"</string>
|
<string name="screen_room_member_list_unbanning_user">"Avbannar %1$s"</string>
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ Me ei soovita krüptimise kasutamist selliste avalike jututubade puhul, millega
|
||||||
<string name="screen_security_and_privacy_encryption_section_header">"Krüptimine"</string>
|
<string name="screen_security_and_privacy_encryption_section_header">"Krüptimine"</string>
|
||||||
<string name="screen_security_and_privacy_encryption_toggle_title">"Võta läbiv krüptimine kasutusele"</string>
|
<string name="screen_security_and_privacy_encryption_toggle_title">"Võta läbiv krüptimine kasutusele"</string>
|
||||||
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Kõik võivad jututoaga liituda"</string>
|
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Kõik võivad jututoaga liituda"</string>
|
||||||
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Avalik"</string>
|
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Kõik"</string>
|
||||||
<string name="screen_security_and_privacy_room_access_footer">"Vali kogukonnad, mille liikmed saavad selle jututoaga liituda ilma kutseta. %1$s"</string>
|
<string name="screen_security_and_privacy_room_access_footer">"Vali kogukonnad, mille liikmed saavad selle jututoaga liituda ilma kutseta. %1$s"</string>
|
||||||
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Halda kogukondi"</string>
|
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Halda kogukondi"</string>
|
||||||
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Liituda saab vaid kutse olemasolul"</string>
|
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Liituda saab vaid kutse olemasolul"</string>
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,17 @@
|
||||||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Ett fel uppstod vid uppdatering av aviseringsinställningen."</string>
|
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Ett fel uppstod vid uppdatering av aviseringsinställningen."</string>
|
||||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Din hemserver stöder inte det här alternativet i krypterade rum, du kanske inte aviseras i vissa rum."</string>
|
<string name="screen_notification_settings_mentions_only_disclaimer">"Din hemserver stöder inte det här alternativet i krypterade rum, du kanske inte aviseras i vissa rum."</string>
|
||||||
<string name="screen_polls_history_title">"Omröstningar"</string>
|
<string name="screen_polls_history_title">"Omröstningar"</string>
|
||||||
<string name="screen_room_change_permissions_administrators">"Endast administratörer"</string>
|
<string name="screen_room_change_permissions_administrators">"Admin"</string>
|
||||||
<string name="screen_room_change_permissions_ban_people">"Banna personer"</string>
|
<string name="screen_room_change_permissions_ban_people">"Banna personer"</string>
|
||||||
<string name="screen_room_change_permissions_delete_messages">"Ta bort meddelanden"</string>
|
<string name="screen_room_change_permissions_delete_messages">"Ta bort meddelanden"</string>
|
||||||
<string name="screen_room_change_permissions_invite_people">"Bjuda in personer och acceptera förfrågningar om att gå med"</string>
|
<string name="screen_room_change_permissions_everyone">"Medlem"</string>
|
||||||
|
<string name="screen_room_change_permissions_invite_people">"Bjud in personer"</string>
|
||||||
|
<string name="screen_room_change_permissions_member_moderation">"Hantera medlemmar"</string>
|
||||||
<string name="screen_room_change_permissions_messages_and_content">"Meddelanden och innehåll"</string>
|
<string name="screen_room_change_permissions_messages_and_content">"Meddelanden och innehåll"</string>
|
||||||
<string name="screen_room_change_permissions_moderators">"Administratörer och moderatorer"</string>
|
<string name="screen_room_change_permissions_moderators">"Moderator"</string>
|
||||||
<string name="screen_room_change_permissions_remove_people">"Ta bort personer och avslå förfrågningar om att gå med"</string>
|
<string name="screen_room_change_permissions_remove_people">"Ta bort personer"</string>
|
||||||
<string name="screen_room_change_permissions_room_avatar">"Byt rumsavatar"</string>
|
<string name="screen_room_change_permissions_room_avatar">"Byt rumsavatar"</string>
|
||||||
<string name="screen_room_change_permissions_room_details">"Redigera rummet"</string>
|
<string name="screen_room_change_permissions_room_details">"Redigera detaljer"</string>
|
||||||
<string name="screen_room_change_permissions_room_name">"Byt rumsnamn"</string>
|
<string name="screen_room_change_permissions_room_name">"Byt rumsnamn"</string>
|
||||||
<string name="screen_room_change_permissions_room_topic">"Byt rumsämne"</string>
|
<string name="screen_room_change_permissions_room_topic">"Byt rumsämne"</string>
|
||||||
<string name="screen_room_change_permissions_send_messages">"Skicka meddelanden"</string>
|
<string name="screen_room_change_permissions_send_messages">"Skicka meddelanden"</string>
|
||||||
|
|
@ -40,7 +42,7 @@
|
||||||
<string name="screen_room_details_badge_encrypted">"Krypterat"</string>
|
<string name="screen_room_details_badge_encrypted">"Krypterat"</string>
|
||||||
<string name="screen_room_details_badge_not_encrypted">"Inte krypterat"</string>
|
<string name="screen_room_details_badge_not_encrypted">"Inte krypterat"</string>
|
||||||
<string name="screen_room_details_badge_public">"Offentligt rum"</string>
|
<string name="screen_room_details_badge_public">"Offentligt rum"</string>
|
||||||
<string name="screen_room_details_edit_room_title">"Redigera rummet"</string>
|
<string name="screen_room_details_edit_room_title">"Redigera detaljer"</string>
|
||||||
<string name="screen_room_details_edition_error">"Ett okänt fel uppstod och informationen kunde inte ändras."</string>
|
<string name="screen_room_details_edition_error">"Ett okänt fel uppstod och informationen kunde inte ändras."</string>
|
||||||
<string name="screen_room_details_edition_error_title">"Kunde inte uppdatera rummet"</string>
|
<string name="screen_room_details_edition_error_title">"Kunde inte uppdatera rummet"</string>
|
||||||
<string name="screen_room_details_encryption_enabled_subtitle">"Meddelanden är säkrade med lås. Bara du och mottagarna har de unika nycklarna för att låsa upp dem."</string>
|
<string name="screen_room_details_encryption_enabled_subtitle">"Meddelanden är säkrade med lås. Bara du och mottagarna har de unika nycklarna för att låsa upp dem."</string>
|
||||||
|
|
@ -65,10 +67,10 @@
|
||||||
<string name="screen_room_details_title">"Rumsinfo"</string>
|
<string name="screen_room_details_title">"Rumsinfo"</string>
|
||||||
<string name="screen_room_details_topic_title">"Ämne"</string>
|
<string name="screen_room_details_topic_title">"Ämne"</string>
|
||||||
<string name="screen_room_details_updating_room">"Uppdaterar rummet …"</string>
|
<string name="screen_room_details_updating_room">"Uppdaterar rummet …"</string>
|
||||||
<string name="screen_room_member_list_banned_empty">"Det finns inga bannade användare i det här rummet."</string>
|
<string name="screen_room_member_list_banned_empty">"Det finns inga bannade användare."</string>
|
||||||
<plurals name="screen_room_member_list_header_title">
|
<plurals name="screen_room_member_list_header_title">
|
||||||
<item quantity="one">"%1$d person"</item>
|
<item quantity="one">"%1$d Person"</item>
|
||||||
<item quantity="other">"%1$d personer"</item>
|
<item quantity="other">"%1$d Personer"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Ta bort och banna medlem"</string>
|
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Ta bort och banna medlem"</string>
|
||||||
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Ta bara bort medlem"</string>
|
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Ta bara bort medlem"</string>
|
||||||
|
|
@ -77,8 +79,8 @@
|
||||||
<string name="screen_room_member_list_manage_member_unban_title">"Avbanna från rummet"</string>
|
<string name="screen_room_member_list_manage_member_unban_title">"Avbanna från rummet"</string>
|
||||||
<string name="screen_room_member_list_mode_banned">"Bannade"</string>
|
<string name="screen_room_member_list_mode_banned">"Bannade"</string>
|
||||||
<string name="screen_room_member_list_mode_members">"Medlemmar"</string>
|
<string name="screen_room_member_list_mode_members">"Medlemmar"</string>
|
||||||
<string name="screen_room_member_list_role_administrator">"Endast administratörer"</string>
|
<string name="screen_room_member_list_role_administrator">"Admin"</string>
|
||||||
<string name="screen_room_member_list_role_moderator">"Administratörer och moderatorer"</string>
|
<string name="screen_room_member_list_role_moderator">"Moderator"</string>
|
||||||
<string name="screen_room_member_list_role_owner">"Ägare"</string>
|
<string name="screen_room_member_list_role_owner">"Ägare"</string>
|
||||||
<string name="screen_room_member_list_room_members_header_title">"Rumsmedlemmar"</string>
|
<string name="screen_room_member_list_room_members_header_title">"Rumsmedlemmar"</string>
|
||||||
<string name="screen_room_member_list_unbanning_user">"Avbannar %1$s"</string>
|
<string name="screen_room_member_list_unbanning_user">"Avbannar %1$s"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_room_details_edit_room_title">"Redigera rummet"</string>
|
<string name="screen_room_details_edit_room_title">"Redigera detaljer"</string>
|
||||||
<string name="screen_room_details_edition_error">"Ett okänt fel uppstod och informationen kunde inte ändras."</string>
|
<string name="screen_room_details_edition_error">"Ett okänt fel uppstod och informationen kunde inte ändras."</string>
|
||||||
<string name="screen_room_details_edition_error_title">"Kunde inte uppdatera rummet"</string>
|
<string name="screen_room_details_edition_error_title">"Kunde inte uppdatera rummet"</string>
|
||||||
<string name="screen_room_details_updating_room">"Uppdaterar rummet …"</string>
|
<string name="screen_room_details_updating_room">"Uppdaterar rummet …"</string>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Denne kommer kunna gå med i rummet igen om denne bjuds in"</string>
|
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Denne kommer kunna gå med i rummet igen om denne bjuds in"</string>
|
||||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Är du säker på att du vill ta bort den här medlemmen?"</string>
|
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Är du säker på att du vill ta bort den här medlemmen?"</string>
|
||||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Visa profil"</string>
|
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Visa profil"</string>
|
||||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Ta bort från rummet"</string>
|
<string name="screen_bottom_sheet_manage_room_member_remove">"Ta bort användare"</string>
|
||||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Ta bort medlem och banna från att gå med i framtiden?"</string>
|
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Ta bort medlem och banna från att gå med i framtiden?"</string>
|
||||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Tar bort %1$s …"</string>
|
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Tar bort %1$s …"</string>
|
||||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Avbanna från rummet"</string>
|
<string name="screen_bottom_sheet_manage_room_member_unban">"Avbanna från rummet"</string>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ Me ei soovita krüptimise kasutamist selliste avalike jututubade puhul, millega
|
||||||
<string name="screen_security_and_privacy_encryption_section_header">"Krüptimine"</string>
|
<string name="screen_security_and_privacy_encryption_section_header">"Krüptimine"</string>
|
||||||
<string name="screen_security_and_privacy_encryption_toggle_title">"Võta läbiv krüptimine kasutusele"</string>
|
<string name="screen_security_and_privacy_encryption_toggle_title">"Võta läbiv krüptimine kasutusele"</string>
|
||||||
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Kõik võivad jututoaga liituda"</string>
|
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Kõik võivad jututoaga liituda"</string>
|
||||||
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Avalik"</string>
|
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Kõik"</string>
|
||||||
<string name="screen_security_and_privacy_room_access_footer">"Vali kogukonnad, mille liikmed saavad selle jututoaga liituda ilma kutseta. %1$s"</string>
|
<string name="screen_security_and_privacy_room_access_footer">"Vali kogukonnad, mille liikmed saavad selle jututoaga liituda ilma kutseta. %1$s"</string>
|
||||||
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Halda kogukondi"</string>
|
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Halda kogukondi"</string>
|
||||||
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Liituda saab vaid kutse olemasolul"</string>
|
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Liituda saab vaid kutse olemasolul"</string>
|
||||||
|
|
|
||||||
|
|
@ -52,4 +52,5 @@ dependencies {
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.features.createroom.test)
|
testImplementation(projects.features.createroom.test)
|
||||||
testImplementation(projects.features.invite.test)
|
testImplementation(projects.features.invite.test)
|
||||||
|
testImplementation(projects.features.rolesandpermissions.test)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,13 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
|
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesEntryPoint
|
||||||
|
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType
|
||||||
import io.element.android.features.space.api.SpaceEntryPoint
|
import io.element.android.features.space.api.SpaceEntryPoint
|
||||||
import io.element.android.features.space.impl.addroom.AddRoomToSpaceNode
|
import io.element.android.features.space.impl.addroom.AddRoomToSpaceNode
|
||||||
import io.element.android.features.space.impl.di.SpaceFlowGraph
|
import io.element.android.features.space.impl.di.SpaceFlowGraph
|
||||||
|
|
@ -38,10 +41,15 @@ import io.element.android.libraries.architecture.callback
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||||
import io.element.android.libraries.matrix.api.spaces.loadAllIncrementally
|
import io.element.android.libraries.matrix.api.spaces.loadAllIncrementally
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
|
|
@ -49,10 +57,12 @@ import kotlinx.parcelize.Parcelize
|
||||||
class SpaceFlowNode(
|
class SpaceFlowNode(
|
||||||
@Assisted val buildContext: BuildContext,
|
@Assisted val buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
room: JoinedRoom,
|
private val room: JoinedRoom,
|
||||||
spaceService: SpaceService,
|
spaceService: SpaceService,
|
||||||
graphFactory: SpaceFlowGraph.Factory,
|
graphFactory: SpaceFlowGraph.Factory,
|
||||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||||
|
private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
|
||||||
|
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||||
) : BaseFlowNode<SpaceFlowNode.NavTarget>(
|
) : BaseFlowNode<SpaceFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Root,
|
initialElement = NavTarget.Root,
|
||||||
|
|
@ -80,6 +90,9 @@ class SpaceFlowNode(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object AddRoom : NavTarget
|
data object AddRoom : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object ChangeOwners : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
|
|
@ -105,6 +118,10 @@ class SpaceFlowNode(
|
||||||
override fun navigateToRolesAndPermissions() {
|
override fun navigateToRolesAndPermissions() {
|
||||||
backstack.push(NavTarget.Settings(SpaceSettingsFlowNode.NavTarget.RolesAndPermissions))
|
backstack.push(NavTarget.Settings(SpaceSettingsFlowNode.NavTarget.RolesAndPermissions))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun navigateToChooseOwners() {
|
||||||
|
backstack.replace(NavTarget.ChangeOwners)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createNode<LeaveSpaceNode>(buildContext, listOf(callback))
|
createNode<LeaveSpaceNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
|
|
@ -177,6 +194,29 @@ class SpaceFlowNode(
|
||||||
}
|
}
|
||||||
createNode<AddRoomToSpaceNode>(buildContext, listOf(callback))
|
createNode<AddRoomToSpaceNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
|
NavTarget.ChangeOwners -> {
|
||||||
|
val node = changeRoomMemberRolesEntryPoint.createNode(
|
||||||
|
parentNode = this,
|
||||||
|
buildContext = buildContext,
|
||||||
|
room = room,
|
||||||
|
listType = ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving,
|
||||||
|
)
|
||||||
|
|
||||||
|
val completionProxy = node as ChangeRoomMemberRolesEntryPoint.NodeProxy
|
||||||
|
sessionCoroutineScope.launch {
|
||||||
|
val changedOwners = withContext(NonCancellable) {
|
||||||
|
completionProxy.waitForCompletion()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedOwners) {
|
||||||
|
backstack.replace(NavTarget.Leave)
|
||||||
|
} else {
|
||||||
|
backstack.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,5 @@ sealed interface AddRoomToSpaceEvent {
|
||||||
data object Save : AddRoomToSpaceEvent
|
data object Save : AddRoomToSpaceEvent
|
||||||
data object ResetSaveAction : AddRoomToSpaceEvent
|
data object ResetSaveAction : AddRoomToSpaceEvent
|
||||||
data object Dismiss : AddRoomToSpaceEvent
|
data object Dismiss : AddRoomToSpaceEvent
|
||||||
|
data class UpdateSearchVisibleRange(val range: IntRange) : AddRoomToSpaceEvent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,6 @@ class AddRoomToSpacePresenter(
|
||||||
LaunchedEffect(searchQuery.text) {
|
LaunchedEffect(searchQuery.text) {
|
||||||
dataSource.setSearchQuery(searchQuery.text.toString())
|
dataSource.setSearchQuery(searchQuery.text.toString())
|
||||||
}
|
}
|
||||||
LaunchedEffect(isSearchActive) {
|
|
||||||
dataSource.setIsActive(isSearchActive)
|
|
||||||
}
|
|
||||||
|
|
||||||
val suggestions by dataSource.suggestions.collectAsState(initial = persistentListOf())
|
val suggestions by dataSource.suggestions.collectAsState(initial = persistentListOf())
|
||||||
|
|
||||||
|
|
@ -111,6 +108,9 @@ class AddRoomToSpacePresenter(
|
||||||
coroutineScope.launch { spaceRoomList.reset() }
|
coroutineScope.launch { spaceRoomList.reset() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is AddRoomToSpaceEvent.UpdateSearchVisibleRange -> coroutineScope.launch {
|
||||||
|
dataSource.updateVisibleRange(event.range)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,13 @@ import io.element.android.libraries.matrix.api.room.recent.getRecentlyVisitedRoo
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
|
import io.element.android.libraries.matrix.api.roomlist.updateVisibleRange
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
@ -58,7 +57,6 @@ class AddRoomToSpaceSearchDataSource(
|
||||||
|
|
||||||
private val roomList = roomListService.createRoomList(
|
private val roomList = roomListService.createRoomList(
|
||||||
pageSize = PAGE_SIZE,
|
pageSize = PAGE_SIZE,
|
||||||
initialFilter = RoomListFilter.all(),
|
|
||||||
source = RoomList.Source.All,
|
source = RoomList.Source.All,
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
)
|
)
|
||||||
|
|
@ -87,7 +85,7 @@ class AddRoomToSpaceSearchDataSource(
|
||||||
}
|
}
|
||||||
|
|
||||||
val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = combine(
|
val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = combine(
|
||||||
roomList.filteredSummaries,
|
roomList.summaries,
|
||||||
spaceChildrenFlow,
|
spaceChildrenFlow,
|
||||||
addedRoomIdsFlow,
|
addedRoomIdsFlow,
|
||||||
) { roomSummaries, childIds, addedIds ->
|
) { roomSummaries, childIds, addedIds ->
|
||||||
|
|
@ -109,12 +107,8 @@ class AddRoomToSpaceSearchDataSource(
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
}.flowOn(coroutineDispatchers.computation)
|
}.flowOn(coroutineDispatchers.computation)
|
||||||
|
|
||||||
suspend fun setIsActive(isActive: Boolean) = coroutineScope {
|
suspend fun updateVisibleRange(visibleRange: IntRange) {
|
||||||
if (isActive) {
|
roomList.updateVisibleRange(visibleRange)
|
||||||
roomList.loadAllIncrementally(this)
|
|
||||||
} else {
|
|
||||||
roomList.reset()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setSearchQuery(searchQuery: String) {
|
suspend fun setSearchQuery(searchQuery: String) {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
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.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
|
@ -43,6 +44,7 @@ import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect
|
||||||
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
||||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
|
|
@ -121,6 +123,10 @@ fun AddRoomToSpaceView(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { rooms ->
|
) { rooms ->
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||||
|
state.eventSink(AddRoomToSpaceEvent.UpdateSearchVisibleRange(visibleRange))
|
||||||
|
}
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
items(rooms, key = { it.roomId }) { roomInfo ->
|
items(rooms, key = { it.roomId }) { roomInfo ->
|
||||||
RoomListItem(
|
RoomListItem(
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ class LeaveSpaceNode(
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun closeLeaveSpaceFlow()
|
fun closeLeaveSpaceFlow()
|
||||||
fun navigateToRolesAndPermissions()
|
fun navigateToRolesAndPermissions()
|
||||||
|
fun navigateToChooseOwners()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val leaveSpaceHandle = matrixClient.spaceService.getLeaveSpaceHandle(room.roomId)
|
private val leaveSpaceHandle = matrixClient.spaceService.getLeaveSpaceHandle(room.roomId)
|
||||||
|
|
@ -57,6 +58,7 @@ class LeaveSpaceNode(
|
||||||
state = state,
|
state = state,
|
||||||
onCancel = callback::closeLeaveSpaceFlow,
|
onCancel = callback::closeLeaveSpaceFlow,
|
||||||
onRolesAndPermissionsClick = callback::navigateToRolesAndPermissions,
|
onRolesAndPermissionsClick = callback::navigateToRolesAndPermissions,
|
||||||
|
onChooseOwnersClick = callback::navigateToChooseOwners,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ class LeaveSpacePresenter(
|
||||||
SelectableSpaceRoom(
|
SelectableSpaceRoom(
|
||||||
spaceRoom = room.spaceRoom,
|
spaceRoom = room.spaceRoom,
|
||||||
isLastOwner = room.isLastOwner,
|
isLastOwner = room.isLastOwner,
|
||||||
|
joinedMembersCount = room.spaceRoom.numJoinedMembers,
|
||||||
isSelected = selectedRoomIds.contains(room.spaceRoom.roomId),
|
isSelected = selectedRoomIds.contains(room.spaceRoom.roomId),
|
||||||
)
|
)
|
||||||
}.toImmutableList()
|
}.toImmutableList()
|
||||||
|
|
@ -130,9 +131,11 @@ class LeaveSpacePresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val currentSpaceToLeave = leaveSpaceRooms.dataOrNull()?.current
|
||||||
return LeaveSpaceState(
|
return LeaveSpaceState(
|
||||||
spaceName = leaveSpaceRooms.dataOrNull()?.current?.spaceRoom?.displayName,
|
spaceName = currentSpaceToLeave?.spaceRoom?.displayName,
|
||||||
isLastOwner = leaveSpaceRooms.dataOrNull()?.current?.isLastOwner == true,
|
needsOwnerChange = currentSpaceToLeave?.let { it.spaceRoom.numJoinedMembers > 1 && it.isLastOwner } == true,
|
||||||
|
areCreatorsPrivileged = currentSpaceToLeave?.areCreatorsPrivileged == true,
|
||||||
selectableSpaceRooms = selectableSpaceRooms,
|
selectableSpaceRooms = selectableSpaceRooms,
|
||||||
leaveSpaceAction = leaveSpaceAction.value,
|
leaveSpaceAction = leaveSpaceAction.value,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
data class LeaveSpaceState(
|
data class LeaveSpaceState(
|
||||||
val spaceName: String?,
|
val spaceName: String?,
|
||||||
val isLastOwner: Boolean,
|
val needsOwnerChange: Boolean,
|
||||||
|
val areCreatorsPrivileged: Boolean,
|
||||||
val selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>>,
|
val selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>>,
|
||||||
val leaveSpaceAction: AsyncAction<Unit>,
|
val leaveSpaceAction: AsyncAction<Unit>,
|
||||||
val eventSink: (LeaveSpaceEvents) -> Unit,
|
val eventSink: (LeaveSpaceEvents) -> Unit,
|
||||||
|
|
@ -25,7 +26,7 @@ data class LeaveSpaceState(
|
||||||
private val selectableRooms: ImmutableList<SelectableSpaceRoom>
|
private val selectableRooms: ImmutableList<SelectableSpaceRoom>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val partition = rooms.partition { it.isLastOwner }
|
val partition = rooms.partition { it.isLastOwner && it.joinedMembersCount > 1 }
|
||||||
lastAdminRooms = partition.first.toImmutableList()
|
lastAdminRooms = partition.first.toImmutableList()
|
||||||
selectableRooms = partition.second.toImmutableList()
|
selectableRooms = partition.second.toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
@ -33,12 +34,12 @@ data class LeaveSpaceState(
|
||||||
/**
|
/**
|
||||||
* True if we should show the quick action to select/deselect all rooms.
|
* True if we should show the quick action to select/deselect all rooms.
|
||||||
*/
|
*/
|
||||||
val showQuickAction = isLastOwner.not() && selectableRooms.isNotEmpty()
|
val showQuickAction = needsOwnerChange.not() && selectableRooms.isNotEmpty()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if we should show the leave button.
|
* True if we should show the leave button.
|
||||||
*/
|
*/
|
||||||
val showLeaveButton = isLastOwner.not() && selectableSpaceRooms is AsyncData.Success
|
val showLeaveButton = needsOwnerChange.not() && selectableSpaceRooms is AsyncData.Success
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if there all the selectable rooms are selected.
|
* True if there all the selectable rooms are selected.
|
||||||
|
|
|
||||||
|
|
@ -109,17 +109,23 @@ class LeaveSpaceStateProvider : PreviewParameterProvider<LeaveSpaceState> {
|
||||||
aLeaveSpaceState(
|
aLeaveSpaceState(
|
||||||
isLastOwner = true,
|
isLastOwner = true,
|
||||||
),
|
),
|
||||||
|
aLeaveSpaceState(
|
||||||
|
isLastOwner = true,
|
||||||
|
areCreatorsPrivileged = true,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aLeaveSpaceState(
|
fun aLeaveSpaceState(
|
||||||
spaceName: String? = "Space name",
|
spaceName: String? = "Space name",
|
||||||
isLastOwner: Boolean = false,
|
isLastOwner: Boolean = false,
|
||||||
|
areCreatorsPrivileged: Boolean = false,
|
||||||
selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>> = AsyncData.Uninitialized,
|
selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>> = AsyncData.Uninitialized,
|
||||||
leaveSpaceAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
leaveSpaceAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
) = LeaveSpaceState(
|
) = LeaveSpaceState(
|
||||||
spaceName = spaceName,
|
spaceName = spaceName,
|
||||||
isLastOwner = isLastOwner,
|
needsOwnerChange = isLastOwner,
|
||||||
|
areCreatorsPrivileged = areCreatorsPrivileged,
|
||||||
selectableSpaceRooms = selectableSpaceRooms,
|
selectableSpaceRooms = selectableSpaceRooms,
|
||||||
leaveSpaceAction = leaveSpaceAction,
|
leaveSpaceAction = leaveSpaceAction,
|
||||||
eventSink = { }
|
eventSink = { }
|
||||||
|
|
@ -128,9 +134,11 @@ fun aLeaveSpaceState(
|
||||||
fun aSelectableSpaceRoom(
|
fun aSelectableSpaceRoom(
|
||||||
spaceRoom: SpaceRoom = aSpaceRoom(),
|
spaceRoom: SpaceRoom = aSpaceRoom(),
|
||||||
isLastOwner: Boolean = false,
|
isLastOwner: Boolean = false,
|
||||||
|
joinedMembersCount: Int = 2,
|
||||||
isSelected: Boolean = false,
|
isSelected: Boolean = false,
|
||||||
) = SelectableSpaceRoom(
|
) = SelectableSpaceRoom(
|
||||||
spaceRoom = spaceRoom,
|
spaceRoom = spaceRoom,
|
||||||
isLastOwner = isLastOwner,
|
isLastOwner = isLastOwner,
|
||||||
|
joinedMembersCount = joinedMembersCount,
|
||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,13 @@ package io.element.android.features.space.impl.leave
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.imePadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
|
@ -40,6 +39,7 @@ import io.element.android.features.space.impl.R
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
import io.element.android.libraries.designsystem.components.BigIcon
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncFailure
|
import io.element.android.libraries.designsystem.components.async.AsyncFailure
|
||||||
|
|
@ -54,7 +54,6 @@ import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
|
@ -71,30 +70,42 @@ fun LeaveSpaceView(
|
||||||
state: LeaveSpaceState,
|
state: LeaveSpaceState,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
onRolesAndPermissionsClick: () -> Unit,
|
onRolesAndPermissionsClick: () -> Unit,
|
||||||
|
onChooseOwnersClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
HeaderFooterPage(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
contentPadding = PaddingValues(bottom = 14.dp),
|
||||||
topBar = {
|
topBar = {
|
||||||
LeaveSpaceHeader(
|
TopAppBar(
|
||||||
state = state,
|
navigationIcon = {
|
||||||
onBackClick = onCancel,
|
BackButton(onClick = onCancel)
|
||||||
|
},
|
||||||
|
title = {},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
containerColor = ElementTheme.colors.bgCanvasDefault,
|
header = {
|
||||||
) { padding ->
|
LeaveSpaceHeader(state = state)
|
||||||
Column(
|
},
|
||||||
modifier = Modifier
|
footer = {
|
||||||
.padding(padding)
|
LeaveSpaceButtons(
|
||||||
.imePadding()
|
showLeaveButton = state.showLeaveButton,
|
||||||
.consumeWindowInsets(padding)
|
selectedRoomsCount = state.selectedRoomsCount,
|
||||||
.fillMaxSize()
|
onLeaveSpace = {
|
||||||
) {
|
state.eventSink(LeaveSpaceEvents.LeaveSpace)
|
||||||
LazyColumn(
|
},
|
||||||
modifier = Modifier
|
onCancel = onCancel,
|
||||||
.weight(1f),
|
showRolesAndPermissionsButton = state.needsOwnerChange && !state.areCreatorsPrivileged,
|
||||||
) {
|
showChooseOwnersButton = state.needsOwnerChange && state.areCreatorsPrivileged,
|
||||||
if (state.isLastOwner.not()) {
|
onChooseOwnersButtonClick = onChooseOwnersClick,
|
||||||
|
onRolesAndPermissionsClick = onRolesAndPermissionsClick,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
if (state.needsOwnerChange.not()) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.padding(top = 20.dp),
|
||||||
|
) {
|
||||||
when (state.selectableSpaceRooms) {
|
when (state.selectableSpaceRooms) {
|
||||||
is AsyncData.Success -> {
|
is AsyncData.Success -> {
|
||||||
// List rooms where the user is the only admin
|
// List rooms where the user is the only admin
|
||||||
|
|
@ -125,18 +136,8 @@ fun LeaveSpaceView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LeaveSpaceButtons(
|
|
||||||
showLeaveButton = state.showLeaveButton,
|
|
||||||
selectedRoomsCount = state.selectedRoomsCount,
|
|
||||||
onLeaveSpace = {
|
|
||||||
state.eventSink(LeaveSpaceEvents.LeaveSpace)
|
|
||||||
},
|
|
||||||
onCancel = onCancel,
|
|
||||||
showRolesAndPermissionsButton = state.isLastOwner,
|
|
||||||
onRolesAndPermissionsClick = onRolesAndPermissionsClick,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
AsyncActionView(
|
AsyncActionView(
|
||||||
async = state.leaveSpaceAction,
|
async = state.leaveSpaceAction,
|
||||||
|
|
@ -149,25 +150,27 @@ fun LeaveSpaceView(
|
||||||
@Composable
|
@Composable
|
||||||
private fun LeaveSpaceHeader(
|
private fun LeaveSpaceHeader(
|
||||||
state: LeaveSpaceState,
|
state: LeaveSpaceState,
|
||||||
onBackClick: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
TopAppBar(
|
|
||||||
navigationIcon = {
|
|
||||||
BackButton(onClick = onBackClick)
|
|
||||||
},
|
|
||||||
title = {},
|
|
||||||
)
|
|
||||||
IconTitleSubtitleMolecule(
|
IconTitleSubtitleMolecule(
|
||||||
modifier = Modifier.padding(top = 0.dp, bottom = 8.dp, start = 24.dp, end = 24.dp),
|
modifier = Modifier.padding(top = 24.dp, bottom = 8.dp, start = 24.dp, end = 24.dp),
|
||||||
iconStyle = BigIcon.Style.AlertSolid,
|
iconStyle = BigIcon.Style.AlertSolid,
|
||||||
title = stringResource(
|
title = if (state.needsOwnerChange) {
|
||||||
if (state.isLastOwner) R.string.screen_leave_space_title_last_admin else R.string.screen_leave_space_title,
|
if (state.areCreatorsPrivileged) {
|
||||||
state.spaceName ?: stringResource(CommonStrings.common_space)
|
stringResource(R.string.screen_leave_space_title_last_owner)
|
||||||
),
|
} else {
|
||||||
|
stringResource(R.string.screen_leave_space_title_last_admin, state.spaceName ?: stringResource(CommonStrings.common_space))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.screen_leave_space_title, state.spaceName ?: stringResource(CommonStrings.common_space))
|
||||||
|
},
|
||||||
subTitle =
|
subTitle =
|
||||||
if (state.isLastOwner) {
|
if (state.needsOwnerChange) {
|
||||||
stringResource(R.string.screen_leave_space_subtitle_last_admin)
|
if (state.areCreatorsPrivileged) {
|
||||||
|
stringResource(R.string.screen_leave_space_subtitle_last_owner, state.spaceName ?: stringResource(CommonStrings.common_space))
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.screen_leave_space_subtitle_last_admin)
|
||||||
|
}
|
||||||
} else if (state.selectableSpaceRooms is AsyncData.Success && state.selectableSpaceRooms.data.isNotEmpty()) {
|
} else if (state.selectableSpaceRooms is AsyncData.Success && state.selectableSpaceRooms.data.isNotEmpty()) {
|
||||||
if (state.hasOnlyLastAdminRoom) {
|
if (state.hasOnlyLastAdminRoom) {
|
||||||
stringResource(R.string.screen_leave_space_subtitle_only_last_admin)
|
stringResource(R.string.screen_leave_space_subtitle_only_last_admin)
|
||||||
|
|
@ -216,10 +219,12 @@ private fun LeaveSpaceButtons(
|
||||||
onLeaveSpace: () -> Unit,
|
onLeaveSpace: () -> Unit,
|
||||||
showRolesAndPermissionsButton: Boolean,
|
showRolesAndPermissionsButton: Boolean,
|
||||||
onRolesAndPermissionsClick: () -> Unit,
|
onRolesAndPermissionsClick: () -> Unit,
|
||||||
|
showChooseOwnersButton: Boolean,
|
||||||
|
onChooseOwnersButtonClick: () -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ButtonColumnMolecule(
|
ButtonColumnMolecule(
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(top = 16.dp)
|
||||||
) {
|
) {
|
||||||
if (showLeaveButton) {
|
if (showLeaveButton) {
|
||||||
val text = if (selectedRoomsCount > 0) {
|
val text = if (selectedRoomsCount > 0) {
|
||||||
|
|
@ -243,6 +248,14 @@ private fun LeaveSpaceButtons(
|
||||||
leadingIcon = IconSource.Vector(CompoundIcons.Settings()),
|
leadingIcon = IconSource.Vector(CompoundIcons.Settings()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (showChooseOwnersButton) {
|
||||||
|
Button(
|
||||||
|
text = stringResource(R.string.screen_leave_space_choose_owners_action),
|
||||||
|
onClick = onChooseOwnersButtonClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
destructive = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
TextButton(
|
TextButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(CommonStrings.action_cancel),
|
text = stringResource(CommonStrings.action_cancel),
|
||||||
|
|
@ -262,6 +275,7 @@ private fun SpaceItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = 66.dp)
|
.heightIn(min = 66.dp)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
.toggleable(
|
.toggleable(
|
||||||
value = selectableSpaceRoom.isSelected,
|
value = selectableSpaceRoom.isSelected,
|
||||||
role = Role.Checkbox,
|
role = Role.Checkbox,
|
||||||
|
|
@ -276,9 +290,9 @@ private fun SpaceItem(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
|
||||||
avatarData = room.getAvatarData(AvatarSize.LeaveSpaceRoom),
|
avatarData = room.getAvatarData(AvatarSize.LeaveSpaceRoom),
|
||||||
avatarType = if (room.isSpace) AvatarType.Space() else AvatarType.Room(),
|
avatarType = if (room.isSpace) AvatarType.Space() else AvatarType.Room(),
|
||||||
)
|
)
|
||||||
|
|
@ -358,5 +372,6 @@ internal fun LeaveSpaceViewPreview(
|
||||||
state = state,
|
state = state,
|
||||||
onCancel = {},
|
onCancel = {},
|
||||||
onRolesAndPermissionsClick = {},
|
onRolesAndPermissionsClick = {},
|
||||||
|
onChooseOwnersClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,6 @@ import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
data class SelectableSpaceRoom(
|
data class SelectableSpaceRoom(
|
||||||
val spaceRoom: SpaceRoom,
|
val spaceRoom: SpaceRoom,
|
||||||
val isLastOwner: Boolean,
|
val isLastOwner: Boolean,
|
||||||
|
val joinedMembersCount: Int,
|
||||||
val isSelected: Boolean,
|
val isSelected: Boolean,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Du wirst aus den folgenden Chats nicht entfernt, weil du der einzige Admin bist:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Du wirst aus den folgenden Chats nicht entfernt, weil du der einzige Admin bist:"</string>
|
||||||
<string name="screen_leave_space_title">"%1$s verlassen?"</string>
|
<string name="screen_leave_space_title">"%1$s verlassen?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Du bist der einzige Administrator für %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Du bist der einzige Administrator für %1$s"</string>
|
||||||
|
<string name="screen_space_add_room_action">"Chat"</string>
|
||||||
<string name="screen_space_add_rooms_room_access_description">"Das Hinzufügen eines Chats hat keinen Einfluss auf die Beitrittsregeln. Um die Regeln zu ändern, gehe zu \"Raum Info\" und dann zu \"Datenschutz und Sicherheit\""</string>
|
<string name="screen_space_add_rooms_room_access_description">"Das Hinzufügen eines Chats hat keinen Einfluss auf die Beitrittsregeln. Um die Regeln zu ändern, gehe zu \"Raum Info\" und dann zu \"Datenschutz und Sicherheit\""</string>
|
||||||
|
<string name="screen_space_empty_state_title">"Füge deinen ersten Chat hinzu"</string>
|
||||||
<string name="screen_space_menu_action_members">"Mitglieder anzeigen"</string>
|
<string name="screen_space_menu_action_members">"Mitglieder anzeigen"</string>
|
||||||
<string name="screen_space_remove_rooms_confirmation_content">"Das Entfernen eines Chats hat keinen Einfluss auf die Beitrittsregeln. Um die Regeln zu ändern, gehe zu \"Raum Info\" und dann zu \"Datenschutz und Sicherheit\""</string>
|
<string name="screen_space_remove_rooms_confirmation_content">"Das Entfernen eines Chats hat keinen Einfluss auf die Beitrittsregeln. Um die Regeln zu ändern, gehe zu \"Raum Info\" und dann zu \"Datenschutz und Sicherheit\""</string>
|
||||||
<plurals name="screen_space_remove_rooms_confirmation_title">
|
<plurals name="screen_space_remove_rooms_confirmation_title">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_leave_space_choose_owners_action">"Vali omanikud"</string>
|
||||||
<string name="screen_leave_space_last_admin_info">"%1$s (Peakasutaja)"</string>
|
<string name="screen_leave_space_last_admin_info">"%1$s (Peakasutaja)"</string>
|
||||||
<plurals name="screen_leave_space_submit">
|
<plurals name="screen_leave_space_submit">
|
||||||
<item quantity="one">"Lahku %1$d-st jututoast ja kogukonnast"</item>
|
<item quantity="one">"Lahku %1$d-st jututoast ja kogukonnast"</item>
|
||||||
|
|
@ -10,7 +11,14 @@
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Sind ei saa järgnevatest jututubadest eemaldada, kuna oled seal/neis ainus peakasutaja:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Sind ei saa järgnevatest jututubadest eemaldada, kuna oled seal/neis ainus peakasutaja:"</string>
|
||||||
<string name="screen_leave_space_title">"Kas lahkud %1$s kogukonnast?"</string>
|
<string name="screen_leave_space_title">"Kas lahkud %1$s kogukonnast?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Sa oled siin ainus peakasutaja: %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Sa oled siin ainus peakasutaja: %1$s"</string>
|
||||||
|
<string name="screen_leave_space_title_last_owner">"Anna omand üle"</string>
|
||||||
|
<string name="screen_space_add_room_action">"Jututuba"</string>
|
||||||
|
<string name="screen_space_empty_state_title">"Lisa oma esimene jututuba"</string>
|
||||||
<string name="screen_space_menu_action_members">"Vaata liikmeid"</string>
|
<string name="screen_space_menu_action_members">"Vaata liikmeid"</string>
|
||||||
|
<plurals name="screen_space_remove_rooms_confirmation_title">
|
||||||
|
<item quantity="one">"Eemalda %1$d jututuba „%2$s“ kogukonnast"</item>
|
||||||
|
<item quantity="other">"Eemalda %1$d jututuba „%2$s“ kogukonnast"</item>
|
||||||
|
</plurals>
|
||||||
<string name="screen_space_settings_leave_space">"Lahku kogukonnast"</string>
|
<string name="screen_space_settings_leave_space">"Lahku kogukonnast"</string>
|
||||||
<string name="screen_space_settings_roles_and_permissions">"Rollid ja õigused"</string>
|
<string name="screen_space_settings_roles_and_permissions">"Rollid ja õigused"</string>
|
||||||
<string name="screen_space_settings_security_and_privacy">"Turvalisus ja privaatsus"</string>
|
<string name="screen_space_settings_security_and_privacy">"Turvalisus ja privaatsus"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_leave_space_choose_owners_action">"Choisir les propriétaires"</string>
|
||||||
<string name="screen_leave_space_last_admin_info">"%1$s (Admin)"</string>
|
<string name="screen_leave_space_last_admin_info">"%1$s (Admin)"</string>
|
||||||
<plurals name="screen_leave_space_submit">
|
<plurals name="screen_leave_space_submit">
|
||||||
<item quantity="one">"Quitter %1$d salon et l’espace"</item>
|
<item quantity="one">"Quitter %1$d salon et l’espace"</item>
|
||||||
|
|
@ -7,9 +8,11 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_leave_space_subtitle">"Sélectionnez les salons que vous souhaitez quitter et dont vous n’êtes pas le seul administrateur:"</string>
|
<string name="screen_leave_space_subtitle">"Sélectionnez les salons que vous souhaitez quitter et dont vous n’êtes pas le seul administrateur:"</string>
|
||||||
<string name="screen_leave_space_subtitle_last_admin">"Vous devez désigner un autre administrateur pour cet espace avant de pouvoir partir."</string>
|
<string name="screen_leave_space_subtitle_last_admin">"Vous devez désigner un autre administrateur pour cet espace avant de pouvoir partir."</string>
|
||||||
|
<string name="screen_leave_space_subtitle_last_owner">"Vous êtes le seul propriétaire de %1$s. Vous devez transférer la propriété de l’espace à quelqu’un d’autre avant de le quitter."</string>
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Vous ne quitterez pas le ou les salons suivants car vous y êtes le seul administrateur:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Vous ne quitterez pas le ou les salons suivants car vous y êtes le seul administrateur:"</string>
|
||||||
<string name="screen_leave_space_title">"Quitter %1$s?"</string>
|
<string name="screen_leave_space_title">"Quitter %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Vous êtes le seul administrateur de %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Vous êtes le seul administrateur de %1$s"</string>
|
||||||
|
<string name="screen_leave_space_title_last_owner">"Transfert de propriété"</string>
|
||||||
<string name="screen_space_add_room_action">"Salon"</string>
|
<string name="screen_space_add_room_action">"Salon"</string>
|
||||||
<string name="screen_space_add_rooms_room_access_description">"Ajouter un salon ne changera pas l’accès au salon. Pour modifier l’accès, aller dans les paramètres du salon puis dans Sécurité & confidentialité."</string>
|
<string name="screen_space_add_rooms_room_access_description">"Ajouter un salon ne changera pas l’accès au salon. Pour modifier l’accès, aller dans les paramètres du salon puis dans Sécurité & confidentialité."</string>
|
||||||
<string name="screen_space_empty_state_title">"Ajoutez votre premier salon"</string>
|
<string name="screen_space_empty_state_title">"Ajoutez votre premier salon"</string>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_leave_space_subtitle">"Ez a tér összes szobájából is eltávolítja."</string>
|
<string name="screen_leave_space_subtitle">"Ez a tér összes szobájából is eltávolítja."</string>
|
||||||
<string name="screen_leave_space_subtitle_last_admin">"Mielőtt elhagyhatná ezt a teret, ki kell jelölnie egy másik adminisztrátort."</string>
|
<string name="screen_leave_space_subtitle_last_admin">"Mielőtt elhagyhatná ezt a teret, ki kell jelölnie egy másik adminisztrátort."</string>
|
||||||
|
<string name="screen_leave_space_subtitle_last_owner">"Ön a(z) %1$s egyetlen tulajdonosa. Mielőtt távozik, át kell ruháznia a tulajdonjogot valaki másra."</string>
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"Nem lesz eltávolítva a következő szobá(k)ból, mert ön az egyetlen adminisztrátor:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"Nem lesz eltávolítva a következő szobá(k)ból, mert ön az egyetlen adminisztrátor:"</string>
|
||||||
<string name="screen_leave_space_title">"Kilép innen: %1$s?"</string>
|
<string name="screen_leave_space_title">"Kilép innen: %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"Ön az egyetlen adminisztrátor itt: %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"Ön az egyetlen adminisztrátor itt: %1$s"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_leave_space_choose_owners_action">"Choose owners"</string>
|
||||||
<string name="screen_leave_space_last_admin_info">"%1$s (Admin)"</string>
|
<string name="screen_leave_space_last_admin_info">"%1$s (Admin)"</string>
|
||||||
<plurals name="screen_leave_space_submit">
|
<plurals name="screen_leave_space_submit">
|
||||||
<item quantity="one">"Leave %1$d room and space"</item>
|
<item quantity="one">"Leave %1$d room and space"</item>
|
||||||
|
|
@ -7,9 +8,11 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_leave_space_subtitle">"Select the rooms you’d like to leave which you\'re not the only administrator for:"</string>
|
<string name="screen_leave_space_subtitle">"Select the rooms you’d like to leave which you\'re not the only administrator for:"</string>
|
||||||
<string name="screen_leave_space_subtitle_last_admin">"You need to assign another admin for this space before you can leave."</string>
|
<string name="screen_leave_space_subtitle_last_admin">"You need to assign another admin for this space before you can leave."</string>
|
||||||
|
<string name="screen_leave_space_subtitle_last_owner">"You are the only owner of %1$s. You need to transfer ownership to someone else before you leave."</string>
|
||||||
<string name="screen_leave_space_subtitle_only_last_admin">"You will not be removed from the following room(s) because you\'re the only administrator:"</string>
|
<string name="screen_leave_space_subtitle_only_last_admin">"You will not be removed from the following room(s) because you\'re the only administrator:"</string>
|
||||||
<string name="screen_leave_space_title">"Leave %1$s?"</string>
|
<string name="screen_leave_space_title">"Leave %1$s?"</string>
|
||||||
<string name="screen_leave_space_title_last_admin">"You are the only admin for %1$s"</string>
|
<string name="screen_leave_space_title_last_admin">"You are the only admin for %1$s"</string>
|
||||||
|
<string name="screen_leave_space_title_last_owner">"Transfer ownership"</string>
|
||||||
<string name="screen_space_add_room_action">"Room"</string>
|
<string name="screen_space_add_room_action">"Room"</string>
|
||||||
<string name="screen_space_add_rooms_room_access_description">"Adding a room will not affect the room access. To change the access go to Room settings > Security & privacy."</string>
|
<string name="screen_space_add_rooms_room_access_description">"Adding a room will not affect the room access. To change the access go to Room settings > Security & privacy."</string>
|
||||||
<string name="screen_space_empty_state_title">"Add your first room"</string>
|
<string name="screen_space_empty_state_title">"Add your first room"</string>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.changeroommemberroles.test.FakeChangeRoomMemberRolesEntryPoint
|
||||||
import io.element.android.features.createroom.api.FakeCreateRoomEntryPoint
|
import io.element.android.features.createroom.api.FakeCreateRoomEntryPoint
|
||||||
import io.element.android.features.space.api.SpaceEntryPoint
|
import io.element.android.features.space.api.SpaceEntryPoint
|
||||||
import io.element.android.features.space.impl.di.FakeSpaceFlowGraph
|
import io.element.android.features.space.impl.di.FakeSpaceFlowGraph
|
||||||
|
|
@ -22,6 +23,7 @@ import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.node.TestParentNode
|
import io.element.android.tests.testutils.node.TestParentNode
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -33,7 +35,7 @@ class DefaultSpaceEntryPointTest {
|
||||||
val mainDispatcherRule = MainDispatcherRule()
|
val mainDispatcherRule = MainDispatcherRule()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test node builder`() {
|
fun `test node builder`() = runTest {
|
||||||
val entryPoint = DefaultSpaceEntryPoint()
|
val entryPoint = DefaultSpaceEntryPoint()
|
||||||
val nodeInputs = SpaceEntryPoint.Inputs(A_ROOM_ID)
|
val nodeInputs = SpaceEntryPoint.Inputs(A_ROOM_ID)
|
||||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||||
|
|
@ -46,6 +48,8 @@ class DefaultSpaceEntryPointTest {
|
||||||
room = FakeJoinedRoom(),
|
room = FakeJoinedRoom(),
|
||||||
graphFactory = FakeSpaceFlowGraph.Factory,
|
graphFactory = FakeSpaceFlowGraph.Factory,
|
||||||
createRoomEntryPoint = FakeCreateRoomEntryPoint(),
|
createRoomEntryPoint = FakeCreateRoomEntryPoint(),
|
||||||
|
changeRoomMemberRolesEntryPoint = FakeChangeRoomMemberRolesEntryPoint(),
|
||||||
|
sessionCoroutineScope = backgroundScope,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val callback = object : SpaceEntryPoint.Callback {
|
val callback = object : SpaceEntryPoint.Callback {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||||
|
|
@ -116,12 +117,15 @@ class AddRoomToSpacePresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - searchResults shows Results when rooms available`() = runTest {
|
fun `present - searchResults shows Results when rooms available`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomList = FakeDynamicRoomList()
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val presenter = createAddRoomToSpacePresenter(roomListService = roomListService)
|
val presenter = createAddRoomToSpacePresenter(roomListService = roomListService)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
awaitItem() // Initial state
|
awaitItem() // Initial state
|
||||||
// Post rooms to the service
|
// Post rooms to the service
|
||||||
roomListService.postAllRooms(
|
roomList.summaries.emit(
|
||||||
listOf(
|
listOf(
|
||||||
aRoomSummary(
|
aRoomSummary(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -296,6 +300,29 @@ class AddRoomToSpacePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - UpdateSearchVisibleRange triggers pagination when near end`() = runTest {
|
||||||
|
val loadMoreLambda = lambdaRecorder<Unit> { }
|
||||||
|
val roomList = FakeDynamicRoomList(loadMoreLambda = loadMoreLambda)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
|
val presenter = createAddRoomToSpacePresenter(roomListService = roomListService)
|
||||||
|
presenter.test {
|
||||||
|
val state = awaitItem()
|
||||||
|
// Post rooms to simulate loaded content
|
||||||
|
roomList.summaries.emit(listOf(aRoomSummary()))
|
||||||
|
advanceUntilIdle()
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
|
// UpdateSearchVisibleRange should trigger loadMore
|
||||||
|
state.eventSink(AddRoomToSpaceEvent.UpdateSearchVisibleRange(IntRange(0, 9)))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assert(loadMoreLambda).isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Dismiss after partial success calls reset`() = runTest {
|
fun `present - Dismiss after partial success calls reset`() = runTest {
|
||||||
val resetResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
val resetResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
|
@ -99,6 +100,21 @@ class AddRoomToSpaceViewTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `displaying search results sends UpdateSearchVisibleRange event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>()
|
||||||
|
val rooms = aSelectRoomInfoList()
|
||||||
|
rule.setAddRoomToSpaceView(
|
||||||
|
anAddRoomToSpaceState(
|
||||||
|
isSearchActive = true,
|
||||||
|
searchResults = SearchBarResultState.Results(rooms),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
eventsRecorder.assertTrue(0) { it is AddRoomToSpaceEvent.UpdateSearchVisibleRange }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAddRoomToSpaceView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAddRoomToSpaceView(
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,6 @@ import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class LeaveSpacePresenterTest {
|
class LeaveSpacePresenterTest {
|
||||||
private val aSpace = aSpaceRoom(
|
|
||||||
roomId = A_SPACE_ID,
|
|
||||||
displayName = A_SPACE_NAME,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = createLeaveSpacePresenter(
|
val presenter = createLeaveSpacePresenter(
|
||||||
|
|
@ -44,7 +39,7 @@ class LeaveSpacePresenterTest {
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val state = awaitItem()
|
val state = awaitItem()
|
||||||
assertThat(state.spaceName).isNull()
|
assertThat(state.spaceName).isNull()
|
||||||
assertThat(state.isLastOwner).isFalse()
|
assertThat(state.needsOwnerChange).isFalse()
|
||||||
assertThat(state.selectableSpaceRooms.isLoading()).isTrue()
|
assertThat(state.selectableSpaceRooms.isLoading()).isTrue()
|
||||||
assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
|
@ -87,7 +82,7 @@ class LeaveSpacePresenterTest {
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.spaceName).isEqualTo(A_SPACE_NAME)
|
assertThat(finalState.spaceName).isEqualTo(A_SPACE_NAME)
|
||||||
assertThat(finalState.isLastOwner).isTrue()
|
assertThat(finalState.needsOwnerChange).isTrue()
|
||||||
// The current state is not in the sub room list
|
// The current state is not in the sub room list
|
||||||
assertThat(finalState.selectableSpaceRooms.dataOrNull()!!).isEmpty()
|
assertThat(finalState.selectableSpaceRooms.dataOrNull()!!).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
@ -145,8 +140,8 @@ class LeaveSpacePresenterTest {
|
||||||
roomsResult = {
|
roomsResult = {
|
||||||
Result.success(
|
Result.success(
|
||||||
listOf(
|
listOf(
|
||||||
LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID), isLastOwner = false),
|
LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID), isLastOwner = false, areCreatorsPrivileged = false),
|
||||||
LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID_2), isLastOwner = true),
|
LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID_2), isLastOwner = true, areCreatorsPrivileged = false),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -157,7 +152,7 @@ class LeaveSpacePresenterTest {
|
||||||
skipItems(3)
|
skipItems(3)
|
||||||
val state = awaitItem()
|
val state = awaitItem()
|
||||||
assertThat(state.spaceName).isNull()
|
assertThat(state.spaceName).isNull()
|
||||||
assertThat(state.isLastOwner).isFalse()
|
assertThat(state.needsOwnerChange).isFalse()
|
||||||
val data = state.selectableSpaceRooms.dataOrNull()!!
|
val data = state.selectableSpaceRooms.dataOrNull()!!
|
||||||
assertThat(data.size).isEqualTo(2)
|
assertThat(data.size).isEqualTo(2)
|
||||||
// Only one room is selectable as the user is the last admin in the other one
|
// Only one room is selectable as the user is the last admin in the other one
|
||||||
|
|
@ -232,6 +227,20 @@ class LeaveSpacePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - needsOwnerChange is false if user is the last joined member`() = runTest {
|
||||||
|
val presenter = createLeaveSpacePresenter(
|
||||||
|
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||||
|
roomsResult = { Result.success(listOf(aLeaveSpaceRoom(spaceRoom = aSpaceRoom(numJoinedMembers = 1), isLastOwner = true))) },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(3)
|
||||||
|
val state = awaitItem()
|
||||||
|
assertThat(state.needsOwnerChange).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createLeaveSpacePresenter(
|
private fun createLeaveSpacePresenter(
|
||||||
leaveSpaceHandle: LeaveSpaceHandle = FakeLeaveSpaceHandle(),
|
leaveSpaceHandle: LeaveSpaceHandle = FakeLeaveSpaceHandle(),
|
||||||
): LeaveSpacePresenter {
|
): LeaveSpacePresenter {
|
||||||
|
|
@ -241,13 +250,18 @@ class LeaveSpacePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val aSpace = aSpaceRoom(
|
||||||
|
roomId = A_SPACE_ID,
|
||||||
|
displayName = A_SPACE_NAME,
|
||||||
|
numJoinedMembers = 2,
|
||||||
|
)
|
||||||
|
|
||||||
private fun aLeaveSpaceRoom(
|
private fun aLeaveSpaceRoom(
|
||||||
spaceRoom: SpaceRoom = aSpaceRoom(
|
spaceRoom: SpaceRoom = aSpace,
|
||||||
roomId = A_SPACE_ID,
|
|
||||||
displayName = A_SPACE_NAME,
|
|
||||||
),
|
|
||||||
isLastOwner: Boolean = false,
|
isLastOwner: Boolean = false,
|
||||||
|
areCreatorsPrivileged: Boolean = false,
|
||||||
) = LeaveSpaceRoom(
|
) = LeaveSpaceRoom(
|
||||||
spaceRoom = spaceRoom,
|
spaceRoom = spaceRoom,
|
||||||
isLastOwner = isLastOwner,
|
isLastOwner = isLastOwner,
|
||||||
|
areCreatorsPrivileged = areCreatorsPrivileged,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<string name="screen_session_verification_compare_emojis_user_subtitle">"Bekräfta att emojierna nedan matchar de som visas på den andra användarens enhet."</string>
|
<string name="screen_session_verification_compare_emojis_user_subtitle">"Bekräfta att emojierna nedan matchar de som visas på den andra användarens enhet."</string>
|
||||||
<string name="screen_session_verification_compare_numbers_subtitle">"Bekräfta att siffrorna nedan matchar de som visas på din andra session."</string>
|
<string name="screen_session_verification_compare_numbers_subtitle">"Bekräfta att siffrorna nedan matchar de som visas på din andra session."</string>
|
||||||
<string name="screen_session_verification_compare_numbers_title">"Jämför siffror"</string>
|
<string name="screen_session_verification_compare_numbers_title">"Jämför siffror"</string>
|
||||||
<string name="screen_session_verification_complete_subtitle">"Din nya session är nu verifierad. Den har tillgång till dina krypterade meddelanden, och andra användare kommer att se den som betrodd."</string>
|
<string name="screen_session_verification_complete_subtitle">"Du kan nu läsa eller skicka meddelanden säkert på din andra enhet."</string>
|
||||||
<string name="screen_session_verification_complete_user_subtitle">"Nu kan du lita på användarens identitet när du skickar eller tar emot meddelanden."</string>
|
<string name="screen_session_verification_complete_user_subtitle">"Nu kan du lita på användarens identitet när du skickar eller tar emot meddelanden."</string>
|
||||||
<string name="screen_session_verification_device_verified">"Enhet verifierad"</string>
|
<string name="screen_session_verification_device_verified">"Enhet verifierad"</string>
|
||||||
<string name="screen_session_verification_enter_recovery_key">"Ange återställningsnyckel"</string>
|
<string name="screen_session_verification_enter_recovery_key">"Ange återställningsnyckel"</string>
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
<string name="screen_session_verification_request_failure_title">"Verifiering misslyckades"</string>
|
<string name="screen_session_verification_request_failure_title">"Verifiering misslyckades"</string>
|
||||||
<string name="screen_session_verification_request_footer">"Fortsätt bara om du initierade denna verifiering."</string>
|
<string name="screen_session_verification_request_footer">"Fortsätt bara om du initierade denna verifiering."</string>
|
||||||
<string name="screen_session_verification_request_subtitle">"Verifiera den andra enheten för att hålla din meddelandehistorik säker."</string>
|
<string name="screen_session_verification_request_subtitle">"Verifiera den andra enheten för att hålla din meddelandehistorik säker."</string>
|
||||||
<string name="screen_session_verification_request_success_subtitle">"Din nya session är nu verifierad. Den har tillgång till dina krypterade meddelanden, och andra användare kommer att se den som betrodd."</string>
|
<string name="screen_session_verification_request_success_subtitle">"Du kan nu läsa eller skicka meddelanden säkert på din andra enhet."</string>
|
||||||
<string name="screen_session_verification_request_success_title">"Enhet verifierad"</string>
|
<string name="screen_session_verification_request_success_title">"Enhet verifierad"</string>
|
||||||
<string name="screen_session_verification_request_title">"Verifiering begärd"</string>
|
<string name="screen_session_verification_request_title">"Verifiering begärd"</string>
|
||||||
<string name="screen_session_verification_they_dont_match">"De matchar inte"</string>
|
<string name="screen_session_verification_they_dont_match">"De matchar inte"</string>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ android_gradle_plugin = "8.13.2"
|
||||||
# When updateing this, please also update the version in the file ./idea/kotlinc.xml
|
# When updateing this, please also update the version in the file ./idea/kotlinc.xml
|
||||||
kotlin = "2.3.0"
|
kotlin = "2.3.0"
|
||||||
kotlinpoet = "2.2.0"
|
kotlinpoet = "2.2.0"
|
||||||
ksp = "2.3.4"
|
ksp = "2.3.5"
|
||||||
firebaseAppDistribution = "5.2.0"
|
firebaseAppDistribution = "5.2.0"
|
||||||
|
|
||||||
# AndroidX
|
# AndroidX
|
||||||
|
|
@ -16,9 +16,9 @@ datastore = "1.2.0"
|
||||||
constraintlayout = "2.2.1"
|
constraintlayout = "2.2.1"
|
||||||
constraintlayout_compose = "1.1.1"
|
constraintlayout_compose = "1.1.1"
|
||||||
lifecycle = "2.10.0"
|
lifecycle = "2.10.0"
|
||||||
activity = "1.12.2"
|
activity = "1.12.3"
|
||||||
media3 = "1.9.1"
|
media3 = "1.9.1"
|
||||||
camera = "1.5.2"
|
camera = "1.5.3"
|
||||||
work = "2.11.1"
|
work = "2.11.1"
|
||||||
|
|
||||||
# Compose
|
# Compose
|
||||||
|
|
@ -52,7 +52,7 @@ haze = "1.7.1"
|
||||||
dependencyAnalysis = "3.5.1"
|
dependencyAnalysis = "3.5.1"
|
||||||
|
|
||||||
# DI
|
# DI
|
||||||
metro = "0.10.1"
|
metro = "0.10.2"
|
||||||
|
|
||||||
# Auto service
|
# Auto service
|
||||||
autoservice = "1.1.1"
|
autoservice = "1.1.1"
|
||||||
|
|
@ -62,7 +62,7 @@ detekt = "1.23.8"
|
||||||
# See https://github.com/pinterest/ktlint/releases/
|
# See https://github.com/pinterest/ktlint/releases/
|
||||||
ktlint = "1.8.0"
|
ktlint = "1.8.0"
|
||||||
androidx-test-ext-junit = "1.3.0"
|
androidx-test-ext-junit = "1.3.0"
|
||||||
kover = "0.9.4"
|
kover = "0.9.5"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# Project
|
# Project
|
||||||
|
|
@ -218,7 +218,7 @@ haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref =
|
||||||
color_picker = "io.mhssn:colorpicker:1.0.0"
|
color_picker = "io.mhssn:colorpicker:1.0.0"
|
||||||
|
|
||||||
# Analytics
|
# Analytics
|
||||||
posthog = "com.posthog:posthog-android:3.29.1"
|
posthog = "com.posthog:posthog-android:3.30.0"
|
||||||
sentry = "io.sentry:sentry-android:8.31.0"
|
sentry = "io.sentry:sentry-android:8.31.0"
|
||||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.29.2"
|
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.29.2"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
package io.element.android.libraries.core.extensions
|
package io.element.android.libraries.core.extensions
|
||||||
|
|
||||||
import java.text.Normalizer
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
||||||
|
|
@ -86,11 +85,6 @@ fun String.safeCapitalize(): String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.withoutAccents(): String {
|
|
||||||
return Normalizer.normalize(this, Normalizer.Form.NFD)
|
|
||||||
.replace("\\p{Mn}+".toRegex(), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val RTL_OVERRIDE_CHAR = '\u202E'
|
private const val RTL_OVERRIDE_CHAR = '\u202E'
|
||||||
private const val LTR_OVERRIDE_CHAR = '\u202D'
|
private const val LTR_OVERRIDE_CHAR = '\u202D'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.utils
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OnVisibleRangeChangeEffect(lazyListState: LazyListState, onChange: (IntRange) -> Unit) {
|
||||||
|
val onChangeUpdated by rememberUpdatedState(onChange)
|
||||||
|
LaunchedEffect(lazyListState) {
|
||||||
|
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo }
|
||||||
|
.map { visibleItemsInfo ->
|
||||||
|
val firstItemIndex = visibleItemsInfo.firstOrNull()?.index ?: 0
|
||||||
|
val size = visibleItemsInfo.size
|
||||||
|
firstItemIndex until firstItemIndex + size
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collectLatest { visibleRange ->
|
||||||
|
onChangeUpdated(visibleRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,25 +8,14 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.roomlist
|
package io.element.android.libraries.matrix.api.roomlist
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RoomList with dynamic filtering and loading.
|
* RoomList with dynamic filtering and loading.
|
||||||
* This is useful for large lists of rooms.
|
* This is useful for large lists of rooms.
|
||||||
* It lets load rooms on demand and filter them.
|
* It lets load rooms on demand and filter them.
|
||||||
*/
|
*/
|
||||||
interface DynamicRoomList : RoomList {
|
interface DynamicRoomList : RoomList {
|
||||||
val currentFilter: StateFlow<RoomListFilter>
|
|
||||||
val loadedPages: StateFlow<Int>
|
|
||||||
val pageSize: Int
|
val pageSize: Int
|
||||||
|
|
||||||
val filteredSummaries: SharedFlow<List<RoomSummary>>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load more rooms into the list if possible.
|
* Load more rooms into the list if possible.
|
||||||
*/
|
*/
|
||||||
|
|
@ -44,28 +33,13 @@ interface DynamicRoomList : RoomList {
|
||||||
suspend fun updateFilter(filter: RoomListFilter)
|
suspend fun updateFilter(filter: RoomListFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
suspend fun DynamicRoomList.updateVisibleRange(
|
||||||
* Offers a way to load all the rooms incrementally.
|
visibleRange: IntRange,
|
||||||
* It will load more room until all are loaded.
|
paginationThreshold: Int = pageSize * 3
|
||||||
* If total number of rooms increase, it will load more pages if needed.
|
) {
|
||||||
* The number of rooms is independent of the filter.
|
val loadedCount = summaries.replayCache.firstOrNull().orEmpty().count()
|
||||||
*/
|
val threshold = loadedCount - paginationThreshold
|
||||||
fun DynamicRoomList.loadAllIncrementally(coroutineScope: CoroutineScope) {
|
if (visibleRange.last >= threshold) {
|
||||||
combine(
|
loadMore()
|
||||||
loadedPages,
|
|
||||||
loadingState,
|
|
||||||
) { loadedPages, loadingState ->
|
|
||||||
loadedPages to loadingState
|
|
||||||
}
|
}
|
||||||
.onEach { (loadedPages, loadingState) ->
|
|
||||||
when (loadingState) {
|
|
||||||
is RoomList.LoadingState.Loaded -> {
|
|
||||||
if (pageSize * loadedPages < loadingState.numberOfRooms) {
|
|
||||||
loadMore()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RoomList.LoadingState.NotLoaded -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.launchIn(coroutineScope)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.roomlist
|
package io.element.android.libraries.matrix.api.roomlist
|
||||||
|
|
||||||
import io.element.android.libraries.core.extensions.withoutAccents
|
|
||||||
|
|
||||||
sealed interface RoomListFilter {
|
sealed interface RoomListFilter {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
|
|
@ -77,7 +75,5 @@ sealed interface RoomListFilter {
|
||||||
*/
|
*/
|
||||||
data class NormalizedMatchRoomName(
|
data class NormalizedMatchRoomName(
|
||||||
val pattern: String
|
val pattern: String
|
||||||
) : RoomListFilter {
|
) : RoomListFilter
|
||||||
val normalizedPattern: String = pattern.withoutAccents()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,11 @@ interface RoomListService {
|
||||||
/**
|
/**
|
||||||
* Creates a room list that can be used to load more rooms and filter them dynamically.
|
* Creates a room list that can be used to load more rooms and filter them dynamically.
|
||||||
* @param pageSize the number of rooms to load at once.
|
* @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.
|
* @param source the source of the rooms, either all rooms or invites.
|
||||||
* @param coroutineScope the coroutine scope to use for the room list operations.
|
* @param coroutineScope the coroutine scope to use for the room list operations.
|
||||||
*/
|
*/
|
||||||
fun createRoomList(
|
fun createRoomList(
|
||||||
pageSize: Int,
|
pageSize: Int,
|
||||||
initialFilter: RoomListFilter,
|
|
||||||
source: RoomList.Source,
|
source: RoomList.Source,
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
): DynamicRoomList
|
): DynamicRoomList
|
||||||
|
|
@ -56,10 +54,10 @@ interface RoomListService {
|
||||||
suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>)
|
suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a [DynamicRoomList] object of all rooms we want to display.
|
* Returns a [RoomList] object with all rooms locally known.
|
||||||
* If you want to get a filtered room list, consider using [createRoomList].
|
* If you want to get a filtered room list, consider using [createRoomList].
|
||||||
*/
|
*/
|
||||||
val allRooms: DynamicRoomList
|
val allRooms: RoomList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The sync indicator as a flow.
|
* The sync indicator as a flow.
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,5 @@ package io.element.android.libraries.matrix.api.spaces
|
||||||
data class LeaveSpaceRoom(
|
data class LeaveSpaceRoom(
|
||||||
val spaceRoom: SpaceRoom,
|
val spaceRoom: SpaceRoom,
|
||||||
val isLastOwner: Boolean,
|
val isLastOwner: Boolean,
|
||||||
|
val areCreatorsPrivileged: Boolean,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,8 @@ class RustMatrixClientFactory(
|
||||||
.requestConfig(
|
.requestConfig(
|
||||||
RequestConfig(
|
RequestConfig(
|
||||||
timeout = 30_000uL,
|
timeout = 30_000uL,
|
||||||
retryLimit = 0u,
|
// retryLimit must be non-zero for the SDK to retry API calls in case of error (including 429 Too Many Requests error).
|
||||||
|
retryLimit = 3u,
|
||||||
// Use default values for the rest
|
// Use default values for the rest
|
||||||
maxConcurrentRequests = null,
|
maxConcurrentRequests = null,
|
||||||
maxRetryTime = null,
|
maxRetryTime = null,
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2023-2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.roomlist
|
|
||||||
|
|
||||||
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
|
|
||||||
|
|
||||||
internal sealed interface RoomListDynamicEvents {
|
|
||||||
data object Reset : RoomListDynamicEvents
|
|
||||||
data object LoadMore : RoomListDynamicEvents
|
|
||||||
data class SetFilter(val filter: RoomListEntriesDynamicFilterKind) : RoomListDynamicEvents
|
|
||||||
}
|
|
||||||
|
|
@ -18,9 +18,8 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.buffer
|
import kotlinx.coroutines.flow.buffer
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import org.matrix.rustcomponents.sdk.Room
|
import org.matrix.rustcomponents.sdk.Room
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListDynamicEntriesController
|
||||||
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
|
||||||
import org.matrix.rustcomponents.sdk.RoomListEntriesListener
|
import org.matrix.rustcomponents.sdk.RoomListEntriesListener
|
||||||
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
||||||
|
|
@ -57,8 +56,8 @@ fun RoomListInterface.loadingStateFlow(): Flow<RoomListLoadingState> =
|
||||||
|
|
||||||
internal fun RoomListInterface.entriesFlow(
|
internal fun RoomListInterface.entriesFlow(
|
||||||
pageSize: Int,
|
pageSize: Int,
|
||||||
roomListDynamicEvents: Flow<RoomListDynamicEvents>,
|
initialFilterKind: RoomListEntriesDynamicFilterKind,
|
||||||
initialFilterKind: RoomListEntriesDynamicFilterKind
|
onControllerCreated: (RoomListDynamicEntriesController) -> Unit,
|
||||||
): Flow<List<RoomListEntriesUpdate>> =
|
): Flow<List<RoomListEntriesUpdate>> =
|
||||||
callbackFlow {
|
callbackFlow {
|
||||||
val listener = object : RoomListEntriesListener {
|
val listener = object : RoomListEntriesListener {
|
||||||
|
|
@ -73,19 +72,7 @@ internal fun RoomListInterface.entriesFlow(
|
||||||
)
|
)
|
||||||
val controller = result.controller()
|
val controller = result.controller()
|
||||||
controller.setFilter(initialFilterKind)
|
controller.setFilter(initialFilterKind)
|
||||||
roomListDynamicEvents.onEach { controllerEvents ->
|
onControllerCreated(controller)
|
||||||
when (controllerEvents) {
|
|
||||||
is RoomListDynamicEvents.SetFilter -> {
|
|
||||||
controller.setFilter(controllerEvents.filter)
|
|
||||||
}
|
|
||||||
is RoomListDynamicEvents.LoadMore -> {
|
|
||||||
controller.addOnePage()
|
|
||||||
}
|
|
||||||
is RoomListDynamicEvents.Reset -> {
|
|
||||||
controller.resetToOnePage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.launchIn(this)
|
|
||||||
awaitClose {
|
awaitClose {
|
||||||
result.entriesStream().cancelAndDestroy()
|
result.entriesStream().cancelAndDestroy()
|
||||||
controller.destroy()
|
controller.destroy()
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.impl.roomlist
|
||||||
import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
|
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.RoomListFilter.Companion.all
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
|
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
@ -18,24 +19,16 @@ import io.element.android.services.analytics.api.finishLongRunningTransaction
|
||||||
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
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
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.RoomListEntriesDynamicFilterKind
|
import org.matrix.rustcomponents.sdk.RoomListDynamicEntriesController
|
||||||
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
||||||
import org.matrix.rustcomponents.sdk.RoomListService
|
import org.matrix.rustcomponents.sdk.RoomListService
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
|
import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
|
||||||
|
|
||||||
private val ROOM_LIST_RUST_FILTERS = listOf(
|
|
||||||
RoomListEntriesDynamicFilterKind.NonLeft,
|
|
||||||
RoomListEntriesDynamicFilterKind.DeduplicateVersions
|
|
||||||
)
|
|
||||||
|
|
||||||
internal class RoomListFactory(
|
internal class RoomListFactory(
|
||||||
private val innerRoomListService: RoomListService,
|
private val innerRoomListService: RoomListService,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
|
|
@ -49,18 +42,14 @@ internal class RoomListFactory(
|
||||||
pageSize: Int,
|
pageSize: Int,
|
||||||
coroutineContext: CoroutineContext,
|
coroutineContext: CoroutineContext,
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
initialFilter: RoomListFilter = RoomListFilter.all(),
|
initialFilter: 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 filteredSummariesFlow = MutableSharedFlow<List<RoomSummary>>(replay = 1, extraBufferCapacity = 1)
|
|
||||||
val summariesFlow = MutableSharedFlow<List<RoomSummary>>(replay = 1, extraBufferCapacity = 1)
|
val summariesFlow = MutableSharedFlow<List<RoomSummary>>(replay = 1, extraBufferCapacity = 1)
|
||||||
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryFactory, analyticsService)
|
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryFactory, analyticsService)
|
||||||
// Makes sure we don't miss any events
|
|
||||||
val dynamicEvents = MutableSharedFlow<RoomListDynamicEvents>(replay = 100)
|
|
||||||
val currentFilter = MutableStateFlow(initialFilter)
|
|
||||||
val loadedPages = MutableStateFlow(1)
|
|
||||||
var innerRoomList: InnerRoomList? = null
|
var innerRoomList: InnerRoomList? = null
|
||||||
|
var dynamicController: RoomListDynamicEntriesController? = null
|
||||||
|
|
||||||
val firstRoomsTransaction = analyticsService.startTransaction("Load first set of rooms", "innerRoomList.entriesFlow")
|
val firstRoomsTransaction = analyticsService.startTransaction("Load first set of rooms", "innerRoomList.entriesFlow")
|
||||||
|
|
||||||
|
|
@ -69,8 +58,10 @@ internal class RoomListFactory(
|
||||||
innerRoomList.let { innerRoomList ->
|
innerRoomList.let { innerRoomList ->
|
||||||
innerRoomList.entriesFlow(
|
innerRoomList.entriesFlow(
|
||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
roomListDynamicEvents = dynamicEvents,
|
initialFilterKind = RoomListFilterMapper.toRustFilter(initialFilter),
|
||||||
initialFilterKind = RoomListEntriesDynamicFilterKind.All(ROOM_LIST_RUST_FILTERS),
|
onControllerCreated = { controller ->
|
||||||
|
dynamicController = controller
|
||||||
|
}
|
||||||
).onEach { update ->
|
).onEach { update ->
|
||||||
if (!firstRoomsTransaction.isFinished()) {
|
if (!firstRoomsTransaction.isFinished()) {
|
||||||
analyticsService.finishLongRunningTransaction(AnalyticsLongRunningTransaction.FirstRoomsDisplayed)
|
analyticsService.finishLongRunningTransaction(AnalyticsLongRunningTransaction.FirstRoomsDisplayed)
|
||||||
|
|
@ -85,61 +76,20 @@ internal class RoomListFactory(
|
||||||
loadingStateFlow.value = it
|
loadingStateFlow.value = it
|
||||||
}
|
}
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
|
|
||||||
combine(
|
|
||||||
currentFilter,
|
|
||||||
summariesFlow
|
|
||||||
) { filter, summaries ->
|
|
||||||
summaries.filter(filter)
|
|
||||||
}.onEach {
|
|
||||||
filteredSummariesFlow.emit(it)
|
|
||||||
}.launchIn(this)
|
|
||||||
}
|
}
|
||||||
}.invokeOnCompletion {
|
}.invokeOnCompletion {
|
||||||
innerRoomList?.destroy()
|
innerRoomList?.destroy()
|
||||||
}
|
}
|
||||||
return RustDynamicRoomList(
|
return RustDynamicRoomList(
|
||||||
summaries = summariesFlow,
|
summaries = summariesFlow,
|
||||||
filteredSummaries = filteredSummariesFlow,
|
|
||||||
loadingState = loadingStateFlow,
|
loadingState = loadingStateFlow,
|
||||||
currentFilter = currentFilter,
|
|
||||||
loadedPages = loadedPages,
|
|
||||||
dynamicEvents = dynamicEvents,
|
|
||||||
processor = processor,
|
processor = processor,
|
||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
|
dynamicController = { dynamicController }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RustDynamicRoomList(
|
|
||||||
override val summaries: MutableSharedFlow<List<RoomSummary>>,
|
|
||||||
override val filteredSummaries: SharedFlow<List<RoomSummary>>,
|
|
||||||
override val loadingState: MutableStateFlow<RoomList.LoadingState>,
|
|
||||||
override val currentFilter: MutableStateFlow<RoomListFilter>,
|
|
||||||
override val loadedPages: MutableStateFlow<Int>,
|
|
||||||
private val dynamicEvents: MutableSharedFlow<RoomListDynamicEvents>,
|
|
||||||
private val processor: RoomSummaryListProcessor,
|
|
||||||
override val pageSize: Int,
|
|
||||||
) : DynamicRoomList {
|
|
||||||
override suspend fun rebuildSummaries() {
|
|
||||||
processor.rebuildRoomSummaries()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateFilter(filter: RoomListFilter) {
|
|
||||||
currentFilter.emit(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun loadMore() {
|
|
||||||
dynamicEvents.emit(RoomListDynamicEvents.LoadMore)
|
|
||||||
loadedPages.getAndUpdate { it + 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun reset() {
|
|
||||||
dynamicEvents.emit(RoomListDynamicEvents.Reset)
|
|
||||||
loadedPages.emit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState {
|
private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is RoomListLoadingState.Loaded -> RoomList.LoadingState.Loaded(maximumNumberOfRooms?.toInt() ?: 0)
|
is RoomListLoadingState.Loaded -> RoomList.LoadingState.Loaded(maximumNumberOfRooms?.toInt() ?: 0)
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2024, 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.roomlist
|
|
||||||
|
|
||||||
import io.element.android.libraries.core.extensions.withoutAccents
|
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
|
||||||
import io.element.android.libraries.matrix.api.room.isDm
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
|
||||||
|
|
||||||
val RoomListFilter.predicate
|
|
||||||
get() = when (this) {
|
|
||||||
is RoomListFilter.All -> { roomSummary -> NonSpacePredicate(roomSummary) || IsInvitedPredicate(roomSummary) }
|
|
||||||
is RoomListFilter.Any -> { roomSummary -> NonSpacePredicate(roomSummary) || IsInvitedPredicate(roomSummary) }
|
|
||||||
RoomListFilter.None -> { _ -> false }
|
|
||||||
RoomListFilter.Category.Group -> { roomSummary: RoomSummary ->
|
|
||||||
!roomSummary.info.isDm && NonInvitedPredicate(roomSummary) && NonSpacePredicate(roomSummary)
|
|
||||||
}
|
|
||||||
RoomListFilter.Category.People -> { roomSummary: RoomSummary ->
|
|
||||||
roomSummary.info.isDm && NonInvitedPredicate(roomSummary) && NonSpacePredicate(roomSummary)
|
|
||||||
}
|
|
||||||
RoomListFilter.Category.Space -> IsSpacePredicate
|
|
||||||
RoomListFilter.Favorite -> { roomSummary: RoomSummary ->
|
|
||||||
roomSummary.info.isFavorite && NonInvitedPredicate(roomSummary) && NonSpacePredicate(roomSummary)
|
|
||||||
}
|
|
||||||
RoomListFilter.Unread -> { roomSummary: RoomSummary ->
|
|
||||||
NonInvitedPredicate(roomSummary) &&
|
|
||||||
NonSpacePredicate(roomSummary) &&
|
|
||||||
(roomSummary.info.numUnreadNotifications > 0 || roomSummary.info.isMarkedUnread)
|
|
||||||
}
|
|
||||||
is RoomListFilter.NormalizedMatchRoomName -> { roomSummary: RoomSummary ->
|
|
||||||
roomSummary.info.name?.withoutAccents().orEmpty().contains(normalizedPattern, ignoreCase = true) &&
|
|
||||||
(NonSpacePredicate(roomSummary) || IsInvitedPredicate(roomSummary))
|
|
||||||
}
|
|
||||||
RoomListFilter.Invite -> IsInvitedPredicate
|
|
||||||
}
|
|
||||||
|
|
||||||
fun List<RoomSummary>.filter(filter: RoomListFilter): List<RoomSummary> {
|
|
||||||
return when (filter) {
|
|
||||||
is RoomListFilter.All -> {
|
|
||||||
val predicates = if (filter.filters.isNotEmpty()) {
|
|
||||||
filter.filters.map { it.predicate }
|
|
||||||
} else {
|
|
||||||
listOf(filter.predicate)
|
|
||||||
}
|
|
||||||
filter { roomSummary -> predicates.all { it(roomSummary) } }
|
|
||||||
}
|
|
||||||
is RoomListFilter.Any -> {
|
|
||||||
val predicates = if (filter.filters.isNotEmpty()) {
|
|
||||||
filter.filters.map { it.predicate }
|
|
||||||
} else {
|
|
||||||
listOf(filter.predicate)
|
|
||||||
}
|
|
||||||
filter { roomSummary -> predicates.any { it(roomSummary) } }
|
|
||||||
}
|
|
||||||
else -> filter(filter.predicate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val IsSpacePredicate = { roomSummary: RoomSummary -> roomSummary.info.isSpace }
|
|
||||||
|
|
||||||
private val NonSpacePredicate = { roomSummary: RoomSummary -> !IsSpacePredicate(roomSummary) }
|
|
||||||
|
|
||||||
private val IsInvitedPredicate = { roomSummary: RoomSummary -> roomSummary.info.currentUserMembership == CurrentUserMembership.INVITED }
|
|
||||||
|
|
||||||
private val NonInvitedPredicate = { roomSummary: RoomSummary -> !IsInvitedPredicate(roomSummary) }
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl.roomlist
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.All
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Any
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Category
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.DeduplicateVersions
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Favourite
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Invite
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonLeft
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonSpace
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.None
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NormalizedMatchRoomName
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Space
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Unread
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListFilterCategory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapper for converting RoomListFilter to Rust SDK filter kinds.
|
||||||
|
*/
|
||||||
|
internal object RoomListFilterMapper {
|
||||||
|
/**
|
||||||
|
* Base rust filters to always apply across all room lists.
|
||||||
|
* These filters ensure we show:
|
||||||
|
* - Non-space, non-left rooms (regular rooms user is part of)
|
||||||
|
* - OR space invites (pending space invitations)
|
||||||
|
* - With version deduplication enabled
|
||||||
|
*/
|
||||||
|
private val RUST_BASE_FILTERS = listOf(
|
||||||
|
Any(
|
||||||
|
listOf(
|
||||||
|
All(listOf(NonSpace, NonLeft)),
|
||||||
|
All(listOf(Space, Invite)),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
DeduplicateVersions
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a RoomListFilter to a Rust SDK RoomListEntriesDynamicFilterKind.
|
||||||
|
* Applies base filters along with the provided filter.
|
||||||
|
*/
|
||||||
|
fun toRustFilter(filter: RoomListFilter): RoomListEntriesDynamicFilterKind {
|
||||||
|
return All(RUST_BASE_FILTERS + mapFilter(filter))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a RoomListFilter to its Rust SDK equivalent.
|
||||||
|
* This replaces the previous RoomListFilter.into() extension function.
|
||||||
|
*/
|
||||||
|
private fun mapFilter(filter: RoomListFilter): RoomListEntriesDynamicFilterKind {
|
||||||
|
return when (filter) {
|
||||||
|
is RoomListFilter.All -> All(filters = filter.filters.map { mapFilter(it) })
|
||||||
|
is RoomListFilter.Any -> Any(filters = filter.filters.map { mapFilter(it) })
|
||||||
|
RoomListFilter.None -> None
|
||||||
|
RoomListFilter.Category.Group -> Category(RoomListFilterCategory.GROUP)
|
||||||
|
RoomListFilter.Category.People -> Category(RoomListFilterCategory.PEOPLE)
|
||||||
|
RoomListFilter.Category.Space -> Space
|
||||||
|
RoomListFilter.Favorite -> Favourite
|
||||||
|
RoomListFilter.Unread -> Unread
|
||||||
|
is RoomListFilter.NormalizedMatchRoomName -> NormalizedMatchRoomName(
|
||||||
|
pattern = filter.pattern
|
||||||
|
)
|
||||||
|
RoomListFilter.Invite -> Invite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl.roomlist
|
||||||
|
|
||||||
|
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.RoomListFilter
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListDynamicEntriesController
|
||||||
|
|
||||||
|
private const val DEFAULT_ADD_PAGES_COUNT = 3
|
||||||
|
|
||||||
|
internal class RustDynamicRoomList(
|
||||||
|
override val summaries: MutableSharedFlow<List<RoomSummary>>,
|
||||||
|
override val loadingState: MutableStateFlow<RoomList.LoadingState>,
|
||||||
|
private val processor: RoomSummaryListProcessor,
|
||||||
|
override val pageSize: Int,
|
||||||
|
private val dynamicController: () -> RoomListDynamicEntriesController?,
|
||||||
|
private val addPagesCount: Int = DEFAULT_ADD_PAGES_COUNT
|
||||||
|
) : DynamicRoomList {
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
override suspend fun rebuildSummaries() {
|
||||||
|
processor.rebuildRoomSummaries()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateFilter(filter: RoomListFilter) {
|
||||||
|
mutex.withLock {
|
||||||
|
dynamicController()?.let { controller ->
|
||||||
|
// Reset pagination when filter changes
|
||||||
|
controller.resetToOnePage()
|
||||||
|
val rustFilter = RoomListFilterMapper.toRustFilter(filter)
|
||||||
|
controller.setFilter(rustFilter)
|
||||||
|
// Then preload some pages
|
||||||
|
controller.addPages(addPagesCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadMore() {
|
||||||
|
mutex.withLock {
|
||||||
|
dynamicController()?.addPages(addPagesCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun reset() {
|
||||||
|
mutex.withLock {
|
||||||
|
dynamicController()?.resetToOnePage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RoomListDynamicEntriesController.addPages(pageCount: Int) = repeat(pageCount) { addOnePage() }
|
||||||
|
}
|
||||||
|
|
@ -11,9 +11,7 @@ package io.element.android.libraries.matrix.impl.roomlist
|
||||||
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.roomlist.DynamicRoomList
|
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.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.impl.room.RoomSyncSubscriber
|
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -28,8 +26,6 @@ import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService
|
import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService
|
||||||
|
|
||||||
private const val DEFAULT_PAGE_SIZE = 20
|
|
||||||
|
|
||||||
internal class RustRoomListService(
|
internal class RustRoomListService(
|
||||||
private val innerRoomListService: InnerRustRoomListService,
|
private val innerRoomListService: InnerRustRoomListService,
|
||||||
private val sessionDispatcher: CoroutineDispatcher,
|
private val sessionDispatcher: CoroutineDispatcher,
|
||||||
|
|
@ -39,13 +35,11 @@ internal class RustRoomListService(
|
||||||
) : RoomListService {
|
) : RoomListService {
|
||||||
override fun createRoomList(
|
override fun createRoomList(
|
||||||
pageSize: Int,
|
pageSize: Int,
|
||||||
initialFilter: RoomListFilter,
|
|
||||||
source: RoomList.Source,
|
source: RoomList.Source,
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
): DynamicRoomList {
|
): DynamicRoomList {
|
||||||
return roomListFactory.createRoomList(
|
return roomListFactory.createRoomList(
|
||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
initialFilter = initialFilter,
|
|
||||||
coroutineContext = sessionDispatcher,
|
coroutineContext = sessionDispatcher,
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
) {
|
) {
|
||||||
|
|
@ -59,18 +53,14 @@ internal class RustRoomListService(
|
||||||
roomSyncSubscriber.batchSubscribe(roomIds)
|
roomSyncSubscriber.batchSubscribe(roomIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val allRooms: DynamicRoomList = roomListFactory.createRoomList(
|
override val allRooms: RoomList = roomListFactory.createRoomList(
|
||||||
pageSize = DEFAULT_PAGE_SIZE,
|
pageSize = Int.MAX_VALUE,
|
||||||
coroutineContext = sessionDispatcher,
|
coroutineContext = sessionDispatcher,
|
||||||
coroutineScope = sessionCoroutineScope,
|
coroutineScope = sessionCoroutineScope,
|
||||||
) {
|
) {
|
||||||
innerRoomListService.allRooms()
|
innerRoomListService.allRooms()
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
|
||||||
allRooms.loadAllIncrementally(sessionCoroutineScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val syncIndicator: StateFlow<RoomListService.SyncIndicator> =
|
override val syncIndicator: StateFlow<RoomListService.SyncIndicator> =
|
||||||
innerRoomListService.syncIndicator()
|
innerRoomListService.syncIndicator()
|
||||||
.map { it.toSyncIndicator() }
|
.map { it.toSyncIndicator() }
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ class RustLeaveSpaceHandle(
|
||||||
LeaveSpaceRoom(
|
LeaveSpaceRoom(
|
||||||
spaceRoom = spaceRoomMapper.map(leaveSpaceRoom.spaceRoom),
|
spaceRoom = spaceRoomMapper.map(leaveSpaceRoom.spaceRoom),
|
||||||
isLastOwner = leaveSpaceRoom.isLastOwner,
|
isLastOwner = leaveSpaceRoom.isLastOwner,
|
||||||
|
areCreatorsPrivileged = leaveSpaceRoom.areCreatorsPrivileged,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2024, 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.roomlist
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class RoomListFilterTest {
|
|
||||||
private val regularRoom = aRoomSummary(
|
|
||||||
isDirect = false,
|
|
||||||
)
|
|
||||||
private val dmRoom = aRoomSummary(
|
|
||||||
isDirect = true,
|
|
||||||
activeMembersCount = 2
|
|
||||||
)
|
|
||||||
private val favoriteRoom = aRoomSummary(
|
|
||||||
isFavorite = true
|
|
||||||
)
|
|
||||||
private val markedAsUnreadRoom = aRoomSummary(
|
|
||||||
isMarkedUnread = true
|
|
||||||
)
|
|
||||||
private val unreadNotificationRoom = aRoomSummary(
|
|
||||||
numUnreadNotifications = 1
|
|
||||||
)
|
|
||||||
private val roomToSearch = aRoomSummary(
|
|
||||||
name = "Room to search"
|
|
||||||
)
|
|
||||||
private val roomWithAccent = aRoomSummary(
|
|
||||||
name = "Frédéric"
|
|
||||||
)
|
|
||||||
private val invitedRoom = aRoomSummary(
|
|
||||||
currentUserMembership = CurrentUserMembership.INVITED
|
|
||||||
)
|
|
||||||
|
|
||||||
private val space = aRoomSummary(
|
|
||||||
isSpace = true
|
|
||||||
)
|
|
||||||
private val invitedSpace = aRoomSummary(
|
|
||||||
isSpace = true,
|
|
||||||
currentUserMembership = CurrentUserMembership.INVITED
|
|
||||||
)
|
|
||||||
|
|
||||||
private val roomSummaries = listOf(
|
|
||||||
regularRoom,
|
|
||||||
dmRoom,
|
|
||||||
favoriteRoom,
|
|
||||||
markedAsUnreadRoom,
|
|
||||||
unreadNotificationRoom,
|
|
||||||
roomToSearch,
|
|
||||||
roomWithAccent,
|
|
||||||
invitedRoom,
|
|
||||||
space,
|
|
||||||
invitedSpace,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter all empty`() = runTest {
|
|
||||||
val filter = RoomListFilter.all()
|
|
||||||
assertThat(roomSummaries.filter(filter)).isEqualTo(roomSummaries - space)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter none`() = runTest {
|
|
||||||
val filter = RoomListFilter.None
|
|
||||||
assertThat(roomSummaries.filter(filter)).isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter people`() = runTest {
|
|
||||||
val filter = RoomListFilter.Category.People
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(dmRoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter group`() = runTest {
|
|
||||||
val filter = RoomListFilter.Category.Group
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(
|
|
||||||
regularRoom,
|
|
||||||
favoriteRoom,
|
|
||||||
markedAsUnreadRoom,
|
|
||||||
unreadNotificationRoom,
|
|
||||||
roomToSearch,
|
|
||||||
roomWithAccent,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter space`() = runTest {
|
|
||||||
val filter = RoomListFilter.Category.Space
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(space, invitedSpace)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter favorite`() = runTest {
|
|
||||||
val filter = RoomListFilter.Favorite
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(favoriteRoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter unread`() = runTest {
|
|
||||||
val filter = RoomListFilter.Unread
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(markedAsUnreadRoom, unreadNotificationRoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter invites`() = runTest {
|
|
||||||
val filter = RoomListFilter.Invite
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(invitedRoom, invitedSpace)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter normalized match room name`() = runTest {
|
|
||||||
val filter = RoomListFilter.NormalizedMatchRoomName("search")
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(roomToSearch)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter normalized match room name with accent`() = runTest {
|
|
||||||
val filter = RoomListFilter.NormalizedMatchRoomName("Fred")
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(roomWithAccent)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter normalized match room name with accent when searching with accent`() = runTest {
|
|
||||||
val filter = RoomListFilter.NormalizedMatchRoomName("Fréd")
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(roomWithAccent)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter all with one match`() = runTest {
|
|
||||||
val filter = RoomListFilter.all(
|
|
||||||
RoomListFilter.Category.Group,
|
|
||||||
RoomListFilter.Favorite
|
|
||||||
)
|
|
||||||
assertThat(roomSummaries.filter(filter)).containsExactly(favoriteRoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Room list filter all with no match`() = runTest {
|
|
||||||
val filter = RoomListFilter.all(
|
|
||||||
RoomListFilter.Category.People,
|
|
||||||
RoomListFilter.Favorite
|
|
||||||
)
|
|
||||||
assertThat(roomSummaries.filter(filter)).isEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -13,34 +13,30 @@ 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.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
|
||||||
|
|
||||||
data class SimplePagedRoomList(
|
class FakeDynamicRoomList(
|
||||||
override val summaries: MutableStateFlow<List<RoomSummary>>,
|
override val summaries: MutableStateFlow<List<RoomSummary>> = MutableStateFlow(emptyList()),
|
||||||
override val loadingState: StateFlow<RoomList.LoadingState>,
|
override val loadingState: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded),
|
||||||
override val currentFilter: MutableStateFlow<RoomListFilter>
|
override val pageSize: Int = Int.MAX_VALUE,
|
||||||
|
val currentFilter: MutableStateFlow<RoomListFilter> = MutableStateFlow(RoomListFilter.None),
|
||||||
|
private val loadMoreLambda: () -> Unit = {},
|
||||||
|
private val resetLambda: () -> Unit = {},
|
||||||
|
private val updateFilterLambda: (RoomListFilter) -> Unit = { filter -> currentFilter.value = filter },
|
||||||
|
private val rebuildSummariesLambda: () -> Unit = {},
|
||||||
) : DynamicRoomList {
|
) : DynamicRoomList {
|
||||||
override val pageSize: Int = Int.MAX_VALUE
|
|
||||||
override val loadedPages = MutableStateFlow(1)
|
|
||||||
|
|
||||||
override val filteredSummaries: SharedFlow<List<RoomSummary>> = summaries
|
|
||||||
|
|
||||||
override suspend fun loadMore() {
|
override suspend fun loadMore() {
|
||||||
// No-op
|
loadMoreLambda()
|
||||||
loadedPages.getAndUpdate { it + 1 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun reset() {
|
override suspend fun reset() {
|
||||||
loadedPages.emit(1)
|
resetLambda()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateFilter(filter: RoomListFilter) {
|
override suspend fun updateFilter(filter: RoomListFilter) {
|
||||||
currentFilter.emit(filter)
|
updateFilterLambda(filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun rebuildSummaries() {
|
override suspend fun rebuildSummaries() {
|
||||||
// No-op
|
rebuildSummariesLambda()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,29 +11,19 @@ package io.element.android.libraries.matrix.test.roomlist
|
||||||
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.roomlist.DynamicRoomList
|
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.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
class FakeRoomListService(
|
class FakeRoomListService(
|
||||||
var subscribeToVisibleRoomsLambda: (List<RoomId>) -> Unit = {},
|
private val subscribeToVisibleRoomsLambda: (List<RoomId>) -> Unit = {},
|
||||||
|
private val createRoomListLambda: (pageSize: Int) -> DynamicRoomList = { pageSize -> FakeDynamicRoomList(pageSize = pageSize) },
|
||||||
|
override val allRooms: RoomList = createRoomListLambda(Int.MAX_VALUE),
|
||||||
) : RoomListService {
|
) : RoomListService {
|
||||||
private val allRoomSummariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList())
|
|
||||||
private val allRoomsLoadingStateFlow = MutableStateFlow<RoomList.LoadingState>(RoomList.LoadingState.NotLoaded)
|
|
||||||
private val roomListStateFlow = MutableStateFlow<RoomListService.State>(RoomListService.State.Idle)
|
private val roomListStateFlow = MutableStateFlow<RoomListService.State>(RoomListService.State.Idle)
|
||||||
private val syncIndicatorStateFlow = MutableStateFlow<RoomListService.SyncIndicator>(RoomListService.SyncIndicator.Hide)
|
private val syncIndicatorStateFlow = MutableStateFlow<RoomListService.SyncIndicator>(RoomListService.SyncIndicator.Hide)
|
||||||
|
|
||||||
suspend fun postAllRooms(roomSummaries: List<RoomSummary>) {
|
|
||||||
allRoomSummariesFlow.emit(roomSummaries)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun postAllRoomsLoadingState(loadingState: RoomList.LoadingState) {
|
|
||||||
allRoomsLoadingStateFlow.emit(loadingState)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun postState(state: RoomListService.State) {
|
suspend fun postState(state: RoomListService.State) {
|
||||||
roomListStateFlow.emit(state)
|
roomListStateFlow.emit(state)
|
||||||
}
|
}
|
||||||
|
|
@ -44,25 +34,14 @@ class FakeRoomListService(
|
||||||
|
|
||||||
override fun createRoomList(
|
override fun createRoomList(
|
||||||
pageSize: Int,
|
pageSize: Int,
|
||||||
initialFilter: RoomListFilter,
|
|
||||||
source: RoomList.Source,
|
source: RoomList.Source,
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
): DynamicRoomList {
|
) = createRoomListLambda(pageSize)
|
||||||
return when (source) {
|
|
||||||
RoomList.Source.All -> allRooms
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>) {
|
override suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>) {
|
||||||
subscribeToVisibleRoomsLambda(roomIds)
|
subscribeToVisibleRoomsLambda(roomIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val allRooms = SimplePagedRoomList(
|
|
||||||
allRoomSummariesFlow,
|
|
||||||
allRoomsLoadingStateFlow,
|
|
||||||
MutableStateFlow(RoomListFilter.all())
|
|
||||||
)
|
|
||||||
|
|
||||||
override val state: StateFlow<RoomListService.State> = roomListStateFlow
|
override val state: StateFlow<RoomListService.State> = roomListStateFlow
|
||||||
|
|
||||||
override val syncIndicator: StateFlow<RoomListService.SyncIndicator> = syncIndicatorStateFlow
|
override val syncIndicator: StateFlow<RoomListService.SyncIndicator> = syncIndicatorStateFlow
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.libraries.roomselect.impl"
|
namespace = "io.element.android.libraries.roomselect.impl"
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDependencyInjection()
|
setupDependencyInjection()
|
||||||
|
|
@ -30,6 +36,6 @@ dependencies {
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
api(projects.libraries.roomselect.api)
|
api(projects.libraries.roomselect.api)
|
||||||
|
|
||||||
testCommonDependencies(libs)
|
testCommonDependencies(libs, includeTestComposeView = true)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,5 @@ sealed interface RoomSelectEvents {
|
||||||
// TODO remove to restore multi-selection
|
// TODO remove to restore multi-selection
|
||||||
data object RemoveSelectedRoom : RoomSelectEvents
|
data object RemoveSelectedRoom : RoomSelectEvents
|
||||||
data object ToggleSearchActive : RoomSelectEvents
|
data object ToggleSearchActive : RoomSelectEvents
|
||||||
|
data class UpdateVisibleRange(val range: IntRange) : RoomSelectEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class RoomSelectPresenter(
|
class RoomSelectPresenter(
|
||||||
|
|
@ -80,6 +81,9 @@ class RoomSelectPresenter(
|
||||||
}
|
}
|
||||||
RoomSelectEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf()
|
RoomSelectEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf()
|
||||||
RoomSelectEvents.ToggleSearchActive -> isSearchActive = !isSearchActive
|
RoomSelectEvents.ToggleSearchActive -> isSearchActive = !isSearchActive
|
||||||
|
is RoomSelectEvents.UpdateVisibleRange -> coroutineScope.launch {
|
||||||
|
dataSource.updateVisibleRange(event.range)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
|
import io.element.android.libraries.matrix.api.roomlist.updateVisibleRange
|
||||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -46,14 +46,11 @@ class RoomSelectSearchDataSource(
|
||||||
|
|
||||||
private val roomList = roomListService.createRoomList(
|
private val roomList = roomListService.createRoomList(
|
||||||
pageSize = PAGE_SIZE,
|
pageSize = PAGE_SIZE,
|
||||||
initialFilter = RoomListFilter.all(),
|
|
||||||
source = RoomList.Source.All,
|
source = RoomList.Source.All,
|
||||||
coroutineScope = coroutineScope
|
coroutineScope = coroutineScope
|
||||||
).apply {
|
)
|
||||||
loadAllIncrementally(coroutineScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = roomList.filteredSummaries
|
val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = roomList.summaries
|
||||||
.map { roomSummaries ->
|
.map { roomSummaries ->
|
||||||
roomSummaries
|
roomSummaries
|
||||||
.filter { it.info.currentUserMembership == CurrentUserMembership.JOINED }
|
.filter { it.info.currentUserMembership == CurrentUserMembership.JOINED }
|
||||||
|
|
@ -63,6 +60,10 @@ class RoomSelectSearchDataSource(
|
||||||
}
|
}
|
||||||
.flowOn(coroutineDispatchers.computation)
|
.flowOn(coroutineDispatchers.computation)
|
||||||
|
|
||||||
|
suspend fun updateVisibleRange(visibleRange: IntRange) {
|
||||||
|
roomList.updateVisibleRange(visibleRange)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun setSearchQuery(searchQuery: String) = coroutineScope {
|
suspend fun setSearchQuery(searchQuery: String) = coroutineScope {
|
||||||
val filter = if (searchQuery.isBlank()) {
|
val filter = if (searchQuery.isBlank()) {
|
||||||
RoomListFilter.all()
|
RoomListFilter.all()
|
||||||
|
|
|
||||||
|
|
@ -43,22 +43,23 @@ open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun aRoomSelectState(
|
internal fun aRoomSelectState(
|
||||||
mode: RoomSelectMode = RoomSelectMode.Forward,
|
mode: RoomSelectMode = RoomSelectMode.Forward,
|
||||||
resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>> = SearchBarResultState.Initial(),
|
resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>> = SearchBarResultState.Initial(),
|
||||||
searchQuery: String = "",
|
searchQuery: String = "",
|
||||||
isSearchActive: Boolean = false,
|
isSearchActive: Boolean = false,
|
||||||
selectedRooms: ImmutableList<SelectRoomInfo> = persistentListOf(),
|
selectedRooms: ImmutableList<SelectRoomInfo> = persistentListOf(),
|
||||||
|
eventSink: (RoomSelectEvents) -> Unit = {},
|
||||||
) = RoomSelectState(
|
) = RoomSelectState(
|
||||||
mode = mode,
|
mode = mode,
|
||||||
resultState = resultState,
|
resultState = resultState,
|
||||||
searchQuery = TextFieldState(initialText = searchQuery),
|
searchQuery = TextFieldState(initialText = searchQuery),
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
selectedRooms = selectedRooms,
|
selectedRooms = selectedRooms,
|
||||||
eventSink = {}
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun aRoomSelectRoomList() = persistentListOf(
|
internal fun aRoomSelectRoomList() = persistentListOf(
|
||||||
aSelectRoomInfo(
|
aSelectRoomInfo(
|
||||||
roomId = RoomId("!room1:domain"),
|
roomId = RoomId("!room1:domain"),
|
||||||
name = "Room with name",
|
name = "Room with name",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
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.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -51,6 +52,7 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
||||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
|
|
@ -100,6 +102,11 @@ fun RoomSelectView(
|
||||||
onBack = { onBackButton(state) }
|
onBack = { onBackButton(state) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||||
|
state.eventSink(RoomSelectEvents.UpdateVisibleRange(visibleRange))
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
|
|
@ -138,7 +145,7 @@ fun RoomSelectView(
|
||||||
resultState = state.resultState,
|
resultState = state.resultState,
|
||||||
showBackButton = false,
|
showBackButton = false,
|
||||||
) { summaries ->
|
) { summaries ->
|
||||||
LazyColumn {
|
LazyColumn(state = lazyListState) {
|
||||||
item {
|
item {
|
||||||
SelectedRoomsHelper(
|
SelectedRoomsHelper(
|
||||||
// TODO state.isForwarding
|
// TODO state.isForwarding
|
||||||
|
|
@ -170,7 +177,7 @@ fun RoomSelectView(
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
if (state.resultState is SearchBarResultState.Results) {
|
if (state.resultState is SearchBarResultState.Results) {
|
||||||
LazyColumn {
|
LazyColumn(state = lazyListState) {
|
||||||
items(state.resultState.results, key = { it.roomId.value }) { roomSummary ->
|
items(state.resultState.results, key = { it.roomId.value }) { roomSummary ->
|
||||||
Column {
|
Column {
|
||||||
RoomSummaryView(
|
RoomSummaryView(
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,17 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
||||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -63,9 +67,12 @@ class RoomSelectPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - update query`() = runTest {
|
fun `present - update query`() = runTest {
|
||||||
val roomSummary = aRoomSummary()
|
val roomSummary = aRoomSummary()
|
||||||
val roomListService = FakeRoomListService().apply {
|
val roomList = FakeDynamicRoomList(
|
||||||
postAllRooms(listOf(roomSummary))
|
summaries = MutableStateFlow(listOf(roomSummary))
|
||||||
}
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val presenter = createRoomSelectPresenter(
|
val presenter = createRoomSelectPresenter(
|
||||||
roomListService = roomListService
|
roomListService = roomListService
|
||||||
)
|
)
|
||||||
|
|
@ -81,12 +88,12 @@ class RoomSelectPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
initialState.searchQuery.setTextAndPlaceCursorAtEnd("string not contained")
|
initialState.searchQuery.setTextAndPlaceCursorAtEnd("string not contained")
|
||||||
assertThat(
|
assertThat(
|
||||||
roomListService.allRooms.currentFilter.value
|
roomList.currentFilter.value
|
||||||
).isEqualTo(
|
).isEqualTo(
|
||||||
RoomListFilter.NormalizedMatchRoomName("string not contained")
|
RoomListFilter.NormalizedMatchRoomName("string not contained")
|
||||||
)
|
)
|
||||||
assertThat(awaitItem().searchQuery.text.toString()).isEqualTo("string not contained")
|
assertThat(awaitItem().searchQuery.text.toString()).isEqualTo("string not contained")
|
||||||
roomListService.postAllRooms(
|
roomList.summaries.emit(
|
||||||
emptyList()
|
emptyList()
|
||||||
)
|
)
|
||||||
assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
|
assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
|
||||||
|
|
@ -96,9 +103,12 @@ class RoomSelectPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - select and remove a room`() = runTest {
|
fun `present - select and remove a room`() = runTest {
|
||||||
val roomSummary = aRoomSummary()
|
val roomSummary = aRoomSummary()
|
||||||
val roomListService = FakeRoomListService().apply {
|
val roomList = FakeDynamicRoomList(
|
||||||
postAllRooms(listOf(roomSummary))
|
summaries = MutableStateFlow(listOf(roomSummary))
|
||||||
}
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
val presenter = createRoomSelectPresenter(
|
val presenter = createRoomSelectPresenter(
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
)
|
)
|
||||||
|
|
@ -114,6 +124,35 @@ class RoomSelectPresenterTest {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - UpdateVisibleRange triggers pagination when near end`() = runTest {
|
||||||
|
val loadMoreLambda = lambdaRecorder<Unit> { }
|
||||||
|
val roomList = FakeDynamicRoomList(
|
||||||
|
summaries = MutableStateFlow(listOf()),
|
||||||
|
loadMoreLambda = loadMoreLambda,
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService(
|
||||||
|
createRoomListLambda = { roomList }
|
||||||
|
)
|
||||||
|
val presenter = createRoomSelectPresenter(roomListService = roomListService)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
// Post some rooms to simulate loaded content
|
||||||
|
val rooms = (1..10).map { aRoomSummary() }
|
||||||
|
roomList.summaries.emit(rooms)
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
|
// UpdateVisibleRange near end should trigger loadMore
|
||||||
|
initialState.eventSink(RoomSelectEvents.UpdateVisibleRange(IntRange(0, 9)))
|
||||||
|
// Give time for the coroutine to complete
|
||||||
|
testScheduler.advanceUntilIdle()
|
||||||
|
|
||||||
|
assert(loadMoreLambda).isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun TestScope.createRoomSelectPresenter(
|
internal fun TestScope.createRoomSelectPresenter(
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@
|
||||||
<string name="action_enable">"Aktivieren"</string>
|
<string name="action_enable">"Aktivieren"</string>
|
||||||
<string name="action_end_poll">"Umfrage beenden"</string>
|
<string name="action_end_poll">"Umfrage beenden"</string>
|
||||||
<string name="action_enter_pin">"PIN eingeben"</string>
|
<string name="action_enter_pin">"PIN eingeben"</string>
|
||||||
|
<string name="action_explore_public_spaces">"Erkunde öffentliche Spaces"</string>
|
||||||
<string name="action_finish">"Fertigstellen"</string>
|
<string name="action_finish">"Fertigstellen"</string>
|
||||||
<string name="action_forgot_password">"Passwort vergessen?"</string>
|
<string name="action_forgot_password">"Passwort vergessen?"</string>
|
||||||
<string name="action_forward">"Weiterleiten"</string>
|
<string name="action_forward">"Weiterleiten"</string>
|
||||||
|
|
@ -303,6 +304,7 @@ Grund: %1$s."</string>
|
||||||
<string name="common_report_a_problem">"Ein Problem melden"</string>
|
<string name="common_report_a_problem">"Ein Problem melden"</string>
|
||||||
<string name="common_report_submitted">"Bericht eingereicht"</string>
|
<string name="common_report_submitted">"Bericht eingereicht"</string>
|
||||||
<string name="common_rich_text_editor">"Rich-Text-Editor"</string>
|
<string name="common_rich_text_editor">"Rich-Text-Editor"</string>
|
||||||
|
<string name="common_role">"Rolle"</string>
|
||||||
<string name="common_room">"Chat"</string>
|
<string name="common_room">"Chat"</string>
|
||||||
<string name="common_room_name">"Chat-Name"</string>
|
<string name="common_room_name">"Chat-Name"</string>
|
||||||
<string name="common_room_name_placeholder">"z.B. Projektname"</string>
|
<string name="common_room_name_placeholder">"z.B. Projektname"</string>
|
||||||
|
|
@ -332,6 +334,7 @@ Grund: %1$s."</string>
|
||||||
<string name="common_server_url">"Server-URL"</string>
|
<string name="common_server_url">"Server-URL"</string>
|
||||||
<string name="common_settings">"Einstellungen"</string>
|
<string name="common_settings">"Einstellungen"</string>
|
||||||
<string name="common_share_space">"Space teilen"</string>
|
<string name="common_share_space">"Space teilen"</string>
|
||||||
|
<string name="common_shared_history">"Neue Mitglieder sehen den Nachrichtenverlauf"</string>
|
||||||
<string name="common_shared_location">"Geteilter Standort"</string>
|
<string name="common_shared_location">"Geteilter Standort"</string>
|
||||||
<string name="common_shared_space">"Gemeinsamer Space"</string>
|
<string name="common_shared_space">"Gemeinsamer Space"</string>
|
||||||
<string name="common_signing_out">"Abmelden"</string>
|
<string name="common_signing_out">"Abmelden"</string>
|
||||||
|
|
@ -383,6 +386,7 @@ Grund: %1$s."</string>
|
||||||
<string name="common_voice_message">"Sprachnachricht"</string>
|
<string name="common_voice_message">"Sprachnachricht"</string>
|
||||||
<string name="common_waiting">"Warten…"</string>
|
<string name="common_waiting">"Warten…"</string>
|
||||||
<string name="common_waiting_for_decryption_key">"Warte auf diese Nachricht"</string>
|
<string name="common_waiting_for_decryption_key">"Warte auf diese Nachricht"</string>
|
||||||
|
<string name="common_world_readable_history">"Jeder kann den Nachrichtenverlauf sehen"</string>
|
||||||
<string name="common_you">"Du"</string>
|
<string name="common_you">"Du"</string>
|
||||||
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"%1$s (%2$s) hat diese Nachricht geteilt, weil du nicht im Chat warst, als sie verschickt wurde."</string>
|
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"%1$s (%2$s) hat diese Nachricht geteilt, weil du nicht im Chat warst, als sie verschickt wurde."</string>
|
||||||
<string name="crypto_event_key_forwarded_unknown_profile_dialog_content">"Diese Nachricht wurde von %1$s weitergeleitet, da du zum Zeitpunkt des Versands kein Mitglied der Gruppe warst."</string>
|
<string name="crypto_event_key_forwarded_unknown_profile_dialog_content">"Diese Nachricht wurde von %1$s weitergeleitet, da du zum Zeitpunkt des Versands kein Mitglied der Gruppe warst."</string>
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@
|
||||||
<string name="a11y_your_avatar">"Sinu tunnuspilt"</string>
|
<string name="a11y_your_avatar">"Sinu tunnuspilt"</string>
|
||||||
<string name="action_accept">"Nõustu"</string>
|
<string name="action_accept">"Nõustu"</string>
|
||||||
<string name="action_add_caption">"Lisa selgitus"</string>
|
<string name="action_add_caption">"Lisa selgitus"</string>
|
||||||
|
<string name="action_add_existing_rooms">"Lisa olemasolevaid jututube"</string>
|
||||||
<string name="action_add_to_timeline">"Lisa ajajoonele"</string>
|
<string name="action_add_to_timeline">"Lisa ajajoonele"</string>
|
||||||
<string name="action_back">"Tagasi"</string>
|
<string name="action_back">"Tagasi"</string>
|
||||||
<string name="action_call">"Helista"</string>
|
<string name="action_call">"Helista"</string>
|
||||||
|
|
@ -75,6 +76,7 @@
|
||||||
<string name="action_copy_text">"Kopeeri tekst"</string>
|
<string name="action_copy_text">"Kopeeri tekst"</string>
|
||||||
<string name="action_create">"Loo"</string>
|
<string name="action_create">"Loo"</string>
|
||||||
<string name="action_create_room">"Loo jututuba"</string>
|
<string name="action_create_room">"Loo jututuba"</string>
|
||||||
|
<string name="action_create_space">"Loo kogukond"</string>
|
||||||
<string name="action_deactivate">"Eemalda konto"</string>
|
<string name="action_deactivate">"Eemalda konto"</string>
|
||||||
<string name="action_deactivate_account">"Eemalda konto kasutusest"</string>
|
<string name="action_deactivate_account">"Eemalda konto kasutusest"</string>
|
||||||
<string name="action_decline">"Keeldu"</string>
|
<string name="action_decline">"Keeldu"</string>
|
||||||
|
|
@ -91,6 +93,7 @@
|
||||||
<string name="action_enable">"Võta kasutusele"</string>
|
<string name="action_enable">"Võta kasutusele"</string>
|
||||||
<string name="action_end_poll">"Lõpeta küsitlus"</string>
|
<string name="action_end_poll">"Lõpeta küsitlus"</string>
|
||||||
<string name="action_enter_pin">"Sisesta PIN-kood"</string>
|
<string name="action_enter_pin">"Sisesta PIN-kood"</string>
|
||||||
|
<string name="action_explore_public_spaces">"Uuri ja avasta avalikke kogukondi"</string>
|
||||||
<string name="action_finish">"Lõpeta"</string>
|
<string name="action_finish">"Lõpeta"</string>
|
||||||
<string name="action_forgot_password">"Kas unustasid salasõna?"</string>
|
<string name="action_forgot_password">"Kas unustasid salasõna?"</string>
|
||||||
<string name="action_forward">"Edasta"</string>
|
<string name="action_forward">"Edasta"</string>
|
||||||
|
|
@ -160,7 +163,7 @@
|
||||||
<string name="action_start_over">"Alusta uuesti"</string>
|
<string name="action_start_over">"Alusta uuesti"</string>
|
||||||
<string name="action_start_verification">"Alusta verifitseerimist"</string>
|
<string name="action_start_verification">"Alusta verifitseerimist"</string>
|
||||||
<string name="action_static_map_load">"Kaardi laadimiseks klõpsa"</string>
|
<string name="action_static_map_load">"Kaardi laadimiseks klõpsa"</string>
|
||||||
<string name="action_take_photo">"Tee pilt"</string>
|
<string name="action_take_photo">"Pildista"</string>
|
||||||
<string name="action_tap_for_options">"Valikuteks klõpsa"</string>
|
<string name="action_tap_for_options">"Valikuteks klõpsa"</string>
|
||||||
<string name="action_translate">"Tõlgi"</string>
|
<string name="action_translate">"Tõlgi"</string>
|
||||||
<string name="action_try_again">"Proovi uuesti"</string>
|
<string name="action_try_again">"Proovi uuesti"</string>
|
||||||
|
|
@ -192,6 +195,7 @@
|
||||||
<string name="common_copied_to_clipboard">"Kopeeritud lõikelauale"</string>
|
<string name="common_copied_to_clipboard">"Kopeeritud lõikelauale"</string>
|
||||||
<string name="common_copyright">"Autoriõigused"</string>
|
<string name="common_copyright">"Autoriõigused"</string>
|
||||||
<string name="common_creating_room">"Loome jututoa…"</string>
|
<string name="common_creating_room">"Loome jututoa…"</string>
|
||||||
|
<string name="common_creating_space">"Loon kogukonda…"</string>
|
||||||
<string name="common_current_user_canceled_knock">"Päring on tühistatud"</string>
|
<string name="common_current_user_canceled_knock">"Päring on tühistatud"</string>
|
||||||
<string name="common_current_user_left_room">"Lahkus jututoast"</string>
|
<string name="common_current_user_left_room">"Lahkus jututoast"</string>
|
||||||
<string name="common_current_user_left_space">"Lahkus kogukonnast"</string>
|
<string name="common_current_user_left_space">"Lahkus kogukonnast"</string>
|
||||||
|
|
@ -290,6 +294,7 @@ Põhjus: %1$s."</string>
|
||||||
<string name="common_reason">"Põhjus"</string>
|
<string name="common_reason">"Põhjus"</string>
|
||||||
<string name="common_recovery_key">"Taastevõti"</string>
|
<string name="common_recovery_key">"Taastevõti"</string>
|
||||||
<string name="common_refreshing">"Värskendame andmeid…"</string>
|
<string name="common_refreshing">"Värskendame andmeid…"</string>
|
||||||
|
<string name="common_removing">"Eemaldan…"</string>
|
||||||
<plurals name="common_replies">
|
<plurals name="common_replies">
|
||||||
<item quantity="one">"%1$d vastus"</item>
|
<item quantity="one">"%1$d vastus"</item>
|
||||||
<item quantity="other">"%1$d vastust"</item>
|
<item quantity="other">"%1$d vastust"</item>
|
||||||
|
|
@ -299,9 +304,10 @@ Põhjus: %1$s."</string>
|
||||||
<string name="common_report_a_problem">"Teata veast"</string>
|
<string name="common_report_a_problem">"Teata veast"</string>
|
||||||
<string name="common_report_submitted">"Veateade on saadetud"</string>
|
<string name="common_report_submitted">"Veateade on saadetud"</string>
|
||||||
<string name="common_rich_text_editor">"Vormindatud teksti toimeti"</string>
|
<string name="common_rich_text_editor">"Vormindatud teksti toimeti"</string>
|
||||||
|
<string name="common_role">"Roll"</string>
|
||||||
<string name="common_room">"Jututuba"</string>
|
<string name="common_room">"Jututuba"</string>
|
||||||
<string name="common_room_name">"Jututoa nimi"</string>
|
<string name="common_room_name">"Jututoa nimi"</string>
|
||||||
<string name="common_room_name_placeholder">"näiteks sinu projekti või seltsingu nimi"</string>
|
<string name="common_room_name_placeholder">"nt. sinu projekti või seltsingu nimi"</string>
|
||||||
<plurals name="common_rooms">
|
<plurals name="common_rooms">
|
||||||
<item quantity="one">"%1$d jututuba"</item>
|
<item quantity="one">"%1$d jututuba"</item>
|
||||||
<item quantity="other">"%1$d jututuba"</item>
|
<item quantity="other">"%1$d jututuba"</item>
|
||||||
|
|
@ -314,6 +320,10 @@ Põhjus: %1$s."</string>
|
||||||
<string name="common_security">"Turvalisus"</string>
|
<string name="common_security">"Turvalisus"</string>
|
||||||
<string name="common_seen_by">"Seda nägi(d)"</string>
|
<string name="common_seen_by">"Seda nägi(d)"</string>
|
||||||
<string name="common_select_account">"Vali kasutajakonto"</string>
|
<string name="common_select_account">"Vali kasutajakonto"</string>
|
||||||
|
<plurals name="common_selected_count">
|
||||||
|
<item quantity="one">"%1$d on valitud"</item>
|
||||||
|
<item quantity="other">"%1$d on valitud"</item>
|
||||||
|
</plurals>
|
||||||
<string name="common_send_to">"Saada kasutajale"</string>
|
<string name="common_send_to">"Saada kasutajale"</string>
|
||||||
<string name="common_sending">"Saadame…"</string>
|
<string name="common_sending">"Saadame…"</string>
|
||||||
<string name="common_sending_failed">"Saatmine ei õnnestunud"</string>
|
<string name="common_sending_failed">"Saatmine ei õnnestunud"</string>
|
||||||
|
|
@ -324,6 +334,7 @@ Põhjus: %1$s."</string>
|
||||||
<string name="common_server_url">"Serveri URL"</string>
|
<string name="common_server_url">"Serveri URL"</string>
|
||||||
<string name="common_settings">"Seadistused"</string>
|
<string name="common_settings">"Seadistused"</string>
|
||||||
<string name="common_share_space">"Jaga kogukonda"</string>
|
<string name="common_share_space">"Jaga kogukonda"</string>
|
||||||
|
<string name="common_shared_history">"Uued liikmed näevad ajalugu"</string>
|
||||||
<string name="common_shared_location">"Jagatud asukoht"</string>
|
<string name="common_shared_location">"Jagatud asukoht"</string>
|
||||||
<string name="common_shared_space">"Jagatud kogukond"</string>
|
<string name="common_shared_space">"Jagatud kogukond"</string>
|
||||||
<string name="common_signing_out">"Logime välja"</string>
|
<string name="common_signing_out">"Logime välja"</string>
|
||||||
|
|
@ -338,6 +349,7 @@ Põhjus: %1$s."</string>
|
||||||
<string name="common_starting_chat">"Alustame vestlust…"</string>
|
<string name="common_starting_chat">"Alustame vestlust…"</string>
|
||||||
<string name="common_sticker">"Kleeps"</string>
|
<string name="common_sticker">"Kleeps"</string>
|
||||||
<string name="common_success">"Õnnestus"</string>
|
<string name="common_success">"Õnnestus"</string>
|
||||||
|
<string name="common_suggested">"Soovitatud"</string>
|
||||||
<string name="common_suggestions">"Soovitused"</string>
|
<string name="common_suggestions">"Soovitused"</string>
|
||||||
<string name="common_syncing">"Sünkroniseerime"</string>
|
<string name="common_syncing">"Sünkroniseerime"</string>
|
||||||
<string name="common_system">"Süsteem"</string>
|
<string name="common_system">"Süsteem"</string>
|
||||||
|
|
@ -374,7 +386,9 @@ Põhjus: %1$s."</string>
|
||||||
<string name="common_voice_message">"Häälsõnum"</string>
|
<string name="common_voice_message">"Häälsõnum"</string>
|
||||||
<string name="common_waiting">"Ootame…"</string>
|
<string name="common_waiting">"Ootame…"</string>
|
||||||
<string name="common_waiting_for_decryption_key">"Ootame selle sõnumi dekrüptimisvõtit"</string>
|
<string name="common_waiting_for_decryption_key">"Ootame selle sõnumi dekrüptimisvõtit"</string>
|
||||||
|
<string name="common_world_readable_history">"Kõik võivad ajalugu näha"</string>
|
||||||
<string name="common_you">"Sina"</string>
|
<string name="common_you">"Sina"</string>
|
||||||
|
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"Kuna sind polnud saatmise ajal jututoas, siis %1$s (%2$s) jagas seda sõnumit sinuga."</string>
|
||||||
<string name="crypto_history_visible">"See jututuba on seadistatud sedaviisi, et ka uued liikmed saavad lugeda varasemat ajalugu. %1$s"</string>
|
<string name="crypto_history_visible">"See jututuba on seadistatud sedaviisi, et ka uued liikmed saavad lugeda varasemat ajalugu. %1$s"</string>
|
||||||
<string name="crypto_identity_change_pin_violation">"Kasutaja %1$s võrguidentiteet on lähtestatud. %2$s"</string>
|
<string name="crypto_identity_change_pin_violation">"Kasutaja %1$s võrguidentiteet on lähtestatud. %2$s"</string>
|
||||||
<string name="crypto_identity_change_pin_violation_new">"Kasutaja %1$s %2$s võrguidentiteet on lähtestatud. %3$s"</string>
|
<string name="crypto_identity_change_pin_violation_new">"Kasutaja %1$s %2$s võrguidentiteet on lähtestatud. %3$s"</string>
|
||||||
|
|
@ -467,6 +481,7 @@ Kas sa oled kindel, et soovid jätkata?"</string>
|
||||||
<string name="screen_share_this_location_action">"Jaga seda asukohta"</string>
|
<string name="screen_share_this_location_action">"Jaga seda asukohta"</string>
|
||||||
<string name="screen_space_list_description">"Sinu loodud kogukonnad ning need, millega oled liitunud."</string>
|
<string name="screen_space_list_description">"Sinu loodud kogukonnad ning need, millega oled liitunud."</string>
|
||||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||||
|
<string name="screen_space_list_empty_state_title">"Jututubade haldamiseks võid luua kogukondi"</string>
|
||||||
<string name="screen_space_list_parent_space">"Kogukond: %1$s"</string>
|
<string name="screen_space_list_parent_space">"Kogukond: %1$s"</string>
|
||||||
<string name="screen_space_list_title">"Kogukonnad"</string>
|
<string name="screen_space_list_title">"Kogukonnad"</string>
|
||||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Sõnum on saatmata, kuna kasutaja %1$s verifitseeritud identiteet on lähtestatud."</string>
|
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Sõnum on saatmata, kuna kasutaja %1$s verifitseeritud identiteet on lähtestatud."</string>
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ Raison : %1$s."</string>
|
||||||
<string name="common_everyone">"Tout le monde"</string>
|
<string name="common_everyone">"Tout le monde"</string>
|
||||||
<string name="common_failed">"Échec"</string>
|
<string name="common_failed">"Échec"</string>
|
||||||
<string name="common_favourite">"Favori"</string>
|
<string name="common_favourite">"Favori"</string>
|
||||||
<string name="common_favourited">"Favorisé"</string>
|
<string name="common_favourited">"Ajouté aux favoris"</string>
|
||||||
<string name="common_file">"Fichier"</string>
|
<string name="common_file">"Fichier"</string>
|
||||||
<string name="common_file_deleted">"Fichier supprimé"</string>
|
<string name="common_file_deleted">"Fichier supprimé"</string>
|
||||||
<string name="common_file_saved">"Fichier enregistré"</string>
|
<string name="common_file_saved">"Fichier enregistré"</string>
|
||||||
|
|
@ -334,6 +334,7 @@ Raison : %1$s."</string>
|
||||||
<string name="common_server_url">"URL du serveur"</string>
|
<string name="common_server_url">"URL du serveur"</string>
|
||||||
<string name="common_settings">"Paramètres"</string>
|
<string name="common_settings">"Paramètres"</string>
|
||||||
<string name="common_share_space">"Partager l’espace"</string>
|
<string name="common_share_space">"Partager l’espace"</string>
|
||||||
|
<string name="common_shared_history">"Les nouveaux membres voient les anciens messages"</string>
|
||||||
<string name="common_shared_location">"Position partagée"</string>
|
<string name="common_shared_location">"Position partagée"</string>
|
||||||
<string name="common_shared_space">"Espace partagé"</string>
|
<string name="common_shared_space">"Espace partagé"</string>
|
||||||
<string name="common_signing_out">"Déconnexion"</string>
|
<string name="common_signing_out">"Déconnexion"</string>
|
||||||
|
|
@ -385,6 +386,7 @@ Raison : %1$s."</string>
|
||||||
<string name="common_voice_message">"Message vocal"</string>
|
<string name="common_voice_message">"Message vocal"</string>
|
||||||
<string name="common_waiting">"En attente…"</string>
|
<string name="common_waiting">"En attente…"</string>
|
||||||
<string name="common_waiting_for_decryption_key">"En attente de la clé de déchiffrement"</string>
|
<string name="common_waiting_for_decryption_key">"En attente de la clé de déchiffrement"</string>
|
||||||
|
<string name="common_world_readable_history">"Tout le monde peut voir les anciens messages"</string>
|
||||||
<string name="common_you">"Vous"</string>
|
<string name="common_you">"Vous"</string>
|
||||||
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"%1$s (%2$s) a partagé ce message avec vous car vous n’étiez pas dans le salon lors de son envoi."</string>
|
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"%1$s (%2$s) a partagé ce message avec vous car vous n’étiez pas dans le salon lors de son envoi."</string>
|
||||||
<string name="crypto_event_key_forwarded_unknown_profile_dialog_content">"%1$s a partagé ce message avec vous car vous n’étiez pas dans le salon lors de son envoi."</string>
|
<string name="crypto_event_key_forwarded_unknown_profile_dialog_content">"%1$s a partagé ce message avec vous car vous n’étiez pas dans le salon lors de son envoi."</string>
|
||||||
|
|
|
||||||
|
|
@ -333,6 +333,7 @@ Ok: %1$s."</string>
|
||||||
<string name="common_server_url">"Kiszolgáló webcíme"</string>
|
<string name="common_server_url">"Kiszolgáló webcíme"</string>
|
||||||
<string name="common_settings">"Beállítások"</string>
|
<string name="common_settings">"Beállítások"</string>
|
||||||
<string name="common_share_space">"Tér megosztása"</string>
|
<string name="common_share_space">"Tér megosztása"</string>
|
||||||
|
<string name="common_shared_history">"Az új tagok látják az előzményeket"</string>
|
||||||
<string name="common_shared_location">"Megosztott tartózkodási hely"</string>
|
<string name="common_shared_location">"Megosztott tartózkodási hely"</string>
|
||||||
<string name="common_shared_space">"Megosztott tér"</string>
|
<string name="common_shared_space">"Megosztott tér"</string>
|
||||||
<string name="common_signing_out">"Kijelentkezés"</string>
|
<string name="common_signing_out">"Kijelentkezés"</string>
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
<string name="action_copy_link_to_message">"Kopiera länk till meddelande"</string>
|
<string name="action_copy_link_to_message">"Kopiera länk till meddelande"</string>
|
||||||
<string name="action_copy_text">"Kopiera text"</string>
|
<string name="action_copy_text">"Kopiera text"</string>
|
||||||
<string name="action_create">"Skapa"</string>
|
<string name="action_create">"Skapa"</string>
|
||||||
<string name="action_create_room">"Skapa ett rum"</string>
|
<string name="action_create_room">"Skapa rum"</string>
|
||||||
<string name="action_deactivate">"Inaktivera"</string>
|
<string name="action_deactivate">"Inaktivera"</string>
|
||||||
<string name="action_deactivate_account">"Inaktivera konto"</string>
|
<string name="action_deactivate_account">"Inaktivera konto"</string>
|
||||||
<string name="action_decline">"Neka"</string>
|
<string name="action_decline">"Neka"</string>
|
||||||
|
|
|
||||||
|
|
@ -386,6 +386,7 @@ Reason: %1$s."</string>
|
||||||
<string name="common_voice_message">"Voice message"</string>
|
<string name="common_voice_message">"Voice message"</string>
|
||||||
<string name="common_waiting">"Waiting…"</string>
|
<string name="common_waiting">"Waiting…"</string>
|
||||||
<string name="common_waiting_for_decryption_key">"Waiting for this message"</string>
|
<string name="common_waiting_for_decryption_key">"Waiting for this message"</string>
|
||||||
|
<string name="common_world_readable_history">"Anyone can see history"</string>
|
||||||
<string name="common_you">"You"</string>
|
<string name="common_you">"You"</string>
|
||||||
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"%1$s (%2$s) shared this message since you were not in the room when it was sent."</string>
|
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"%1$s (%2$s) shared this message since you were not in the room when it was sent."</string>
|
||||||
<string name="crypto_event_key_forwarded_unknown_profile_dialog_content">"%1$s shared this message since you were not in the room when it was sent."</string>
|
<string name="crypto_event_key_forwarded_unknown_profile_dialog_content">"%1$s shared this message since you were not in the room when it was sent."</string>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:153b2806d0c7d5365655f251574dc4d8efdabb2ed7df730d6868ef26c4220aeb
|
oid sha256:74b77bc74d63b75022a0a9359f5bdf3954cfb0666655668944ff15921c319471
|
||||||
size 35747
|
size 34408
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue