Skip to content

Commit a423f6d

Browse files
committed
count new messages
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
1 parent 5d1abc6 commit a423f6d

1 file changed

Lines changed: 55 additions & 39 deletions

File tree

  • app/src/main/java/com/nextcloud/talk/ui/chat

app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import androidx.compose.foundation.background
1313
import androidx.compose.foundation.layout.Arrangement
1414
import androidx.compose.foundation.layout.Box
1515
import androidx.compose.foundation.layout.Column
16+
import androidx.compose.foundation.layout.PaddingValues
1617
import androidx.compose.foundation.layout.Row
1718
import androidx.compose.foundation.layout.Spacer
1819
import androidx.compose.foundation.layout.fillMaxSize
@@ -36,6 +37,7 @@ import androidx.compose.runtime.LaunchedEffect
3637
import androidx.compose.runtime.MutableState
3738
import androidx.compose.runtime.derivedStateOf
3839
import androidx.compose.runtime.getValue
40+
import androidx.compose.runtime.mutableIntStateOf
3941
import androidx.compose.runtime.mutableStateOf
4042
import androidx.compose.runtime.remember
4143
import androidx.compose.runtime.rememberCoroutineScope
@@ -78,54 +80,60 @@ fun GetNewChatView(
7880
val coroutineScope = rememberCoroutineScope()
7981

8082
val lastNewestIdRef = remember {
81-
object {
82-
var value: String? = null
83-
}
83+
object { var value: String? = null }
8484
}
8585

86+
// Track unread messages count
87+
var unreadCount by remember { mutableIntStateOf(0) }
88+
89+
// Determine if user is at newest message
8690
val isAtNewest by remember(listState) {
8791
derivedStateOf {
8892
listState.firstVisibleItemIndex == 0 &&
8993
listState.firstVisibleItemScrollOffset == 0
9094
}
9195
}
9296

93-
val showScrollToNewest by remember {
94-
derivedStateOf { !isAtNewest }
95-
}
97+
// Show floating scroll-to-newest button when not at newest
98+
val showScrollToNewest by remember { derivedStateOf { !isAtNewest } }
9699

100+
// Track newest message and show unread popup
97101
LaunchedEffect(chatItems) {
98102
if (chatItems.isEmpty()) return@LaunchedEffect
99103

100-
val newestId =
101-
chatItems.firstNotNullOfOrNull { it.messageOrNull()?.id }
102-
104+
val newestId = chatItems.firstNotNullOfOrNull { it.messageOrNull()?.id }
103105
val previousNewestId = lastNewestIdRef.value
104106

105107
val isNearBottom = listState.firstVisibleItemIndex <= 2
106-
val hasNewMessage =
107-
previousNewestId != null && newestId != previousNewestId
108+
val hasNewMessage = previousNewestId != null && newestId != previousNewestId
108109

109110
if (hasNewMessage) {
110111
if (isNearBottom) {
111112
listState.animateScrollToItem(0)
113+
unreadCount = 0
112114
} else {
115+
unreadCount++
113116
showUnreadPopup.value = true
114117
}
115118
}
116119

117120
lastNewestIdRef.value = newestId
118121
}
119122

123+
// Hide unread popup when user scrolls to newest
120124
LaunchedEffect(listState) {
121125
snapshotFlow { listState.firstVisibleItemIndex }
122126
.map { it <= 2 }
123127
.distinctUntilChanged()
124128
.collect { nearBottom ->
125-
if (nearBottom) showUnreadPopup.value = false
129+
if (nearBottom) {
130+
showUnreadPopup.value = false
131+
unreadCount = 0
132+
}
126133
}
127134
}
128135

136+
// Load more when near end
129137
LaunchedEffect(listState, chatItems.size) {
130138
snapshotFlow {
131139
val layoutInfo = listState.layoutInfo
@@ -146,12 +154,12 @@ fun GetNewChatView(
146154
}
147155
}
148156

157+
// Sticky date header
149158
val stickyDateHeaderText by remember(listState, chatItems) {
150159
derivedStateOf {
151160
chatItems.getOrNull(
152161
listState.layoutInfo.visibleItemsInfo
153-
.lastOrNull()
154-
?.index ?: 0
162+
.lastOrNull()?.index ?: 0
155163
)?.let { item ->
156164
when (item) {
157165
is ChatViewModel.ChatItem.MessageItem ->
@@ -180,9 +188,7 @@ fun GetNewChatView(
180188

181189
val stickyDateHeaderAlpha by animateFloatAsState(
182190
targetValue = if (stickyDateHeader && stickyDateHeaderText.isNotEmpty()) 1f else 0f,
183-
animationSpec = tween(
184-
durationMillis = if (stickyDateHeader) 500 else 1000
185-
),
191+
animationSpec = tween(durationMillis = if (stickyDateHeader) 500 else 1000),
186192
label = ""
187193
)
188194

@@ -191,23 +197,15 @@ fun GetNewChatView(
191197
state = listState,
192198
reverseLayout = true,
193199
verticalArrangement = Arrangement.spacedBy(8.dp),
200+
contentPadding = PaddingValues(bottom = 20.dp),
194201
modifier = Modifier
195-
.padding(
196-
start = 12.dp,
197-
end = 12.dp
198-
)
202+
.padding(start = 12.dp, end = 12.dp)
199203
.fillMaxSize()
200204
) {
201-
items(
202-
chatItems,
203-
key = { it.stableKey() }
204-
) { chatItem ->
205-
205+
items(chatItems, key = { it.stableKey() }) { chatItem ->
206206
when (chatItem) {
207207
is ChatViewModel.ChatItem.MessageItem -> {
208-
val isBlinkingState =
209-
remember { mutableStateOf(false) }
210-
208+
val isBlinkingState = remember { mutableStateOf(false) }
211209
GetComposableForMessage(
212210
message = chatItem.message,
213211
conversationThreadId = conversationThreadId,
@@ -222,6 +220,7 @@ fun GetNewChatView(
222220
}
223221
}
224222

223+
// Sticky date header
225224
Surface(
226225
modifier = Modifier
227226
.align(Alignment.TopCenter)
@@ -232,19 +231,17 @@ fun GetNewChatView(
232231
) {
233232
Text(
234233
stickyDateHeaderText,
235-
modifier = Modifier.padding(
236-
horizontal = 12.dp,
237-
vertical = 6.dp
238-
)
234+
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
239235
)
240236
}
241237

238+
// Unread messages popup
242239
if (showUnreadPopup.value) {
243240
UnreadMessagesPopup(
241+
unreadCount = unreadCount,
244242
onClick = {
245-
coroutineScope.launch {
246-
listState.scrollToItem(0)
247-
}
243+
coroutineScope.launch { listState.scrollToItem(0) }
244+
unreadCount = 0
248245
showUnreadPopup.value = false
249246
},
250247
modifier = Modifier
@@ -253,6 +250,7 @@ fun GetNewChatView(
253250
)
254251
}
255252

253+
// Floating scroll-to-newest button
256254
AnimatedVisibility(
257255
visible = showScrollToNewest,
258256
modifier = Modifier
@@ -263,9 +261,8 @@ fun GetNewChatView(
263261
) {
264262
Surface(
265263
onClick = {
266-
coroutineScope.launch {
267-
listState.scrollToItem(0)
268-
}
264+
coroutineScope.launch { listState.scrollToItem(0) }
265+
unreadCount = 0
269266
},
270267
shape = CircleShape,
271268
color = colorScheme.surface.copy(alpha = 0.9f),
@@ -284,6 +281,25 @@ fun GetNewChatView(
284281
}
285282
}
286283

284+
@Composable
285+
fun UnreadMessagesPopup(
286+
unreadCount: Int,
287+
onClick: () -> Unit,
288+
modifier: Modifier = Modifier
289+
) {
290+
Surface(
291+
onClick = onClick,
292+
shape = RoundedCornerShape(20.dp),
293+
tonalElevation = 3.dp,
294+
modifier = modifier
295+
) {
296+
Text(
297+
text = "$unreadCount new message${if (unreadCount > 1) "s" else ""}",
298+
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp)
299+
)
300+
}
301+
}
302+
287303
@Composable
288304
fun DateHeader(date: LocalDate) {
289305
val text = when (date) {

0 commit comments

Comments
 (0)