@@ -13,6 +13,7 @@ import androidx.compose.foundation.background
1313import androidx.compose.foundation.layout.Arrangement
1414import androidx.compose.foundation.layout.Box
1515import androidx.compose.foundation.layout.Column
16+ import androidx.compose.foundation.layout.PaddingValues
1617import androidx.compose.foundation.layout.Row
1718import androidx.compose.foundation.layout.Spacer
1819import androidx.compose.foundation.layout.fillMaxSize
@@ -36,6 +37,7 @@ import androidx.compose.runtime.LaunchedEffect
3637import androidx.compose.runtime.MutableState
3738import androidx.compose.runtime.derivedStateOf
3839import androidx.compose.runtime.getValue
40+ import androidx.compose.runtime.mutableIntStateOf
3941import androidx.compose.runtime.mutableStateOf
4042import androidx.compose.runtime.remember
4143import 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
288304fun DateHeader (date : LocalDate ) {
289305 val text = when (date) {
0 commit comments