Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: retain filters and simplify navigation [WPB-14518] #3737

Merged
merged 4 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 4 additions & 74 deletions app/src/main/kotlin/com/wire/android/navigation/HomeDestination.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,14 @@
package com.wire.android.navigation

import androidx.annotation.DrawableRes
import androidx.navigation.NavBackStackEntry
import com.ramcosta.composedestinations.spec.Direction
import com.wire.android.R
import com.wire.android.ui.destinations.AllConversationsScreenDestination
import com.wire.android.ui.destinations.ArchiveScreenDestination
import com.wire.android.ui.destinations.FavoritesConversationsScreenDestination
import com.wire.android.ui.destinations.FolderConversationsScreenDestination
import com.wire.android.ui.destinations.GroupConversationsScreenDestination
import com.wire.android.ui.destinations.OneOnOneConversationsScreenDestination
import com.wire.android.ui.destinations.SettingsScreenDestination
import com.wire.android.ui.destinations.VaultScreenDestination
import com.wire.android.ui.destinations.WhatsNewScreenDestination
import com.wire.android.util.ui.UIText
import com.wire.kalium.logic.data.conversation.ConversationFilter
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf

@Suppress("LongParameterList")
sealed class HomeDestination(
Expand All @@ -45,10 +37,6 @@ sealed class HomeDestination(
val withUserAvatar: Boolean = true,
val direction: Direction
) {

internal fun NavBackStackEntry.baseRouteMatches(): Boolean = direction.route.getBaseRoute() == destination.route?.getBaseRoute()
open fun entryMatches(entry: NavBackStackEntry): Boolean = entry.baseRouteMatches()

data object Conversations : HomeDestination(
title = UIText.StringResource(R.string.conversations_screen_title),
icon = R.drawable.ic_conversation,
Expand All @@ -57,43 +45,6 @@ sealed class HomeDestination(
direction = AllConversationsScreenDestination
)

data object Favorites : HomeDestination(
title = UIText.StringResource(R.string.label_filter_favorites),
icon = R.drawable.ic_conversation,
isSearchable = true,
withNewConversationFab = true,
direction = FavoritesConversationsScreenDestination
)

data class Folder(
val folderNavArgs: FolderNavArgs
) : HomeDestination(
title = UIText.DynamicString(folderNavArgs.folderName),
icon = R.drawable.ic_conversation,
isSearchable = true,
withNewConversationFab = true,
direction = FolderConversationsScreenDestination(folderNavArgs)
) {
override fun entryMatches(entry: NavBackStackEntry): Boolean =
entry.baseRouteMatches() && FolderConversationsScreenDestination.argsFrom(entry).folderId == folderNavArgs.folderId
}

data object Group : HomeDestination(
title = UIText.StringResource(R.string.label_filter_group),
icon = R.drawable.ic_conversation,
isSearchable = true,
withNewConversationFab = true,
direction = GroupConversationsScreenDestination
)

data object OneOnOne : HomeDestination(
title = UIText.StringResource(R.string.label_filter_one_on_one),
icon = R.drawable.ic_conversation,
isSearchable = true,
withNewConversationFab = true,
direction = OneOnOneConversationsScreenDestination
)

data object Settings : HomeDestination(
title = UIText.StringResource(R.string.settings_screen_title),
icon = R.drawable.ic_settings,
Expand Down Expand Up @@ -130,32 +81,11 @@ sealed class HomeDestination(

companion object {
private const val ITEM_NAME_PREFIX = "HomeNavigationItem."
fun values(): PersistentList<HomeDestination> =
persistentListOf(Conversations, Favorites, Group, OneOnOne, Settings, Vault, Archive, Support, WhatsNew)
}
}

fun HomeDestination.currentFilter(): ConversationFilter {
return when (this) {
HomeDestination.Conversations -> ConversationFilter.All
HomeDestination.Favorites -> ConversationFilter.Favorites
HomeDestination.Group -> ConversationFilter.Groups
HomeDestination.OneOnOne -> ConversationFilter.OneOnOne
is HomeDestination.Folder -> ConversationFilter.Folder(folderName = folderNavArgs.folderName, folderId = folderNavArgs.folderId)
HomeDestination.Archive,
HomeDestination.Settings,
HomeDestination.Support,
HomeDestination.Vault,
HomeDestination.WhatsNew -> ConversationFilter.All
}
}
fun fromRoute(fullRoute: String): HomeDestination? =
values().find { it.direction.route.getBaseRoute() == fullRoute.getBaseRoute() }

fun ConversationFilter.toDestination(): HomeDestination {
return when (this) {
ConversationFilter.All -> HomeDestination.Conversations
ConversationFilter.Favorites -> HomeDestination.Favorites
ConversationFilter.Groups -> HomeDestination.Group
ConversationFilter.OneOnOne -> HomeDestination.OneOnOne
is ConversationFilter.Folder -> HomeDestination.Folder(FolderNavArgs(folderId, folderName))
fun values(): Array<HomeDestination> =
arrayOf(Conversations, Settings, Vault, Archive, Support, WhatsNew)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import com.wire.kalium.logic.feature.session.CurrentSessionUseCase
import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigResult
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
Expand Down Expand Up @@ -61,7 +60,7 @@ class CallActivityViewModel @Inject constructor(
}

fun switchAccountIfNeeded(userId: UserId, actions: SwitchAccountActions) {
viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch(dispatchers.io()) {
val shouldSwitchAccount = when (val result = currentSession()) {
is CurrentSessionResult.Failure.Generic -> true
CurrentSessionResult.Failure.SessionNotFound -> true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.ui.common.topappbar

import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.wire.kalium.logic.data.conversation.ConversationFilter
import dev.ahmedmourad.bundlizer.Bundlizer

@Composable
fun rememberConversationFilterState(): ConversationFilterState = rememberSaveable(saver = ConversationFilterState.saver()) {
ConversationFilterState()
}

class ConversationFilterState(initialValue: ConversationFilter = ConversationFilter.All) {
var filter: ConversationFilter by mutableStateOf(initialValue)
private set

fun changeFilter(newFilter: ConversationFilter) {
filter = newFilter
}

companion object {
fun saver(): Saver<ConversationFilterState, Bundle> = Saver(
save = {
Bundlizer.bundle(ConversationFilter.serializer(), it.filter)
},
restore = {
ConversationFilterState(Bundlizer.unbundle(ConversationFilter.serializer(), it))
}
)
}
}
21 changes: 5 additions & 16 deletions app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
Expand All @@ -64,13 +63,11 @@ import com.ramcosta.composedestinations.result.ResultRecipient
import com.wire.android.R
import com.wire.android.appLogger
import com.wire.android.di.hiltViewModelScoped
import com.wire.android.navigation.FolderNavArgs
import com.wire.android.navigation.HomeDestination
import com.wire.android.navigation.NavigationCommand
import com.wire.android.navigation.Navigator
import com.wire.android.navigation.WireDestination
import com.wire.android.navigation.handleNavigation
import com.wire.android.navigation.toDestination
import com.wire.android.ui.NavGraphs
import com.wire.android.ui.analytics.AnalyticsUsageViewModel
import com.wire.android.ui.common.CollapsingTopBarScaffold
Expand Down Expand Up @@ -121,14 +118,8 @@ fun HomeScreen(
)
) {
homeViewModel.checkRequirements { it.navigate(navigator::navigate) }
val homeDestinations = remember(foldersViewModel.state().folders) {
HomeDestination.values()
.plus(
foldersViewModel.state().folders.map { HomeDestination.Folder(FolderNavArgs(it.id, it.name)) }
)
}

val homeScreenState = rememberHomeScreenState(navigator, homeDestinations = homeDestinations)
val homeScreenState = rememberHomeScreenState(navigator)
val notificationsPermissionDeniedDialogState = rememberVisibilityState<PermissionPermanentlyDeniedDialogState>()
val showNotificationsPermissionDeniedDialog = {
notificationsPermissionDeniedDialogState.show(
Expand Down Expand Up @@ -318,6 +309,8 @@ fun HomeContent(
exit = shrinkVertically() + fadeOut(),
) {
HomeTopBar(
title = currentTitle.asString(),
currentFilter = currentConversationFilter,
navigationItem = currentNavigationItem,
userAvatarData = homeState.userAvatarData,
elevation = dimensions().spacing0x, // CollapsingTopBarScaffold manages applied elevation
Expand Down Expand Up @@ -347,7 +340,7 @@ fun HomeContent(
}
},
collapsingEnabled = !searchBarState.isSearchActive,
contentLazyListState = homeStateHolder.nullAbleLazyListStateFor(currentNavigationItem),
contentLazyListState = homeStateHolder.lazyListStateFor(currentNavigationItem),
content = {
/**
* This "if" is a workaround, otherwise it can crash because of the SubcomposeLayout's nature.
Expand Down Expand Up @@ -407,11 +400,7 @@ fun HomeContent(
ConversationFilterSheetContent(
onChangeFilter = { filter ->
filterSheetState.hide()
openHomeDestination(filter.toDestination())
},
onChangeFolder = {
filterSheetState.hide()
openHomeDestination(it.toDestination())
homeStateHolder.changeFilter(filter)
},
filterSheetState = sheetContentState
)
Expand Down
53 changes: 36 additions & 17 deletions app/src/main/kotlin/com/wire/android/ui/home/HomeStateHolder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
package com.wire.android.ui.home

import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.rememberDrawerState
Expand All @@ -34,10 +33,13 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import com.wire.android.navigation.HomeDestination
import com.wire.android.navigation.HomeDestination.Conversations
import com.wire.android.navigation.Navigator
import com.wire.android.navigation.getBaseRoute
import com.wire.android.navigation.rememberTrackingAnimatedNavController
import com.wire.android.ui.common.topappbar.ConversationFilterState
import com.wire.android.ui.common.topappbar.rememberConversationFilterState
import com.wire.android.ui.common.topappbar.search.SearchBarState
import com.wire.android.ui.common.topappbar.search.rememberSearchbarState
import com.wire.android.ui.home.conversationslist.filter.uiText
import com.wire.kalium.logic.data.conversation.ConversationFilter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

Expand All @@ -49,18 +51,34 @@ class HomeStateHolder(
val searchBarState: SearchBarState,
val navigator: Navigator,
private val currentNavigationItemState: State<HomeDestination>,
private val lazyListStates: Map<HomeDestination, LazyListState>,
private val conversationFilterState: ConversationFilterState,
) {
val currentNavigationItem
get() = currentNavigationItemState.value

fun lazyListStateFor(destination: HomeDestination): LazyListState {
return lazyListStates[destination] ?: error("No LazyListState found for $destination")
}
val currentConversationFilter
get() = conversationFilterState.filter

fun nullAbleLazyListStateFor(destination: HomeDestination): LazyListState? {
return lazyListStates[destination]
}
val currentTitle
get() = when (currentNavigationItemState.value) {
Conversations -> conversationFilterState.filter.uiText()
else -> currentNavigationItemState.value.title
}

private val lazyListStatesMap = mutableMapOf<String, LazyListState>()

fun lazyListStateFor(
destination: HomeDestination,
conversationFilter: ConversationFilter = ConversationFilter.All,
): LazyListState =
lazyListStatesMap.getOrPut(
key = destination.itemName + when (destination) {
Conversations -> ":$conversationFilter" // each filter has its own scroll state
else -> "" // other destinations shouldn't care about the conversation filter
}
) {
LazyListState()
}

fun closeDrawer() {
coroutineScope.launch {
Expand All @@ -73,38 +91,39 @@ class HomeStateHolder(
drawerState.open()
}
}

fun changeFilter(filter: ConversationFilter) = conversationFilterState.changeFilter(filter)
}

@Composable
fun rememberHomeScreenState(
navigator: Navigator,
homeDestinations: List<HomeDestination>,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
navController: NavHostController = rememberTrackingAnimatedNavController { route ->
homeDestinations.find { it.direction.route.getBaseRoute() == route }?.itemName
navController: NavHostController = rememberTrackingAnimatedNavController {
HomeDestination.fromRoute(it)?.itemName
},
drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed)
): HomeStateHolder {

val searchBarState = rememberSearchbarState()
val navBackStackEntry by navController.currentBackStackEntryAsState()

val currentNavigationItemState = remember(homeDestinations) {
val currentNavigationItemState = remember {
derivedStateOf {
navBackStackEntry?.let { entry -> homeDestinations.find { it.entryMatches(entry) } } ?: Conversations
navBackStackEntry?.destination?.route?.let { HomeDestination.fromRoute(it) } ?: Conversations
}
}
val lazyListStates = homeDestinations.associateWith { rememberLazyListState() }
val conversationFilterState = rememberConversationFilterState()

return remember(homeDestinations) {
return remember {
HomeStateHolder(
coroutineScope = coroutineScope,
navController = navController,
drawerState = drawerState,
searchBarState = searchBarState,
navigator = navigator,
currentNavigationItemState = currentNavigationItemState,
lazyListStates = lazyListStates
conversationFilterState = conversationFilterState,
)
}
}
Loading
Loading