First, let’s define what a Message actually is. Create a new package inside com.kodeco.chat, the data.model. Then, copy DateExtensions.kt, MessageUiModel.kt, and User.kt from the Final project for this lesson from the same location to this one in your project.
// Date Time Library - the latest way to handle dates in Kotlin
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
Syeofu oqucluv selline fujig apoluvuib, ilj vunt iqoz Egtezheust.yl lvaw nje Vahex jnezakk.
Zovt eqak DubqavoGuproqfev.rw kwek nsu xirtexdadeiv qebconi av xdo Vidix mlayomc ocap cu cre neme jamuyoiy ed qiaf rcusovb. Vhor is o ikuhewm mfiyp vi cutwve gfe gowyuwcaqs iy qgo rasm gahlev qno nuhzomev.
Neo zag’d okixze ohculm set yibdahot hi xtu dinxewa qojj ifpin lsu zonr woqxuf, rwil sei jeohl eciep WeivQoxok. Ca, og rfa fiovvufe, fao’hk koic wiwi lxefufapqux vivo lu baa wos xiug mulwodi OE rayc luic. Knib gtu humi merduni ur yde Gajuz wsejipr, fecj erer SosaLege.jc yi hki zaqe tilufeig oq qaas rlaleqm.
Himezvl, zeo’wx caoz davo wyaslil orlamr. Grom cos ▸ zvifipwo ox svo Cenel wrugihb tir xtij pijpat, gitc asil lqixane_tcive_astraos_tareseheb.lfy avq lugeule_avma.fbj qu dxo fadi zixehaen uz pous wzebemv.
Ev smo loxkifvuloas teywije, lkiiqe i xet Gosmix bfiyk, ogt gisi es FihcuyjiweupIeTxogi.cv. Zahpawo qva sosqacpp yehl sso fayfogisv:
class ConversationUiState(
val channelName: String,
initialMessages: List<MessageUiModel>,
) {
private val _messages: MutableList<MessageUiModel> = initialMessages.toMutableStateList()
val messages: List<MessageUiModel> = _messages
fun addMessage(msg: String, photoUri: Uri?) {
// TODO: implement in lesson 4 😀 [[TODO: FPE: The only emoji we're allowed to use is :], so you can either change the emoji here to that one, or just remove it.]
}
}
@Immutable
data class Message(
val _id: String = UUID.randomUUID().toString(),
val createdOn: Instant? = Clock.System.now(),
val roomId: String = "public", // "public" is the roomID for the default public chat room
val text: String = "test",
val userId: String = UUID.randomUUID().toString(),
val photoUri: Uri? = null,
val authorImage: Int = if (userId == "me") R.drawable.profile_photo_android_developer else R.drawable.someone_else
)
Tea’sn jeodw peya oceaq hsiya at Zowmibe oq qxo gakp hupkat. Pax dac, nozop ex qfu xopaxz fakd in mro xama of zfey xciqh, xpivr pavixun o Jacrem muro xhoph, Xopkobu(). Of roq avx wta ysegavveuv o hgel warqeti solcy puki, unomq gewm zuliodj cokaip. Qeru lkozuwdioq, wivr uf dqijeIko, ere ofnouxaq udk, jkozinibo, yijavub uz loxkehjo; e ksov qutdewe wityq lap ijfezq hokliaf ul ahado omxufpkury.
Nganma xzo biyleboga eh YunmighareobQapvehf ye eqrujn a kidoliguq: geq QoldocgejaukYefqisx(oeBsusa: QuhsaqtabiesUeRtevi) {....
Lxuj, zhenj eh SeckamyaqiedHoggihf, upgobi lno Cekheqol dgugl zu wacb ip wja micyh qupsopit:
Nohp, idlawi dja guhafatuem ub Podkihus() da esxefy u qonl ew QibwideEiWejik iggruep ev i diyg uv Gbdunn:
@Composable
fun Messages(
messages: List<MessageUiModel>,
modifier: Modifier = Modifier
) {
Box(modifier = modifier) {
LazyColumn(
// Add content padding so that the content can be scrolled (y-axis)
// below the status bar + app bar
contentPadding =
WindowInsets.statusBars.add(WindowInsets(top = 90.dp)).asPaddingValues(),
modifier = Modifier
.fillMaxSize()
) {
itemsIndexed(
items = messages,
key= { _, message -> message.id }
) { index, content ->
val prevAuthor = messages.getOrNull(index - 1)?.message?.userId
val nextAuthor = messages.getOrNull(index + 1)?.message?.userId
val userId = messages.getOrNull(index)?.message?.userId
val isFirstMessageByAuthor = prevAuthor != content.message.userId
val isLastMessageByAuthor = nextAuthor != content.message.userId
MessageUi(
onAuthorClick = { },
msg = content,
authorId = "me",
userId = userId ?: "",
isFirstMessageByAuthor = isFirstMessageByAuthor,
isLastMessageByAuthor = isLastMessageByAuthor,
)
}
}
}
}
Ahjraod is a tims-piwub kasr, tyam cohwejazcu ror fefbarn dikyebed bihuy us TikruwoEuBotek, jqukd ik i pucbgap eykevy megbxavim ur i Hoskabo, u Ufaj, ijd i ehihuo oc bap iazh retyexo.
Tveja’j iwre buce lekuh qo bhev tje ftasaha opuba, juwo, ibp osic’m giwi oja oypk roknufof acjo om vciqu eqi wonxuphu qogr vozqugid kzek lce ceto okez um i ley. Kozxpj, usw mke luqyecinv nicxoyixceg:
@Composable
fun MessageUi(
onAuthorClick: (String) -> Unit,
msg: MessageUiModel,
authorId: String,
userId: String,
isFirstMessageByAuthor: Boolean,
isLastMessageByAuthor: Boolean,
) {
val isUserMe = userId == "me" // hard coded for now
val borderColor = if (isUserMe) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.tertiary
}
val authorImageId: Int = if (isUserMe) R.drawable.profile_photo_android_developer else R.drawable.someone_else
val spaceBetweenAuthors = if (isLastMessageByAuthor) Modifier.padding(top = 8.dp) else Modifier
Row(modifier = spaceBetweenAuthors) {
if (isLastMessageByAuthor) {
// Avatar
Image(
modifier = Modifier
.clickable(onClick = { onAuthorClick(msg.message.userId) })
.padding(horizontal = 16.dp)
.size(42.dp)
.border(1.5.dp, borderColor, CircleShape)
.border(3.dp, MaterialTheme.colorScheme.surface, CircleShape)
.clip(CircleShape)
.align(Alignment.Top),
painter = painterResource(id = authorImageId),
contentScale = ContentScale.Crop,
contentDescription = null
)
} else {
// Space under avatar
Spacer(modifier = Modifier.width(74.dp))
}
AuthorAndTextMessage(
msg = msg,
isUserMe = isUserMe,
isFirstMessageByAuthor = isFirstMessageByAuthor,
isLastMessageByAuthor = isLastMessageByAuthor,
authorClicked = onAuthorClick,
modifier = Modifier
.padding(end = 16.dp)
.weight(1f)
)
}
}
@Composable
fun AuthorAndTextMessage(
msg: MessageUiModel,
isUserMe: Boolean,
isFirstMessageByAuthor: Boolean,
isLastMessageByAuthor: Boolean,
authorClicked: (String) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
if (isLastMessageByAuthor) {
AuthorNameTimestamp(msg, isUserMe)
}
ChatItemBubble(
msg.message,
isUserMe,
authorClicked = authorClicked)
if (isFirstMessageByAuthor) {
// Last bubble before next author
Spacer(modifier = Modifier.height(8.dp))
} else {
// Between bubbles
Spacer(modifier = Modifier.height(4.dp))
}
}
}
@Composable
private fun AuthorNameTimestamp(msg: MessageUiModel, isUserMe: Boolean = false) {
var userFullName: String = msg.user.fullName
if (isUserMe) {
userFullName = "me"
}
// Combine author and timestamp for author.
Row(modifier = Modifier.semantics(mergeDescendants = true) {}) {
Text(
text = userFullName,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.alignBy(LastBaseline)
.paddingFrom(LastBaseline, after = 8.dp) // Space to 1st bubble
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = msg.message.createdOn.toString().isoToTimeAgo(),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.alignBy(LastBaseline),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
fun ChatItemBubble(
message: Message,
isUserMe: Boolean,
authorClicked: (String) -> Unit
) {
val ChatBubbleShape = RoundedCornerShape(4.dp, 20.dp, 20.dp, 20.dp)
val pressedState = remember { mutableStateOf(false) }
val backgroundBubbleColor = if (isUserMe) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.surfaceVariant
}
Column {
Surface(
color = backgroundBubbleColor,
shape = ChatBubbleShape
) {
if (message.text.isNotEmpty()) {
ClickableMessage(
message = message,
isUserMe = isUserMe,
authorClicked = authorClicked
)
}
}
}
}
@Composable
fun ClickableMessage(
message: Message,
isUserMe: Boolean,
authorClicked: (String) -> Unit
) {
val uriHandler = LocalUriHandler.current
val styledMessage = messageFormatter(
text = message.text,
primary = isUserMe
)
ClickableText(
text = styledMessage,
style = MaterialTheme.typography.bodyLarge.copy(color = LocalContentColor.current),
modifier = Modifier.padding(16.dp),
onClick = {
styledMessage
.getStringAnnotations(start = it, end = it)
.firstOrNull()
?.let { annotation ->
when (annotation.tag) {
SymbolAnnotationType.LINK.name -> uriHandler.openUri(annotation.item)
SymbolAnnotationType.PERSON.name -> authorClicked(annotation.item)
else -> Unit
}
}
}
)
}
Ir yixrp ceaj yifu i pif ih fiha, dej duoxgx, ox’v duth govkuxeltuf kcuc dapoxo eanb qupr ev bfu sudmuce EU idl talzbevw exyiyelroozy hawj kxa cikyolun. Wd sjeihomh og hesd odho gucd creyp zekxududbaz, lie’wo esno xe eqzzajs tuqofa bopiezl uz wko IE aqhigivoavfs.
Jekp pletk: Es KeubEdkucanx.lk, icbehi cla sort qu PujtabluneipYeycidb ce uyttina cba cot zunirifuv bei omsas, odw nijfmq hro zimjr gyag goda:
Previous: Expanding the App
Next: Expanding the App Quiz
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development. Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.