Coverage Summary for Class: ConversationsHomeScreenKt (debug.com.greybox.projectmesh.messaging.ui.screens)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| ConversationsHomeScreenKt$ConversationItem$2 |
|
| ConversationsHomeScreenKt$ConversationsHomeScreen$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/4)
|
| ConversationsHomeScreenKt$ConversationsHomeScreen$2$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/3)
|
| ConversationsHomeScreenKt$ConversationsHomeScreen$2$2 |
0%
(0/1)
|
0%
(0/4)
|
0%
(0/4)
|
0%
(0/24)
|
| ConversationsHomeScreenKt$ConversationsHomeScreen$3 |
|
| ConversationsHomeScreenKt$ConversationsList$1 |
0%
(0/1)
|
|
| ConversationsHomeScreenKt$ConversationsList$1$1$1$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/6)
|
| ConversationsHomeScreenKt$ConversationsList$1$invoke$$inlined$items$default$1 |
0%
(0/1)
|
|
| ConversationsHomeScreenKt$ConversationsList$1$invoke$$inlined$items$default$2 |
0%
(0/1)
|
|
| ConversationsHomeScreenKt$ConversationsList$1$invoke$$inlined$items$default$3 |
0%
(0/1)
|
|
| ConversationsHomeScreenKt$ConversationsList$1$invoke$$inlined$items$default$4 |
0%
(0/1)
|
|
| ConversationsHomeScreenKt$ConversationsList$2 |
|
| ConversationsHomeScreenKt$EmptyConversationsView$2 |
|
| ConversationsHomeScreenKt$ErrorView$2 |
|
| Total |
0%
(0/15)
|
0%
(0/4)
|
0%
(0/7)
|
0%
(0/37)
|
package com.greybox.projectmesh.messaging.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.border
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Chat
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.*
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.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.greybox.projectmesh.ViewModelFactory
import com.greybox.projectmesh.messaging.data.entities.Conversation
import com.greybox.projectmesh.messaging.ui.models.ConversationsHomeScreenModel
import com.greybox.projectmesh.messaging.ui.viewmodels.ConversationsHomeScreenViewModel
import com.greybox.projectmesh.messaging.utils.MessageUtils
import org.kodein.di.compose.localDI
/**
* Main Composable for the Conversations Home screen.
*
* @param onConversationSelected Callback when a conversation is selected.
* @param viewModel [ConversationsHomeScreenViewModel] providing the UI state.
*/
@Composable
fun ConversationsHomeScreen(
onConversationSelected: (String) -> Unit,
viewModel: ConversationsHomeScreenViewModel = viewModel(
factory = ViewModelFactory(
di = localDI(),
owner = LocalSavedStateRegistryOwner.current,
vmFactory = { di, _ ->
ConversationsHomeScreenViewModel(di)
},
defaultArgs = null
)
)
){
val uiState: ConversationsHomeScreenModel by viewModel.uiState.collectAsState(
initial = ConversationsHomeScreenModel(isLoading = true)
)
Box(modifier = Modifier.fillMaxSize()) {
when {
uiState.isLoading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
uiState.error != null -> {
ErrorView(
errorMessage = uiState.error!!,
onRetry = { viewModel.refreshConversations() }
)
}
uiState.conversations.isEmpty() -> {
EmptyConversationsView()
}
else -> {
ConversationsList(
conversations = uiState.conversations,
onConversationClick = { conversation ->
//mark as read when opening the conversation
viewModel.markConversationAsRead(conversation.id)
//when user is online (has an IP address), navigate to chat
if (conversation.isOnline && conversation.userAddress != null) {
onConversationSelected(conversation.userAddress)
} else {
//when user if offline, still show the chat history ->
// handle in MainActivity with a disabled send button
onConversationSelected(conversation.id)
}
}
)
}
}
}
}
/**
* Displays a scrollable list of conversations.
*
* @param conversations List of [Conversation] objects to display.
* @param onConversationClick Callback when a conversation item is clicked.
*/
@Composable
fun ConversationsList(
conversations: List<Conversation>,
onConversationClick: (Conversation) -> Unit
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(vertical = 8.dp)
) {
items(conversations) { conversation ->
ConversationItem(
conversation = conversation,
onClick = { onConversationClick(conversation) }
)
Divider(
modifier = Modifier.padding(start = 72.dp, end = 16.dp),
color = MaterialTheme.colorScheme.surfaceVariant
)
}
}
}
/**
* Displays an individual conversation item with avatar, status, last message, and unread count.
*
* @param conversation The [Conversation] to display.
* @param onClick Callback for when the conversation item is clicked.
*/
@Composable
fun ConversationItem(
conversation: Conversation,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
//avatar/profile picture with online status indicator
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(
color = if (conversation.isOnline)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.surfaceVariant
)
.border(
width = 2.dp,
color = if (conversation.isOnline) Color.Green else Color.Transparent,
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Text(
text = conversation.userName.firstOrNull()?.toString() ?: "?",
color = Color.White,
fontSize = 20.sp
)
//online status indicator
if (conversation.isOnline) {
Box(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(Color.Green)
.border(1.dp, Color.White, CircleShape)
.align(Alignment.BottomEnd)
)
}
}
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically
){
Text(
text = conversation.userName,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer (modifier = Modifier.width(8.dp))
//status indicator text
if (conversation.isOnline) {
Text (
text = "Online",
style = MaterialTheme.typography.labelSmall,
color = Color.Green,
fontSize = 10.sp
)
}
}
//show the timestamp of the last message
Text(
text = if (conversation.lastMessageTime > 0) {
MessageUtils.formatTimestamp(conversation.lastMessageTime)
} else {
""
},
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
// Unread count badge
if (conversation.unreadCount > 0) {
Box(
modifier = Modifier
.size(24.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center
) {
Text(
text = conversation.unreadCount.toString(),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelSmall
)
}
}
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
//last message with status indicator
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f)
) {
//online/offline indicator
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(
if (conversation.isOnline) Color.Green else Color.Gray
)
)
Spacer(modifier = Modifier.width(8.dp))
// Last message content
Text(
text = conversation.lastMessage ?: "No messages yet",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
}
//unread count badge
if (conversation.unreadCount > 0) {
Box(
modifier = Modifier
.size(20.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center
) {
Text(
text = conversation.unreadCount.toString(),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelSmall
)
}
}
}
}
}
}
/**
* Displays a placeholder view when there are no conversations.
*/
@Composable
fun EmptyConversationsView() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Default.Chat,
contentDescription = null,
modifier = Modifier.size(72.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "No Conversations Yet",
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Connect with users on the network to start chatting",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 32.dp)
)
}
}
/**
* Displays an error view with retry button when conversation loading fails.
*
* @param errorMessage The error message to display.
* @param onRetry Callback triggered when retry button is pressed.
*/
@Composable
fun ErrorView(
errorMessage: String,
onRetry: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Default.Error,
contentDescription = null,
modifier = Modifier.size(72.dp),
tint = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Error Loading Conversations",
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = errorMessage,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 32.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onRetry,
contentPadding = PaddingValues(horizontal = 16.dp)
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = null
)
Spacer(modifier = Modifier.width(8.dp))
Text("Retry")
}
}
}