Skip to content

Commit

Permalink
feat: archived conversation list [WPB-4429] (#2295)
Browse files Browse the repository at this point in the history
  • Loading branch information
Garzas committed Oct 4, 2023
1 parent b2e2292 commit 7340e57
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 101 deletions.
Expand Up @@ -82,6 +82,7 @@ sealed class HomeDestination(
data object Archive : HomeDestination(
title = R.string.archive_screen_title,
icon = R.drawable.ic_archive,
isSearchable = true,
direction = ArchiveScreenDestination
)

Expand Down
9 changes: 9 additions & 0 deletions app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt
Expand Up @@ -85,6 +85,7 @@ import com.wire.android.ui.home.conversations.details.GroupConversationActionTyp
import com.wire.android.ui.home.conversations.details.GroupConversationDetailsNavBackArgs
import com.wire.android.ui.home.conversationslist.ConversationListState
import com.wire.android.ui.home.conversationslist.ConversationListViewModel
import com.wire.android.ui.home.conversationslist.model.ConversationsSource
import com.wire.android.ui.home.drawer.HomeDrawer
import com.wire.android.ui.home.drawer.HomeDrawerState
import com.wire.android.ui.home.drawer.HomeDrawerViewModel
Expand All @@ -111,6 +112,14 @@ fun HomeScreen(
showNotificationsFlow.launch()
}

LaunchedEffect(homeScreenState.currentNavigationItem) {
when (homeScreenState.currentNavigationItem) {
HomeDestination.Archive -> conversationListViewModel.updateConversationsSource(ConversationsSource.ARCHIVE)
HomeDestination.Conversations -> conversationListViewModel.updateConversationsSource(ConversationsSource.MAIN)
else -> {}
}
}

handleSnackBarMessage(
snackbarHostState = homeScreenState.snackBarHostState,
conversationListSnackBarState = homeScreenState.snackbarState,
Expand Down
Expand Up @@ -36,14 +36,41 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.wire.android.R
import com.wire.android.navigation.HomeNavGraph
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.home.HomeStateHolder
import com.wire.android.ui.home.conversationslist.ConversationItemType
import com.wire.android.ui.home.conversationslist.ConversationRouterHomeBridge
import com.wire.android.ui.home.conversationslist.model.ConversationsSource
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireTypography

/**
* ArchiveScreen composable function.
*
* This screen leverages the ConversationRouterHomeBridge to render its UI and logic.
* Reasons for using ConversationRouterHomeBridge:
* 1. **Consistency**: Ensures a uniform UI/UX between the Archive and Conversation screens.
* 2. **Code Efficiency**: Eliminates redundancy by reusing shared logic and components.
* 3. **Flexibility**: Accommodates distinct data queries while retaining core UI logic.
* 4. **Maintainability**: Centralizes updates, reducing potential bugs and inconsistencies.
* 5. **Optimization**: Speeds up the development cycle by reusing established components.
*/
@HomeNavGraph
@Destination
@Composable
fun ArchiveScreen() {
ArchivedConversationsEmptyStateScreen()
fun ArchiveScreen(homeStateHolder: HomeStateHolder) {
with(homeStateHolder) {
ConversationRouterHomeBridge(
navigator = navigator,
conversationItemType = ConversationItemType.ALL_CONVERSATIONS,
onHomeBottomSheetContentChanged = ::changeBottomSheetContent,
onOpenBottomSheet = ::openBottomSheet,
onCloseBottomSheet = ::closeBottomSheet,
onSnackBarStateChanged = ::setSnackBarState,
searchBarState = searchBarState,
isBottomSheetVisible = ::isBottomSheetVisible,
conversationsSource = ConversationsSource.ARCHIVE
)
}
}

@Composable
Expand Down Expand Up @@ -78,6 +105,6 @@ fun ArchivedConversationsEmptyStateScreen() {

@Preview(showBackground = false)
@Composable
fun PreviewArchiveScreen() {
ArchiveScreen()
fun PreviewArchiveEmptyScreen() {
ArchivedConversationsEmptyStateScreen()
}
Expand Up @@ -41,8 +41,11 @@ import com.wire.android.ui.home.conversationslist.model.BlockState
import com.wire.android.ui.home.conversationslist.model.ConversationFolder
import com.wire.android.ui.home.conversationslist.model.ConversationInfo
import com.wire.android.ui.home.conversationslist.model.ConversationItem
import com.wire.android.ui.home.conversationslist.model.ConversationsSource
import com.wire.android.ui.home.conversationslist.model.DialogState
import com.wire.android.ui.home.conversationslist.model.GroupDialogState
import com.wire.android.ui.home.conversationslist.model.SearchQuery
import com.wire.android.ui.home.conversationslist.model.SearchQueryUpdate
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.android.util.ui.WireSessionImageLoader
import com.wire.kalium.logic.data.conversation.ConversationDetails
Expand Down Expand Up @@ -78,17 +81,17 @@ import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusU
import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase
import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCase
import com.wire.kalium.logic.feature.team.Result
import com.wire.kalium.logic.functional.combine
import com.wire.kalium.util.DateTimeUtil
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Date
Expand Down Expand Up @@ -123,10 +126,24 @@ class ConversationListViewModel @Inject constructor(

var requestInProgress: Boolean by mutableStateOf(false)

private val mutableSearchQueryFlow = MutableStateFlow("")
private val mutableSearchQueryFlow = MutableSharedFlow<SearchQueryUpdate>()

private val searchQueryFlow = mutableSearchQueryFlow
.asStateFlow()
.scan(SearchQuery("", ConversationsSource.MAIN)) { currentSearchQuery, update ->
when (update) {
is SearchQueryUpdate.UpdateQuery -> currentSearchQuery.copy(text = update.text)
is SearchQueryUpdate.UpdateConversationsSource -> {
if (currentSearchQuery.source != update.source) {
currentSearchQuery.copy(
text = "",
source = update.source
)
} else {
currentSearchQuery
}
}
}
}
.debounce(SearchPeopleViewModel.DEFAULT_SEARCH_QUERY_DEBOUNCE)

var establishedCallConversationId: QualifiedID? = null
Expand All @@ -151,8 +168,8 @@ class ConversationListViewModel @Inject constructor(
observeEstablishedCall()
}
viewModelScope.launch {
searchQueryFlow.combine(
observeConversationListDetails(includeArchived = false)
searchQueryFlow.flatMapLatest { searchQuery ->
observeConversationListDetails(fromArchive = searchQuery.source == ConversationsSource.ARCHIVE)
.map {
it.map { conversationDetails ->
conversationDetails.toConversationItem(
Expand All @@ -161,24 +178,30 @@ class ConversationListViewModel @Inject constructor(
)
}
}
)
.map { (searchQuery, conversationItems) -> conversationItems.withFolders().toImmutableMap() to searchQuery }
.collect { (conversationsWithFolders, searchQuery) ->
conversationListState = conversationListState.copy(
conversationSearchResult = if (searchQuery.isEmpty()) {
.map { conversationItems ->
conversationItems.withFolders(source = searchQuery.source)
.toImmutableMap() to searchQuery
}
}
.map { (conversationsWithFolders, searchQuery) ->
conversationListState.copy(
conversationSearchResult = if (searchQuery.text.isEmpty()) {
conversationsWithFolders
} else {
searchConversation(
conversationsWithFolders.values.flatten(),
searchQuery
).withFolders().toImmutableMap()
searchQuery.text
).withFolders(source = searchQuery.source).toImmutableMap()
},
hasNoConversations = conversationsWithFolders.isEmpty(),
foldersWithConversations = conversationsWithFolders,
// TODO: missing other lists and counters (for bottom tabs if we decide to bring them back)
searchQuery = searchQuery
searchQuery = searchQuery.text
)
}
.flowOn(dispatcher.io())
.collect {
conversationListState = it
}
}
}

Expand Down Expand Up @@ -211,77 +234,48 @@ class ConversationListViewModel @Inject constructor(
}

@Suppress("ComplexMethod")
private fun List<ConversationItem>.withFolders(): Map<ConversationFolder, List<ConversationItem>> {
val unreadConversations = filter {
when (it.mutedStatus) {
MutedConversationStatus.AllAllowed -> when (it.badgeEventType) {
BadgeEventType.Blocked -> false
BadgeEventType.Deleted -> false
BadgeEventType.Knock -> true
BadgeEventType.MissedCall -> true
BadgeEventType.None -> false
BadgeEventType.ReceivedConnectionRequest -> true
BadgeEventType.SentConnectRequest -> false
BadgeEventType.UnreadMention -> true
is BadgeEventType.UnreadMessage -> true
BadgeEventType.UnreadReply -> true
}

MutedConversationStatus.OnlyMentionsAndRepliesAllowed -> when (it.badgeEventType) {
BadgeEventType.UnreadReply -> true
BadgeEventType.UnreadMention -> true
BadgeEventType.ReceivedConnectionRequest -> true
else -> false
private fun List<ConversationItem>.withFolders(source: ConversationsSource): Map<ConversationFolder, List<ConversationItem>> {
return when (source) {
ConversationsSource.ARCHIVE -> {
buildMap {
if (this@withFolders.isNotEmpty()) put(ConversationFolder.WithoutHeader, this@withFolders)
}
}

MutedConversationStatus.AllMuted -> false
} || (it is ConversationItem.GroupConversation && it.hasOnGoingCall)
}

val remainingConversations = this - unreadConversations.toSet()

return buildMap {
if (unreadConversations.isNotEmpty()) put(ConversationFolder.Predefined.NewActivities, unreadConversations)
if (remainingConversations.isNotEmpty()) put(ConversationFolder.Predefined.Conversations, remainingConversations)
}
}

@Suppress("ComplexMethod", "NoMultipleSpaces")
private fun List<ConversationDetails>.toConversationsFoldersMap(): Map<ConversationFolder, List<ConversationItem>> {
val unreadConversations = filter {
when (it.conversation.mutedStatus) {
MutedConversationStatus.AllAllowed ->
when (it) {
is Group -> it.unreadEventCount.isNotEmpty()
is OneOne -> it.unreadEventCount.isNotEmpty()
else -> false // TODO should connection requests also be listed on "new activities"?
}

MutedConversationStatus.OnlyMentionsAndRepliesAllowed ->
when (it) {
is Group -> it.unreadEventCount.containsKey(UnreadEventType.MENTION) ||
it.unreadEventCount.containsKey(UnreadEventType.REPLY)

is OneOne -> it.unreadEventCount.containsKey(UnreadEventType.MENTION) ||
it.unreadEventCount.containsKey(UnreadEventType.REPLY)

else -> false
}
ConversationsSource.MAIN -> {
val unreadConversations = filter {
when (it.mutedStatus) {
MutedConversationStatus.AllAllowed -> when (it.badgeEventType) {
BadgeEventType.Blocked -> false
BadgeEventType.Deleted -> false
BadgeEventType.Knock -> true
BadgeEventType.MissedCall -> true
BadgeEventType.None -> false
BadgeEventType.ReceivedConnectionRequest -> true
BadgeEventType.SentConnectRequest -> false
BadgeEventType.UnreadMention -> true
is BadgeEventType.UnreadMessage -> true
BadgeEventType.UnreadReply -> true
}

else -> false
} ||
(it is Connection && it.connection.status == ConnectionState.PENDING) ||
(it is Group && it.hasOngoingCall)
}
MutedConversationStatus.OnlyMentionsAndRepliesAllowed -> when (it.badgeEventType) {
BadgeEventType.UnreadReply -> true
BadgeEventType.UnreadMention -> true
BadgeEventType.ReceivedConnectionRequest -> true
else -> false
}

val remainingConversations = this - unreadConversations.toSet()
MutedConversationStatus.AllMuted -> false
} || (it is ConversationItem.GroupConversation && it.hasOnGoingCall)
}

val unreadConversationsItems = unreadConversations.toConversationItemList()
val remainingConversationsItems = remainingConversations.toConversationItemList()
val remainingConversations = this - unreadConversations.toSet()

return buildMap {
if (unreadConversationsItems.isNotEmpty()) put(ConversationFolder.Predefined.NewActivities, unreadConversationsItems)
if (remainingConversationsItems.isNotEmpty()) put(ConversationFolder.Predefined.Conversations, remainingConversationsItems)
buildMap {
if (unreadConversations.isNotEmpty()) put(ConversationFolder.Predefined.NewActivities, unreadConversations)
if (remainingConversations.isNotEmpty()) put(ConversationFolder.Predefined.Conversations, remainingConversations)
}
}
}
}

Expand Down Expand Up @@ -399,15 +393,15 @@ class ConversationListViewModel @Inject constructor(
}
}

private fun List<ConversationDetails>.toConversationItemList(): List<ConversationItem> =
filter { it is Group || it is OneOne || it is Connection }
.map {
it.toConversationItem(wireSessionImageLoader, userTypeMapper)
}

fun searchConversation(searchQuery: TextFieldValue) {
viewModelScope.launch {
mutableSearchQueryFlow.emit(searchQuery.text)
mutableSearchQueryFlow.emit(SearchQueryUpdate.UpdateQuery(searchQuery.text))
}
}

fun updateConversationsSource(source: ConversationsSource) {
viewModelScope.launch {
mutableSearchQueryFlow.emit(SearchQueryUpdate.UpdateConversationsSource(source))
}
}

Expand Down
Expand Up @@ -55,6 +55,7 @@ import com.wire.android.ui.home.conversationslist.all.AllConversationScreenConte
import com.wire.android.ui.home.conversationslist.call.CallsScreenContent
import com.wire.android.ui.home.conversationslist.mention.MentionScreenContent
import com.wire.android.ui.home.conversationslist.model.ConversationItem
import com.wire.android.ui.home.conversationslist.model.ConversationsSource
import com.wire.android.ui.home.conversationslist.model.DialogState
import com.wire.android.ui.home.conversationslist.model.GroupDialogState
import com.wire.android.ui.home.conversationslist.search.SearchConversationScreen
Expand All @@ -75,11 +76,16 @@ fun ConversationRouterHomeBridge(
onCloseBottomSheet: () -> Unit,
onSnackBarStateChanged: (HomeSnackbarState) -> Unit,
searchBarState: SearchBarState,
isBottomSheetVisible: () -> Boolean
isBottomSheetVisible: () -> Boolean,
conversationsSource: ConversationsSource = ConversationsSource.MAIN
) {
val viewModel: ConversationListViewModel = hiltViewModel()
val context = LocalContext.current

LaunchedEffect(conversationsSource) {
viewModel.updateConversationsSource(conversationsSource)
}

MicrophoneBTPermissionsDeniedDialog(
shouldShow = viewModel.conversationListState.shouldShowCallingPermissionDialog,
onDismiss = viewModel::dismissCallingPermissionDialog,
Expand Down
Expand Up @@ -45,6 +45,7 @@ import com.wire.android.navigation.HomeNavGraph
import com.wire.android.ui.common.dialogs.calling.JoinAnywayDialog
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.home.HomeStateHolder
import com.wire.android.ui.home.archive.ArchivedConversationsEmptyStateScreen
import com.wire.android.ui.home.conversationslist.ConversationItemType
import com.wire.android.ui.home.conversationslist.ConversationListViewModel
import com.wire.android.ui.home.conversationslist.ConversationRouterHomeBridge
Expand Down Expand Up @@ -80,6 +81,7 @@ fun AllConversationScreen(homeStateHolder: HomeStateHolder) {
fun AllConversationScreenContent(
conversations: ImmutableMap<ConversationFolder, List<ConversationItem>>,
hasNoConversations: Boolean,
isFromArchive: Boolean = false,
viewModel: ConversationListViewModel = hiltViewModel(),
onEditConversation: (ConversationItem) -> Unit,
onOpenConversationNotificationsSettings: (ConversationItem) -> Unit,
Expand All @@ -99,7 +101,11 @@ fun AllConversationScreenContent(
)
}
if (hasNoConversations) {
ConversationListEmptyStateScreen()
if (isFromArchive) {
ArchivedConversationsEmptyStateScreen()
} else {
ConversationListEmptyStateScreen()
}
} else {
ConversationList(
lazyListState = lazyListState,
Expand Down Expand Up @@ -154,6 +160,7 @@ fun ConversationListEmptyStateScreen() {
)
}
}

@Preview
@Composable
fun PreviewAllConversationScreen() {
Expand Down

0 comments on commit 7340e57

Please sign in to comment.