From 29f4df66f2fa043cef823b2593e87934195b45cd Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Tue, 2 Jun 2026 01:59:00 +0200 Subject: [PATCH] Message formatting --- .../1.json | 27 +++-- .../compose/database/dao/ChatMessageDao.kt | 4 +- .../compose/database/dao/NostrDao.kt | 2 +- .../compose/database/model/ChatMessage.kt | 11 +- .../repository/DatabaseChatRepository.kt | 20 ++++ .../compose/repository/ChatRepository.kt | 9 ++ .../ui/composable/ChatRoomDetailScreen.kt | 4 +- .../ChatRoomDetailMessageListViewModel.kt | 113 ++++++++++++++++-- 8 files changed, 158 insertions(+), 32 deletions(-) diff --git a/composeApp/schemas/ac.cord.auxiliary.compose.database.AuxDatabase/1.json b/composeApp/schemas/ac.cord.auxiliary.compose.database.AuxDatabase/1.json index 9822f930..b1ab12c4 100644 --- a/composeApp/schemas/ac.cord.auxiliary.compose.database.AuxDatabase/1.json +++ b/composeApp/schemas/ac.cord.auxiliary.compose.database.AuxDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "3594831027f19047682084075fd88ace", + "identityHash": "65a72c3df37d4135fefcfb91e43296ff", "entities": [ { "tableName": "BroadcastNostrEventReceipt", @@ -276,12 +276,12 @@ }, { "tableName": "ChatMessage", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `senderPublicKey` TEXT NOT NULL, `isUserMessage` INTEGER NOT NULL, `chatRoomId` TEXT NOT NULL, `replyToEventId` TEXT, `quotedEventId` TEXT, `content` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`chatRoomId`) REFERENCES `ChatRoom`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`id`) REFERENCES `GiftWrapPayload`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `senderPublicKey` TEXT NOT NULL, `isUserMessage` INTEGER NOT NULL, `chatRoomId` TEXT NOT NULL, `giftWrapPayloadId` TEXT, `replyToMessageId` INTEGER, `quotedMessageId` INTEGER, `content` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, FOREIGN KEY(`chatRoomId`) REFERENCES `ChatRoom`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`giftWrapPayloadId`) REFERENCES `GiftWrapPayload`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", "columnName": "id", - "affinity": "TEXT", + "affinity": "INTEGER", "notNull": true }, { @@ -303,14 +303,19 @@ "notNull": true }, { - "fieldPath": "replyToEventId", - "columnName": "replyToEventId", + "fieldPath": "giftWrapPayloadId", + "columnName": "giftWrapPayloadId", "affinity": "TEXT" }, { - "fieldPath": "quotedEventId", - "columnName": "quotedEventId", - "affinity": "TEXT" + "fieldPath": "replyToMessageId", + "columnName": "replyToMessageId", + "affinity": "INTEGER" + }, + { + "fieldPath": "quotedMessageId", + "columnName": "quotedMessageId", + "affinity": "INTEGER" }, { "fieldPath": "content", @@ -348,7 +353,7 @@ } ], "primaryKey": { - "autoGenerate": false, + "autoGenerate": true, "columnNames": [ "id" ] @@ -370,7 +375,7 @@ "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ - "id" + "giftWrapPayloadId" ], "referencedColumns": [ "id" @@ -2625,7 +2630,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3594831027f19047682084075fd88ace')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '65a72c3df37d4135fefcfb91e43296ff')" ] } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ChatMessageDao.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ChatMessageDao.kt index c855e1ba..9e76f720 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ChatMessageDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ChatMessageDao.kt @@ -10,10 +10,10 @@ import kotlinx.coroutines.flow.Flow @Dao interface ChatMessageDao { - @Query("SELECT * FROM ChatMessage WHERE chatRoomId = :chatRoomId") + @Query("SELECT * FROM ChatMessage WHERE chatRoomId = :chatRoomId ORDER BY createdAt DESC") fun observeChatMessagesByChatRoomId(chatRoomId: String): Flow> - @Query("SELECT * FROM ChatMessage WHERE chatRoomId = :chatRoomId") + @Query("SELECT * FROM ChatMessage WHERE chatRoomId = :chatRoomId ORDER BY createdAt DESC") fun getChatMessagesByChatRoomId(chatRoomId: String): List @Upsert diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt index 87c6eac2..dabcfd08 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt @@ -382,7 +382,7 @@ abstract class NostrDao( // Save chatMessage database.chatMessageDao().upsert( ChatMessage( - id = giftWrapPayload.id, + giftWrapPayloadId = giftWrapPayload.id, senderPublicKey = giftWrapPayload.publicKey, isUserMessage = SeedManager.activePublicKey().toHex() == giftWrapPayload.publicKey, chatRoomId = chatRoomId, diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/ChatMessage.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/ChatMessage.kt index 72f25ab6..7020deb9 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/ChatMessage.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/ChatMessage.kt @@ -22,22 +22,23 @@ import kotlin.time.Instant ForeignKey( entity = GiftWrapPayload::class, parentColumns = ["id"], - childColumns = ["id"], + childColumns = ["giftWrapPayloadId"], onDelete = ForeignKey.CASCADE, ), ], ) data class ChatMessage( - @PrimaryKey - val id: HexKey, + @PrimaryKey(autoGenerate = true) + val id: Long = 0, val senderPublicKey: String, val isUserMessage: Boolean, val chatRoomId: String, + val giftWrapPayloadId: String? = null, - val replyToEventId: String? = null, - val quotedEventId: String? = null, + val replyToMessageId: Long? = null, + val quotedMessageId: Long? = null, val content: String, diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt index 8046e4f3..6844364f 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt @@ -1,11 +1,16 @@ package ac.cord.auxiliary.compose.database.repository import ac.cord.auxiliary.compose.database.AuxDatabase +import ac.cord.auxiliary.compose.database.model.ChatMessage import ac.cord.auxiliary.compose.database.model.ChatRoom +import ac.cord.auxiliary.compose.database.model.GiftWrapPayload import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatMessage import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom import ac.cord.auxiliary.compose.repository.ChatRepository import co.touchlab.kermit.Logger +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import fr.acinq.lightning.utils.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -42,6 +47,21 @@ class DatabaseChatRepository( return null } + override suspend fun sendChatMessage( + text: String, + chatRoom: ChatRoom + ) { + logger.i("sendChatMessage($text): $chatRoom") + database.chatMessageDao().upsert( + ChatMessage( + content = text, + chatRoomId = chatRoom.id, + senderPublicKey = chatRoom.userPublicKey, + isUserMessage = true + ) + ) + } + override fun updateChatRoomSubject( publicKey: String, subject: String diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt index a7ad8fd4..59b32ca4 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt @@ -17,6 +17,8 @@ interface ChatRepository { suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String? = null): LocalChatRoom? + suspend fun sendChatMessage(text: String, chatRoom: ChatRoom) + fun updateChatRoomSubject(publicKey: String, subject: String): ChatRoom? companion object { @@ -42,6 +44,13 @@ interface ChatRepository { return null } + override suspend fun sendChatMessage( + text: String, + chatRoom: ChatRoom + ) { + // TODO("Not yet implemented") + } + override fun updateChatRoomSubject(publicKey: String, subject: String): ChatRoom? { return null } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt index 06a8948f..8efe31e8 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt @@ -181,7 +181,9 @@ fun ChatRoomDetailScreen( } else { IconButton( onClick = { - + chatRoomDetailMessageListViewModel.sendMessage( + textFieldState = textFieldState + ) } ) { Icon( diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomDetailMessageListViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomDetailMessageListViewModel.kt index d68a1e55..dfc931df 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomDetailMessageListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomDetailMessageListViewModel.kt @@ -1,6 +1,8 @@ package ac.cord.auxiliary.compose.ui.view.model +import ac.cord.auxiliary.compose.database.model.ChatMessage import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom +import ac.cord.auxiliary.compose.extensions.toFormattedTimeAndDateString import ac.cord.auxiliary.compose.repository.ChatRepository import ac.cord.auxiliary.compose.repository.NostrRepository import ac.cord.auxiliary.compose.ui.composable.navigation.routes.ChatRoomDetailRoute @@ -8,15 +10,27 @@ import ac.cord.auxiliary.compose.ui.composable.navigation.routes.Route import ac.cord.auxiliary.compose.ui.view.state.ChatRoomDetailMessageListUIState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Key +import androidx.compose.material.icons.filled.KeyOff +import androidx.compose.material.icons.filled.LockClock +import androidx.compose.material.icons.filled.Signpost +import androidx.compose.material.icons.filled.VpnKey import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon import androidx.compose.material3.LoadingIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -25,6 +39,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -32,10 +47,12 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import co.touchlab.kermit.Logger +import fr.acinq.lightning.utils.UUID import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch +import kotlin.uuid.Uuid class ChatRoomDetailMessageListViewModel( initialChatRoomDetailMessageListUIState: ChatRoomDetailMessageListUIState, @@ -57,7 +74,7 @@ class ChatRoomDetailMessageListViewModel( logger.d("observeChatRoomFeed") viewModelScope.launch(Dispatchers.IO) { chatRepository.observeChatMessageListByChatRoomId( - localChatRoom.chatRoom.userPublicKey + localChatRoom.chatRoom.id ).distinctUntilChanged().collect { chatMessages -> logger.d("getChatMessageListByChatRoomId: $chatMessages") chatRoomDetailMessageListUIState = ChatRoomDetailMessageListUIState.Loaded( @@ -145,24 +162,81 @@ class ChatRoomDetailMessageListViewModel( items = chatRoomDetailMessageListUIState.chatMessageList, key = { it.chatMessage.id } ) { localChatMessage -> - Card( - modifier = Modifier.fillMaxWidth(), - onClick = { + Row { + if (localChatMessage.chatMessage.isUserMessage) { + Spacer( + modifier = Modifier.width(40.dp) + ) + + Spacer( + modifier = Modifier.weight(1f) + ) } - ) { - Column( - modifier = Modifier.padding(20.dp), - horizontalAlignment = Alignment.CenterHorizontally, + + + + Card( + onClick = { + + } ) { - Text( - text = localChatMessage.chatMessage.senderPublicKey // TODO: Get send name... + Column( + modifier = Modifier.padding(10.dp), + horizontalAlignment = if (localChatMessage.chatMessage.isUserMessage) { + Alignment.End + } else { + Alignment.Start + }, + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + if (localChatMessage.chatMessage.isUserMessage.not()) { + Text( + text = localChatMessage.chatMessage.senderPublicKey, // TODO: Get send name... + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + style = MaterialTheme.typography.labelSmall + ) + } + + Text( + text = localChatMessage.chatMessage.content, + style = MaterialTheme.typography.bodyMedium + ) + + Row( + horizontalArrangement = Arrangement.spacedBy( + 10.dp, + Alignment.End + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = localChatMessage.chatMessage.createdAt.toFormattedTimeAndDateString(), + style = MaterialTheme.typography.labelSmall + ) + if (localChatMessage.chatMessage.isUserMessage) { + Icon( + Icons.Default.KeyOff, + contentDescription = "Message status" + ) + } + + } + } + } + + if (!localChatMessage.chatMessage.isUserMessage) { + Spacer( + modifier = Modifier.weight(1f) ) - Text( - text = localChatMessage.chatMessage.content + Spacer( + modifier = Modifier.width(40.dp) ) } + } + } } } @@ -191,6 +265,21 @@ class ChatRoomDetailMessageListViewModel( } } + fun sendMessage(textFieldState: TextFieldState) { + if (textFieldState.text.isNotBlank()) { + val text = textFieldState.text.toString() + textFieldState.clearText() + + viewModelScope.launch(Dispatchers.IO) { + chatRepository.sendChatMessage( + text = text, + chatRoom = localChatRoom.chatRoom + ) + } + + } + } + companion object { const val TAG = "ChatRoomDetailMessageListViewModel"