chore: merge the develop branch into master #1

Merged
reya merged 43 commits from develop into master 2026-05-23 00:50:13 +00:00
4 changed files with 109 additions and 80 deletions
Showing only changes of commit e5710f376e - Show all commits

View File

@@ -47,6 +47,7 @@ import androidx.compose.ui.unit.dp
import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_arrow_back
import coop.composeapp.generated.resources.ic_send
import kotlinx.coroutines.flow.first
import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.UnsignedEvent
import su.reya.coop.LocalNostrViewModel
@@ -56,6 +57,7 @@ import su.reya.coop.roomId
import su.reya.coop.shared.Avatar
import su.reya.coop.shared.displayNameFlow
import su.reya.coop.shared.pictureFlow
import su.reya.coop.short
@Composable
fun ChatScreen(
@@ -91,7 +93,16 @@ fun ChatScreen(
messages.addAll(initialMessages)
// Get msg relays for each member
viewModel.chatRoomConnect(id)
val results = viewModel.chatRoomConnect(id)
results.forEach { (member, relays) ->
if (relays.isNotEmpty()) {
val metadata = viewModel.getMetadata(member).first { it != null }
val profile = metadata?.asRecord()
val name = profile?.displayName ?: profile?.name ?: member.short()
snackbarHostState.showSnackbar("Connected to messaging relays for $name")
}
}
// Stop loading spinner
setLoading(false)
@@ -113,7 +124,11 @@ fun ChatScreen(
TopAppBar(
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Box {
if (loading) {
LoadingIndicator(
modifier = Modifier.size(32.dp),
)
} else {
Avatar(
picture = picture,
description = displayName,
@@ -148,19 +163,12 @@ fun ChatScreen(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
) {
if (loading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
LoadingIndicator()
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(bottom = innerPadding.calculateBottomPadding())
) {
if (groupedMessages.isNotEmpty()) {
LazyColumn(
modifier = Modifier
.weight(1f)
@@ -169,7 +177,9 @@ fun ChatScreen(
reverseLayout = true
) {
groupedMessages.forEach { (dateHeader, messagesInGroup) ->
items(messagesInGroup, key = { it.id()?.toBech32()!! }) { event ->
items(
messagesInGroup,
key = { it.id()?.toBech32()!! }) { event ->
ChatMessage(event)
}
item {
@@ -177,6 +187,25 @@ fun ChatScreen(
}
}
}
} else {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "No messages yet",
style = MaterialTheme.typography.titleLargeEmphasized,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = "Your conversations will appear here.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline
)
}
}
}
ChatInput(
value = text,
onValueChange = { text = it },
@@ -188,7 +217,6 @@ fun ChatScreen(
}
}
}
}
)
}
@@ -223,10 +251,10 @@ fun ChatMessage(
}
val containerColor =
if (isMine) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.secondaryContainer
if (isMine) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.tertiaryContainer
val contentColor =
if (isMine) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSecondaryContainer
if (isMine) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onTertiaryContainer
Box(
modifier = Modifier

View File

@@ -360,19 +360,19 @@ fun ChatRoom(room: Room, onClick: () -> Unit) {
)
}
val defaultMenuList = listOf(
"Messaging Relays",
"Spam Filter",
"Contacts",
"Settings",
"About"
)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun BottomMenuList() {
val viewModel = LocalNostrViewModel.current
val defaultMenuList = listOf(
"Messaging Relays" to { },
"Spam Filter" to { },
"Contacts" to { },
"Settings" to { },
"About" to { }
)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -381,15 +381,14 @@ fun BottomMenuList() {
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
) {
defaultMenuList.forEachIndexed { index, item ->
defaultMenuList.forEachIndexed { index, (title, action) ->
SegmentedListItem(
checked = false,
onCheckedChange = { },
onClick = { action() },
shapes = ListItemDefaults.segmentedShapes(
index = index,
count = defaultMenuList.size
),
content = { Text(text = item) },
content = { Text(text = title) },
)
}
}

View File

@@ -70,8 +70,6 @@ class Nostr {
private set
var deviceSigner: AsyncNostrSigner? = null
private set
var msgRelayList: Map<PublicKey, List<RelayUrl>> = emptyMap()
private set
var sentEvents: MutableMap<EventId, List<RelayUrl>> = mutableMapOf()
private set
var rumorMap: MutableMap<EventId, EventId> = mutableMapOf()
@@ -253,11 +251,15 @@ class Nostr {
}
}
else -> {}
else -> {
/* Ignore other event kinds */
}
}
}
else -> {}
else -> {
/* Ignore other message types */
}
}
}
}
@@ -306,20 +308,10 @@ class Nostr {
}
if (event.kind().asStd()?.equals(KindStandard.INBOX_RELAYS) == true) {
// Get all gift wrap events for current user
// Get all gift wrap events for the current user
if (isSignedByUser(event = event)) {
getUserMessages(msgRelayList = event)
}
// Connect to all msg relays for the currently active chat room
if (id.startsWith("room-")) {
launch {
chatRoomAuth(event)
}
}
// Cache the relay list for future use
setMsgRelay(pubkey = event.author(), event = event)
}
if (event.kind().asStd()?.equals(KindStandard.GIFT_WRAP) == true) {
@@ -368,20 +360,15 @@ class Nostr {
}
}
is ClientNotification.NewEvent -> {
// TODO: Handle new event
}
is ClientNotification.Shutdown -> {
break
}
}
}
}
private fun setMsgRelay(pubkey: PublicKey, event: Event) {
val relays = nip17ExtractRelayList(event)
msgRelayList = msgRelayList + (pubkey to relays)
else -> {
/* Ignore other message types */
}
}
}
}
private suspend fun getCachedRumor(giftId: EventId): UnsignedEvent? {
@@ -644,33 +631,49 @@ class Nostr {
}
}
suspend fun chatRoomConnect(id: Long, members: List<PublicKey>) {
suspend fun chatRoomConnect(members: List<PublicKey>): Map<PublicKey, List<RelayUrl>> {
try {
val results = mutableMapOf<PublicKey, MutableList<RelayUrl>>()
members.forEach { member ->
results[member] = mutableListOf<RelayUrl>()
val kind = Kind.fromStd(KindStandard.INBOX_RELAYS)
val filter = Filter().kind(kind).author(member).limit(1u)
val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)
client?.subscribe(
val stream = client?.streamEvents(
target = ReqTarget.auto(listOf(filter)),
closeOn = opts,
id = "room-${id}"
id = "room-${member.toBech32().substring(0, 10)}",
timeout = Duration.parse("3s"),
policy = ReqExitPolicy.ExitOnEose
)
stream?.next()?.let { res ->
if (res.event != null) {
// Connect to the msg relays
connectMsgRelays(res.event!!)
// Mark the member as connected
results[member]?.add(res.relayUrl)
}
} catch (e: Exception) {
throw IllegalStateException("Failed to connect to chat room: ${e.message}", e)
}
}
suspend fun chatRoomAuth(event: Event) {
return results
} catch (e: Exception) {
throw IllegalStateException("Failed to fetch relays: ${e.message}", e)
}
}
suspend fun connectMsgRelays(event: Event) {
try {
val urls = nip17ExtractRelayList(event);
for (url in urls) {
if (client?.relay(url) == null) {
client?.addRelay(url)
client?.connectRelay(url)
}
}
} catch (e: Exception) {
throw IllegalStateException("Failed to authenticate chat room: ${e.message}", e)
throw IllegalStateException("Failed to connect to relays: ${e.message}", e)
}
}

View File

@@ -373,16 +373,15 @@ class NostrViewModel(
return emptyList()
}
fun chatRoomConnect(roomId: Long) {
viewModelScope.launch {
try {
suspend fun chatRoomConnect(roomId: Long): Map<PublicKey, List<RelayUrl>> {
val room = getChatRoom(roomId)
val members = room.members
nostr.chatRoomConnect(roomId, members.toList())
} catch (e: Exception) {
return runCatching {
nostr.chatRoomConnect(members.toList())
}.getOrElse { e ->
showError("Error: ${e.message}")
}
members.associateWith { emptyList<RelayUrl>() }
}
}