forked from reya/coop-mobile
feat: support grouped new chat requests (#23)
Reviewed-on: https://git.reya.su/reya/coop-mobile/pulls/23
This commit is contained in:
@@ -44,6 +44,7 @@ import su.reya.coop.screens.NewIdentityScreen
|
||||
import su.reya.coop.screens.OnboardingScreen
|
||||
import su.reya.coop.screens.ProfileScreen
|
||||
import su.reya.coop.screens.RelayScreen
|
||||
import su.reya.coop.screens.RequestListScreen
|
||||
import su.reya.coop.screens.ScanScreen
|
||||
import su.reya.coop.screens.UpdateProfileScreen
|
||||
|
||||
@@ -164,6 +165,9 @@ fun App(viewModel: NostrViewModel) {
|
||||
entry<Screen.Home> {
|
||||
HomeScreen()
|
||||
}
|
||||
entry<Screen.RequestList> {
|
||||
RequestListScreen()
|
||||
}
|
||||
entry<Screen.Onboarding> {
|
||||
OnboardingScreen()
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ sealed interface Screen : NavKey {
|
||||
@Serializable
|
||||
data object Home : Screen
|
||||
|
||||
@Serializable
|
||||
data object RequestList : Screen
|
||||
|
||||
@Serializable
|
||||
data class Chat(val id: Long) : Screen
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
@@ -84,7 +85,9 @@ import coop.composeapp.generated.resources.Res
|
||||
import coop.composeapp.generated.resources.ic_close
|
||||
import coop.composeapp.generated.resources.ic_new_chat
|
||||
import coop.composeapp.generated.resources.ic_qr
|
||||
import coop.composeapp.generated.resources.ic_request
|
||||
import coop.composeapp.generated.resources.ic_scanner
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import rust.nostr.sdk.PublicKey
|
||||
@@ -93,6 +96,7 @@ import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.LocalScanResult
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
import su.reya.coop.Room
|
||||
import su.reya.coop.RoomKind
|
||||
import su.reya.coop.Screen
|
||||
import su.reya.coop.ago
|
||||
import su.reya.coop.shared.Avatar
|
||||
@@ -111,8 +115,14 @@ fun HomeScreen() {
|
||||
val clipboardManager = LocalClipboard.current
|
||||
val viewModel = LocalNostrViewModel.current
|
||||
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val sheetState = rememberModalBottomSheetState(true)
|
||||
val listState = rememberLazyListState()
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
|
||||
val currentUser = viewModel.currentUser() ?: return
|
||||
val currentUserProfile = viewModel.getMetadata(currentUser) ?: return
|
||||
val currentUserProfile = viewModel.getMetadata(currentUser)
|
||||
|
||||
val userProfile by currentUserProfile.collectAsStateWithLifecycle()
|
||||
val chatRooms by viewModel.chatRooms.collectAsStateWithLifecycle()
|
||||
@@ -120,11 +130,6 @@ fun HomeScreen() {
|
||||
val isPartialProcessedGiftWrap by viewModel.isPartialProcessedGiftWrap.collectAsState(initial = false)
|
||||
val isBannerDismissed by viewModel.isNotificationBannerDismissed.collectAsState()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val sheetState = rememberModalBottomSheetState(true)
|
||||
val listState = rememberLazyListState()
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
|
||||
val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
@@ -140,6 +145,11 @@ fun HomeScreen() {
|
||||
// State will be updated by LifecycleResumeEffect
|
||||
}
|
||||
|
||||
// Partition chat rooms into requests and ongoing
|
||||
val (requests, ongoing) = remember(chatRooms) {
|
||||
chatRooms.partition { it.kind == RoomKind.Request }
|
||||
}
|
||||
|
||||
LifecycleResumeEffect(context) {
|
||||
isNotificationEnabled = NotificationManagerCompat.from(context).areNotificationsEnabled()
|
||||
onPauseOrDispose { }
|
||||
@@ -350,7 +360,11 @@ fun HomeScreen() {
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(chatRooms.toList(), key = { it.id }) { room ->
|
||||
if (requests.isNotEmpty()) {
|
||||
item { NewRequests(requests) }
|
||||
}
|
||||
|
||||
items(ongoing, key = { it.id }) { room ->
|
||||
ChatRoom(
|
||||
room = room,
|
||||
onClick = { navigator.navigate(Screen.Chat(room.id)) }
|
||||
@@ -603,6 +617,89 @@ fun HomeScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewRequests(requests: List<Room>) {
|
||||
val navigator = LocalNavigator.current
|
||||
val viewModel = LocalNostrViewModel.current
|
||||
|
||||
val total = requests.size
|
||||
val firstRoom = requests.getOrNull(0)
|
||||
val secondRoom = requests.getOrNull(1)
|
||||
|
||||
val firstName by remember(firstRoom) {
|
||||
firstRoom?.displayNameFlow(viewModel) ?: flowOf("")
|
||||
}.collectAsStateWithLifecycle("Loading...")
|
||||
|
||||
val secondName by remember(secondRoom) {
|
||||
secondRoom?.displayNameFlow(viewModel) ?: flowOf("")
|
||||
}.collectAsStateWithLifecycle("")
|
||||
|
||||
val supportingText = when {
|
||||
total == 1 && firstRoom != null -> {
|
||||
val message = firstRoom.lastMessage ?: ""
|
||||
"$firstName: $message"
|
||||
}
|
||||
|
||||
total == 2 -> {
|
||||
"$firstName and $secondName"
|
||||
}
|
||||
|
||||
total > 2 -> {
|
||||
val othersCount = total - 2
|
||||
val othersText = if (othersCount == 1) "1 other" else "$othersCount others"
|
||||
"$firstName, $secondName and $othersText"
|
||||
}
|
||||
|
||||
else -> ""
|
||||
}
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier.clickable {
|
||||
navigator.navigate(Screen.RequestList)
|
||||
},
|
||||
leadingContent = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(MaterialShapes.Clover4Leaf.toShape()),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.size(48.dp),
|
||||
color = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.ic_request),
|
||||
contentDescription = "Requests",
|
||||
tint = MaterialTheme.colorScheme.onTertiaryFixed
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = "Requests",
|
||||
style = MaterialTheme.typography.titleMediumEmphasized
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
if (supportingText.isNotEmpty()) {
|
||||
Text(
|
||||
text = supportingText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun ChatRoom(room: Room, onClick: () -> Unit) {
|
||||
@@ -636,7 +733,8 @@ fun ChatRoom(room: Room, onClick: () -> Unit) {
|
||||
if (!room.lastMessage.isNullOrBlank()) {
|
||||
Text(
|
||||
text = room.lastMessage!!,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package su.reya.coop.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coop.composeapp.generated.resources.Res
|
||||
import coop.composeapp.generated.resources.ic_arrow_back
|
||||
import kotlinx.coroutines.launch
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
import su.reya.coop.RoomKind
|
||||
import su.reya.coop.Screen
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RequestListScreen() {
|
||||
val navigator = LocalNavigator.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val viewModel = LocalNostrViewModel.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val listState = rememberLazyListState()
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
val chatRooms by viewModel.chatRooms.collectAsStateWithLifecycle()
|
||||
|
||||
// Get all request rooms
|
||||
val requests = remember(chatRooms) {
|
||||
chatRooms.filter { it.kind == RoomKind.Request }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
title = {
|
||||
Text("New Requests", style = MaterialTheme.typography.titleMediumEmphasized)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navigator.goBack() }) {
|
||||
Icon(
|
||||
painter = org.jetbrains.compose.resources.painterResource(Res.drawable.ic_arrow_back),
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier.padding(top = innerPadding.calculateTopPadding()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
|
||||
) {
|
||||
PullToRefreshBox(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
isRefreshing = isRefreshing,
|
||||
state = pullToRefreshState,
|
||||
onRefresh = {
|
||||
scope.launch {
|
||||
isRefreshing = true
|
||||
viewModel.refreshChatRooms()
|
||||
isRefreshing = false
|
||||
}
|
||||
},
|
||||
indicator = {
|
||||
PullToRefreshDefaults.LoadingIndicator(
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = isRefreshing,
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
)
|
||||
}
|
||||
) {
|
||||
if (requests.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "No requests yet",
|
||||
style = MaterialTheme.typography.titleLargeEmphasized.copy(
|
||||
fontWeight = FontWeight.SemiBold
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = "New chat requests will appear here.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(requests.toList(), key = { it.id }) { room ->
|
||||
ChatRoom(
|
||||
room = room,
|
||||
onClick = { navigator.navigate(Screen.Chat(room.id)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user