chore: merge the develop branch into master #1
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user