Message formatting

This commit is contained in:
Kgothatso Ngako committed 2026-06-02 01:59:00 +02:00
1 parent d1f0a60826
commit 29f4df66f2
8 files changed
+158 -32

No files matched your search

@@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "3594831027f19047682084075fd88ace", "identityHash": "65a72c3df37d4135fefcfb91e43296ff",
"entities": [ "entities": [
{ {
"tableName": "BroadcastNostrEventReceipt", "tableName": "BroadcastNostrEventReceipt",
@@ -276,12 +276,12 @@
}, },
{ {
"tableName": "ChatMessage", "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": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
"columnName": "id", "columnName": "id",
"affinity": "TEXT", "affinity": "INTEGER",
"notNull": true "notNull": true
}, },
{ {
@@ -303,14 +303,19 @@
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "replyToEventId", "fieldPath": "giftWrapPayloadId",
"columnName": "replyToEventId", "columnName": "giftWrapPayloadId",
"affinity": "TEXT" "affinity": "TEXT"
}, },
{ {
"fieldPath": "quotedEventId", "fieldPath": "replyToMessageId",
"columnName": "quotedEventId", "columnName": "replyToMessageId",
"affinity": "TEXT" "affinity": "INTEGER"
},
{
"fieldPath": "quotedMessageId",
"columnName": "quotedMessageId",
"affinity": "INTEGER"
}, },
{ {
"fieldPath": "content", "fieldPath": "content",
@@ -348,7 +353,7 @@
} }
], ],
"primaryKey": { "primaryKey": {
"autoGenerate": false, "autoGenerate": true,
"columnNames": [ "columnNames": [
"id" "id"
] ]
@@ -370,7 +375,7 @@
"onDelete": "CASCADE", "onDelete": "CASCADE",
"onUpdate": "NO ACTION", "onUpdate": "NO ACTION",
"columns": [ "columns": [
"id" "giftWrapPayloadId"
], ],
"referencedColumns": [ "referencedColumns": [
"id" "id"
@@ -2625,7 +2630,7 @@
], ],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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')"
] ]
} }
} }
@@ -10,10 +10,10 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface ChatMessageDao { 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<List<LocalChatMessage>> fun observeChatMessagesByChatRoomId(chatRoomId: String): Flow<List<LocalChatMessage>>
@Query("SELECT * FROM ChatMessage WHERE chatRoomId = :chatRoomId") @Query("SELECT * FROM ChatMessage WHERE chatRoomId = :chatRoomId ORDER BY createdAt DESC")
fun getChatMessagesByChatRoomId(chatRoomId: String): List<LocalChatMessage> fun getChatMessagesByChatRoomId(chatRoomId: String): List<LocalChatMessage>
@Upsert @Upsert
@@ -382,7 +382,7 @@ abstract class NostrDao(
// Save chatMessage // Save chatMessage
database.chatMessageDao().upsert( database.chatMessageDao().upsert(
ChatMessage( ChatMessage(
id = giftWrapPayload.id, giftWrapPayloadId = giftWrapPayload.id,
senderPublicKey = giftWrapPayload.publicKey, senderPublicKey = giftWrapPayload.publicKey,
isUserMessage = SeedManager.activePublicKey().toHex() == giftWrapPayload.publicKey, isUserMessage = SeedManager.activePublicKey().toHex() == giftWrapPayload.publicKey,
chatRoomId = chatRoomId, chatRoomId = chatRoomId,
@@ -22,22 +22,23 @@ import kotlin.time.Instant
ForeignKey( ForeignKey(
entity = GiftWrapPayload::class, entity = GiftWrapPayload::class,
parentColumns = ["id"], parentColumns = ["id"],
childColumns = ["id"], childColumns = ["giftWrapPayloadId"],
onDelete = ForeignKey.CASCADE, onDelete = ForeignKey.CASCADE,
), ),
], ],
) )
data class ChatMessage( data class ChatMessage(
@PrimaryKey @PrimaryKey(autoGenerate = true)
val id: HexKey, val id: Long = 0,
val senderPublicKey: String, val senderPublicKey: String,
val isUserMessage: Boolean, val isUserMessage: Boolean,
val chatRoomId: String, val chatRoomId: String,
val giftWrapPayloadId: String? = null,
val replyToEventId: String? = null, val replyToMessageId: Long? = null,
val quotedEventId: String? = null, val quotedMessageId: Long? = null,
val content: String, val content: String,
@@ -1,11 +1,16 @@
package ac.cord.auxiliary.compose.database.repository package ac.cord.auxiliary.compose.database.repository
import ac.cord.auxiliary.compose.database.AuxDatabase 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.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.LocalChatMessage
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
import ac.cord.auxiliary.compose.repository.ChatRepository import ac.cord.auxiliary.compose.repository.ChatRepository
import co.touchlab.kermit.Logger 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.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -42,6 +47,21 @@ class DatabaseChatRepository(
return null 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( override fun updateChatRoomSubject(
publicKey: String, publicKey: String,
subject: String subject: String
@@ -17,6 +17,8 @@ interface ChatRepository {
suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String? = null): LocalChatRoom? suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String? = null): LocalChatRoom?
suspend fun sendChatMessage(text: String, chatRoom: ChatRoom)
fun updateChatRoomSubject(publicKey: String, subject: String): ChatRoom? fun updateChatRoomSubject(publicKey: String, subject: String): ChatRoom?
companion object { companion object {
@@ -42,6 +44,13 @@ interface ChatRepository {
return null return null
} }
override suspend fun sendChatMessage(
text: String,
chatRoom: ChatRoom
) {
// TODO("Not yet implemented")
}
override fun updateChatRoomSubject(publicKey: String, subject: String): ChatRoom? { override fun updateChatRoomSubject(publicKey: String, subject: String): ChatRoom? {
return null return null
} }
@@ -181,7 +181,9 @@ fun ChatRoomDetailScreen(
} else { } else {
IconButton( IconButton(
onClick = { onClick = {
chatRoomDetailMessageListViewModel.sendMessage(
textFieldState = textFieldState
)
} }
) { ) {
Icon( Icon(
@@ -1,6 +1,8 @@
package ac.cord.auxiliary.compose.ui.view.model 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.database.model.intermdiate.LocalChatRoom
import ac.cord.auxiliary.compose.extensions.toFormattedTimeAndDateString
import ac.cord.auxiliary.compose.repository.ChatRepository import ac.cord.auxiliary.compose.repository.ChatRepository
import ac.cord.auxiliary.compose.repository.NostrRepository import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.ChatRoomDetailRoute 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 ac.cord.auxiliary.compose.ui.view.state.ChatRoomDetailMessageListUIState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.Card
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -25,6 +39,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@@ -32,10 +47,12 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import fr.acinq.lightning.utils.UUID
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.uuid.Uuid
class ChatRoomDetailMessageListViewModel( class ChatRoomDetailMessageListViewModel(
initialChatRoomDetailMessageListUIState: ChatRoomDetailMessageListUIState, initialChatRoomDetailMessageListUIState: ChatRoomDetailMessageListUIState,
@@ -57,7 +74,7 @@ class ChatRoomDetailMessageListViewModel(
logger.d("observeChatRoomFeed") logger.d("observeChatRoomFeed")
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
chatRepository.observeChatMessageListByChatRoomId( chatRepository.observeChatMessageListByChatRoomId(
localChatRoom.chatRoom.userPublicKey localChatRoom.chatRoom.id
).distinctUntilChanged().collect { chatMessages -> ).distinctUntilChanged().collect { chatMessages ->
logger.d("getChatMessageListByChatRoomId: $chatMessages") logger.d("getChatMessageListByChatRoomId: $chatMessages")
chatRoomDetailMessageListUIState = ChatRoomDetailMessageListUIState.Loaded( chatRoomDetailMessageListUIState = ChatRoomDetailMessageListUIState.Loaded(
@@ -145,24 +162,81 @@ class ChatRoomDetailMessageListViewModel(
items = chatRoomDetailMessageListUIState.chatMessageList, items = chatRoomDetailMessageListUIState.chatMessageList,
key = { it.chatMessage.id } key = { it.chatMessage.id }
) { localChatMessage -> ) { localChatMessage ->
Card( Row {
modifier = Modifier.fillMaxWidth(),
onClick = {
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( Column(
text = localChatMessage.chatMessage.senderPublicKey // TODO: Get send name... 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( Spacer(
text = localChatMessage.chatMessage.content 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 { companion object {
const val TAG = "ChatRoomDetailMessageListViewModel" const val TAG = "ChatRoomDetailMessageListViewModel"