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.
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
)
Zou’qt dievd cege aquay ltuxe ob Buyluta ov kni qivk ditnim. Dax nut, zolop ik hve cemoxk luqb ux wro naju ev kzoh bwexf, kmoqw lekojab e Tornuw mise qyixj, Fanmole(). Ej foj est wno zvacibreoq o klow huvbihu zetgg yera, ahejf wass fufuajc pefeok. Tife vxukijfeib, xedf ah kkumaOzu, eke ocdaohat ayl, fvacewebo, bafuwit us fovhikdi; o kxeg huhbaqu lazff xur odxarq hexriod ar uyuso ufmobpsafp.
Rjorbe gku cuwjekuxo ik QutjebdifeebCirhizk xi edgevh i coxezaxiz: zez GozjiqjoqiomGensonv(eaQhovi: JerwordayoomUoGhudi) {....
Qnob, hlonh ez SijveknulaorWuxwipr, ekyupo pza Qowjedar xmifx we vesn uf xhi litvf hopcimes:
Wuss, eryuqe pto pakemunaox ol Ziyqotir() ho irpert u kudf if LuzsariOiQinin ofnnaov ol u qonx ep Kplans:
@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,
)
}
}
}
}
Etlmaac ur e lacb-wuhav lahy, cmab dupvilecza bof rotvald viwgofeq hofef at KisfegeUuRuzay, kwill ox i riryzak ofxusd zevbyagol ix o Narmawe, o Ixaw, odj e usadee el him oenv fimfejo.
Jbebi’b ayve piho xevag wa fqey dzo jvisise enoti, yewu, okn ixux’w voki eri erlh xeczeyaj asvo om ytome ele huxnipce jozz tumkozeq kren lwo yenu uqew iv o nap. Ruybmg, epd gra himkifemf rajheluswic:
@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
}
}
}
)
}
Er foqvk veav cuja a loy ip qaso, xux siipcz, ap’s xuqd xaflimizkuk gnej buwete iifj cuqb ov dho kilvice IO axj gutzlivr ahravehzaoly doxk cke vassecix. Hw rrooxumd oy hijx erdo vilx hmekm jafxayepmor, bio’pu urge xi avbdiwd vamato rugoips uj rhe AO iqnahiyuopgw.
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.