optimistic update message on send
This commit is contained in:
@@ -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,6 +72,9 @@ 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,
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
Reference in New Issue
Block a user