Skip to content

Commit

Permalink
section selector (inbox/sent)
Browse files Browse the repository at this point in the history
  • Loading branch information
migulyaev committed Sep 22, 2023
1 parent d370e8c commit 1abd7c0
Show file tree
Hide file tree
Showing 10 changed files with 306 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package tech.relaycorp.letro.messages.list

import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import tech.relaycorp.letro.messages.model.ExtendedConversation

sealed interface ConversationsListContent {

data class Conversations(
val conversations: List<ExtendedConversation>,
) : ConversationsListContent

data class Empty(
@DrawableRes val image: Int,
@StringRes val text: Int,
) : ConversationsListContent
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
@file:Suppress("NAME_SHADOWING")

package tech.relaycorp.letro.messages.list

import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
Expand All @@ -25,13 +29,16 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import tech.relaycorp.letro.R
import tech.relaycorp.letro.messages.model.ExtendedConversation
import tech.relaycorp.letro.messages.model.ExtendedMessage
import tech.relaycorp.letro.ui.common.BottomSheetAction
import tech.relaycorp.letro.ui.common.LetroActionsBottomSheet
import tech.relaycorp.letro.ui.theme.BodyLargeProminent
import tech.relaycorp.letro.ui.theme.BodyMediumProminent
import tech.relaycorp.letro.ui.theme.LabelSmallProminent
Expand All @@ -46,32 +53,65 @@ fun ConversationsListScreen(
) {
val conversations by viewModel.conversations.collectAsState()
val isOnboardingVisible by viewModel.isOnboardingMessageVisible.collectAsState()
val sectionSelectorState by viewModel.conversationSectionState.collectAsState()

Column(
modifier = Modifier.fillMaxSize(),
) {
if (isOnboardingVisible) {
ConversationsOnboardingView(
onCloseClick = { viewModel.onCloseOnboardingButtonClick() },
Box(modifier = Modifier.fillMaxSize()) {
if (sectionSelectorState.sectionSelector.isOpened) {
LetroActionsBottomSheet(
actions = sectionSelectorState.sectionSelector.sections
.map {
BottomSheetAction(
icon = it.icon,
title = it.title,
action = { viewModel.onSectionChosen(it) },
isChosen = it == sectionSelectorState.currentSection,
)
},
onDismissRequest = { viewModel.onConversationSectionDialogDismissed() },
)
}
Box {
if (conversations.isEmpty()) {
Column {
Spacer(modifier = Modifier.height(24.dp))
EmptyConversationsView()
}
} else {
Column {
if (isOnboardingVisible) {
ConversationsOnboardingView(
onCloseClick = { viewModel.onCloseOnboardingButtonClick() },
)
}
Box {
LazyColumn {
items(conversations) { conversation ->
Conversation(
conversation = conversation,
noSubjectText = conversationsStringsProvider.noSubject,
onConversationClick = {
onConversationClick(conversation)
items(1) {
ConversationsSectionSelector(
text = stringResource(id = sectionSelectorState.currentSection.title),
icon = painterResource(id = sectionSelectorState.currentSection.icon),
onClick = {
viewModel.onConversationSectionSelectorClick()
},
)
}

when (val conversations = conversations) {
is ConversationsListContent.Conversations -> {
items(conversations.conversations) { conversation ->
Conversation(
conversation = conversation,
noSubjectText = conversationsStringsProvider.noSubject,
onConversationClick = {
onConversationClick(conversation)
},
)
}
}
is ConversationsListContent.Empty -> {
items(1) {
Column {
Spacer(modifier = Modifier.height(24.dp))
EmptyConversationsView(
image = conversations.image,
text = conversations.text,
)
}
}
}
}
}
}
}
Expand Down Expand Up @@ -148,16 +188,59 @@ private fun Conversation(
}

@Composable
private fun EmptyConversationsView() {
private fun ConversationsSectionSelector(
text: String,
icon: Painter,
onClick: () -> Unit,
) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = Modifier.fillMaxWidth(),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(
horizontal = 16.dp,
vertical = 16.dp,
)
.clickable { onClick() },
) {
Icon(
painter = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary,
)
Spacer(modifier = Modifier.width(9.dp))
Text(
text = text,
style = MaterialTheme.typography.BodyLargeProminent,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.width(2.dp))
Icon(
painter = painterResource(id = R.drawable.arrow_down),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}

@Composable
private fun EmptyConversationsView(
@DrawableRes image: Int,
@StringRes text: Int,
) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(painter = painterResource(id = R.drawable.empty_inbox_image), contentDescription = null)
Image(painter = painterResource(id = image), contentDescription = null)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(id = R.string.conversations_empty_stub),
text = stringResource(id = text),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Expand Down Expand Up @@ -262,3 +345,10 @@ private fun Onboarding_Preview() {
ConversationsOnboardingView {
}
}

@Preview(showBackground = true)
@Composable
private fun SectionSelector_Preview() {
ConversationsSectionSelector(text = "inbox", icon = painterResource(id = R.drawable.inbox)) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,52 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import tech.relaycorp.letro.R
import tech.relaycorp.letro.account.model.Account
import tech.relaycorp.letro.account.storage.AccountRepository
import tech.relaycorp.letro.messages.model.ExtendedConversation
import tech.relaycorp.letro.messages.list.section.ConversationSectionInfo
import tech.relaycorp.letro.messages.onboarding.ConversationsOnboardingManager
import tech.relaycorp.letro.messages.repository.ConversationsRepository
import javax.inject.Inject

@Suppress("NAME_SHADOWING")
@HiltViewModel
class ConversationsListViewModel @Inject constructor(
private val conversationsRepository: ConversationsRepository,
private val conversationsOnboardingManager: ConversationsOnboardingManager,
private val accountRepository: AccountRepository,
) : ViewModel() {

val conversations: StateFlow<List<ExtendedConversation>>
get() = conversationsRepository.conversations
private val _conversationSectionInfoState = MutableStateFlow(ConversationsSectionState())
val conversationSectionState: StateFlow<ConversationsSectionState>
get() = _conversationSectionInfoState

val conversations: StateFlow<ConversationsListContent>
get() = combine(
conversationsRepository.conversations,
conversationSectionState,
) { conversations, currentTab ->
val conversations = when (currentTab.currentSection) {
ConversationSectionInfo.Inbox -> {
conversations.filter { it.messages.any { !it.isOutgoing } }
}
ConversationSectionInfo.Sent -> {
conversations.filter { it.messages.any { it.isOutgoing } }
}
}
if (conversations.isNotEmpty()) {
ConversationsListContent.Conversations(conversations)
} else {
getEmptyConversationsStubInfo()
}
}
.stateIn(viewModelScope, SharingStarted.Eagerly, initialValue = getEmptyConversationsStubInfo())

private val _isOnboardingMessageVisible = MutableStateFlow(false)
val isOnboardingMessageVisible: StateFlow<Boolean>
Expand All @@ -42,6 +70,37 @@ class ConversationsListViewModel @Inject constructor(
}
}

fun onSectionChosen(section: ConversationSectionInfo) {
_conversationSectionInfoState.update {
it.copy(
currentSection = section,
sectionSelector = it.sectionSelector.copy(
isOpened = false,
),
)
}
}

fun onConversationSectionSelectorClick() {
_conversationSectionInfoState.update {
it.copy(
sectionSelector = it.sectionSelector.copy(
isOpened = true,
),
)
}
}

fun onConversationSectionDialogDismissed() {
_conversationSectionInfoState.update {
it.copy(
sectionSelector = it.sectionSelector.copy(
isOpened = false,
),
)
}
}

fun onCloseOnboardingButtonClick() {
viewModelScope.launch {
currentAccount?.let {
Expand All @@ -50,4 +109,25 @@ class ConversationsListViewModel @Inject constructor(
}
}
}

private fun getEmptyConversationsStubInfo() = ConversationsListContent.Empty(
image = when (_conversationSectionInfoState.value.currentSection) {
ConversationSectionInfo.Inbox -> R.drawable.empty_inbox_image
ConversationSectionInfo.Sent -> R.drawable.empty_inbox_image
},
text = when (_conversationSectionInfoState.value.currentSection) {
ConversationSectionInfo.Inbox -> R.string.conversations_empty_inbox_stub
ConversationSectionInfo.Sent -> R.string.conversations_empty_sent_stub
},
)
}

data class ConversationsSectionState(
val currentSection: ConversationSectionInfo = ConversationSectionInfo.Inbox,
val sectionSelector: ConversationsSectionSelector = ConversationsSectionSelector(),
)

data class ConversationsSectionSelector(
val isOpened: Boolean = false,
val sections: List<ConversationSectionInfo> = ConversationSectionInfo.allSections(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package tech.relaycorp.letro.messages.list.section

import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import tech.relaycorp.letro.R

sealed class ConversationSectionInfo(
@StringRes val title: Int,
@DrawableRes val icon: Int,
) {

object Inbox : ConversationSectionInfo(
title = R.string.inbox,
icon = R.drawable.inbox,
)

object Sent : ConversationSectionInfo(
title = R.string.sent,
icon = R.drawable.sent,
)

companion object {
fun allSections() = listOf(
Inbox,
Sent,
)
}
}
Loading

0 comments on commit 1abd7c0

Please sign in to comment.