1616
1717package io.getstream.chat.android.compose.sample.ui.component
1818
19+ import androidx.annotation.StringRes
20+ import androidx.compose.foundation.layout.Arrangement
21+ import androidx.compose.foundation.layout.Column
22+ import androidx.compose.foundation.layout.PaddingValues
23+ import androidx.compose.foundation.layout.Row
24+ import androidx.compose.foundation.layout.fillMaxWidth
25+ import androidx.compose.foundation.layout.size
26+ import androidx.compose.foundation.lazy.LazyColumn
27+ import androidx.compose.foundation.lazy.LazyListScope
28+ import androidx.compose.foundation.lazy.itemsIndexed
1929import androidx.compose.material.icons.Icons
2030import androidx.compose.material.icons.outlined.Info
2131import androidx.compose.material3.ExperimentalMaterial3Api
2232import androidx.compose.material3.ModalBottomSheet
33+ import androidx.compose.material3.Text
2334import androidx.compose.runtime.Composable
35+ import androidx.compose.runtime.collectAsState
2436import androidx.compose.runtime.getValue
2537import androidx.compose.runtime.mutableStateOf
2638import androidx.compose.runtime.remember
39+ import androidx.compose.runtime.rememberCoroutineScope
2740import androidx.compose.runtime.setValue
41+ import androidx.compose.ui.Alignment
2842import androidx.compose.ui.Modifier
2943import androidx.compose.ui.graphics.vector.rememberVectorPainter
44+ import androidx.compose.ui.res.stringResource
45+ import androidx.compose.ui.text.style.TextOverflow
46+ import androidx.compose.ui.tooling.preview.Preview
47+ import androidx.compose.ui.unit.dp
48+ import io.getstream.chat.android.client.ChatClient
49+ import io.getstream.chat.android.client.extensions.deliveredReadsOf
50+ import io.getstream.chat.android.client.extensions.readsOf
3051import io.getstream.chat.android.compose.sample.R
52+ import io.getstream.chat.android.compose.state.DateFormatType
3153import io.getstream.chat.android.compose.state.messageoptions.MessageOptionItemState
54+ import io.getstream.chat.android.compose.ui.components.Timestamp
55+ import io.getstream.chat.android.compose.ui.components.avatar.Avatar
3256import io.getstream.chat.android.compose.ui.theme.ChatComponentFactory
3357import io.getstream.chat.android.compose.ui.theme.ChatTheme
58+ import io.getstream.chat.android.models.Channel
59+ import io.getstream.chat.android.models.ChannelUserRead
3460import io.getstream.chat.android.models.Message
61+ import io.getstream.chat.android.models.User
62+ import io.getstream.chat.android.state.extensions.watchChannelAsState
3563import io.getstream.chat.android.ui.common.state.messages.CustomAction
3664import io.getstream.chat.android.ui.common.state.messages.MessageAction
65+ import io.getstream.chat.android.ui.common.utils.extensions.initials
66+ import kotlinx.coroutines.CoroutineScope
67+ import kotlinx.coroutines.flow.Flow
68+ import kotlinx.coroutines.flow.filterNotNull
69+ import kotlinx.coroutines.flow.flatMapLatest
70+ import kotlinx.coroutines.flow.map
71+ import java.util.Calendar
72+ import java.util.Date
73+ import kotlin.time.Duration.Companion.hours
3774
3875/* *
3976 * Factory for creating components related to message info.
@@ -64,7 +101,7 @@ class MessageInfoComponentFactory : ChatComponentFactory {
64101 iconPainter = rememberVectorPainter(Icons .Outlined .Info ),
65102 iconColor = ChatTheme .colors.textLowEmphasis,
66103 action = CustomAction (message, mapOf (" message_info" to true )),
67- )
104+ ),
68105 ) + messageOptions
69106
70107 val extendedOnMessageAction: (MessageAction ) -> Unit = { action ->
@@ -87,7 +124,15 @@ class MessageInfoComponentFactory : ChatComponentFactory {
87124 },
88125 containerColor = ChatTheme .colors.appBackground,
89126 ) {
90- // TODO Replace with a proper Message Info Screen
127+ val coroutineScope = rememberCoroutineScope()
128+ val state by readsOf(message, coroutineScope).collectAsState(null )
129+ state?.let {
130+ val (reads, deliveredReads) = it
131+ MessageInfoContent (
132+ reads = reads,
133+ deliveredReads = deliveredReads,
134+ )
135+ }
91136 }
92137 } else if (! dismissed) {
93138 super .MessageMenu (
@@ -101,4 +146,167 @@ class MessageInfoComponentFactory : ChatComponentFactory {
101146 )
102147 }
103148 }
149+
150+ @Composable
151+ private fun readsOf (
152+ message : Message ,
153+ coroutineScope : CoroutineScope ,
154+ ): Flow <Pair <List <ChannelUserRead >, List<ChannelUserRead>>> = ChatClient .instance()
155+ .watchChannelAsState(
156+ cid = message.cid,
157+ messageLimit = 0 ,
158+ coroutineScope = coroutineScope,
159+ ).filterNotNull()
160+ .flatMapLatest { it.reads }
161+ .map {
162+ val channel = Channel (read = it)
163+
164+ val reads = channel.readsOf(message)
165+ .sortedByDescending(ChannelUserRead ::lastRead)
166+
167+ val deliveredReads = channel.deliveredReadsOf(message)
168+ .sortedByDescending { it.lastDeliveredAt ? : Date (0 ) } - reads
169+
170+ reads to deliveredReads
171+ }
172+ }
173+
174+ @Composable
175+ private fun MessageInfoContent (
176+ reads : List <ChannelUserRead >,
177+ deliveredReads : List <ChannelUserRead >,
178+ ) {
179+ LazyColumn (
180+ modifier = Modifier .fillMaxWidth(),
181+ contentPadding = PaddingValues (
182+ start = 16 .dp,
183+ end = 16 .dp,
184+ bottom = 16 .dp,
185+ ),
186+ ) {
187+ // Read by section
188+ section(
189+ items = reads,
190+ labelResId = R .string.message_info_read_by,
191+ skipTopPadding = true ,
192+ )
193+ // Delivered to section
194+ section(
195+ items = deliveredReads,
196+ labelResId = R .string.message_info_delivered_to,
197+ )
198+ }
199+ }
200+
201+ private fun LazyListScope.section (
202+ items : List <ChannelUserRead >,
203+ @StringRes labelResId : Int ,
204+ skipTopPadding : Boolean = false,
205+ ) {
206+ if (items.isNotEmpty()) {
207+ item {
208+ if (skipTopPadding) {
209+ PaneTitle (
210+ text = stringResource(labelResId, items.size),
211+ padding = PaddingValues (
212+ start = 16 .dp,
213+ bottom = 8 .dp,
214+ end = 16 .dp,
215+ ),
216+ )
217+ } else {
218+ PaneTitle (text = stringResource(labelResId, items.size))
219+ }
220+ }
221+ itemsIndexed(
222+ items = items,
223+ key = { _, item -> item.user.id },
224+ ) { index, item ->
225+ PaneRow (
226+ index = index,
227+ lastIndex = items.lastIndex,
228+ ) {
229+ ReadItem (userRead = item)
230+ }
231+ }
232+ }
233+ }
234+
235+ @Composable
236+ private fun ReadItem (
237+ userRead : ChannelUserRead ,
238+ ) {
239+ Row (
240+ horizontalArrangement = Arrangement .spacedBy(12 .dp),
241+ verticalAlignment = Alignment .CenterVertically ,
242+ ) {
243+ Avatar (
244+ modifier = Modifier .size(48 .dp),
245+ imageUrl = userRead.user.image,
246+ initials = userRead.user.initials,
247+ contentDescription = userRead.user.name,
248+ )
249+
250+ Column (
251+ verticalArrangement = Arrangement .spacedBy(4 .dp),
252+ ) {
253+ Text (
254+ text = userRead.user.name.takeIf (String ::isNotBlank) ? : userRead.user.id,
255+ style = ChatTheme .typography.bodyBold,
256+ color = ChatTheme .colors.textHighEmphasis,
257+ maxLines = 1 ,
258+ overflow = TextOverflow .Ellipsis ,
259+ )
260+
261+ Timestamp (
262+ date = userRead.lastDeliveredAt ? : userRead.lastRead,
263+ formatType = DateFormatType .RELATIVE ,
264+ )
265+ }
266+ }
267+ }
268+
269+ @Suppress(" MagicNumber" )
270+ @Preview
271+ @Composable
272+ private fun MessageInfoScreenPreview () {
273+ val sentDate = Calendar .getInstance().apply {
274+ set(2025 , Calendar .AUGUST , 15 , 8 , 15 )
275+ }.time
276+ val user1 = User (id = " jane" , name = " Jane Doe" )
277+ val user2 = User (id = " bob" , name = " Bob Smith" )
278+ val user3 = User (id = " alice" , name = " Alice Johnson" )
279+ val reads = listOf (
280+ ChannelUserRead (
281+ user = user1,
282+ lastReceivedEventDate = Date (),
283+ unreadMessages = 0 ,
284+ lastRead = sentDate.apply { time + = 2 .hours.inWholeMilliseconds },
285+ lastReadMessageId = null ,
286+ ),
287+ ChannelUserRead (
288+ user = user2,
289+ lastReceivedEventDate = Date (),
290+ unreadMessages = 0 ,
291+ lastRead = sentDate.apply { time + = 3 .hours.inWholeMilliseconds },
292+ lastReadMessageId = null ,
293+ ),
294+ )
295+ val deliveredReads = listOf (
296+ ChannelUserRead (
297+ user = user3,
298+ lastReceivedEventDate = Date (),
299+ unreadMessages = 0 ,
300+ lastRead = Date (),
301+ lastReadMessageId = null ,
302+ lastDeliveredAt = sentDate.apply { time + = 1 .hours.inWholeMilliseconds },
303+ lastDeliveredMessageId = null ,
304+ ),
305+ )
306+ ChatTheme {
307+ MessageInfoContent (
308+ deliveredReads = deliveredReads,
309+ reads = reads,
310+ )
311+ }
104312}
0 commit comments