Browse Source

Merge pull request #2422 from element-hq/feature/fga/room_list_filters

[Feature] Room list filters
pull/2440/head
ganfra 7 months ago committed by GitHub
parent
commit
57f99a9090
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
  2. 3
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
  3. 7
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt
  4. 32
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt
  5. 5
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/di/RoomListModule.kt
  6. 38
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFilter.kt
  7. 22
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEvents.kt
  8. 98
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt
  9. 28
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt
  10. 45
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersStateProvider.kt
  11. 173
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt
  12. 4
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
  13. 125
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTests.kt
  14. 72
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersViewTests.kt
  15. 57
      libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/FadingEdge.kt
  16. 7
      libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
  17. 1
      libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt
  18. 5
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt
  19. 1
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt
  20. 1
      libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt
  21. 5
      samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt
  22. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_0,NEXUS_5,1.0,en].png
  23. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_1,NEXUS_5,1.0,en].png
  24. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_0,NEXUS_5,1.0,en].png
  25. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_1,NEXUS_5,1.0,en].png
  26. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-11_12_null,NEXUS_5,1.0,en].png
  27. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Night-11_13_null,NEXUS_5,1.0,en].png
  28. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_0,NEXUS_5,1.0,en].png
  29. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_1,NEXUS_5,1.0,en].png
  30. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_0,NEXUS_5,1.0,en].png
  31. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_1,NEXUS_5,1.0,en].png
  32. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_1,NEXUS_5,1.0,en].png
  33. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_13,NEXUS_5,1.0,en].png
  34. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_2,NEXUS_5,1.0,en].png
  35. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_3,NEXUS_5,1.0,en].png
  36. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_4,NEXUS_5,1.0,en].png
  37. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_5,NEXUS_5,1.0,en].png
  38. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_7,NEXUS_5,1.0,en].png
  39. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_1,NEXUS_5,1.0,en].png
  40. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_13,NEXUS_5,1.0,en].png
  41. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_2,NEXUS_5,1.0,en].png
  42. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_3,NEXUS_5,1.0,en].png
  43. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_4,NEXUS_5,1.0,en].png
  44. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_5,NEXUS_5,1.0,en].png
  45. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_7,NEXUS_5,1.0,en].png

7
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt

@ -37,6 +37,7 @@ import io.element.android.features.networkmonitor.api.NetworkStatus @@ -37,6 +37,7 @@ import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
import io.element.android.features.roomlist.impl.search.RoomListSearchState
@ -82,6 +83,7 @@ class RoomListPresenter @Inject constructor( @@ -82,6 +83,7 @@ class RoomListPresenter @Inject constructor(
private val roomListDataSource: RoomListDataSource,
private val featureFlagService: FeatureFlagService,
private val indicatorService: IndicatorService,
private val filtersPresenter: Presenter<RoomListFiltersState>,
private val searchPresenter: Presenter<RoomListSearchState>,
private val migrationScreenPresenter: MigrationScreenPresenter,
private val sessionPreferencesStore: SessionPreferencesStore,
@ -102,6 +104,8 @@ class RoomListPresenter @Inject constructor( @@ -102,6 +104,8 @@ class RoomListPresenter @Inject constructor(
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
}
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
val filtersState = filtersPresenter.present()
val searchState = searchPresenter.present()
LaunchedEffect(Unit) {
@ -168,9 +172,10 @@ class RoomListPresenter @Inject constructor( @@ -168,9 +172,10 @@ class RoomListPresenter @Inject constructor(
invitesState = inviteStateDataSource.inviteState(),
contextMenu = contextMenu.value,
leaveRoomState = leaveRoomState,
filtersState = filtersState,
searchState = searchState,
displayMigrationStatus = isMigrating,
eventSink = ::handleEvents
eventSink = ::handleEvents,
)
}

3
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt

@ -18,6 +18,7 @@ package io.element.android.features.roomlist.impl @@ -18,6 +18,7 @@ package io.element.android.features.roomlist.impl
import androidx.compose.runtime.Immutable
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchState
import io.element.android.libraries.architecture.AsyncData
@ -37,10 +38,12 @@ data class RoomListState( @@ -37,10 +38,12 @@ data class RoomListState(
val invitesState: InvitesState,
val contextMenu: ContextMenu,
val leaveRoomState: LeaveRoomState,
val filtersState: RoomListFiltersState,
val searchState: RoomListSearchState,
val displayMigrationStatus: Boolean,
val eventSink: (RoomListEvents) -> Unit,
) {
val displayFilters = filtersState.isFeatureEnabled && !displayMigrationStatus
val displayEmptyState = roomList is AsyncData.Success && roomList.data.isEmpty()
sealed interface ContextMenu {

7
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt

@ -20,6 +20,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -20,6 +20,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.leaveroom.api.aLeaveRoomState
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchState
@ -39,18 +41,19 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> { @@ -39,18 +41,19 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
override val values: Sequence<RoomListState>
get() = sequenceOf(
aRoomListState(),
aRoomListState(securityBannerState = SecurityBannerState.SessionVerification),
aRoomListState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
aRoomListState(hasNetworkConnection = false),
aRoomListState(invitesState = InvitesState.SeenInvites),
aRoomListState(invitesState = InvitesState.NewInvites),
aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")),
aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)),
aRoomListState(securityBannerState = SecurityBannerState.SessionVerification),
aRoomListState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
aRoomListState(roomList = AsyncData.Success(persistentListOf())),
aRoomListState(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())),
aRoomListState(matrixUser = null, displayMigrationStatus = true),
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
aRoomListState(filtersState = aRoomListFiltersState(isFeatureEnabled = true)),
)
}
@ -65,6 +68,7 @@ internal fun aRoomListState( @@ -65,6 +68,7 @@ internal fun aRoomListState(
contextMenu: RoomListState.ContextMenu = RoomListState.ContextMenu.Hidden,
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
searchState: RoomListSearchState = aRoomListSearchState(),
filtersState: RoomListFiltersState = aRoomListFiltersState(isFeatureEnabled = false),
displayMigrationStatus: Boolean = false,
eventSink: (RoomListEvents) -> Unit = {}
) = RoomListState(
@ -78,6 +82,7 @@ internal fun aRoomListState( @@ -78,6 +82,7 @@ internal fun aRoomListState(
contextMenu = contextMenu,
leaveRoomState = leaveRoomState,
searchState = searchState,
filtersState = filtersState,
displayMigrationStatus = displayMigrationStatus,
eventSink = eventSink,
)

32
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt

@ -55,6 +55,7 @@ import io.element.android.features.roomlist.impl.components.RequestVerificationH @@ -55,6 +55,7 @@ import io.element.android.features.roomlist.impl.components.RequestVerificationH
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
import io.element.android.features.roomlist.impl.components.RoomListTopBar
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
import io.element.android.features.roomlist.impl.filters.RoomListFiltersView
import io.element.android.features.roomlist.impl.migration.MigrationScreenView
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchView
@ -207,16 +208,21 @@ private fun RoomListContent( @@ -207,16 +208,21 @@ private fun RoomListContent(
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
RoomListTopBar(
matrixUser = state.matrixUser,
showAvatarIndicator = state.showAvatarIndicator,
areSearchResultsDisplayed = state.searchState.isSearchActive,
onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) },
onMenuActionClicked = onMenuActionClicked,
onOpenSettings = onOpenSettings,
scrollBehavior = scrollBehavior,
displayMenuItems = !state.displayMigrationStatus,
)
Column {
RoomListTopBar(
matrixUser = state.matrixUser,
showAvatarIndicator = state.showAvatarIndicator,
areSearchResultsDisplayed = state.searchState.isSearchActive,
onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) },
onMenuActionClicked = onMenuActionClicked,
onOpenSettings = onOpenSettings,
scrollBehavior = scrollBehavior,
displayMenuItems = !state.displayMigrationStatus,
)
if (state.displayFilters) {
RoomListFiltersView(state = state.filtersState)
}
}
},
content = { padding ->
LazyColumn(
@ -272,7 +278,11 @@ private fun RoomListContent( @@ -272,7 +278,11 @@ private fun RoomListContent(
}
}
if (state.displayEmptyState) {
EmptyRoomListView(onCreateRoomClicked)
if (state.filtersState.hasAnyFilterSelected) {
// TODO add empty state for filtered rooms
} else {
EmptyRoomListView(onCreateRoomClicked)
}
}
MigrationScreenView(isMigrating = state.displayMigrationStatus)
},

5
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/di/RoomListModule.kt

@ -19,6 +19,8 @@ package io.element.android.features.roomlist.impl.di @@ -19,6 +19,8 @@ package io.element.android.features.roomlist.impl.di
import com.squareup.anvil.annotations.ContributesTo
import dagger.Binds
import dagger.Module
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
import io.element.android.features.roomlist.impl.search.RoomListSearchState
import io.element.android.libraries.architecture.Presenter
@ -29,4 +31,7 @@ import io.element.android.libraries.di.SessionScope @@ -29,4 +31,7 @@ import io.element.android.libraries.di.SessionScope
interface RoomListModule {
@Binds
fun bindSearchPresenter(presenter: RoomListSearchPresenter): Presenter<RoomListSearchState>
@Binds
fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter<RoomListFiltersState>
}

38
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFilter.kt

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl.filters
import io.element.android.features.roomlist.impl.R
/**
* Enum class representing the different filters that can be applied to the room list.
* Order is important.
*/
enum class RoomListFilter(val stringResource: Int) {
Rooms(R.string.screen_roomlist_filter_rooms),
People(R.string.screen_roomlist_filter_people),
Unread(R.string.screen_roomlist_filter_unreads),
Favourites(R.string.screen_roomlist_filter_favourites);
val oppositeFilter: RoomListFilter?
get() = when (this) {
Rooms -> People
People -> Rooms
Unread -> null
Favourites -> null
}
}

22
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEvents.kt

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl.filters
sealed interface RoomListFiltersEvents {
data object ClearSelectedFilters : RoomListFiltersEvents
data class ToggleFilter(val filter: RoomListFilter) : RoomListFiltersEvents
}

98
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt

@ -0,0 +1,98 @@ @@ -0,0 +1,98 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl.filters
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import kotlinx.collections.immutable.toPersistentList
import javax.inject.Inject
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
class RoomListFiltersPresenter @Inject constructor(
private val roomListService: RoomListService,
private val featureFlagService: FeatureFlagService,
) : Presenter<RoomListFiltersState> {
@Composable
override fun present(): RoomListFiltersState {
val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomListFilters).collectAsState(false)
var unselectedFilters: Set<RoomListFilter> by rememberSaveable {
mutableStateOf(RoomListFilter.entries.toSet())
}
var selectedFilters: Set<RoomListFilter> by rememberSaveable {
mutableStateOf(emptySet())
}
fun updateFilters(newSelectedFilters: Set<RoomListFilter>) {
selectedFilters = newSelectedFilters
unselectedFilters = RoomListFilter.entries.toSet() -
selectedFilters -
selectedFilters.mapNotNull { it.oppositeFilter }.toSet()
}
fun handleEvents(event: RoomListFiltersEvents) {
when (event) {
is RoomListFiltersEvents.ToggleFilter -> {
val newSelectedFilters = if (selectedFilters.contains(event.filter)) {
selectedFilters - event.filter
} else {
selectedFilters + event.filter
}
updateFilters(newSelectedFilters)
}
RoomListFiltersEvents.ClearSelectedFilters -> {
updateFilters(newSelectedFilters = emptySet())
}
}
}
LaunchedEffect(isFeatureEnabled) {
if (!isFeatureEnabled) {
updateFilters(emptySet())
}
}
LaunchedEffect(selectedFilters) {
val allRoomsFilter = MatrixRoomListFilter.All(
selectedFilters.map { roomListFilter ->
when (roomListFilter) {
RoomListFilter.Rooms -> MatrixRoomListFilter.Category.Group
RoomListFilter.People -> MatrixRoomListFilter.Category.People
RoomListFilter.Unread -> MatrixRoomListFilter.Unread
RoomListFilter.Favourites -> MatrixRoomListFilter.Favorite
}
}.plus(MatrixRoomListFilter.NonLeft)
)
roomListService.allRooms.updateFilter(allRoomsFilter)
}
return RoomListFiltersState(
unselectedFilters = unselectedFilters.toPersistentList(),
selectedFilters = selectedFilters.toPersistentList(),
isFeatureEnabled = isFeatureEnabled,
eventSink = ::handleEvents
)
}
}

28
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl.filters
import kotlinx.collections.immutable.ImmutableList
data class RoomListFiltersState(
val unselectedFilters: ImmutableList<RoomListFilter>,
val selectedFilters: ImmutableList<RoomListFilter>,
val isFeatureEnabled: Boolean,
val eventSink: (RoomListFiltersEvents) -> Unit,
) {
val hasAnyFilterSelected = selectedFilters.isNotEmpty()
}

45
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersStateProvider.kt

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl.filters
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
class RoomListFiltersStateProvider : PreviewParameterProvider<RoomListFiltersState> {
override val values: Sequence<RoomListFiltersState>
get() = sequenceOf(
aRoomListFiltersState(),
aRoomListFiltersState(
selectedFilters = persistentListOf(RoomListFilter.Rooms, RoomListFilter.Favourites),
unselectedFilters = persistentListOf(RoomListFilter.Unread),
),
)
}
fun aRoomListFiltersState(
unselectedFilters: ImmutableList<RoomListFilter> = RoomListFilter.entries.toImmutableList(),
selectedFilters: ImmutableList<RoomListFilter> = persistentListOf(),
isFeatureEnabled: Boolean = true,
eventSink: (RoomListFiltersEvents) -> Unit = {},
) = RoomListFiltersState(
unselectedFilters = unselectedFilters,
selectedFilters = selectedFilters,
isFeatureEnabled = isFeatureEnabled,
eventSink = eventSink,
)

173
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt

@ -0,0 +1,173 @@ @@ -0,0 +1,173 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl.filters
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.modifiers.fadingEdge
import io.element.android.libraries.designsystem.modifiers.horizontalFadingEdgesBrush
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
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.Text
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import kotlinx.collections.immutable.ImmutableList
@Composable
fun RoomListFiltersView(
state: RoomListFiltersState,
modifier: Modifier = Modifier
) {
fun onClearFiltersClicked() {
state.eventSink(RoomListFiltersEvents.ClearSelectedFilters)
}
fun onFilterClicked(filter: RoomListFilter) {
state.eventSink(RoomListFiltersEvents.ToggleFilter(filter))
}
val startPadding = if (state.hasAnyFilterSelected) 4.dp else 16.dp
Row(
modifier = modifier.padding(start = startPadding, end = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedVisibility(visible = state.hasAnyFilterSelected) {
RoomListClearFiltersButton(
modifier = Modifier.testTag(TestTags.homeScreenClearFilters),
onClick = ::onClearFiltersClicked
)
}
val lazyListState = rememberLazyListState()
val fadingEdgesBrush = horizontalFadingEdgesBrush(
showLeft = lazyListState.canScrollBackward,
showRight = lazyListState.canScrollForward
)
LazyRow(
modifier = Modifier
.fillMaxWidth()
.fadingEdge(fadingEdgesBrush),
state = lazyListState,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
roomListFilters(state.selectedFilters, selected = true, onClick = ::onFilterClicked)
roomListFilters(state.unselectedFilters, selected = false, onClick = ::onFilterClicked)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.roomListFilters(
filters: ImmutableList<RoomListFilter>,
selected: Boolean,
onClick: (RoomListFilter) -> Unit,
) {
items(
items = filters,
key = { it.ordinal },
) { filter ->
RoomListFilterView(
modifier = Modifier.animateItemPlacement(),
roomListFilter = filter,
selected = selected,
onClick = onClick,
)
}
}
@Composable
private fun RoomListClearFiltersButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
IconButton(
modifier = modifier,
onClick = onClick,
) {
Box(
modifier = Modifier
.clip(CircleShape)
.background(ElementTheme.colors.bgActionPrimaryRest)
) {
Icon(
modifier = Modifier.align(Alignment.Center),
imageVector = CompoundIcons.Close(),
tint = ElementTheme.colors.iconOnSolidPrimary,
contentDescription = stringResource(id = io.element.android.libraries.ui.strings.R.string.action_clear),
)
}
}
}
@Composable
private fun RoomListFilterView(
roomListFilter: RoomListFilter,
selected: Boolean,
onClick: (RoomListFilter) -> Unit,
modifier: Modifier = Modifier
) {
FilterChip(
selected = selected,
onClick = { onClick(roomListFilter) },
modifier = modifier
.minimumInteractiveComponentSize()
.height(36.dp),
shape = CircleShape,
colors = FilterChipDefaults.filterChipColors(
containerColor = ElementTheme.colors.bgCanvasDefault,
selectedContainerColor = ElementTheme.colors.bgActionPrimaryRest,
labelColor = ElementTheme.colors.textPrimary,
selectedLabelColor = ElementTheme.colors.textOnSolidPrimary,
),
label = {
Text(text = stringResource(id = roomListFilter.stringResource))
}
)
}
@PreviewsDayNight
@Composable
internal fun RoomListFiltersViewPreview(@PreviewParameter(RoomListFiltersStateProvider::class) state: RoomListFiltersState) = ElementPreview {
RoomListFiltersView(
state = state,
)
}

4
features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt

@ -31,6 +31,8 @@ import io.element.android.features.roomlist.impl.datasource.FakeInviteDataSource @@ -31,6 +31,8 @@ import io.element.android.features.roomlist.impl.datasource.FakeInviteDataSource
import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
import io.element.android.features.roomlist.impl.migration.InMemoryMigrationScreenStore
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
import io.element.android.features.roomlist.impl.model.createRoomListRoomSummary
@ -612,6 +614,7 @@ class RoomListPresenterTests { @@ -612,6 +614,7 @@ class RoomListPresenterTests {
migrationScreenStore = InMemoryMigrationScreenStore(),
),
analyticsService: AnalyticsService = FakeAnalyticsService(),
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
) = RoomListPresenter(
client = client,
@ -637,6 +640,7 @@ class RoomListPresenterTests { @@ -637,6 +640,7 @@ class RoomListPresenterTests {
migrationScreenPresenter = migrationScreenPresenter,
searchPresenter = searchPresenter,
sessionPreferencesStore = sessionPreferencesStore,
filtersPresenter = filtersPresenter,
analyticsService = analyticsService,
)
}

125
features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTests.kt

@ -0,0 +1,125 @@ @@ -0,0 +1,125 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl.filters
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.tests.testutils.awaitLastSequentialItem
import kotlinx.coroutines.test.runTest
import org.junit.Test
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
class RoomListFiltersPresenterTests {
@Test
fun `present - initial state`() = runTest {
val presenter = createRoomListFiltersPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().let { state ->
assertThat(state.selectedFilters).isEmpty()
assertThat(state.hasAnyFilterSelected).isFalse()
assertThat(state.unselectedFilters).containsExactly(
RoomListFilter.Rooms,
RoomListFilter.People,
RoomListFilter.Unread,
RoomListFilter.Favourites,
)
}
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - toggle rooms filter`() = runTest {
val roomListService = FakeRoomListService()
val presenter = createRoomListFiltersPresenter(roomListService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink.invoke(RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms))
awaitLastSequentialItem().let { state ->
assertThat(state.selectedFilters).containsExactly(RoomListFilter.Rooms)
assertThat(state.hasAnyFilterSelected).isTrue()
assertThat(state.unselectedFilters).containsExactly(
RoomListFilter.Unread,
RoomListFilter.Favourites,
)
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
assertThat(roomListCurrentFilter.filters).containsExactly(
MatrixRoomListFilter.NonLeft,
MatrixRoomListFilter.Category.Group,
)
state.eventSink.invoke(RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms))
}
awaitLastSequentialItem().let { state ->
assertThat(state.selectedFilters).isEmpty()
assertThat(state.hasAnyFilterSelected).isFalse()
assertThat(state.unselectedFilters).containsExactly(
RoomListFilter.Rooms,
RoomListFilter.People,
RoomListFilter.Unread,
RoomListFilter.Favourites,
)
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
assertThat(roomListCurrentFilter.filters).containsExactly(
MatrixRoomListFilter.NonLeft,
)
}
}
}
@Test
fun `present - clear filters event`() = runTest {
val roomListService = FakeRoomListService()
val presenter = createRoomListFiltersPresenter(roomListService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink.invoke(RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms))
awaitLastSequentialItem().let { state ->
assertThat(state.selectedFilters).isNotEmpty()
assertThat(state.hasAnyFilterSelected).isTrue()
state.eventSink.invoke(RoomListFiltersEvents.ClearSelectedFilters)
}
awaitLastSequentialItem().let { state ->
assertThat(state.selectedFilters).isEmpty()
assertThat(state.hasAnyFilterSelected).isFalse()
}
}
}
}
fun createRoomListFiltersPresenter(
roomListService: RoomListService = FakeRoomListService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
): RoomListFiltersPresenter {
return RoomListFiltersPresenter(
roomListService = roomListService,
featureFlagService = featureFlagService,
)
}

72
features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersViewTests.kt

@ -0,0 +1,72 @@ @@ -0,0 +1,72 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl.filters
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomlist.impl.R
import io.element.android.libraries.testtags.TestTags
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressTag
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class RoomListFiltersViewTests {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on filters generates expected Event`() {
val eventsRecorder = EventsRecorder<RoomListFiltersEvents>()
rule.setContent {
RoomListFiltersView(
state = aRoomListFiltersState(eventSink = eventsRecorder),
)
}
rule.clickOn(R.string.screen_roomlist_filter_rooms)
eventsRecorder.assertList(
listOf(
RoomListFiltersEvents.ToggleFilter(RoomListFilter.Rooms),
)
)
}
@Test
fun `clicking on clear filters generates expected Event`() {
val eventsRecorder = EventsRecorder<RoomListFiltersEvents>()
rule.setContent {
RoomListFiltersView(
state = aRoomListFiltersState(
unselectedFilters = persistentListOf(),
selectedFilters = RoomListFilter.entries.toImmutableList(),
eventSink = eventsRecorder
),
)
}
rule.pressTag(TestTags.homeScreenClearFilters.value)
eventsRecorder.assertList(
listOf(
RoomListFiltersEvents.ClearSelectedFilters,
)
)
}
}

57
libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/FadingEdge.kt

@ -0,0 +1,57 @@ @@ -0,0 +1,57 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.modifiers
import androidx.compose.animation.animateColorAsState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
@Composable
fun horizontalFadingEdgesBrush(
showLeft: Boolean,
showRight: Boolean,
percent: Float = 0.1f,
): Brush {
val leftColor by animateColorAsState(
targetValue = if (showLeft) Color.Transparent else Color.White,
label = "AnimateLeftColor",
)
val rightColor by animateColorAsState(
targetValue = if (showRight) Color.Transparent else Color.White,
label = "AnimateRightColor",
)
return Brush.horizontalGradient(
0f to leftColor,
percent to Color.White,
1f - percent to Color.White,
1f to rightColor
)
}
fun Modifier.fadingEdge(brush: Brush) = this
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithContent {
drawContent()
drawRect(brush = brush, blendMode = BlendMode.DstIn)
}

7
libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt

@ -75,4 +75,11 @@ enum class FeatureFlags( @@ -75,4 +75,11 @@ enum class FeatureFlags(
defaultValue = true,
isFinished = false,
),
RoomListFilters(
key = "feature.roomlistfilters",
title = "Room list filters",
description = "Allow user to filter the room list",
defaultValue = true,
isFinished = false,
),
}

1
libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt

@ -40,6 +40,7 @@ class StaticFeatureFlagProvider @Inject constructor() : @@ -40,6 +40,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
FeatureFlags.PinUnlock -> true
FeatureFlags.Mentions -> true
FeatureFlags.MarkAsUnread -> false
FeatureFlags.RoomListFilters -> false
}
} else {
false

5
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt

@ -57,6 +57,11 @@ sealed interface RoomListFilter { @@ -57,6 +57,11 @@ sealed interface RoomListFilter {
*/
data object Unread : RoomListFilter
/**
* A filter that matches rooms that are marked as favorite.
*/
data object Favorite : RoomListFilter
/**
* A filter that matches either Group or People rooms.
*/

1
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt

@ -31,5 +31,6 @@ fun RoomListFilter.toRustFilter(): RoomListEntriesDynamicFilterKind { @@ -31,5 +31,6 @@ fun RoomListFilter.toRustFilter(): RoomListEntriesDynamicFilterKind {
RoomListFilter.None -> RoomListEntriesDynamicFilterKind.None
is RoomListFilter.NormalizedMatchRoomName -> RoomListEntriesDynamicFilterKind.NormalizedMatchRoomName(pattern)
RoomListFilter.Unread -> RoomListEntriesDynamicFilterKind.Unread
RoomListFilter.Favorite -> RoomListEntriesDynamicFilterKind.Favourite
}
}

1
libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt

@ -52,6 +52,7 @@ object TestTags { @@ -52,6 +52,7 @@ object TestTags {
* Room list / Home screen.
*/
val homeScreenSettings = TestTag("home_screen-settings")
val homeScreenClearFilters = TestTag("home_screen-clear_filters")
/**
* Room detail screen.

5
samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt

@ -28,6 +28,7 @@ import io.element.android.features.roomlist.impl.RoomListView @@ -28,6 +28,7 @@ import io.element.android.features.roomlist.impl.RoomListView
import io.element.android.features.roomlist.impl.datasource.DefaultInviteStateDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
import io.element.android.features.roomlist.impl.migration.SharedPrefsMigrationScreenStore
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
@ -120,6 +121,10 @@ class RoomListScreen( @@ -120,6 +121,10 @@ class RoomListScreen(
sessionId = matrixClient.sessionId,
sessionCoroutineScope = Singleton.appScope
),
filtersPresenter = RoomListFiltersPresenter(
roomListService = matrixClient.roomListService,
featureFlagService = featureFlagService,
),
analyticsService = NoopAnalyticsService(),
)

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-10_11_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-11_12_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Night-10_12_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Night-11_13_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-11_12_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_0,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-11_12_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_1,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-11_13_null_0,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_0,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-11_13_null_1,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_1,NEXUS_5,1.0,en].png

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_13,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_5,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_7,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_13,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_5,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_7,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save