Add description to chat room

This commit is contained in:
Kgothatso Ngako
2026-07-12 21:49:59 +02:00
parent 7e1be3c3f6
commit f53b407e58
14 changed files with 213 additions and 21 deletions

View File

@@ -1,26 +1,32 @@
package at.torch.compose.database.dao
import androidx.room3.Dao
import androidx.room3.Delete
import androidx.room3.Query
import androidx.room3.Transaction
import androidx.room3.Upsert
import at.torch.compose.database.model.ChatRoom
import at.torch.compose.database.model.intermdiate.LocalChatRoom
import kotlinx.coroutines.flow.Flow
@Dao
interface ChatRoomDao {
@Transaction
@Query("SELECT * FROM ChatRoom WHERE id = :id")
fun findChatRoomById(id: String): at.torch.compose.database.model.intermdiate.LocalChatRoom?
fun findChatRoomById(id: String): LocalChatRoom?
@Transaction
@Query("SELECT * FROM ChatRoom WHERE userPublicKey = :userPublicKey")
fun getChatRoomListByUserPublicKey(userPublicKey: String): List<at.torch.compose.database.model.intermdiate.LocalChatRoom>
fun getChatRoomListByUserPublicKey(userPublicKey: String): List<LocalChatRoom>
@Transaction
@Query("SELECT * FROM ChatRoom WHERE userPublicKey = :userPublicKey")
fun observeChatRoomListByUserPublicKey(userPublicKey: String): Flow<List<at.torch.compose.database.model.intermdiate.LocalChatRoom>>
fun observeChatRoomListByUserPublicKey(userPublicKey: String): Flow<List<LocalChatRoom>>
@Upsert
suspend fun upsert(chatRoom: at.torch.compose.database.model.ChatRoom)
suspend fun upsert(chatRoom: ChatRoom)
@Delete
suspend fun delete(chatRoom: ChatRoom)
}

View File

@@ -56,6 +56,8 @@ abstract class MarmotOutboundDao(
@Transaction
open suspend fun createMlsDirectMessageChatRoom(
name: String? = null,
description: String? = null,
userPublicKey: HexKey,
peerPublicKey: HexKey,
peerKeyPackage: MarmotKeyPackage,
@@ -78,7 +80,8 @@ abstract class MarmotOutboundDao(
id = nostrGroupId,
userPublicKey = userPublicKey,
mlsGroupState = mlsGroup.saveState().encodeTls().toHex(),
subject = null,
subject = name,
description = description
)
database.chatRoomDao().upsert(
chatRoom
@@ -275,7 +278,6 @@ abstract class MarmotOutboundDao(
)
}
)
// TODO: Save chatMessage
}
}

View File

@@ -510,6 +510,7 @@ abstract class NostrDao(
id = decryptedGiftWrapPayload.chatRoomId,
userPublicKey = userPublicKey,
subject = decryptedGiftWrapPayload.parseSubject(),
description = null,
createdAt = decryptedGiftWrapPayload.createdAt,
initialGiftWrapPayloadId = decryptedGiftWrapPayload.id,
mlsGroupState = null
@@ -736,6 +737,7 @@ abstract class NostrDao(
id = nostrGroupId,
userPublicKey = userPublicKey,
subject = group.currentMarmotData()?.name?.ifBlank { null },
description = group.currentMarmotData()?.description?.ifBlank { null },
initialGiftWrapPayloadId = decryptedGiftWrapPayload.id,
createdAt = decryptedGiftWrapPayload.createdAt,
mlsGroupState = group.saveState().encodeTls().toHex(),

View File

@@ -28,7 +28,14 @@ abstract class NostrNip17Dao(
val logger = Logger.withTag(TAG)
@Transaction
open suspend fun getOrCreateChatRoom(chatRoomId: String, activeUserPublicKey: HexKey, relayHint: String?, defaultSubject: String? = null, mlsGroupState: String? = null): LocalChatRoom? {
open suspend fun getOrCreateChatRoom(
chatRoomId: HexKey,
activeUserPublicKey: HexKey,
relayHint: String?,
defaultSubject: String? = null,
description: String? = null,
mlsGroupState: String? = null
): LocalChatRoom? {
logger.d("getOrCreateChatRoom: $chatRoomId")
@@ -51,7 +58,8 @@ abstract class NostrNip17Dao(
id = chatRoomId,
userPublicKey = activeUserPublicKey,
subject = defaultSubject,
mlsGroupState = mlsGroupState
mlsGroupState = mlsGroupState,
description = description
)
database.chatRoomDao().upsert(chatRoom)

View File

@@ -58,6 +58,8 @@ data class ChatRoom(
*/
val subject: String?,
val description: String?,
val mlsGroupState: String?,
/**
@@ -75,6 +77,8 @@ data class ChatRoom(
// Might want to focus on the other last messages...
val initialGiftWrapPayloadId: HexKey? = null,
val leftGroupAt: Instant? = null,
override val createdAt: Instant = Clock.System.now(),
override val updatedAt: Instant = createdAt,
override val savedAt: Instant = createdAt,

View File

@@ -3,6 +3,7 @@ package at.torch.compose.database.repository
import at.torch.compose.database.GENESIS_AT
import at.torch.compose.database.TorchDatabase
import at.torch.compose.database.model.ChatMessage
import at.torch.compose.database.model.ChatRoom
import at.torch.compose.database.model.GiftWrapPayload
import at.torch.compose.database.model.MarmotInnerEvent
import at.torch.compose.database.model.MarmotKeyPackage
@@ -14,7 +15,6 @@ import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroup
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupState
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
@@ -69,6 +69,7 @@ class DatabaseChatRepository(
activeUserPublicKey: HexKey,
relayHint: String?,
defaultSubject: String?,
description: String?,
mlsGroupState: String?
): LocalChatRoom? = try {
return database.nostrNip17Dao().getOrCreateChatRoom(
@@ -76,6 +77,7 @@ class DatabaseChatRepository(
activeUserPublicKey = activeUserPublicKey,
relayHint = relayHint,
defaultSubject = defaultSubject,
description = description,
mlsGroupState = mlsGroupState
)
} catch (e: Throwable) {
@@ -120,11 +122,15 @@ class DatabaseChatRepository(
}
override suspend fun createMlsDirectMessage(
name: String?,
description: String?,
userPublicKey: HexKey,
peerPublicKey: HexKey,
peerKeyPackage: MarmotKeyPackage
): String {
return database.marmotOutboundDao().createMlsDirectMessageChatRoom(
name = name,
description = description,
userPublicKey = userPublicKey,
peerPublicKey = peerPublicKey,
peerKeyPackage = peerKeyPackage
@@ -155,6 +161,7 @@ class DatabaseChatRepository(
)
)
}
if (mlsGroup != null) {
// Create ChatRumor...
val createdAt = Clock.System.now().epochSeconds
@@ -345,6 +352,112 @@ class DatabaseChatRepository(
}
}
override suspend fun leaveChatRoom(localChatRoom: LocalChatRoom) {
val text = "Left group."
val mlsGroup = localChatRoom.chatRoom.mlsGroupState?.let { mlsGroupState ->
MlsGroup.restore(
MlsGroupState.decodeTls(
mlsGroupState.hexToByteArray()
)
)
}
if (mlsGroup != null) {
// Create ChatRumor...
val createdAt = Clock.System.now().epochSeconds
val marmotInnerEventKind = ChatEvent.KIND
val marmotInnerEventId = EventHasher.hashId(
pubKey = localChatRoom.chatRoom.userPublicKey,
createdAt = createdAt,
tags = emptyArray(),
content = text,
kind = marmotInnerEventKind
)
database.marmotInnerEventDao().upsert(
MarmotInnerEvent(
id = marmotInnerEventId,
publicKey = localChatRoom.chatRoom.userPublicKey,
createdAt = Instant.fromEpochSeconds(createdAt),
tags = emptyArray(),
content = text,
chatRoomId = localChatRoom.chatRoom.id,
kind = marmotInnerEventKind
)
)
database.chatMessageDao().upsert(
ChatMessage(
content = text,
chatRoomId = localChatRoom.chatRoom.id,
senderPublicKey = localChatRoom.chatRoom.userPublicKey,
isUserMessage = true,
giftWrapPayloadId = null,
marmotGroupEventId = null,
marmotInnerEventId = marmotInnerEventId,
)
)
} else {
val innerEventKind = ChatMessageEvent.KIND
// Send GiftWrapMessage
val receiverTags = localChatRoom.localParticipants.filter { participant ->
participant.participant.participantPublicKey != localChatRoom.chatRoom.userPublicKey // TODO: Allow this where it's notes to self...
}.map { localParticipant ->
PTag.assemble(
localParticipant.participant.participantPublicKey,
localParticipant.participant.relayHint?.let { NormalizedRelayUrl(it) }
)
}
val createdAt = Clock.System.now().epochSeconds
val giftWrapPayloadId = EventHasher.hashId(
pubKey = localChatRoom.chatRoom.userPublicKey,
createdAt = createdAt,
tags = receiverTags.toTypedArray(),
content = text,
kind = innerEventKind
)
database.giftWrapPayloadDao().upsert(
GiftWrapPayload(
id = giftWrapPayloadId,
kind = innerEventKind,
tags = receiverTags.toTypedArray(),
createdAt = Instant.fromEpochSeconds(createdAt),
content = text,
chatRoomId = localChatRoom.chatRoom.id,
publicKey = localChatRoom.chatRoom.userPublicKey
)
)
database.chatMessageDao().upsert(
ChatMessage(
content = text,
chatRoomId = localChatRoom.chatRoom.id,
senderPublicKey = localChatRoom.chatRoom.userPublicKey,
isUserMessage = true,
giftWrapPayloadId = giftWrapPayloadId,
marmotGroupEventId = null,
marmotInnerEventId = null
)
)
}
}
override suspend fun softDeleteChatRoom(chatRoom: ChatRoom) {
database.chatRoomDao().upsert(
chatRoom.copy(
deletedAt = Clock.System.now()
)
)
}
override suspend fun deleteChatRoom(chatRoom: ChatRoom) {
database.chatRoomDao().delete(chatRoom)
}
companion object {
const val TAG = "DatabaseChatRepository"
}

View File

@@ -7,13 +7,11 @@ import at.torch.compose.database.model.Participant
import at.torch.compose.database.model.Profile
import at.torch.compose.database.model.intermdiate.LocalChatMessage
import at.torch.compose.database.model.intermdiate.LocalChatRoom
import at.torch.compose.database.model.intermdiate.LocalProfile
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import fr.acinq.secp256k1.Hex
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@@ -35,6 +33,7 @@ interface ChatRepository {
activeUserPublicKey: HexKey,
relayHint: String?,
defaultSubject: String?,
description: String?,
mlsGroupState: String?
): LocalChatRoom?
@@ -45,6 +44,8 @@ interface ChatRepository {
suspend fun getProfileWithPublicKey(publicKey: HexKey): Profile?
suspend fun createMlsDirectMessage(
name: String? = null,
description: String? = null,
userPublicKey: HexKey,
peerPublicKey: HexKey,
peerKeyPackage: MarmotKeyPackage
@@ -68,6 +69,12 @@ interface ChatRepository {
suspend fun sealGiftWrapPayload(giftWrapPayload: GiftWrapPayload, nostrSignerSync: NostrSignerSync)
suspend fun leaveChatRoom(localChatRoom: LocalChatRoom)
suspend fun softDeleteChatRoom(chatRoom: ChatRoom)
suspend fun deleteChatRoom(chatRoom: ChatRoom)
companion object {
val NO_OP_CHAT_REPOSITORY = object: ChatRepository {
override suspend fun observeChatRoomListByPublicKey(publicKey: String): Flow<List<LocalChatRoom>> {
@@ -100,6 +107,7 @@ interface ChatRepository {
activeUserPublicKey: HexKey,
relayHint: String?,
defaultSubject: String?,
description: String?,
mlsGroupState: String?
): LocalChatRoom? {
return null
@@ -118,6 +126,8 @@ interface ChatRepository {
}
override suspend fun createMlsDirectMessage(
name: String?,
description: String?,
userPublicKey: HexKey,
peerPublicKey: HexKey,
peerKeyPackage: MarmotKeyPackage
@@ -155,6 +165,18 @@ interface ChatRepository {
) {
TODO("Not yet implemented")
}
override suspend fun leaveChatRoom(localChatRoom: LocalChatRoom) {
TODO("Not yet implemented")
}
override suspend fun softDeleteChatRoom(chatRoom: ChatRoom) {
TODO("Not yet implemented")
}
override suspend fun deleteChatRoom(chatRoom: ChatRoom) {
TODO("Not yet implemented")
}
}
}
}

View File

@@ -219,6 +219,7 @@ private fun ChatRoomDetailScreenPreview() {
id = "hex",
userPublicKey = "hex",
subject = "Room Title",
description = "See something... say something.",
initialGiftWrapPayloadId = "sdfaer",
mlsGroupState = null
),

View File

@@ -206,12 +206,12 @@ fun ChatRoomDetailScreen(
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.secondary
),
enabled = true, // Disable after we leave group...
enabled = chatRoomDetailUIState.localChatRoom.chatRoom.leftGroupAt == null, // Disable after we leave group...
onClick = {
onNavigateToRoute.invoke(
ImplementationPendingRoute(
"Leave Group"
)
// Leave group logic
chatRoomDetailViewModel.leaveGroup(
localChatRoom = chatRoomDetailUIState.localChatRoom,
onNavigateToRoute = onNavigateToRoute
)
}
) {
@@ -233,7 +233,7 @@ fun ChatRoomDetailScreen(
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error
),
enabled = true, // TODO: Only enabled after we leave group...
enabled = chatRoomDetailUIState.localChatRoom.chatRoom.leftGroupAt != null,
onClick = {
onNavigateToRoute.invoke(
ImplementationPendingRoute(
@@ -316,7 +316,8 @@ private fun ChatRoomMessagingScreenPreview() {
chatRoom = ChatRoom(
id = "",
userPublicKey = "",
subject = "Message title",
subject = "Message Title",
description = "Description",
initialGiftWrapPayloadId = "sdfaer",
mlsGroupState = null
),

View File

@@ -340,6 +340,7 @@ private fun ChatRoomMessagingScreenPreview() {
id = "",
userPublicKey = "",
subject = "Message title",
description = "See something. Say somethin",
initialGiftWrapPayloadId = "sdfaer",
mlsGroupState = null
),

View File

@@ -227,6 +227,7 @@ private fun ChatRoomDetailScreenPreview() {
id = "",
userPublicKey = "",
subject = "Message title",
description = "See something... say something.",
initialGiftWrapPayloadId = "sdfaer",
mlsGroupState = null
),

View File

@@ -84,6 +84,7 @@ class ChatRoomCreationViewModel(
activeUserPublicKey = keyPair.pubKey.toHexKey(),
relayHint = null,
defaultSubject = name.toString(),
description = description.toString(),
mlsGroupState = group.saveState().encodeTls().toHex()
)

View File

@@ -9,8 +9,12 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import at.torch.compose.database.model.ChatRoom
import at.torch.compose.database.model.intermdiate.LocalChatRoom
import at.torch.compose.repository.ChatRepository
import at.torch.compose.repository.NostrRepository
import at.torch.compose.ui.composable.navigation.routes.ImplementationPendingRoute
import at.torch.compose.ui.composable.navigation.routes.Route
import at.torch.compose.ui.view.state.ChatRoomDetailUIState
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -53,6 +57,22 @@ class ChatRoomDetailViewModel(
}
}
fun leaveGroup(
localChatRoom: LocalChatRoom,
onNavigateToRoute: (Route) -> Unit
) {
viewModelScope.launch(Dispatchers.IO) {
chatRepository.leaveChatRoom(localChatRoom)
viewModelScope.launch(Dispatchers.Main) {
onNavigateToRoute.invoke(
ImplementationPendingRoute(
"Leave Group"
)
)
}
}
}
companion object {
private const val TAG = "ChatRoomDetailViewModel"