Merge pull request #4964 from element-hq/feature/bma/roomListNavigationBar
Home navigation bar
This commit is contained in:
commit
8bc7bed5cb
37 changed files with 313 additions and 64 deletions
|
|
@ -51,6 +51,8 @@ dependencies {
|
||||||
implementation(projects.features.rageshake.api)
|
implementation(projects.features.rageshake.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
implementation(libs.haze)
|
||||||
|
implementation(libs.haze.materials)
|
||||||
implementation(projects.features.reportroom.api)
|
implementation(projects.features.reportroom.api)
|
||||||
api(projects.features.home.api)
|
api(projects.features.home.api)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,6 @@
|
||||||
|
|
||||||
package io.element.android.features.home.impl
|
package io.element.android.features.home.impl
|
||||||
|
|
||||||
sealed interface HomeEvents
|
sealed interface HomeEvents {
|
||||||
|
data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvents
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 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.features.home.impl
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
|
||||||
|
enum class HomeNavigationBarItem(
|
||||||
|
@StringRes
|
||||||
|
val labelRes: Int,
|
||||||
|
) {
|
||||||
|
Chats(
|
||||||
|
labelRes = R.string.screen_roomlist_main_space_title
|
||||||
|
),
|
||||||
|
Spaces(
|
||||||
|
// TODO Create a new entry in Localazy
|
||||||
|
labelRes = R.string.screen_roomlist_main_space_title
|
||||||
|
);
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun icon() = when (this) {
|
||||||
|
Chats -> CompoundIcons.ChatSolid()
|
||||||
|
// TODO Spaces -> CompoundIcons.Workspace()
|
||||||
|
Spaces -> CompoundIcons.Code()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(index: Int): HomeNavigationBarItem {
|
||||||
|
return entries.getOrElse(index) { Chats }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,14 +10,20 @@ package io.element.android.features.home.impl
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListState
|
import io.element.android.features.home.impl.roomlist.RoomListState
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.indicator.api.IndicatorService
|
import io.element.android.libraries.indicator.api.IndicatorService
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
|
|
@ -31,6 +37,7 @@ class HomePresenter @Inject constructor(
|
||||||
private val roomListPresenter: Presenter<RoomListState>,
|
private val roomListPresenter: Presenter<RoomListState>,
|
||||||
private val logoutPresenter: Presenter<DirectLogoutState>,
|
private val logoutPresenter: Presenter<DirectLogoutState>,
|
||||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
) : Presenter<HomeState> {
|
) : Presenter<HomeState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): HomeState {
|
override fun present(): HomeState {
|
||||||
|
|
@ -38,31 +45,42 @@ class HomePresenter @Inject constructor(
|
||||||
val isOnline by syncService.isOnline.collectAsState()
|
val isOnline by syncService.isOnline.collectAsState()
|
||||||
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
||||||
val roomListState = roomListPresenter.present()
|
val roomListState = roomListPresenter.present()
|
||||||
|
val isSpaceFeatureEnabled by remember {
|
||||||
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space)
|
||||||
|
}.collectAsState(initial = false)
|
||||||
|
var currentHomeNavigationBarItemOrdinal by rememberSaveable { mutableIntStateOf(HomeNavigationBarItem.Chats.ordinal) }
|
||||||
|
val currentHomeNavigationBarItem by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
HomeNavigationBarItem.from(currentHomeNavigationBarItemOrdinal)
|
||||||
|
}
|
||||||
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Force a refresh of the profile
|
// Force a refresh of the profile
|
||||||
client.getUserProfile()
|
client.getUserProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avatar indicator
|
// Avatar indicator
|
||||||
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
|
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
|
||||||
|
|
||||||
val directLogoutState = logoutPresenter.present()
|
val directLogoutState = logoutPresenter.present()
|
||||||
|
|
||||||
fun handleEvents(event: HomeEvents) {
|
fun handleEvents(event: HomeEvents) {
|
||||||
// TODO
|
when (event) {
|
||||||
|
is HomeEvents.SelectHomeNavigationBarItem -> {
|
||||||
|
currentHomeNavigationBarItemOrdinal = event.item.ordinal
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||||
|
|
||||||
return HomeState(
|
return HomeState(
|
||||||
matrixUser = matrixUser.value,
|
matrixUser = matrixUser.value,
|
||||||
showAvatarIndicator = showAvatarIndicator,
|
showAvatarIndicator = showAvatarIndicator,
|
||||||
hasNetworkConnection = isOnline,
|
hasNetworkConnection = isOnline,
|
||||||
|
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
||||||
roomListState = roomListState,
|
roomListState = roomListState,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
canReportBug = canReportBug,
|
canReportBug = canReportBug,
|
||||||
directLogoutState = directLogoutState,
|
directLogoutState = directLogoutState,
|
||||||
|
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,13 @@ data class HomeState(
|
||||||
val matrixUser: MatrixUser,
|
val matrixUser: MatrixUser,
|
||||||
val showAvatarIndicator: Boolean,
|
val showAvatarIndicator: Boolean,
|
||||||
val hasNetworkConnection: Boolean,
|
val hasNetworkConnection: Boolean,
|
||||||
|
val currentHomeNavigationBarItem: HomeNavigationBarItem,
|
||||||
val roomListState: RoomListState,
|
val roomListState: RoomListState,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val canReportBug: Boolean,
|
val canReportBug: Boolean,
|
||||||
val directLogoutState: DirectLogoutState,
|
val directLogoutState: DirectLogoutState,
|
||||||
|
val isSpaceFeatureEnabled: Boolean,
|
||||||
val eventSink: (HomeEvents) -> Unit,
|
val eventSink: (HomeEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val displayActions = true
|
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListState
|
import io.element.android.features.home.impl.roomlist.RoomListState
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListStateProvider
|
import io.element.android.features.home.impl.roomlist.RoomListStateProvider
|
||||||
import io.element.android.features.home.impl.roomlist.aRoomListState
|
import io.element.android.features.home.impl.roomlist.aRoomListState
|
||||||
|
import io.element.android.features.home.impl.roomlist.aRoomsContentState
|
||||||
|
import io.element.android.features.home.impl.roomlist.generateRoomListRoomSummaryList
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
|
|
@ -24,6 +26,19 @@ open class HomeStateProvider : PreviewParameterProvider<HomeState> {
|
||||||
aHomeState(),
|
aHomeState(),
|
||||||
aHomeState(hasNetworkConnection = false),
|
aHomeState(hasNetworkConnection = false),
|
||||||
aHomeState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
|
aHomeState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
|
||||||
|
aHomeState(
|
||||||
|
isSpaceFeatureEnabled = true,
|
||||||
|
roomListState = aRoomListState(
|
||||||
|
// Add more rooms to see the blur effect under the NavigationBar
|
||||||
|
contentState = aRoomsContentState(
|
||||||
|
summaries = generateRoomListRoomSummaryList(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
aHomeState(
|
||||||
|
isSpaceFeatureEnabled = true,
|
||||||
|
currentHomeNavigationBarItem = HomeNavigationBarItem.Spaces,
|
||||||
|
),
|
||||||
) + RoomListStateProvider().values.map {
|
) + RoomListStateProvider().values.map {
|
||||||
aHomeState(roomListState = it)
|
aHomeState(roomListState = it)
|
||||||
}
|
}
|
||||||
|
|
@ -34,8 +49,10 @@ internal fun aHomeState(
|
||||||
showAvatarIndicator: Boolean = false,
|
showAvatarIndicator: Boolean = false,
|
||||||
hasNetworkConnection: Boolean = true,
|
hasNetworkConnection: Boolean = true,
|
||||||
snackbarMessage: SnackbarMessage? = null,
|
snackbarMessage: SnackbarMessage? = null,
|
||||||
|
currentHomeNavigationBarItem: HomeNavigationBarItem = HomeNavigationBarItem.Chats,
|
||||||
roomListState: RoomListState = aRoomListState(),
|
roomListState: RoomListState = aRoomListState(),
|
||||||
canReportBug: Boolean = true,
|
canReportBug: Boolean = true,
|
||||||
|
isSpaceFeatureEnabled: Boolean = false,
|
||||||
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
||||||
eventSink: (HomeEvents) -> Unit = {}
|
eventSink: (HomeEvents) -> Unit = {}
|
||||||
) = HomeState(
|
) = HomeState(
|
||||||
|
|
@ -45,6 +62,8 @@ internal fun aHomeState(
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
canReportBug = canReportBug,
|
canReportBug = canReportBug,
|
||||||
directLogoutState = directLogoutState,
|
directLogoutState = directLogoutState,
|
||||||
|
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
||||||
roomListState = roomListState,
|
roomListState = roomListState,
|
||||||
|
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,15 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalHazeMaterialsApi::class)
|
||||||
|
|
||||||
package io.element.android.features.home.impl
|
package io.element.android.features.home.impl
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -19,10 +24,19 @@ import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
|
import dev.chrisbanes.haze.hazeSource
|
||||||
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
|
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||||
|
import dev.chrisbanes.haze.rememberHazeState
|
||||||
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.home.impl.components.RoomListContentView
|
import io.element.android.features.home.impl.components.RoomListContentView
|
||||||
|
|
@ -41,7 +55,10 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||||
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.NavigationBar
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.NavigationBarItem
|
||||||
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.Text
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -138,10 +155,19 @@ private fun HomeScaffold(
|
||||||
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||||
val roomListState: RoomListState = state.roomListState
|
val roomListState: RoomListState = state.roomListState
|
||||||
|
|
||||||
|
BackHandler(
|
||||||
|
enabled = state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats,
|
||||||
|
) {
|
||||||
|
state.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats))
|
||||||
|
}
|
||||||
|
|
||||||
|
val hazeState = rememberHazeState()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
topBar = {
|
topBar = {
|
||||||
RoomListTopBar(
|
RoomListTopBar(
|
||||||
|
title = stringResource(state.currentHomeNavigationBarItem.labelRes),
|
||||||
matrixUser = state.matrixUser,
|
matrixUser = state.matrixUser,
|
||||||
showAvatarIndicator = state.showAvatarIndicator,
|
showAvatarIndicator = state.showAvatarIndicator,
|
||||||
areSearchResultsDisplayed = roomListState.searchState.isSearchActive,
|
areSearchResultsDisplayed = roomListState.searchState.isSearchActive,
|
||||||
|
|
@ -150,25 +176,83 @@ private fun HomeScaffold(
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
displayMenuItems = state.displayActions,
|
displayMenuItems = state.displayActions,
|
||||||
displayFilters = roomListState.displayFilters,
|
displayFilters = roomListState.displayFilters && state.currentHomeNavigationBarItem == HomeNavigationBarItem.Chats,
|
||||||
filtersState = roomListState.filtersState,
|
filtersState = roomListState.filtersState,
|
||||||
canReportBug = state.canReportBug,
|
canReportBug = state.canReportBug,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
bottomBar = {
|
||||||
|
if (state.isSpaceFeatureEnabled) {
|
||||||
|
NavigationBar(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
modifier = Modifier
|
||||||
|
.hazeEffect(
|
||||||
|
state = hazeState,
|
||||||
|
style = HazeMaterials.regular(),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
HomeNavigationBarItem.entries.forEach { item ->
|
||||||
|
NavigationBarItem(
|
||||||
|
selected = state.currentHomeNavigationBarItem == item,
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(HomeEvents.SelectHomeNavigationBarItem(item))
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = item.icon(),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text(stringResource(item.labelRes))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
RoomListContentView(
|
when (state.currentHomeNavigationBarItem) {
|
||||||
contentState = roomListState.contentState,
|
HomeNavigationBarItem.Chats -> {
|
||||||
filtersState = roomListState.filtersState,
|
RoomListContentView(
|
||||||
hideInvitesAvatars = roomListState.hideInvitesAvatars,
|
contentState = roomListState.contentState,
|
||||||
eventSink = roomListState.eventSink,
|
filtersState = roomListState.filtersState,
|
||||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
hideInvitesAvatars = roomListState.hideInvitesAvatars,
|
||||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
eventSink = roomListState.eventSink,
|
||||||
onRoomClick = ::onRoomClick,
|
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||||
onCreateRoomClick = onCreateRoomClick,
|
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||||
modifier = Modifier
|
onRoomClick = ::onRoomClick,
|
||||||
.padding(padding)
|
onCreateRoomClick = onCreateRoomClick,
|
||||||
.consumeWindowInsets(padding)
|
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80,
|
||||||
)
|
// and include provided bottom padding
|
||||||
|
contentBottomPadding = 80.dp + padding.calculateBottomPadding(),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(
|
||||||
|
top = padding.calculateTopPadding(),
|
||||||
|
bottom = 0.dp,
|
||||||
|
start = padding.calculateStartPadding(LocalLayoutDirection.current),
|
||||||
|
end = padding.calculateEndPadding(LocalLayoutDirection.current),
|
||||||
|
)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
|
.hazeSource(state = hazeState)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HomeNavigationBarItem.Spaces -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
text = "Spaces are coming soon!",
|
||||||
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (state.displayActions) {
|
if (state.displayActions) {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
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
|
||||||
|
|
@ -66,6 +67,7 @@ fun RoomListContentView(
|
||||||
onConfirmRecoveryKeyClick: () -> Unit,
|
onConfirmRecoveryKeyClick: () -> Unit,
|
||||||
onRoomClick: (RoomListRoomSummary) -> Unit,
|
onRoomClick: (RoomListRoomSummary) -> Unit,
|
||||||
onCreateRoomClick: () -> Unit,
|
onCreateRoomClick: () -> Unit,
|
||||||
|
contentBottomPadding: Dp,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
|
|
@ -93,6 +95,7 @@ fun RoomListContentView(
|
||||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||||
onRoomClick = onRoomClick,
|
onRoomClick = onRoomClick,
|
||||||
|
contentBottomPadding = contentBottomPadding,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -164,6 +167,7 @@ private fun RoomsView(
|
||||||
onSetUpRecoveryClick: () -> Unit,
|
onSetUpRecoveryClick: () -> Unit,
|
||||||
onConfirmRecoveryKeyClick: () -> Unit,
|
onConfirmRecoveryKeyClick: () -> Unit,
|
||||||
onRoomClick: (RoomListRoomSummary) -> Unit,
|
onRoomClick: (RoomListRoomSummary) -> Unit,
|
||||||
|
contentBottomPadding: Dp,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
|
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
|
||||||
|
|
@ -179,6 +183,7 @@ private fun RoomsView(
|
||||||
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
onSetUpRecoveryClick = onSetUpRecoveryClick,
|
||||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||||
onRoomClick = onRoomClick,
|
onRoomClick = onRoomClick,
|
||||||
|
contentBottomPadding = contentBottomPadding,
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -192,6 +197,7 @@ private fun RoomsViewList(
|
||||||
onSetUpRecoveryClick: () -> Unit,
|
onSetUpRecoveryClick: () -> Unit,
|
||||||
onConfirmRecoveryKeyClick: () -> Unit,
|
onConfirmRecoveryKeyClick: () -> Unit,
|
||||||
onRoomClick: (RoomListRoomSummary) -> Unit,
|
onRoomClick: (RoomListRoomSummary) -> Unit,
|
||||||
|
contentBottomPadding: Dp,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
@ -210,8 +216,7 @@ private fun RoomsViewList(
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80
|
contentPadding = PaddingValues(bottom = contentBottomPadding)
|
||||||
contentPadding = PaddingValues(bottom = 80.dp)
|
|
||||||
) {
|
) {
|
||||||
when (state.securityBannerState) {
|
when (state.securityBannerState) {
|
||||||
SecurityBannerState.SetUpRecovery -> {
|
SecurityBannerState.SetUpRecovery -> {
|
||||||
|
|
@ -324,5 +329,6 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
|
||||||
onConfirmRecoveryKeyClick = {},
|
onConfirmRecoveryKeyClick = {},
|
||||||
onRoomClick = {},
|
onRoomClick = {},
|
||||||
onCreateRoomClick = {},
|
onCreateRoomClick = {},
|
||||||
|
contentBottomPadding = 0.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ private val avatarBloomSize = 430.dp
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomListTopBar(
|
fun RoomListTopBar(
|
||||||
|
title: String,
|
||||||
matrixUser: MatrixUser,
|
matrixUser: MatrixUser,
|
||||||
showAvatarIndicator: Boolean,
|
showAvatarIndicator: Boolean,
|
||||||
areSearchResultsDisplayed: Boolean,
|
areSearchResultsDisplayed: Boolean,
|
||||||
|
|
@ -90,6 +91,7 @@ fun RoomListTopBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
DefaultRoomListTopBar(
|
DefaultRoomListTopBar(
|
||||||
|
title = title,
|
||||||
matrixUser = matrixUser,
|
matrixUser = matrixUser,
|
||||||
showAvatarIndicator = showAvatarIndicator,
|
showAvatarIndicator = showAvatarIndicator,
|
||||||
areSearchResultsDisplayed = areSearchResultsDisplayed,
|
areSearchResultsDisplayed = areSearchResultsDisplayed,
|
||||||
|
|
@ -108,6 +110,7 @@ fun RoomListTopBar(
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun DefaultRoomListTopBar(
|
private fun DefaultRoomListTopBar(
|
||||||
|
title: String,
|
||||||
matrixUser: MatrixUser,
|
matrixUser: MatrixUser,
|
||||||
showAvatarIndicator: Boolean,
|
showAvatarIndicator: Boolean,
|
||||||
areSearchResultsDisplayed: Boolean,
|
areSearchResultsDisplayed: Boolean,
|
||||||
|
|
@ -194,7 +197,7 @@ private fun DefaultRoomListTopBar(
|
||||||
scrolledContainerColor = Color.Transparent,
|
scrolledContainerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
title = {
|
title = {
|
||||||
Text(text = stringResource(id = R.string.screen_roomlist_main_space_title))
|
Text(text = title)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
NavigationIcon(
|
NavigationIcon(
|
||||||
|
|
@ -315,6 +318,7 @@ private fun NavigationIcon(
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DefaultRoomListTopBarPreview() = ElementPreview {
|
internal fun DefaultRoomListTopBarPreview() = ElementPreview {
|
||||||
DefaultRoomListTopBar(
|
DefaultRoomListTopBar(
|
||||||
|
title = stringResource(R.string.screen_roomlist_main_space_title),
|
||||||
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
|
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
|
||||||
showAvatarIndicator = false,
|
showAvatarIndicator = false,
|
||||||
areSearchResultsDisplayed = false,
|
areSearchResultsDisplayed = false,
|
||||||
|
|
@ -334,6 +338,7 @@ internal fun DefaultRoomListTopBarPreview() = ElementPreview {
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview {
|
internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview {
|
||||||
DefaultRoomListTopBar(
|
DefaultRoomListTopBar(
|
||||||
|
title = stringResource(R.string.screen_roomlist_main_space_title),
|
||||||
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
|
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
|
||||||
showAvatarIndicator = true,
|
showAvatarIndicator = true,
|
||||||
areSearchResultsDisplayed = false,
|
areSearchResultsDisplayed = false,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
|
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||||
override val values: Sequence<RoomListState>
|
override val values: Sequence<RoomListState>
|
||||||
|
|
@ -113,3 +114,18 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun generateRoomListRoomSummaryList(
|
||||||
|
numberOfRooms: Int = 10,
|
||||||
|
): ImmutableList<RoomListRoomSummary> {
|
||||||
|
return List(numberOfRooms) { index ->
|
||||||
|
aRoomListRoomSummary(
|
||||||
|
name = "Room#$index",
|
||||||
|
numberOfUnreadMessages = 0,
|
||||||
|
timestamp = "14:16",
|
||||||
|
lastMessage = "A message",
|
||||||
|
avatarData = AvatarData("!id$index", "${(65 + index % 26).toChar()}", size = AvatarSize.RoomListItem),
|
||||||
|
id = "!roomId$index:domain",
|
||||||
|
)
|
||||||
|
}.toPersistentList()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ import io.element.android.features.home.impl.roomlist.aRoomListState
|
||||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.indicator.api.IndicatorService
|
import io.element.android.libraries.indicator.api.IndicatorService
|
||||||
import io.element.android.libraries.indicator.test.FakeIndicatorService
|
import io.element.android.libraries.indicator.test.FakeIndicatorService
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
|
@ -27,6 +30,7 @@ import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
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
|
||||||
|
|
@ -58,6 +62,21 @@ class HomePresenterTest {
|
||||||
assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME)
|
assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME)
|
||||||
assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL)
|
assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||||
assertThat(withUserState.showAvatarIndicator).isFalse()
|
assertThat(withUserState.showAvatarIndicator).isFalse()
|
||||||
|
assertThat(withUserState.isSpaceFeatureEnabled).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - space feature enabled`() = runTest {
|
||||||
|
val presenter = createHomePresenter(
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.Space.key to true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.isSpaceFeatureEnabled).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,12 +114,27 @@ class HomePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - NavigationBar change`() = runTest {
|
||||||
|
val presenter = createHomePresenter()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
|
||||||
|
initialState.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun TestScope.createHomePresenter(
|
private fun TestScope.createHomePresenter(
|
||||||
client: MatrixClient = FakeMatrixClient(),
|
client: MatrixClient = FakeMatrixClient(),
|
||||||
syncService: SyncService = FakeSyncService(),
|
syncService: SyncService = FakeSyncService(),
|
||||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||||
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true },
|
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true },
|
||||||
indicatorService: IndicatorService = FakeIndicatorService(),
|
indicatorService: IndicatorService = FakeIndicatorService(),
|
||||||
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService()
|
||||||
) = HomePresenter(
|
) = HomePresenter(
|
||||||
client = client,
|
client = client,
|
||||||
syncService = syncService,
|
syncService = syncService,
|
||||||
|
|
@ -109,5 +143,6 @@ class HomePresenterTest {
|
||||||
logoutPresenter = { aDirectLogoutState() },
|
logoutPresenter = { aDirectLogoutState() },
|
||||||
roomListPresenter = { aRoomListState() },
|
roomListPresenter = { aRoomListState() },
|
||||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||||
|
featureFlagService = featureFlagService,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ appyx = "1.7.1"
|
||||||
sqldelight = "2.1.0"
|
sqldelight = "2.1.0"
|
||||||
wysiwyg = "2.38.4"
|
wysiwyg = "2.38.4"
|
||||||
telephoto = "0.16.0"
|
telephoto = "0.16.0"
|
||||||
|
haze = "1.6.4"
|
||||||
|
|
||||||
# Dependency analysis
|
# Dependency analysis
|
||||||
dependencyAnalysis = "2.19.0"
|
dependencyAnalysis = "2.19.0"
|
||||||
|
|
@ -191,6 +192,8 @@ maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
|
||||||
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
|
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
|
||||||
opusencoder = "io.element.android:opusencoder:1.2.0"
|
opusencoder = "io.element.android:opusencoder:1.2.0"
|
||||||
zxing_cpp = "io.github.zxing-cpp:android:2.3.0"
|
zxing_cpp = "io.github.zxing-cpp:android:2.3.0"
|
||||||
|
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
|
||||||
|
haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
|
||||||
|
|
||||||
# Analytics
|
# Analytics
|
||||||
posthog = "com.posthog:posthog-android:3.19.0"
|
posthog = "com.posthog:posthog-android:3.19.0"
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,13 @@ enum class FeatureFlags(
|
||||||
defaultValue = { false },
|
defaultValue = { false },
|
||||||
isFinished = false,
|
isFinished = false,
|
||||||
),
|
),
|
||||||
|
Space(
|
||||||
|
key = "feature.space",
|
||||||
|
title = "Spaces",
|
||||||
|
description = "Spaces are under active development, only developers should enable this flog for now.",
|
||||||
|
defaultValue = { false },
|
||||||
|
isFinished = false,
|
||||||
|
),
|
||||||
MediaUploadOnSendQueue(
|
MediaUploadOnSendQueue(
|
||||||
key = "feature.media_upload_through_send_queue",
|
key = "feature.media_upload_through_send_queue",
|
||||||
title = "Media upload through send queue",
|
title = "Media upload through send queue",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cd29643f2af1dd5edd37c6ebfc6059db3444810a9dc8002bb5abc8475b4b6b0c
|
oid sha256:660e96d8c627d8f24bbfbc150a3e2b7e74f6819e7fbaa581c5872f544ed9685e
|
||||||
size 5788
|
size 46034
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:20e6d5a1c7218304ede990c1906ec628593acbe57a6496b98a2c1fe9c87d1ae3
|
oid sha256:f65131302fab3b0ba710528947376b57230a74731aa6549473998c30f4e07f6e
|
||||||
size 105294
|
size 40681
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6041a7693307a30183606e7f8f2b605ba4c9bfb8c7ad5d8122837749813d6e2c
|
oid sha256:cd29643f2af1dd5edd37c6ebfc6059db3444810a9dc8002bb5abc8475b4b6b0c
|
||||||
size 99928
|
size 5788
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:20e6d5a1c7218304ede990c1906ec628593acbe57a6496b98a2c1fe9c87d1ae3
|
||||||
|
size 105294
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6041a7693307a30183606e7f8f2b605ba4c9bfb8c7ad5d8122837749813d6e2c
|
||||||
|
size 99928
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a7b0dc90002ec59917ed0c64dae785088117c5aca61961c3d71dc7c05d2fa72b
|
oid sha256:51d89e7c0e8c205c230351c4a00e0d560f3f54c6b423ae3b2b801fd6d00343bf
|
||||||
size 82238
|
size 83509
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4385b380efbae924d4a8fece278a61003d40acbd795fef1aa2eab4c5fadbf367
|
oid sha256:e7383bea26cbd09cff981e6b4202f7e9bca788094ea482241b45eaefcbf16127
|
||||||
size 60691
|
size 34927
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7d64fa49cfca399eb073a8efac881aebbfed38726116d3113f9a562245ac9a41
|
oid sha256:a7b0dc90002ec59917ed0c64dae785088117c5aca61961c3d71dc7c05d2fa72b
|
||||||
size 60495
|
size 82238
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:020d732b7b3a4f43918bee7b9424ffd03fe20074c5823341811a1c700551b2f2
|
oid sha256:4385b380efbae924d4a8fece278a61003d40acbd795fef1aa2eab4c5fadbf367
|
||||||
size 58734
|
size 60691
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:63f8198b96e49f9a29dad22ef2249503d1528aec874599d9daab3f8634d662d7
|
oid sha256:7d64fa49cfca399eb073a8efac881aebbfed38726116d3113f9a562245ac9a41
|
||||||
size 99704
|
size 60495
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:660e96d8c627d8f24bbfbc150a3e2b7e74f6819e7fbaa581c5872f544ed9685e
|
oid sha256:020d732b7b3a4f43918bee7b9424ffd03fe20074c5823341811a1c700551b2f2
|
||||||
size 46034
|
size 58734
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f65131302fab3b0ba710528947376b57230a74731aa6549473998c30f4e07f6e
|
oid sha256:63f8198b96e49f9a29dad22ef2249503d1528aec874599d9daab3f8634d662d7
|
||||||
size 40681
|
size 99704
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d1931e55ba639d0732a75612f691b034fb1b5f2d82fa1e9719dadc6ae2c5f980
|
oid sha256:6be30df0d8f5ba0a57c720bcc16eefc3a5b7dacfc8115a8d47a42edab3e63e82
|
||||||
size 5662
|
size 53250
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4c93289bc30c3b590360f916a55a86a5cc98028bd2201cb410f329de85ed615c
|
oid sha256:1c6dc3c0c72d1ea8a980f393d7cfe2c9e0392016c8fc25b901817dd077d8526c
|
||||||
size 111059
|
size 47265
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7563629f9df4012d7cc35b2805e1ac381ccdb171935b35cad0d1ea60186e3e7d
|
oid sha256:d1931e55ba639d0732a75612f691b034fb1b5f2d82fa1e9719dadc6ae2c5f980
|
||||||
size 105496
|
size 5662
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4c93289bc30c3b590360f916a55a86a5cc98028bd2201cb410f329de85ed615c
|
||||||
|
size 111059
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7563629f9df4012d7cc35b2805e1ac381ccdb171935b35cad0d1ea60186e3e7d
|
||||||
|
size 105496
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ca1e0f43d447faf770ee7aa4f443f3a310f520c9437cbd5baca9ac7ce36d2aae
|
oid sha256:3962f724784add8dc33c7327dd866cab76eff0d53abb781b94e974ae68212e0b
|
||||||
size 88613
|
size 90485
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:acde7691ec0ebe690bcb29478963107e2dc0fe6fa54892b3600f50e0cf1db2a3
|
oid sha256:4850c3002637166016ac75129c0fd4081c540848969b40eb84c4f60a4366c111
|
||||||
size 69463
|
size 43857
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:38e55635243d77c084053c4feea03dcedabd549aa556f4a622ec2d629bb4f426
|
oid sha256:ca1e0f43d447faf770ee7aa4f443f3a310f520c9437cbd5baca9ac7ce36d2aae
|
||||||
size 69240
|
size 88613
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1fc45bd4279456b75cadcb8381451f9e6a0e1fadaf4a8f22274c94aa89c7c348
|
oid sha256:acde7691ec0ebe690bcb29478963107e2dc0fe6fa54892b3600f50e0cf1db2a3
|
||||||
size 67496
|
size 69463
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8d9f42109a12e450a13d1166f7d7790913e1dd847cd44f00a2c881d825cad9b4
|
oid sha256:38e55635243d77c084053c4feea03dcedabd549aa556f4a622ec2d629bb4f426
|
||||||
size 105496
|
size 69240
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6be30df0d8f5ba0a57c720bcc16eefc3a5b7dacfc8115a8d47a42edab3e63e82
|
oid sha256:1fc45bd4279456b75cadcb8381451f9e6a0e1fadaf4a8f22274c94aa89c7c348
|
||||||
size 53250
|
size 67496
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1c6dc3c0c72d1ea8a980f393d7cfe2c9e0392016c8fc25b901817dd077d8526c
|
oid sha256:8d9f42109a12e450a13d1166f7d7790913e1dd847cd44f00a2c881d825cad9b4
|
||||||
size 47265
|
size 105496
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue