Message formatting

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

View File

@@ -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')"
]
}
}

View File

@@ -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<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>
@Upsert

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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
}

View File

@@ -181,7 +181,9 @@ fun ChatRoomDetailScreen(
} else {
IconButton(
onClick = {
chatRoomDetailMessageListViewModel.sendMessage(
textFieldState = textFieldState
)
}
) {
Icon(

View File

@@ -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"