Merge pull request #1827 from vitorpamplona/claude/implement-nip-38-q8DOF

Enhance user status display with emoji support and metadata tags
This commit is contained in:
Vitor Pamplona
2026-03-13 13:32:41 -04:00
committed by GitHub
4 changed files with 94 additions and 11 deletions
@@ -59,9 +59,12 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.navigation.routes.routeForUser
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@@ -77,8 +80,13 @@ import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.tags.aTag.firstTaggedAddress
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.events.firstTaggedEvent
import com.vitorpamplona.quartz.nip01Core.tags.people.firstTaggedUserId
import com.vitorpamplona.quartz.nip30CustomEmoji.taggedEmojis
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
@@ -220,12 +228,24 @@ fun DisplayStatus(
accountViewModel: AccountViewModel,
nav: INav,
) {
val emojis =
remember(event) {
val emojiList = event.taggedEmojis()
if (emojiList.isEmpty()) {
persistentMapOf()
} else {
emojiList.associate { it.code to it.url }.toImmutableMap()
}
}
DisplayStatusInner(
event.content,
event.dTag(),
event.firstTaggedUrl()?.ifBlank { null },
event.firstTaggedAddress(),
event.firstTaggedEvent(),
event.firstTaggedUserId(),
emojis,
accountViewModel,
nav,
)
@@ -238,11 +258,13 @@ fun DisplayStatusInner(
url: String?,
nostrATag: Address?,
nostrETag: ETag?,
nostrPTag: String?,
emojis: ImmutableMap<String, String>,
accountViewModel: AccountViewModel,
nav: INav,
) {
when (type) {
"music" -> {
StatusEvent.MUSIC -> {
Icon(
imageVector = CustomHashTagIcons.Tunestr,
null,
@@ -254,13 +276,24 @@ fun DisplayStatusInner(
else -> {}
}
Text(
text = content,
fontSize = Font14SP,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (emojis.isNotEmpty()) {
CreateTextWithEmoji(
text = content,
emojis = emojis,
color = MaterialTheme.colorScheme.placeholderText,
fontSize = Font14SP,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} else {
Text(
text = content,
fontSize = Font14SP,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
if (url != null) {
val uri = LocalUriHandler.current
@@ -320,6 +353,23 @@ fun DisplayStatusInner(
}
}
}
} else if (nostrPTag != null) {
LoadUser(baseUserHex = nostrPTag, accountViewModel) { user ->
if (user != null) {
Spacer(modifier = StdHorzSpacer)
IconButton(
modifier = Size15Modifier,
onClick = { nav.nav(routeForUser(nostrPTag)) },
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colorScheme.lessImportantLink,
)
}
}
}
}
}
@@ -31,7 +31,8 @@ class UserStatusAction {
suspend fun create(
newStatus: String,
signer: NostrSigner,
): StatusEvent = StatusEvent.create(newStatus, "general", expiration = null, signer)
type: String = StatusEvent.GENERAL,
): StatusEvent = StatusEvent.create(newStatus, type, signer = signer)
suspend fun update(
oldStatus: AddressableNote,
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.UserDependencies
import com.vitorpamplona.quartz.nip40Expiration.expiration
import com.vitorpamplona.quartz.nip40Expiration.isExpired
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -43,6 +44,9 @@ class UserStatusCache : UserDependencies {
// if it's already there, quick exit
if (statuses.value.contains(note) || note.event?.content.isNullOrBlank()) return
// don't add expired statuses
if (note.event?.isExpired() == true) return
statuses.update {
(it + note).sortedWith(sortModel).toImmutableList()
}
@@ -56,4 +60,15 @@ class UserStatusCache : UserDependencies {
(it - deleteNote).toImmutableList()
}
}
fun removeExpired() {
statuses.update { list ->
val filtered = list.filter { it.event?.isExpired() != true }
if (filtered.size != list.size) {
filtered.toImmutableList()
} else {
list
}
}
}
}
@@ -25,6 +25,10 @@ import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.firstTagValue
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
@@ -41,10 +45,18 @@ class StatusEvent(
companion object {
const val KIND = 30315
const val GENERAL = "general"
const val MUSIC = "music"
suspend fun create(
msg: String,
type: String,
expiration: Long?,
type: String = GENERAL,
expiration: Long? = null,
url: String? = null,
profileId: HexKey? = null,
eventId: HexKey? = null,
addressableId: String? = null,
emojiTags: List<EmojiUrlTag>? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): StatusEvent {
@@ -52,6 +64,11 @@ class StatusEvent(
tags.add(arrayOf("d", type))
expiration?.let { tags.add(arrayOf("expiration", it.toString())) }
url?.let { tags.add(arrayOf("r", it)) }
profileId?.let { tags.add(PTag.assemble(it, null)) }
eventId?.let { tags.add(ETag.assemble(it, null, null)) }
addressableId?.let { tags.add(ATag.assemble(it, null)) }
emojiTags?.forEach { tags.add(it.toTagArray()) }
return signer.sign(createdAt, KIND, tags.toTypedArray(), msg)
}