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.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_arrow_back
@@ -51,7 +50,7 @@ import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.UnsignedEvent
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.humanReadable
import su.reya.coop.formatAsGroupHeader
import su.reya.coop.roomId
import su.reya.coop.shared.Avatar
import su.reya.coop.shared.displayNameFlow
@@ -73,6 +72,9 @@ fun ChatScreen(
var loading by remember { mutableStateOf(true) }
val messages = remember { mutableStateListOf<UnsignedEvent>() }
val groupedMessages = remember(messages.toList()) {
messages.groupBy { it.createdAt().formatAsGroupHeader() }
}
fun setLoading(value: Boolean) {
loading = value
@@ -96,10 +98,12 @@ fun ChatScreen(
// Handle new messages
viewModel.newEvents.collect { event ->
if (event.roomId() == id) {
if (event.id() !in messages.map { it.id() }) {
messages.add(0, event)
}
}
}
}
Scaffold(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
@@ -163,9 +167,14 @@ fun ChatScreen(
contentPadding = PaddingValues(16.dp),
reverseLayout = true
) {
items(messages.toList(), key = { it.id()?.toBech32()!! }) { event ->
groupedMessages.forEach { (dateHeader, messagesInGroup) ->
items(messagesInGroup, key = { it.id()?.toBech32()!! }) { event ->
ChatMessage(event)
}
item {
DateSeparator(dateHeader)
}
}
}
ChatInput(
value = text,
@@ -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
fun ChatMessage(
rumor: UnsignedEvent
@@ -211,12 +236,6 @@ fun ChatMessage(
Column(
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(
color = containerColor,
contentColor = contentColor,

View File

@@ -179,6 +179,7 @@ fun HomeScreen(onOpenChat: (Long) -> Unit) {
picture = userProfile?.asRecord()?.picture,
description = userProfile?.asRecord()?.displayName,
shape = MaterialShapes.Cookie9Sided.toShape(),
modifier = Modifier.fillMaxSize()
)
}
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.UnsignedEvent
import rust.nostr.sdk.UnwrappedGift
import rust.nostr.sdk.giftWrapAsync
import rust.nostr.sdk.initLogger
import rust.nostr.sdk.makePrivateMsgAsync
import rust.nostr.sdk.nip17ExtractRelayList
import kotlin.time.Duration
@@ -551,7 +551,8 @@ class Nostr {
to: List<PublicKey>,
content: String,
subject: String? = null,
replies: List<EventId> = emptyList()
replies: List<EventId> = emptyList(),
onNewMessage: ((UnsignedEvent) -> Unit)? = null
) {
try {
val currentUser =
@@ -578,20 +579,32 @@ class Nostr {
}
}
for (receiver in to.plus(currentUser)) {
// Construct the gift wrap event
val event = makePrivateMsgAsync(
signer = signer,
receiver = receiver,
message = content,
rumorExtraTags = tags
)
for (receiver in listOf(currentUser) + to) {
// Construct the rumor event
// NEVER SIGN this event with the current user signer
val rumor = EventBuilder
.privateMsgRumor(receiver = receiver, message = content)
.tags(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
client?.sendEvent(
event = event,
event = gift,
target = SendEventTarget.toNip17(),
ackPolicy = AckPolicy.none(),
authenticationTimeout = Duration.parse("2s")

View File

@@ -333,7 +333,12 @@ class NostrViewModel(
to = room.members.toList(),
content = message,
subject = room.subject,
replies = replies
replies = replies,
onNewMessage = { event ->
viewModelScope.launch {
_newEvents.emit(event)
}
}
)
} catch (e: Exception) {
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 {
val timeZone = TimeZone.currentSystemDefault()
val inputInstant = Instant.fromEpochSeconds(this.asSecs().toLong())