Skip to content

Commit

Permalink
feat: View conversations, read/unread messages, conversation deleting (
Browse files Browse the repository at this point in the history
…#56)

- view conversations
- read/unread state for the conversations
- conversation deleting
- support conversations with an empty subject
- counters in the tab bar
- fixed blinks on the conversation list screen
- fixed bug with chip displaying the wrong text
  • Loading branch information
migulyaev authored Sep 20, 2023
1 parent 5a2df50 commit 2ac9506
Show file tree
Hide file tree
Showing 30 changed files with 813 additions and 132 deletions.
18 changes: 15 additions & 3 deletions app/schemas/tech.relaycorp.letro.storage.LetroDatabase/1.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "12466b7fa30a962804c156ee00283227",
"identityHash": "94e396e1f0c1805cb42e862839856f3f",
"entities": [
{
"tableName": "account",
Expand Down Expand Up @@ -110,7 +110,7 @@
},
{
"tableName": "conversations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`keyId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `conversationId` BLOB NOT NULL, `ownerVeraId` TEXT NOT NULL, `contactVeraId` TEXT NOT NULL, `subject` TEXT)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`keyId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `conversationId` BLOB NOT NULL, `ownerVeraId` TEXT NOT NULL, `contactVeraId` TEXT NOT NULL, `isRead` INTEGER NOT NULL, `subject` TEXT, `isArchived` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "keyId",
Expand All @@ -136,11 +136,23 @@
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isRead",
"columnName": "isRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subject",
"columnName": "subject",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isArchived",
"columnName": "isArchived",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
Expand Down Expand Up @@ -212,7 +224,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '12466b7fa30a962804c156ee00283227')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '94e396e1f0c1805cb42e862839856f3f')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import tech.relaycorp.letro.ui.theme.LargeProminent
import tech.relaycorp.letro.ui.theme.LetroColor
import tech.relaycorp.letro.ui.theme.SmallProminent
import tech.relaycorp.letro.ui.utils.SnackbarStringsProvider
import tech.relaycorp.letro.utils.ext.showSnackbar

@OptIn(ExperimentalMaterial3Api::class)
@Composable
Expand All @@ -54,9 +55,7 @@ fun ContactsScreen(

LaunchedEffect(Unit) {
viewModel.showContactDeletedSnackbarSignal.collect {
snackbarHostState.showSnackbar(
message = snackbarStringsProvider.contactDeleted,
)
snackbarHostState.showSnackbar(this, snackbarStringsProvider.contactDeleted)
}
}

Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/tech/relaycorp/letro/di/MainModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,29 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import tech.relaycorp.letro.ui.utils.ConversationsStringsProvider
import tech.relaycorp.letro.ui.utils.ConversationsStringsProviderImpl
import tech.relaycorp.letro.ui.utils.SnackbarStringsProvider
import tech.relaycorp.letro.ui.utils.SnackbarStringsProviderImpl
import tech.relaycorp.letro.ui.utils.StringsProvider
import tech.relaycorp.letro.ui.utils.StringsProviderImpl

@Module
@InstallIn(ActivityComponent::class)
interface MainModule {

@Binds
fun bindStringsProvider(
impl: StringsProviderImpl,
): StringsProvider

@Binds
fun bindSnackbarStringsProvider(
impl: SnackbarStringsProviderImpl,
): SnackbarStringsProvider

@Binds
fun bindConversationsStringsProvider(
impl: ConversationsStringsProviderImpl,
): ConversationsStringsProvider
}
11 changes: 8 additions & 3 deletions app/src/main/java/tech/relaycorp/letro/home/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ import androidx.hilt.navigation.compose.hiltViewModel
import tech.relaycorp.letro.contacts.ContactsViewModel
import tech.relaycorp.letro.contacts.model.Contact
import tech.relaycorp.letro.contacts.ui.ContactsScreen
import tech.relaycorp.letro.home.tabs.LetroTabs
import tech.relaycorp.letro.messages.list.ConversationsListScreen
import tech.relaycorp.letro.messages.list.ConversationsViewModel
import tech.relaycorp.letro.ui.utils.SnackbarStringsProvider
import tech.relaycorp.letro.messages.model.ExtendedConversation
import tech.relaycorp.letro.ui.utils.StringsProvider

@Composable
fun HomeScreen(
homeViewModel: HomeViewModel,
snackbarStringsProvider: SnackbarStringsProvider,
stringsProvider: StringsProvider,
onConversationClick: (ExtendedConversation) -> Unit,
onEditContactClick: (Contact) -> Unit,
snackbarHostState: SnackbarHostState,
conversationsViewModel: ConversationsViewModel = hiltViewModel(),
Expand All @@ -38,13 +41,15 @@ fun HomeScreen(
when (uiState.currentTab) {
TAB_CHATS -> Column {
ConversationsListScreen(
conversationsStringsProvider = stringsProvider.conversations,
onConversationClick = onConversationClick,
viewModel = conversationsViewModel,
)
}
TAB_CONTACTS -> ContactsScreen(
viewModel = contactsViewModel,
snackbarHostState = snackbarHostState,
snackbarStringsProvider = snackbarStringsProvider,
snackbarStringsProvider = stringsProvider.snackbar,
onEditContactClick = { onEditContactClick(it) },
)
TAB_NOTIFICATIONS -> Column {
Expand Down
31 changes: 30 additions & 1 deletion app/src/main/java/tech/relaycorp/letro/home/HomeViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import tech.relaycorp.letro.messages.model.ExtendedConversation
import tech.relaycorp.letro.messages.repository.ConversationsRepository
import javax.inject.Inject

@HiltViewModel
class HomeViewModel @Inject constructor() : ViewModel() {
class HomeViewModel @Inject constructor(
private val conversationsRepository: ConversationsRepository,
) : ViewModel() {

private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState>
Expand All @@ -22,6 +26,14 @@ class HomeViewModel @Inject constructor() : ViewModel() {
val createNewMessageSignal: SharedFlow<Unit>
get() = _createNewMessageSignal

init {
viewModelScope.launch {
conversationsRepository.conversations.collect {
updateTabBadges(it)
}
}
}

fun onTabClick(index: Int) {
viewModelScope.launch {
_uiState.update {
Expand Down Expand Up @@ -60,6 +72,22 @@ class HomeViewModel @Inject constructor() : ViewModel() {
}
}

private fun updateTabBadges(conversations: List<ExtendedConversation>) {
val unreadConversationsCount = conversations.count { !it.isRead }
val badge = when {
unreadConversationsCount > 9 -> "9+"
unreadConversationsCount > 0 -> unreadConversationsCount.toString()
else -> null
}
_uiState.update {
it.copy(
tabCounters = mapOf(
TAB_CHATS to badge,
),
)
}
}

private fun getFloatingActionButtonConfig(tabIndex: Int) = when (tabIndex) {
TAB_CHATS -> HomeFloatingActionButtonConfig.ChatListFloatingActionButtonConfig
TAB_CONTACTS -> HomeFloatingActionButtonConfig.ContactsFloatingActionButtonConfig
Expand All @@ -70,6 +98,7 @@ class HomeViewModel @Inject constructor() : ViewModel() {

data class HomeUiState(
val currentTab: Int = TAB_CHATS,
val tabCounters: Map<Int, String?> = emptyMap(),
val floatingActionButtonConfig: HomeFloatingActionButtonConfig? = HomeFloatingActionButtonConfig.ChatListFloatingActionButtonConfig,
val isAddContactFloatingMenuVisible: Boolean = false,
)
Expand Down
134 changes: 134 additions & 0 deletions app/src/main/java/tech/relaycorp/letro/home/tabs/LetroTabs.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package tech.relaycorp.letro.home.tabs

import android.annotation.SuppressLint
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
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.home.HomeViewModel
import tech.relaycorp.letro.ui.theme.LetroColor

@SuppressLint
@Composable
fun LetroTabs(
viewModel: HomeViewModel,
modifier: Modifier = Modifier,
) {
val uiState by viewModel.uiState.collectAsState()

val tabTitles = listOf(
stringResource(id = R.string.top_bar_tab_conversations),
stringResource(id = R.string.top_bar_tab_contacts),
stringResource(id = R.string.top_bar_tab_notifications),
)
val tabCounters = uiState.tabCounters

ScrollableTabRow(
selectedTabIndex = uiState.currentTab,
containerColor = LetroColor.SurfaceContainerHigh,
contentColor = LetroColor.OnSurfaceContainerHigh,
edgePadding = 9.dp,
indicator = {
TabRowDefaults.Indicator(
color = LetroColor.OnSurfaceContainerHigh,
modifier = Modifier.tabIndicatorOffset(it[uiState.currentTab]),
)
},
modifier = modifier,
) {
tabTitles.forEachIndexed { index, title ->
BadgedTab(
selected = uiState.currentTab == index,
onClick = {
viewModel.onTabClick(index)
},
text = title,
badge = tabCounters[index],
modifier = Modifier
.padding(horizontal = 0.dp)
.alpha(if (uiState.currentTab == index) 1f else 0.6f),
)
}
}
}

@Composable
private fun BadgedTab(
modifier: Modifier = Modifier,
selected: Boolean,
onClick: () -> Unit,
text: String,
badge: String? = null,
) {
Tab(
selected = selected,
onClick = { onClick() },
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = text,
style = MaterialTheme.typography.labelLarge,
color = LetroColor.OnSurfaceContainerHigh,
maxLines = 1,
)
if (badge != null) {
Spacer(modifier = Modifier.width(4.dp))
TabBadge(text = badge)
}
}
},
selectedContentColor = LetroColor.OnSurfaceContainerHigh,
unselectedContentColor = MaterialTheme.colorScheme.error,
modifier = modifier,
)
}

@Composable
private fun TabBadge(
text: String,
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(
color = LetroColor.OnSurfaceContainerHigh,
shape = CircleShape,
)
.size(16.dp),
) {
Text(
text = text,
style = MaterialTheme.typography.labelSmall,
color = LetroColor.SurfaceContainerHigh,
maxLines = 1,
)
}
}

@Preview(showBackground = true)
@Composable
fun PreviewTabBadge() {
TabBadge(text = "1")
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ fun CreateNewMessageScreen(
value = recipientTextFieldValueState,
textStyle = MaterialTheme.typography.bodyLarge,
onValueChange = {
if (uiState.showRecipientAsChip) {
return@LetroTextField
}
recipientTextFieldValueState = it
viewModel.onRecipientTextChanged(it.text)
},
Expand Down
Loading

0 comments on commit 2ac9506

Please sign in to comment.