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
5 changed files with 84 additions and 25 deletions
Showing only changes of commit d56847f5d4 - Show all commits

View File

@@ -42,7 +42,6 @@ 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.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_arrow_back import coop.composeapp.generated.resources.ic_arrow_back
@@ -51,7 +50,7 @@ import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.UnsignedEvent import rust.nostr.sdk.UnsignedEvent
import su.reya.coop.LocalNostrViewModel import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.humanReadable import su.reya.coop.formatAsGroupHeader
import su.reya.coop.roomId import su.reya.coop.roomId
import su.reya.coop.shared.Avatar import su.reya.coop.shared.Avatar
import su.reya.coop.shared.displayNameFlow import su.reya.coop.shared.displayNameFlow
@@ -73,7 +72,10 @@ fun ChatScreen(
var loading by remember { mutableStateOf(true) } var loading by remember { mutableStateOf(true) }
val messages = remember { mutableStateListOf<UnsignedEvent>() } val messages = remember { mutableStateListOf<UnsignedEvent>() }
val groupedMessages = remember(messages.toList()) {
messages.groupBy { it.createdAt().formatAsGroupHeader() }
}
fun setLoading(value: Boolean) { fun setLoading(value: Boolean) {
loading = value loading = value
} }
@@ -96,7 +98,9 @@ fun ChatScreen(
// Handle new messages // Handle new messages
viewModel.newEvents.collect { event -> viewModel.newEvents.collect { event ->
if (event.roomId() == id) { if (event.roomId() == id) {
messages.add(0, event) if (event.id() !in messages.map { it.id() }) {
messages.add(0, event)
}
} }
} }
} }
@@ -163,8 +167,13 @@ fun ChatScreen(
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
reverseLayout = true reverseLayout = true
) { ) {
items(messages.toList(), key = { it.id()?.toBech32()!! }) { event -> groupedMessages.forEach { (dateHeader, messagesInGroup) ->
ChatMessage(event) items(messagesInGroup, key = { it.id()?.toBech32()!! }) { event ->
ChatMessage(event)
}
item {
DateSeparator(dateHeader)
}
} }
} }
ChatInput( ChatInput(
@@ -182,6 +191,22 @@ fun ChatScreen(
) )
} }
@Composable
fun DateSeparator(date: String) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = date,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.outline
)
}
}
@Composable @Composable
fun ChatMessage( fun ChatMessage(
rumor: UnsignedEvent rumor: UnsignedEvent
@@ -211,12 +236,6 @@ fun ChatMessage(
Column( Column(
horizontalAlignment = if (isMine) Alignment.End else Alignment.Start horizontalAlignment = if (isMine) Alignment.End else Alignment.Start
) { ) {
Text(
text = rumor.createdAt().humanReadable(),
style = MaterialTheme.typography.labelSmall,
textAlign = if (isMine) TextAlign.End else TextAlign.Start,
)
Spacer(modifier = Modifier.size(4.dp))
Surface( Surface(
color = containerColor, color = containerColor,
contentColor = contentColor, contentColor = contentColor,

View File

@@ -179,6 +179,7 @@ fun HomeScreen(onOpenChat: (Long) -> Unit) {
picture = userProfile?.asRecord()?.picture, picture = userProfile?.asRecord()?.picture,
description = userProfile?.asRecord()?.displayName, description = userProfile?.asRecord()?.displayName,
shape = MaterialShapes.Cookie9Sided.toShape(), shape = MaterialShapes.Cookie9Sided.toShape(),
modifier = Modifier.fillMaxSize()
) )
} }
Spacer(modifier = Modifier.size(8.dp)) Spacer(modifier = Modifier.size(8.dp))

View File

@@ -42,8 +42,8 @@ import rust.nostr.sdk.TagKind
import rust.nostr.sdk.Timestamp import rust.nostr.sdk.Timestamp
import rust.nostr.sdk.UnsignedEvent import rust.nostr.sdk.UnsignedEvent
import rust.nostr.sdk.UnwrappedGift import rust.nostr.sdk.UnwrappedGift
import rust.nostr.sdk.giftWrapAsync
import rust.nostr.sdk.initLogger import rust.nostr.sdk.initLogger
import rust.nostr.sdk.makePrivateMsgAsync
import rust.nostr.sdk.nip17ExtractRelayList import rust.nostr.sdk.nip17ExtractRelayList
import kotlin.time.Duration import kotlin.time.Duration
@@ -551,7 +551,8 @@ class Nostr {
to: List<PublicKey>, to: List<PublicKey>,
content: String, content: String,
subject: String? = null, subject: String? = null,
replies: List<EventId> = emptyList() replies: List<EventId> = emptyList(),
onNewMessage: ((UnsignedEvent) -> Unit)? = null
) { ) {
try { try {
val currentUser = val currentUser =
@@ -578,20 +579,32 @@ class Nostr {
} }
} }
for (receiver in to.plus(currentUser)) { for (receiver in listOf(currentUser) + to) {
// Construct the gift wrap event // Construct the rumor event
val event = makePrivateMsgAsync( // NEVER SIGN this event with the current user signer
signer = signer, val rumor = EventBuilder
receiver = receiver, .privateMsgRumor(receiver = receiver, message = content)
message = content, .tags(tags)
rumorExtraTags = tags .build(currentUser)
) // Ensure the event ID is set
.ensureId()
println("Sending message to: ${receiver.toBech32()}") // Emit the rumor to the chat screen
if (receiver == currentUser) {
onNewMessage?.invoke(rumor)
}
// Construct the gift wrap event
val gift = giftWrapAsync(
signer = signer,
receiverPubkey = receiver,
rumor = rumor,
extraTags = tags
)
// Send the event to receiver's NIP-17 relays // Send the event to receiver's NIP-17 relays
client?.sendEvent( client?.sendEvent(
event = event, event = gift,
target = SendEventTarget.toNip17(), target = SendEventTarget.toNip17(),
ackPolicy = AckPolicy.none(), ackPolicy = AckPolicy.none(),
authenticationTimeout = Duration.parse("2s") authenticationTimeout = Duration.parse("2s")

View File

@@ -333,7 +333,12 @@ class NostrViewModel(
to = room.members.toList(), to = room.members.toList(),
content = message, content = message,
subject = room.subject, subject = room.subject,
replies = replies replies = replies,
onNewMessage = { event ->
viewModelScope.launch {
_newEvents.emit(event)
}
}
) )
} catch (e: Exception) { } catch (e: Exception) {
showError("Error: ${e.message}") showError("Error: ${e.message}")

View File

@@ -125,6 +125,27 @@ fun Timestamp.ago(): String {
} }
} }
fun Timestamp.formatAsGroupHeader(): String {
val timeZone = TimeZone.currentSystemDefault()
val inputInstant = Instant.fromEpochSeconds(this.asSecs().toLong())
val inputDate = inputInstant.toLocalDateTime(timeZone).date
val now = Clock.System.now()
val today = now.toLocalDateTime(timeZone).date
val yesterday = today.minus(1, DateTimeUnit.DAY)
return when (inputDate) {
today -> "Today"
yesterday -> "Yesterday"
else -> {
val day = inputDate.day.toString().padStart(2, '0')
val month = inputDate.month.number.toString().padStart(2, '0')
val year = inputDate.year.toString().takeLast(2)
"$day/$month/$year"
}
}
}
fun Timestamp.humanReadable(): String { fun Timestamp.humanReadable(): String {
val timeZone = TimeZone.currentSystemDefault() val timeZone = TimeZone.currentSystemDefault()
val inputInstant = Instant.fromEpochSeconds(this.asSecs().toLong()) val inputInstant = Instant.fromEpochSeconds(this.asSecs().toLong())