mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
fix(sonar-s1871): match ChannelMessage/Metadata explicitly, not via IsInPublicChatChannel
fix(sonar-s1871): merge PayInvoice/Nwc error arms via IErrorResponseLike fix(sonar-s1871): drop redundant is CommentEvent branch in ThreadFeedView fix(sonar-s1871): merge NPub/NProfile route arms via IPubKeyEntity fix(sonar-s1871): merge Error/Notice debug-message arms via IRelayDebugMessageText fix(sonar-s1871): merge LiveActivities/MeetingRoom arms via LiveStreamLike fix(sonar-s1871): merge channel-message/metadata arms via IsInPublicChatChannel fix(sonar-s1871): collapse channel-draft reply handling via BaseThreadedEvent fix(sonar-s1871): drop redundant is CommentEvent arm in NoteCompose fix(sonar-s1871): merge Failed/Error arms in NIP-05 badge fix(sonar-s1871): merge Verifying/NotStarted arms in NIP-05 badge fix(sonar-s1871): merge note-backed RenderOption arms via NoteBackedName fix(sonar-s1871): drop redundant is PrivateDmEvent arm in RouteMaker fix(sonar-s1871): merge GiftWrap/SealedRumor arms in RouteMaker via HasInnerEvent fix(sonar-s1871): merge Connecting/Connected arms in CallSession state collector fix(sonar-s1871): collapse playback-state when to if/else in CurrentPlayPositionCacher fix(sonar-s1871): drop redundant is CommentEvent arm in sendPublicReply fix(sonar-s1871): merge addressable-filter arms via AddressableTopFilter fix(sonar-s1871): merge repost branches in LocalCache via BaseRepostEvent fix(sonar-s1871): merge badge-set branches in LocalCache referenced-notes when
This commit is contained in:
@@ -84,6 +84,10 @@ val DefaultSignerPermissions =
|
||||
sealed class TopFilter(
|
||||
val code: String,
|
||||
) {
|
||||
interface AddressableTopFilter {
|
||||
val address: Address
|
||||
}
|
||||
|
||||
@Serializable
|
||||
object Global : TopFilter(" Global ")
|
||||
|
||||
@@ -104,18 +108,21 @@ sealed class TopFilter(
|
||||
|
||||
@Serializable
|
||||
class PeopleList(
|
||||
val address: Address,
|
||||
) : TopFilter(address.toValue())
|
||||
override val address: Address,
|
||||
) : TopFilter(address.toValue()),
|
||||
AddressableTopFilter
|
||||
|
||||
@Serializable
|
||||
class MuteList(
|
||||
val address: Address,
|
||||
) : TopFilter(address.toValue())
|
||||
override val address: Address,
|
||||
) : TopFilter(address.toValue()),
|
||||
AddressableTopFilter
|
||||
|
||||
@Serializable
|
||||
class Community(
|
||||
val address: Address,
|
||||
) : TopFilter("Community/${address.toValue()}")
|
||||
override val address: Address,
|
||||
) : TopFilter("Community/${address.toValue()}"),
|
||||
AddressableTopFilter
|
||||
|
||||
@Serializable
|
||||
class Hashtag(
|
||||
|
||||
@@ -116,6 +116,7 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.BaseRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
@@ -1030,14 +1031,9 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
}
|
||||
|
||||
is AcceptedBadgeSetEvent -> {
|
||||
event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.badgeAwardDefinitions().map { getOrCreateAddressableNote(it) }
|
||||
}
|
||||
|
||||
is ProfileBadgesEvent -> {
|
||||
event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.badgeAwardDefinitions().map { getOrCreateAddressableNote(it) }
|
||||
is AcceptedBadgeSetEvent, is ProfileBadgesEvent -> {
|
||||
event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
}
|
||||
|
||||
is BadgeAwardEvent -> {
|
||||
@@ -1048,17 +1044,11 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||
}
|
||||
|
||||
is RepostEvent -> {
|
||||
is RepostEvent, is GenericRepostEvent -> {
|
||||
val repost = event as BaseRepostEvent
|
||||
listOfNotNull(
|
||||
event.boostedEventId()?.let { checkGetOrCreateNote(it) },
|
||||
event.boostedAddress()?.let { getOrCreateAddressableNote(it) },
|
||||
)
|
||||
}
|
||||
|
||||
is GenericRepostEvent -> {
|
||||
listOfNotNull(
|
||||
event.boostedEventId()?.let { checkGetOrCreateNote(it) },
|
||||
event.boostedAddress()?.let { getOrCreateAddressableNote(it) },
|
||||
repost.boostedEventId()?.let { checkGetOrCreateNote(it) },
|
||||
repost.boostedAddress()?.let { getOrCreateAddressableNote(it) },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+3
-30
@@ -96,38 +96,11 @@ class FeedTopNavFilterState(
|
||||
AllFollowsFeedFlow(allFollows, followsRelays, blockedRelays, proxyRelays)
|
||||
}
|
||||
|
||||
is TopFilter.Community -> {
|
||||
is TopFilter.Community, is TopFilter.PeopleList, is TopFilter.MuteList -> {
|
||||
val addressFilter = listName as TopFilter.AddressableTopFilter
|
||||
NoteFeedFlow(
|
||||
LocalCache
|
||||
.getOrCreateAddressableNote(listName.address)
|
||||
.flow()
|
||||
.metadata.stateFlow,
|
||||
signer,
|
||||
followsRelays,
|
||||
blockedRelays,
|
||||
proxyRelays,
|
||||
caches,
|
||||
)
|
||||
}
|
||||
|
||||
is TopFilter.PeopleList -> {
|
||||
NoteFeedFlow(
|
||||
LocalCache
|
||||
.getOrCreateAddressableNote(listName.address)
|
||||
.flow()
|
||||
.metadata.stateFlow,
|
||||
signer,
|
||||
followsRelays,
|
||||
blockedRelays,
|
||||
proxyRelays,
|
||||
caches,
|
||||
)
|
||||
}
|
||||
|
||||
is TopFilter.MuteList -> {
|
||||
NoteFeedFlow(
|
||||
LocalCache
|
||||
.getOrCreateAddressableNote(listName.address)
|
||||
.getOrCreateAddressableNote(addressFilter.address)
|
||||
.flow()
|
||||
.metadata.stateFlow,
|
||||
signer,
|
||||
|
||||
+2
-8
@@ -206,15 +206,9 @@ class NotificationReplyReceiver : BroadcastReceiver() {
|
||||
)
|
||||
}
|
||||
|
||||
is CommentEvent -> {
|
||||
CommentEvent.replyBuilder(
|
||||
msg = replyText,
|
||||
replyingTo = EventHintBundle(targetEvent),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Non-threaded events (e.g. long-form articles) use NIP-22 comments.
|
||||
// NIP-22 CommentEvent and other non-threaded events (e.g. long-form articles)
|
||||
// both reply via NIP-22 comments.
|
||||
CommentEvent.replyBuilder(
|
||||
msg = replyText,
|
||||
replyingTo = EventHintBundle(targetEvent),
|
||||
|
||||
+16
-27
@@ -55,36 +55,25 @@ class CurrentPlayPositionCacher(
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
currentUrl?.let { uri ->
|
||||
when (playbackState) {
|
||||
Player.STATE_IDLE -> {
|
||||
// only saves if it wqs playing
|
||||
if (!isLiveStreaming && abs(player.currentPosition) > 1) {
|
||||
cache.add(uri, player.currentPosition)
|
||||
}
|
||||
}
|
||||
|
||||
Player.STATE_READY -> {
|
||||
if (!isLiveStreaming) {
|
||||
cache.get(uri)?.let { lastPosition ->
|
||||
// Restore the saved position only if it's meaningfully far from
|
||||
// the player's current position. Position values are in
|
||||
// milliseconds, so the previous `5 * 60` constant was a 300 ms
|
||||
// threshold — small enough to trigger a seek (and an extra buffer
|
||||
// flush right at playback start) for almost any saved position.
|
||||
// 5 s gives the user a perceptible "resumed where I left off"
|
||||
// without forcing a re-seek for trivially short clips.
|
||||
if (abs(player.currentPosition - lastPosition) > 5_000) {
|
||||
player.seekTo(lastPosition)
|
||||
}
|
||||
if (playbackState == Player.STATE_READY) {
|
||||
if (!isLiveStreaming) {
|
||||
cache.get(uri)?.let { lastPosition ->
|
||||
// Restore the saved position only if it's meaningfully far from
|
||||
// the player's current position. Position values are in
|
||||
// milliseconds, so the previous `5 * 60` constant was a 300 ms
|
||||
// threshold — small enough to trigger a seek (and an extra buffer
|
||||
// flush right at playback start) for almost any saved position.
|
||||
// 5 s gives the user a perceptible "resumed where I left off"
|
||||
// without forcing a re-seek for trivially short clips.
|
||||
if (abs(player.currentPosition - lastPosition) > 5_000) {
|
||||
player.seekTo(lastPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
// only saves if it wqs playing
|
||||
if (!isLiveStreaming && abs(player.currentPosition) > 1) {
|
||||
cache.add(uri, player.currentPosition)
|
||||
}
|
||||
} else {
|
||||
// STATE_IDLE / STATE_BUFFERING / STATE_ENDED: only saves if it was playing.
|
||||
if (!isLiveStreaming && abs(player.currentPosition) > 1) {
|
||||
cache.add(uri, player.currentPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,17 +197,10 @@ class CallSession(
|
||||
ensureForegroundService()
|
||||
}
|
||||
|
||||
is CallState.Connecting -> {
|
||||
audioManager.stopRinging()
|
||||
audioManager.stopRingbackTone()
|
||||
withContext(Dispatchers.IO) { audioManager.switchToCallAudioMode() }
|
||||
audioManager.acquireProximityWakeLock()
|
||||
CallNotifier.cancelIncomingCall(context)
|
||||
updateForegroundServiceNotification()
|
||||
registerNetworkCallback()
|
||||
}
|
||||
|
||||
is CallState.Connected -> {
|
||||
is CallState.Connecting, is CallState.Connected -> {
|
||||
// Same setup on both entry paths: Offering -> Connecting -> Connected,
|
||||
// and direct -> Connected (e.g. answered-elsewhere). All ops below are
|
||||
// idempotent so re-running on Connecting -> Connected is safe.
|
||||
audioManager.stopRinging()
|
||||
audioManager.stopRingbackTone()
|
||||
withContext(Dispatchers.IO) { audioManager.switchToCallAudioMode() }
|
||||
|
||||
+6
-19
@@ -36,7 +36,6 @@ import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||
@@ -50,6 +49,7 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.HasInnerEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
@@ -166,14 +166,9 @@ fun routeForInner(
|
||||
Route.CalendarEventDetail(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag())
|
||||
}
|
||||
|
||||
is GiftWrapEvent -> {
|
||||
noteEvent.innerEventId?.let {
|
||||
routeFor(LocalCache.getOrCreateNote(it), loggedIn)
|
||||
}
|
||||
}
|
||||
|
||||
is SealedRumorEvent -> {
|
||||
noteEvent.innerEventId?.let {
|
||||
is GiftWrapEvent, is SealedRumorEvent -> {
|
||||
val wrap = noteEvent as HasInnerEvent
|
||||
wrap.innerEventId?.let {
|
||||
routeFor(LocalCache.getOrCreateNote(it), loggedIn)
|
||||
}
|
||||
}
|
||||
@@ -309,17 +304,9 @@ fun routeReplyTo(
|
||||
Route.NewShortNote(baseReplyTo = note.idHex)
|
||||
}
|
||||
|
||||
is PrivateDmEvent -> {
|
||||
routeToMessage(
|
||||
room = noteEvent.chatroomKey(account.userProfile().pubkeyHex),
|
||||
draftMessage = null,
|
||||
replyId = noteEvent.id,
|
||||
draftId = null,
|
||||
account = account,
|
||||
)
|
||||
}
|
||||
|
||||
is ChatroomKeyable -> {
|
||||
// Covers PrivateDmEvent (NIP-04) and BaseDMGroupEvent (NIP-17) — both
|
||||
// implement ChatroomKeyable with identical routing semantics here.
|
||||
routeToMessage(
|
||||
room = noteEvent.chatroomKey(account.userProfile().pubkeyHex),
|
||||
draftMessage = null,
|
||||
|
||||
+4
-14
@@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.screen.GeoHashName
|
||||
import com.vitorpamplona.amethyst.ui.screen.HashtagName
|
||||
import com.vitorpamplona.amethyst.ui.screen.InterestSetName
|
||||
import com.vitorpamplona.amethyst.ui.screen.Name
|
||||
import com.vitorpamplona.amethyst.ui.screen.NoteBackedName
|
||||
import com.vitorpamplona.amethyst.ui.screen.PeopleListName
|
||||
import com.vitorpamplona.amethyst.ui.screen.RelayName
|
||||
import com.vitorpamplona.amethyst.ui.screen.ResourceName
|
||||
@@ -297,20 +298,9 @@ fun RenderOption(
|
||||
// Note-backed names: subscribe to the note so the displayed title updates as
|
||||
// the corresponding event arrives from relays. The displayed string itself is
|
||||
// produced by Name.name(), which already has the right precedence rules.
|
||||
is PeopleListName -> {
|
||||
val noteState by observeNote(option.note, accountViewModel)
|
||||
val name = remember(noteState) { option.name(context) }
|
||||
Text(text = name, fontSize = Font14SP, color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
|
||||
is CommunityName -> {
|
||||
val noteState by observeNote(option.note, accountViewModel)
|
||||
val name = remember(noteState) { option.name(context) }
|
||||
Text(text = name, fontSize = Font14SP, color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
|
||||
is FavoriteAlgoFeedName -> {
|
||||
val noteState by observeNote(option.note, accountViewModel)
|
||||
is PeopleListName, is CommunityName, is FavoriteAlgoFeedName -> {
|
||||
val backed = option as NoteBackedName
|
||||
val noteState by observeNote(backed.note, accountViewModel)
|
||||
val name = remember(noteState) { option.name(context) }
|
||||
Text(text = name, fontSize = Font14SP, color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
|
||||
+6
-20
@@ -465,16 +465,9 @@ fun RenderNIP05VerifiedSymbol(
|
||||
) {
|
||||
CrossfadeIfEnabled(targetState = state, accountViewModel = accountViewModel) {
|
||||
when (it) {
|
||||
is Nip05VerifState.Verifying -> {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Downloading,
|
||||
contentDescription = stringRes(id = R.string.nip05_checking),
|
||||
modifier = modifier,
|
||||
tint = Color.Yellow,
|
||||
)
|
||||
}
|
||||
|
||||
is Nip05VerifState.NotStarted -> {
|
||||
is Nip05VerifState.Verifying, is Nip05VerifState.NotStarted -> {
|
||||
// Treat "not started" and "in flight" as a single loading visual so the
|
||||
// badge doesn't blank-flash before the first check fires.
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Downloading,
|
||||
contentDescription = stringRes(id = R.string.nip05_checking),
|
||||
@@ -492,16 +485,9 @@ fun RenderNIP05VerifiedSymbol(
|
||||
)
|
||||
}
|
||||
|
||||
is Nip05VerifState.Failed -> {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Report,
|
||||
contentDescription = stringRes(id = R.string.nip05_failed),
|
||||
modifier = modifier,
|
||||
tint = Color.Red,
|
||||
)
|
||||
}
|
||||
|
||||
is Nip05VerifState.Error -> {
|
||||
is Nip05VerifState.Failed, is Nip05VerifState.Error -> {
|
||||
// Failed (pubkey mismatch) and Error (HTTP/network) carry distinct retry
|
||||
// timing but look the same to the user — both mean "badge isn't showing".
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Report,
|
||||
contentDescription = stringRes(id = R.string.nip05_failed),
|
||||
|
||||
@@ -238,7 +238,6 @@ import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
@@ -1271,20 +1270,6 @@ private fun RenderNoteRow(
|
||||
)
|
||||
}
|
||||
|
||||
is CommentEvent -> {
|
||||
RenderTextEvent(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
unPackReply,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
is NIP90ContentDiscoveryResponseEvent -> {
|
||||
RenderNIP90ContentDiscoveryResponse(
|
||||
baseNote,
|
||||
|
||||
@@ -326,6 +326,15 @@ sealed class Name {
|
||||
open fun name(context: Context) = name()
|
||||
}
|
||||
|
||||
/**
|
||||
* Names whose displayed title is derived from a Nostr note (community
|
||||
* definition, people list, DVM algo feed). UI layers can subscribe to
|
||||
* the underlying `note` so the title updates as the event arrives.
|
||||
*/
|
||||
interface NoteBackedName {
|
||||
val note: AddressableNote
|
||||
}
|
||||
|
||||
@Stable
|
||||
class GeoHashName(
|
||||
val geoHashTag: String,
|
||||
@@ -358,8 +367,9 @@ class ResourceName(
|
||||
|
||||
@Stable
|
||||
class PeopleListName(
|
||||
val note: AddressableNote,
|
||||
) : Name() {
|
||||
override val note: AddressableNote,
|
||||
) : Name(),
|
||||
NoteBackedName {
|
||||
override fun name(): String {
|
||||
val noteEvent = note.event
|
||||
return if (noteEvent is PeopleListEvent) {
|
||||
@@ -374,8 +384,9 @@ class PeopleListName(
|
||||
|
||||
@Stable
|
||||
class CommunityName(
|
||||
val note: AddressableNote,
|
||||
) : Name() {
|
||||
override val note: AddressableNote,
|
||||
) : Name(),
|
||||
NoteBackedName {
|
||||
override fun name(): String {
|
||||
val definition = note.event as? CommunityDefinitionEvent
|
||||
val label = definition?.name()?.ifBlank { null } ?: note.dTag()
|
||||
@@ -385,8 +396,9 @@ class CommunityName(
|
||||
|
||||
@Stable
|
||||
class FavoriteAlgoFeedName(
|
||||
val note: AddressableNote,
|
||||
) : Name() {
|
||||
override val note: AddressableNote,
|
||||
) : Name(),
|
||||
NoteBackedName {
|
||||
override fun name(): String =
|
||||
(note.event as? AppDefinitionEvent)?.appMetaData()?.name?.takeIf { it.isNotBlank() }
|
||||
?: note.dTag()
|
||||
|
||||
+8
-10
@@ -264,16 +264,14 @@ open class ChannelNewMessageViewModel :
|
||||
wantsForwardZapTo = true
|
||||
}
|
||||
|
||||
if (draftEvent as? ChannelMessageEvent != null) {
|
||||
val replyId = draftEvent.reply()?.eventId
|
||||
if (replyId != null) {
|
||||
replyTo.value = accountViewModel.checkGetOrCreateNote(replyId)
|
||||
}
|
||||
} else if (draftEvent as? LiveActivitiesChatMessageEvent != null) {
|
||||
val replyId = draftEvent.reply()?.eventId
|
||||
if (replyId != null) {
|
||||
replyTo.value = accountViewModel.checkGetOrCreateNote(replyId)
|
||||
}
|
||||
// Both event kinds extend BaseThreadedEvent and share `reply()`. Cast through
|
||||
// both candidates so the elvis result lands on the common supertype, then
|
||||
// load the addressed reply note in a single block.
|
||||
val threadedDraft =
|
||||
(draftEvent as? ChannelMessageEvent)
|
||||
?: (draftEvent as? LiveActivitiesChatMessageEvent)
|
||||
threadedDraft?.reply()?.eventId?.let { replyId ->
|
||||
replyTo.value = accountViewModel.checkGetOrCreateNote(replyId)
|
||||
}
|
||||
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
|
||||
+5
-9
@@ -133,15 +133,11 @@ private fun ChatroomEntry(
|
||||
|
||||
val baseNoteEvent = lastMessage.event
|
||||
when (baseNoteEvent) {
|
||||
is ChannelMessageEvent -> {
|
||||
baseNoteEvent.channelId()?.let {
|
||||
LoadPublicChatChannel(it, accountViewModel) { channel ->
|
||||
ChannelRoomCompose(lastMessage, channel, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ChannelMetadataEvent -> {
|
||||
// ChannelMessageEvent and ChannelMetadataEvent both expose channelId() via
|
||||
// IsInPublicChatChannel and render identically here. Matched explicitly (rather
|
||||
// than on the IsInPublicChatChannel interface) so the channel-admin events
|
||||
// ChannelHideMessageEvent/ChannelMuteUserEvent keep falling through to `else`.
|
||||
is ChannelMessageEvent, is ChannelMetadataEvent -> {
|
||||
baseNoteEvent.channelId()?.let {
|
||||
LoadPublicChatChannel(it, accountViewModel) { channel ->
|
||||
ChannelRoomCompose(lastMessage, channel, accountViewModel, nav)
|
||||
|
||||
+7
-18
@@ -69,9 +69,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutabl
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.LiveStreamLike
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -101,23 +101,12 @@ fun RenderLiveActivityThumb(
|
||||
) {
|
||||
val card by observeNoteAndMap(baseNote, accountViewModel) {
|
||||
when (val noteEvent = it.event) {
|
||||
is LiveActivitiesEvent -> {
|
||||
is LiveStreamLike -> {
|
||||
// Both LiveActivitiesEvent (kind 30311) and MeetingRoomEvent (kind 30312)
|
||||
// expose the same NIP-53 streaming tags via LiveStreamLike. address()/dTag()
|
||||
// come from the shared BaseAddressableEvent parent.
|
||||
LiveActivityCard(
|
||||
id = noteEvent.address(),
|
||||
name = noteEvent.dTag(),
|
||||
cover = noteEvent.image()?.ifBlank { null },
|
||||
media = noteEvent.streaming(),
|
||||
subject = noteEvent.title()?.ifBlank { null },
|
||||
content = noteEvent.summary(),
|
||||
participants = noteEvent.participants().toImmutableList(),
|
||||
status = noteEvent.status(),
|
||||
starts = noteEvent.starts(),
|
||||
)
|
||||
}
|
||||
|
||||
is MeetingRoomEvent -> {
|
||||
LiveActivityCard(
|
||||
id = noteEvent.address(),
|
||||
id = (noteEvent as BaseAddressableEvent).address(),
|
||||
name = noteEvent.dTag(),
|
||||
cover = noteEvent.image()?.ifBlank { null },
|
||||
media = noteEvent.streaming(),
|
||||
|
||||
+4
-11
@@ -136,6 +136,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.ErrorDebugMessage
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.IRelayDebugMessage
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.IRelayDebugMessageText
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.NoticeDebugMessage
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.SpamDebugMessage
|
||||
@@ -1016,17 +1017,9 @@ private fun RenderDebugMessage(msg: IRelayDebugMessage) {
|
||||
}
|
||||
|
||||
when (msg) {
|
||||
is ErrorDebugMessage -> {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = msg.message,
|
||||
style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NoticeDebugMessage -> {
|
||||
is IRelayDebugMessageText -> {
|
||||
// ErrorDebugMessage and NoticeDebugMessage both carry plain-text bodies and
|
||||
// render identically here; the label/colour above already distinguishes them.
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = msg.message,
|
||||
|
||||
+4
-6
@@ -50,6 +50,7 @@ import com.vitorpamplona.quartz.nip05DnsIdentifiers.INip05Client
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Id
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.IPubKeyEntity
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
|
||||
@@ -191,12 +192,9 @@ class SearchBarViewModel(
|
||||
.getOrNull()
|
||||
?: return@mapLatest null
|
||||
when (parsed) {
|
||||
is NPub -> {
|
||||
LocalCache.consume(parsed)
|
||||
Route.Profile(parsed.hex)
|
||||
}
|
||||
|
||||
is NProfile -> {
|
||||
// Both NPub (npub1…) and NProfile (nprofile1…, npub + relay hints)
|
||||
// resolve to the same profile route by hex pubkey.
|
||||
is IPubKeyEntity -> {
|
||||
LocalCache.consume(parsed)
|
||||
Route.Profile(parsed.hex)
|
||||
}
|
||||
|
||||
-13
@@ -248,7 +248,6 @@ import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEven
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
@@ -766,18 +765,6 @@ private fun FullBleedNoteCompose(
|
||||
RenderCalendarDateSlotEvent(baseNote, accountViewModel, nav)
|
||||
} else if (noteEvent is GoalEvent) {
|
||||
RenderGoal(baseNote, accountViewModel, nav)
|
||||
} else if (noteEvent is CommentEvent) {
|
||||
RenderTextEvent(
|
||||
baseNote,
|
||||
false,
|
||||
canPreview,
|
||||
quotesLeft = 3,
|
||||
unPackReply = ReplyRenderType.NONE,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
} else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) {
|
||||
RenderRepost(baseNote, quotesLeft = 3, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is RelayDiscoveryEvent) {
|
||||
|
||||
+6
-10
@@ -31,13 +31,13 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.IErrorResponseLike
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
@@ -567,17 +567,13 @@ class WalletViewModel : ViewModel() {
|
||||
fetchBalance()
|
||||
}
|
||||
|
||||
is PayInvoiceErrorResponse -> {
|
||||
is IErrorResponseLike -> {
|
||||
// Both PayInvoiceErrorResponse (method-specific, kept for
|
||||
// back-compat) and NwcErrorResponse (generic) reduce to the
|
||||
// same user-visible "payment failed" message.
|
||||
_sendState.value =
|
||||
SendState.Error(
|
||||
response.error?.message ?: PAYMENT_FAILED,
|
||||
)
|
||||
}
|
||||
|
||||
is NwcErrorResponse -> {
|
||||
_sendState.value =
|
||||
SendState.Error(
|
||||
response.error?.message ?: PAYMENT_FAILED,
|
||||
response.errorMessage() ?: PAYMENT_FAILED,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+9
-4
@@ -90,6 +90,11 @@ sealed interface IRelayDebugMessage {
|
||||
val time: Long
|
||||
}
|
||||
|
||||
/** Debug messages that carry a single plain-text payload (notices, errors). */
|
||||
sealed interface IRelayDebugMessageText : IRelayDebugMessage {
|
||||
val message: String
|
||||
}
|
||||
|
||||
class SpamDebugMessage(
|
||||
val link1: String,
|
||||
val link2: String,
|
||||
@@ -97,11 +102,11 @@ class SpamDebugMessage(
|
||||
) : IRelayDebugMessage
|
||||
|
||||
class NoticeDebugMessage(
|
||||
val message: String,
|
||||
override val message: String,
|
||||
override val time: Long = TimeUtils.now(),
|
||||
) : IRelayDebugMessage
|
||||
) : IRelayDebugMessageText
|
||||
|
||||
class ErrorDebugMessage(
|
||||
val message: String,
|
||||
override val message: String,
|
||||
override val time: Long = TimeUtils.now(),
|
||||
) : IRelayDebugMessage
|
||||
) : IRelayDebugMessageText
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip18Reposts
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
interface BaseRepostEvent {
|
||||
fun boostedEventId(): HexKey?
|
||||
|
||||
fun boostedAddress(): Address?
|
||||
}
|
||||
+4
-3
@@ -53,7 +53,8 @@ class GenericRepostEvent(
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
PubKeyHintProvider,
|
||||
AddressHintProvider {
|
||||
AddressHintProvider,
|
||||
BaseRepostEvent {
|
||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
|
||||
@@ -70,9 +71,9 @@ class GenericRepostEvent(
|
||||
|
||||
fun boostedATag() = tags.lastNotNullOfOrNull(ATag::parse)
|
||||
|
||||
fun boostedAddress() = tags.lastNotNullOfOrNull(ATag::parseAddress)
|
||||
override fun boostedAddress() = tags.lastNotNullOfOrNull(ATag::parseAddress)
|
||||
|
||||
fun boostedEventId() = tags.lastNotNullOfOrNull(ETag::parseId)
|
||||
override fun boostedEventId() = tags.lastNotNullOfOrNull(ETag::parseId)
|
||||
|
||||
fun boostedAddressIds() = tags.lastNotNullOfOrNull(ATag::parseAddressId)
|
||||
|
||||
|
||||
@@ -53,7 +53,8 @@ class RepostEvent(
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
PubKeyHintProvider,
|
||||
AddressHintProvider {
|
||||
AddressHintProvider,
|
||||
BaseRepostEvent {
|
||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
|
||||
@@ -70,9 +71,9 @@ class RepostEvent(
|
||||
|
||||
fun boostedATag() = tags.lastNotNullOfOrNull(ATag::parse)
|
||||
|
||||
fun boostedAddress() = tags.lastNotNullOfOrNull(ATag::parseAddress)
|
||||
override fun boostedAddress() = tags.lastNotNullOfOrNull(ATag::parseAddress)
|
||||
|
||||
fun boostedEventId() = tags.lastNotNullOfOrNull(ETag::parseId)
|
||||
override fun boostedEventId() = tags.lastNotNullOfOrNull(ETag::parseId)
|
||||
|
||||
fun boostedAddressIds() = tags.lastNotNullOfOrNull(ATag::parseAddressId)
|
||||
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip19Bech32.entities
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
/**
|
||||
* NIP-19 bech32 entities that encode a single public key — NPub (npub1…)
|
||||
* and NProfile (nprofile1…, npub + relay hints). Lets routing and lookup
|
||||
* code resolve to a profile without branching on the concrete entity.
|
||||
*/
|
||||
interface IPubKeyEntity : Entity {
|
||||
val hex: HexKey
|
||||
}
|
||||
+3
-2
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip19Bech32.entities
|
||||
import addHex
|
||||
import addStringIfNotNull
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip19Bech32.TlvTypes
|
||||
@@ -34,9 +35,9 @@ import com.vitorpamplona.quartz.nip19Bech32.toNProfile
|
||||
|
||||
@Immutable
|
||||
data class NProfile(
|
||||
val hex: String,
|
||||
override val hex: HexKey,
|
||||
val relay: List<NormalizedRelayUrl>,
|
||||
) : Entity {
|
||||
) : IPubKeyEntity {
|
||||
companion object {
|
||||
fun parse(bytes: ByteArray): NProfile? {
|
||||
if (bytes.isEmpty()) return null
|
||||
|
||||
@@ -28,8 +28,8 @@ import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
|
||||
@Immutable
|
||||
data class NPub(
|
||||
val hex: String,
|
||||
) : Entity {
|
||||
override val hex: HexKey,
|
||||
) : IPubKeyEntity {
|
||||
companion object {
|
||||
fun parse(bytes: ByteArray): NPub? {
|
||||
if (bytes.isEmpty()) return null
|
||||
|
||||
+17
-2
@@ -27,11 +27,23 @@ abstract class Response(
|
||||
val resultType: String,
|
||||
) : OptimizedSerializable
|
||||
|
||||
/**
|
||||
* NIP-47 responses that carry a human-readable error message. Lets UI
|
||||
* collapse generic NwcErrorResponse and method-specific *ErrorResponse
|
||||
* handling into one branch.
|
||||
*/
|
||||
interface IErrorResponseLike {
|
||||
fun errorMessage(): String?
|
||||
}
|
||||
|
||||
// Generic error response for any method
|
||||
class NwcErrorResponse(
|
||||
resultType: String,
|
||||
val error: NwcError? = null,
|
||||
) : Response(resultType)
|
||||
) : Response(resultType),
|
||||
IErrorResponseLike {
|
||||
override fun errorMessage() = error?.message
|
||||
}
|
||||
|
||||
// pay_invoice success response
|
||||
class PayInvoiceSuccessResponse(
|
||||
@@ -46,11 +58,14 @@ class PayInvoiceSuccessResponse(
|
||||
// pay_invoice error response (kept for backward compatibility)
|
||||
class PayInvoiceErrorResponse(
|
||||
val error: PayInvoiceErrorParams? = null,
|
||||
) : Response(NwcMethod.PAY_INVOICE) {
|
||||
) : Response(NwcMethod.PAY_INVOICE),
|
||||
IErrorResponseLike {
|
||||
class PayInvoiceErrorParams(
|
||||
val code: NwcErrorCode? = null,
|
||||
val message: String? = null,
|
||||
)
|
||||
|
||||
override fun errorMessage() = error?.message
|
||||
}
|
||||
|
||||
// pay_keysend success response
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip53LiveActivities
|
||||
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag
|
||||
|
||||
/**
|
||||
* Shared surface of the NIP-53 streaming + meeting-room events. Both
|
||||
* LiveActivitiesEvent (kind 30311) and MeetingRoomEvent (kind 30312)
|
||||
* expose the same title/summary/image/streaming/starts/status/participants
|
||||
* tags so that callers — discovery feeds, cards, notifications — can
|
||||
* render them uniformly without branching on the concrete subclass.
|
||||
*/
|
||||
interface LiveStreamLike {
|
||||
fun title(): String?
|
||||
|
||||
fun summary(): String?
|
||||
|
||||
fun image(): String?
|
||||
|
||||
fun streaming(): String?
|
||||
|
||||
fun starts(): Long?
|
||||
|
||||
fun status(): StatusTag.STATUS?
|
||||
|
||||
fun participants(): List<ParticipantTag>
|
||||
}
|
||||
+10
-8
@@ -38,6 +38,7 @@ import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag
|
||||
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.LiveStreamLike
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.MeetingSpaceTag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.CurrentParticipantsTag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.EndsTag
|
||||
@@ -64,7 +65,8 @@ class MeetingRoomEvent(
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
AddressHintProvider,
|
||||
PubKeyHintProvider {
|
||||
PubKeyHintProvider,
|
||||
LiveStreamLike {
|
||||
override fun eventHints(): List<EventIdHint> {
|
||||
val pinnedEvents = pinned()
|
||||
if (pinnedEvents.isEmpty()) return emptyList()
|
||||
@@ -95,21 +97,21 @@ class MeetingRoomEvent(
|
||||
|
||||
fun interactiveRoom() = tags.firstNotNullOfOrNull(MeetingSpaceTag::parse)
|
||||
|
||||
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
|
||||
override fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
|
||||
|
||||
fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse)
|
||||
override fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse)
|
||||
|
||||
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
|
||||
override fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
|
||||
|
||||
fun streaming() = tags.firstNotNullOfOrNull(StreamingTag::parse)
|
||||
override fun streaming() = tags.firstNotNullOfOrNull(StreamingTag::parse)
|
||||
|
||||
fun recording() = tags.firstNotNullOfOrNull(RecordingTag::parse)
|
||||
|
||||
fun starts() = tags.firstNotNullOfOrNull(StartsTag::parse)
|
||||
override fun starts() = tags.firstNotNullOfOrNull(StartsTag::parse)
|
||||
|
||||
fun ends() = tags.firstNotNullOfOrNull(EndsTag::parse)
|
||||
|
||||
fun status() = checkStatus(tags.firstNotNullOfOrNull(StatusTag::parseEnum))
|
||||
override fun status() = checkStatus(tags.firstNotNullOfOrNull(StatusTag::parseEnum))
|
||||
|
||||
fun isLive() = status() == StatusTag.STATUS.LIVE
|
||||
|
||||
@@ -119,7 +121,7 @@ class MeetingRoomEvent(
|
||||
|
||||
fun participantKeys(): List<HexKey> = tags.mapNotNull(ParticipantTag::parseKey)
|
||||
|
||||
fun participants() = tags.mapNotNull(ParticipantTag::parse)
|
||||
override fun participants() = tags.mapNotNull(ParticipantTag::parse)
|
||||
|
||||
fun relays() = tags.mapNotNull(RelayListTag::parse).flatten()
|
||||
|
||||
|
||||
+10
-8
@@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag
|
||||
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.LiveStreamLike
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.CurrentParticipantsTag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.EndsTag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag
|
||||
@@ -60,7 +61,8 @@ class LiveActivitiesEvent(
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
PubKeyHintProvider {
|
||||
PubKeyHintProvider,
|
||||
LiveStreamLike {
|
||||
override fun eventHints(): List<EventIdHint> {
|
||||
val pinnedEvents = pinned()
|
||||
if (pinnedEvents.isEmpty()) return emptyList()
|
||||
@@ -85,21 +87,21 @@ class LiveActivitiesEvent(
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(ParticipantTag::parseKey)
|
||||
|
||||
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
|
||||
override fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
|
||||
|
||||
fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse)
|
||||
override fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse)
|
||||
|
||||
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
|
||||
override fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
|
||||
|
||||
fun streaming() = tags.firstNotNullOfOrNull(StreamingTag::parse)
|
||||
override fun streaming() = tags.firstNotNullOfOrNull(StreamingTag::parse)
|
||||
|
||||
fun recording() = tags.firstNotNullOfOrNull(RecordingTag::parse)
|
||||
|
||||
fun starts() = tags.firstNotNullOfOrNull(StartsTag::parse)
|
||||
override fun starts() = tags.firstNotNullOfOrNull(StartsTag::parse)
|
||||
|
||||
fun ends() = tags.firstNotNullOfOrNull(EndsTag::parse)
|
||||
|
||||
fun status() = checkStatus(tags.firstNotNullOfOrNull(StatusTag::parseEnum))
|
||||
override fun status() = checkStatus(tags.firstNotNullOfOrNull(StatusTag::parseEnum))
|
||||
|
||||
fun isLive() = status() == StatusTag.STATUS.LIVE
|
||||
|
||||
@@ -109,7 +111,7 @@ class LiveActivitiesEvent(
|
||||
|
||||
fun participantKeys(): List<HexKey> = tags.mapNotNull(ParticipantTag::parseKey)
|
||||
|
||||
fun participants() = tags.mapNotNull(ParticipantTag::parse)
|
||||
override fun participants() = tags.mapNotNull(ParticipantTag::parse)
|
||||
|
||||
fun relays() = tags.mapNotNull(RelayListTag::parse).flatten()
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip59Giftwrap
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
/**
|
||||
* Marker for NIP-59 wrappers that carry an inner-event id once decrypted —
|
||||
* GiftWrapEvent (and its subclasses) and SealedRumorEvent. Lets callers walk
|
||||
* to the wrapped event without branching on the concrete wrapper type.
|
||||
*/
|
||||
interface HasInnerEvent {
|
||||
val innerEventId: HexKey?
|
||||
}
|
||||
+4
-2
@@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip40Expiration.ExpirationTag
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.HasInnerEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.HostStub
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
|
||||
@@ -39,10 +40,11 @@ class SealedRumorEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : WrappedEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
) : WrappedEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
HasInnerEvent {
|
||||
@kotlinx.serialization.Transient
|
||||
@kotlin.jvm.Transient
|
||||
var innerEventId: HexKey? = null
|
||||
override var innerEventId: HexKey? = null
|
||||
|
||||
fun copyNoContent(): SealedRumorEvent {
|
||||
val copy =
|
||||
|
||||
+4
-2
@@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
|
||||
import com.vitorpamplona.quartz.nip40Expiration.ExpirationTag
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.HasInnerEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.HostStub
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
@@ -44,10 +45,11 @@ open class GiftWrapEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
kind: Int = KIND,
|
||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig),
|
||||
HasInnerEvent {
|
||||
@kotlinx.serialization.Transient
|
||||
@kotlin.jvm.Transient
|
||||
var innerEventId: HexKey? = null
|
||||
override var innerEventId: HexKey? = null
|
||||
|
||||
open fun copyNoContent(): GiftWrapEvent {
|
||||
val copy =
|
||||
|
||||
Reference in New Issue
Block a user