Merge remote-tracking branch 'origin/main' into claude/affectionate-ptolemy-0q909o

This commit is contained in:
Claude
2026-09-27 00:14:08 +00:00
662 changed files with 5312 additions and 4112 deletions
+2 -2
View File
@@ -43,10 +43,10 @@ Relay frame ──► LocalCache.insertOrUpdateNote() ──► LocalCacheFlow e
- `kind3FollowList = Kind3FollowListState(...)` ← NIP-02 ContactList (kind 3)
- `nip65RelayList = Nip65RelayListState(...)` ← NIP-65 RelayList (kind 10002), plus siblings `dmRelayList`, `searchRelayList`, `blockedRelayList`, `trustedRelayList`, `proxyRelayList`, `broadcastRelayList`, `indexerRelayList`, …
- `muteList = MuteListState(...)` ← NIP-51 MuteList (kind 10000)
- `bookmarkState = BookmarkListState(...)` ← NIP-51 Bookmarks (kind 10003), plus `labeledBookmarkLists`, `pinState`, `interestSets`, `peopleLists`, `followLists`, `hashtagList`, `geohashList`, `communityList`, `emoji`, `blossomServers`, …
- `bookmarkState = BookmarkListState(...)` ← NIP-51 Bookmarks (kind 10003), plus `bookmarkSets`, `pinState`, `interestSets`, `peopleLists`, `followLists`, `hashtagList`, `geohashList`, `communityList`, `emoji`, `blossomServers`, …
- Derived/merged views: `hiddenUsers`, `allFollows`, `homeRelays`, `outboxRelays`, `dmRelays`, `notificationRelays`, `trustedRelays`, and the `live*FollowListsPerRelay` outbox loaders.
- **The pattern:** each `XState` class pins its addressable note via `cache.getOrCreateAddressableNote(address)` (a long-term reference so GC/eviction can't drop it), exposes `val flow: StateFlow<…>` derived from the note's metadata flow (decrypted through a per-feature `DecryptionCache`, with backup fallback from `AccountSettings`, `stateIn(scope, Eagerly, …)`), and offers suspend mutation helpers (e.g. `MuteListState.hideUser(pubkey)`) that build the updated signed event. Consumers read `account.muteList.flow`, never a raw `MutableStateFlow` on `Account`.
- Encrypted lists pair the state object with a `DecryptionCache` sibling (`muteListDecryptionCache`, `peopleListDecryptionCache`, …) so NIP-44 decryption results are cached per event.
- Encrypted lists pair the state object with a `DecryptionCache` sibling (`muteListDecryptionCache`, `followSetDecryptionCache`, …) so NIP-44 decryption results are cached per event.
- UI reads via `collectAsStateWithLifecycle` on Android and `collectAsState` on Desktop.
- Sibling files per feature live alongside: `AccountSettings.kt`, `AccountSyncedSettings.kt`, plus per-NIP state classes under `model/nip02FollowLists/`, `model/nip51Lists/`, `model/nip65RelayList/`, etc.
@@ -16,8 +16,8 @@ been renamed, grep `Account.kt` for the class name.)
| `userMetadata` | `UserMetadataState` | 0 | `amethyst/.../model/nip01UserMetadata/` |
| `kind3FollowList` | `Kind3FollowListState` | 3 | `model/nip02FollowLists/` |
| `muteList` (+ `muteListDecryptionCache`) | `MuteListState` | 10000 | `model/nip51Lists/muteList/` |
| `blockPeopleList`, `peopleLists` | `BlockPeopleListState`, `PeopleListsState` | NIP-51 people sets | `model/nip51Lists/peopleList/` |
| `followLists` | `FollowListsState` | NIP-51 follow sets | `model/nip51Lists/peopleList/` |
| `blockPeopleList`, `peopleLists` | `BlockPeopleListState`, `FollowSetsState` | NIP-51 people sets | `model/nip51Lists/followSets/` |
| `followLists` | `StarterPacksState` | NIP-51 follow sets | `model/nip51Lists/followSets/` |
| `hiddenUsers` | `HiddenUsersState` — derived from `muteList.flow` + `blockPeopleList.flow` | — | `model/nip51Lists/` |
| `allFollows` | `MergedFollowListsState` — merges kind3 + people/follow/hashtag/geohash/community lists | — | `model/serverList/` |
@@ -31,7 +31,7 @@ been renamed, grep `Account.kt` for the class name.)
| `blockedRelayList` | `BlockedRelayListState` | 10006 | `model/nip51Lists/blockedRelays/` |
| `localRelayList` | `LocalRelayListState` | local | `model/localRelays/` |
| `privateStorageRelayList` | `PrivateStorageRelayListState` | private storage | `model/edits/` |
| `keyPackageRelayList`, `trustedRelayList`, `proxyRelayList`, `broadcastRelayList`, `indexerRelayList`, `relayFeedsList` | per-feature `…RelayListState` classes, each with a `DecryptionCache` sibling | custom relay sets | `model/nip51Lists/…` |
| `keyPackageRelayList`, `trustedRelayList`, `proxyRelayList`, `broadcastRelayList`, `indexerRelayList`, `favoriteRelayList` | per-feature `…RelayListState` classes, each with a `DecryptionCache` sibling | custom relay sets | `model/nip51Lists/…` |
Derived relay views (merge several of the above): `homeRelays`
(`AccountHomeRelayState`), `outboxRelays`, `dmRelays`, `notificationRelays`,
@@ -43,10 +43,10 @@ Derived relay views (merge several of the above): `homeRelays`
| Account property | State class | Kind | Package |
|------------------|-------------|------|---------|
| `bookmarkState` (and legacy `oldBookmarkState`) | `BookmarkListState` | 10003 | `model/nip51Lists/` |
| `labeledBookmarkLists` | `LabeledBookmarkListsState` | NIP-51 bookmark sets | `model/nip51Lists/labeledBookmarkLists/` |
| `bookmarkSets` | `BookmarkSetsState` | NIP-51 bookmark sets | `model/nip51Lists/bookmarkSets/` |
| `pinState` | `PinListState` | NIP-51 | `model/nip51Lists/` |
| `interestSets` | `InterestSetsState` | NIP-51 interest sets | `model/nip51Lists/interestSets/` |
| `hashtagList` / `geohashList` | `HashtagListState` / `GeohashListState` | NIP-51 | `model/nip51Lists/hashtagLists/`, `…/geohashLists/` |
| `hashtagList` / `geohashList` | `InterestListState` / `GeohashListState` | NIP-51 | `model/nip51Lists/interestLists/`, `…/geohashLists/` |
| `communityList` | `CommunityListState` | NIP-72 communities | `model/nip72Communities/` |
| `favoriteAlgoFeedsList` | `FavoriteAlgoFeedsListState` | NIP-51 | `model/nip51Lists/` |
| `emoji`, `ownedEmojiPacks` | `EmojiPackState`, `OwnedEmojiPacksState` | 10030 | `commons/.../commons/model/nip30CustomEmojis/` |
+1 -1
View File
@@ -29,7 +29,7 @@ abstract class NostrSigner(val pubKey: HexKey) {
abstract fun nip04Decrypt(ciphertext: String, fromPubKey: HexKey, onReady: (String) -> Unit)
abstract fun nip44Encrypt(...)
abstract fun nip44Decrypt(...)
abstract fun decryptZapEvent(event: LnZapRequestEvent, onReady: (LnZapRequestEvent) -> Unit)
abstract fun decryptZapEvent(event: ZapRequestEvent, onReady: (ZapRequestEvent) -> Unit)
}
```
@@ -26,16 +26,16 @@ event signatures; the 10040→assertion link is **consumer-side convention** (se
| Kind | Class | Kind class | d-tag = the subject | Content |
|---|---|---|---|---|
| 10040 | `list/TrustProviderListEvent` | replaceable | *(none — always `""`)* | NIP-44 private provider entries (optional) |
| 30382 | `users/ContactCardEvent` | addressable | **target user's pubkey** (hex) | NIP-44 private tags (petname/summary/emoji) |
| 30382 | `users/UserAssertionEvent` | addressable | **target user's pubkey** (hex) | NIP-44 private tags (petname/summary/emoji) |
| 30383 | `events/EventAssertionEvent` | addressable | **target event id** (hex) | `""` |
| 30384 | `addressables/AddressableAssertionEvent` | addressable | **target coordinate** `kind:pubkey:dtag` | `""` |
| 30385 | `externalIds/ExternalIdAssertionEvent` | addressable | **external identifier** (e.g. `isbn:978-0-13-468599-1`) | `""` |
Addresses: `ContactCardEvent.createAddress(owner, target)` → `Address(30382, owner, target)`
Addresses: `UserAssertionEvent.createAddress(owner, target)` → `Address(30382, owner, target)`
(owner = signer, target = subject). `TrustProviderListEvent.createAddress(pubKey)` uses
`FIXED_D_TAG = ""`. `AssertionEventTest.eventKindsAreCorrect` pins all five numbers.
`ContactCardEvent` is also a `SearchableEvent` — it indexes only the **public** petname/summary
`UserAssertionEvent` is also a `SearchableEvent` — it indexes only the **public** petname/summary
tags plus topics; the encrypted card content is intentionally never indexed.
## The 10040 provider entry (`ServiceProviderTag` / `ServiceType`)
@@ -91,9 +91,9 @@ The same kind serves two roles, distinguished **by author**:
`followers`, `hops`, …); this is what 10040 discovery points at.
2. **The account's own contact cards (nicknames, NIP-81-style)** — signed by the account,
one per target user. The petname, summary, and their NIP-30 emoji mappings **always live in
the NIP-44 encrypted content, never in public tags** (`ContactCardEvent.build`/
`updatePetNameAndSummary` strip stray public copies; asserted by `ContactCardPetNameTest`).
`commons/.../ContactCardsState.kt` keys everything on `author == account` and ignores
the NIP-44 encrypted content, never in public tags** (`UserAssertionEvent.build`/
`updatePetNameAndSummary` strip stray public copies; asserted by `UserAssertionPetNameTest`).
`commons/.../UserAssertionsState.kt` keys everything on `author == account` and ignores
provider cards.
## Tag vocabulary and value semantics
@@ -104,7 +104,7 @@ value — a bad tag is *dropped*, never an error) + `assemble(value)` → `[name
validation (rank isn't clamped, hours aren't checked against 0–23, counts may be negative) —
consumers must defend.
**On 30382** (`users/tags/`, accessors on `ContactCardEvent` and as `TagArray` extensions in
**On 30382** (`users/tags/`, accessors on `UserAssertionEvent` and as `TagArray` extensions in
`users/TagArrayExt.kt` so they also work on decrypted private arrays):
| Tag name | Accessor | Type | Semantics |
@@ -138,7 +138,7 @@ val providers: List<ServiceProviderTag> = updated.serviceProviders() //
val private = updated.privateTags(signer)?.serviceProviders() // private side
// Provider-style contact card (public metrics) — the GrapeRankPublisher pattern:
val card = ContactCardEvent.create(
val card = UserAssertionEvent.create(
targetUser = subjectPubkey,
signer = providerSigner,
publicInitializer = {
@@ -225,8 +225,8 @@ card's `a`-tag (`30382:<provider>:<target>`).
- **Publisher**: `quartz/.../experimental/graperank/GrapeRankPublisher.kt` (canonical 30382
writer), `cli/.../graperank/` (`amy graperank register|unregister|providers|publish`).
- **Client model**: `commons/.../model/nip85TrustedAssertions/` (`ContactCardsState`,
`UserCardsCache`, `ContactCardDecryptionCache`, `TrustProviderListDecryptionCache`),
- **Client model**: `commons/.../model/nip85TrustedAssertions/` (`UserAssertionsState`,
`UserCardsCache`, `UserAssertionDecryptionCache`, `TrustProviderListDecryptionCache`),
`amethyst/.../model/trustedAssertions/TrustProviderListState.kt`.
- **Relay plumbing**: `commons/.../relayClient/assemblers/ContactCardFilters.kt`,
`amethyst/.../reqCommand/user/watchers/UserCardsSubAssembler.kt`.
@@ -79,30 +79,38 @@ companion object {
### Zap Request/Receipt (kinds 9734, 9735)
```kotlin
class LnZapRequestEvent(...) : Event(...)
class ZapRequestEvent(...) : Event(...)
// Created by client, sent to Lightning Address
class LnZapEvent(...) : Event(...)
class ZapReceiptEvent(...) : Event(...)
// Receipt from LSP, contains bolt11 + embedded zap request
val zapRequest: LnZapRequestEvent? by lazy { containedPost() }
val zapRequest: ZapRequestEvent? by lazy { containedPost() }
val amount: BigDecimal? by lazy { /* parse from bolt11 */ }
```
### Long-Form Content (kind 30023)
```kotlin
class LongTextNoteEvent(...) : BaseAddressableEvent(...)
class LongFormContentEvent(...) : BaseAddressableEvent(...)
// Blog posts, articles
// Addressable via kind:pubkey:d-tag
```
### Lists (kinds 10000-30004)
### Lists (NIP-51)
Each list kind is its own class under `nip51Lists/`, not a subtype of a shared list event.
Lists with private (NIP-44 encrypted) items extend one of two bases:
```kotlin
sealed class PeopleListEvent : BaseAddressableEvent {
object MuteList : PeopleListEvent(10000)
object PinList : PeopleListEvent(10001)
object BookmarkList : PeopleListEvent(10003)
// ... 18 list types total
}
// Base for sets (kind 30000-39999); some replaceable lists use it too (MuteListEvent, InterestListEvent)
abstract class PrivateTagArrayEvent(...) : BaseAddressableEvent(...)
// Replaceable lists (kind 10000-19999)
abstract class PrivateReplaceableTagArrayEvent(...) : BaseReplaceableEvent(...)
class MuteListEvent(...) : PrivateTagArrayEvent(...) // kind 10000
class PinListEvent(...) : BaseReplaceableEvent(...) // kind 10001, public only
class BookmarkListEvent(...) : PrivateReplaceableTagArrayEvent(...) // kind 10003
class InterestListEvent(...) : PrivateTagArrayEvent(...) // kind 10015
class FollowSetEvent(...) : PrivateTagArrayEvent(...) // kind 30000 follow sets
class BookmarkSetEvent(...) : PrivateTagArrayEvent(...) // kind 30003 bookmark sets
class StarterPackEvent(...) : BaseAddressableEvent(...) // kind 39089 starter packs
```
## Event Interfaces
@@ -279,7 +287,7 @@ val metadata2 = MetadataEvent.createNew(name = "Alice Updated", picture = "url2"
### Event Deletion
```kotlin
// Delete events
val deletion = DeletionEvent.create(
val deletion = DeletionRequestEvent.create(
deleteEvents = listOf(eventId1, eventId2),
reason = "Spam",
signer = signer
@@ -15,7 +15,7 @@ under `experimental/`**. The categorized list below may lag behind —
| 04 | `nip04Dm/` | EncryptedDmEvent.kt | Legacy encrypted DMs (deprecated for NIP-17) |
| 05 | `nip05DnsIdentifiers/` | UserHexResolver.kt, Nip05Client.kt | Internet identifiers; `resolveUserHexOrNull` resolves hex/npub/nprofile/`name@domain` → pubkey (see references/nip05-identifiers.md) |
| 06 | `nip06KeyDerivation/` | Mnemonic-related | BIP-39 key derivation |
| 09 | `nip09Deletions/` | DeletionEvent.kt | Event deletion requests (kind 5) |
| 09 | `nip09Deletions/` | DeletionRequestEvent.kt | Event deletion requests (kind 5) |
| 11 | `nip11RelayInfo/` | RelayInformation.kt | Relay metadata |
| 13 | `nip13Pow/` | ProofOfWork.kt | Proof of work |
| 14 | `nip14Subject/` | Subject tags | Subject tags for text notes |
@@ -33,7 +33,7 @@ ent for NIP-04) |
| 10 | `nip10Notes/` | TextNoteEvent.kt | Text notes with threading (kind 1) |
| 18 | `nip18Reposts/` | RepostEvent.kt, GenericRepostEvent.kt | Reposts (kind 6, 16) |
| 22 | `nip22Comments/` | CommentEvent.kt | Comments (kind 1111) |
| 23 | `nip23LongContent/` | LongTextNoteEvent.kt | Long-form content (kind 30023) |
| 23 | `nip23LongContent/` | LongFormContentEvent.kt | Long-form content (kind 30023) |
| 25 | `nip25Reactions/` | ReactionEvent.kt | Reactions (kind 7) |
| 31 | `nip31Alts/` | Alt tags | Alt description tags |
| 36 | `nip36SensitiveContent/` | Content warnings | Content warning tags |
@@ -62,7 +62,7 @@ ent for NIP-04) |
| 46 | `nip46RemoteSigner/` | NostrConnectEvent.kt | Remote signer protocol (bunker) |
| 47 | `nip47WalletConnect/` | Nostr Wallet Connect | Wallet connection protocol |
| 56 | `nip56Reports/` | ReportEvent.kt | Reports (kind 1984) |
| 57 | `nip57Zaps/` | LnZapEvent.kt, LnZapRequestEvent.kt | Lightning zaps (kinds 9734, 9735) |
| 57 | `nip57Zaps/` | ZapReceiptEvent.kt, ZapRequestEvent.kt | Lightning zaps (kinds 9734, 9735) |
| 58 | `nip58Badges/` | Badge events | Badge definitions & awards (kinds 30009, 8) |
| 59 | `nip59Giftwrap/` | GiftWrapEvent.kt | Gift-wrapped events for privacy |
| 75 | `nip75ZapGoals/` | ZapGoalEvent.kt | Zap goals (kind 9041) |
@@ -76,7 +76,7 @@ ent for NIP-04) |
| 35 | `nip35Torrents/` | Torrent events | Torrent tracking |
| 52 | `nip52Calendar/` | Calendar events | Calendar time-based/date-based (kinds 31922-31925) |
| 53 | `nip53LiveActivities/` | LiveActivitiesEvent.kt | Live events/streaming (kind 30311) |
| 54 | `nip54Wiki/` | WikiNoteEvent.kt | Wiki pages (kind 30818) |
| 54 | `nip54Wiki/` | WikiArticleEvent.kt | Wiki pages (kind 30818) |
| 68 | `nip68Picture/` | Picture metadata | Picture metadata |
| 71 | `nip71Video/` | 7 video event types | Video events (kinds 34235, 35235, 1234, 1235) |
| 72 | `nip72ModCommunities/` | Community events | Moderated communities (kinds 34550, 34551, 9041) |
@@ -84,7 +84,7 @@ ent for NIP-04) |
| 89 | `nip89AppHandlers/` | AppDefinitionEvent.kt | App recommendations (kinds 31990, 31989) |
| 90 | `nip90Dvms/` | DVM job events | Data Vending Machines (DVMs) (kinds 5000-7000) |
| 92 | `nip92IMeta/` | IMeta tags | Image metadata tags |
| 94 | `nip94FileMetadata/` | FileHeaderEvent.kt, FileStorageEvent.kt | File metadata (kind 1063) |
| 94 | `nip94FileMetadata/` | FileMetadataEvent.kt, FileStorageEvent.kt | File metadata (kind 1063) |
| 96 | `nip96FileStorage/` | HTTP file storage | HTTP-based file storage |
| 99 | `nip99Classifieds/` | ClassifiedsEvent.kt | Classifieds/marketplace (kind 30402) |
| A0 | `nipA0VoiceMessages/` | Voice messages | Voice message events |
@@ -93,7 +93,7 @@ ent for NIP-04) |
### Web/Storage/Other
| NIP | Directory | Key Files | Description |
|-----|-----------|-----------|-------------|
| 38 | `nip38UserStatus/` | StatusEvent.kt | User status (kind 30315) |
| 38 | `nip38UserStatus/` | UserStatusEvent.kt | User status (kind 30315) |
| 60 | `nip60Payment/` | Wallet events | Wallet info (kind 13194) |
| 61 | `nip61PaymentRequest/` | Nut zaps | Cashu payment requests |
| 64 | `nip64Chess/` | Chess moves | Chess move events |
+11 -11
View File
@@ -650,15 +650,15 @@ val custom = ReactionEvent.build("🤙", targetEvent)
### NIP-57 — Zap request (kind 9734)
```kotlin
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent
val template = LnZapRequestEvent.build(
val template = ZapRequestEvent.build(
message = "Great post!",
relays = listOf("wss://relay.damus.io"),
target = targetEvent,
zapType = LnZapRequestEvent.ZapType.PUBLIC
zapType = ZapRequestEvent.ZapType.PUBLIC
)
val zapRequest: LnZapRequestEvent = signer.sign(template)
val zapRequest: ZapRequestEvent = signer.sign(template)
```
### NIP-59 — Gift wrap / sealed DM (kind 1059 + 14)
@@ -678,9 +678,9 @@ val (dmEvent, giftWrap) = NIP17Factory.create(
### NIP-23 — Long-form article (kind 30023)
```kotlin
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent
val template = LongTextNoteEvent.build(
val template = LongFormContentEvent.build(
body = markdownContent,
title = "My Article",
image = "https://example.com/cover.jpg",
@@ -882,16 +882,16 @@ use `Nip11RelayInformation.fromJson(json)`.
| 0 | User metadata | 01 | `MetadataEvent` |
| 1 | Text note | 10 | `TextNoteEvent` |
| 3 | Follow list | 02 | `ContactListEvent` |
| 4 | Legacy DM | 04 | `PrivateDmEvent` |
| 5 | Deletion | 09 | `DeletionEvent` |
| 4 | Legacy DM | 04 | `EncryptedDmEvent` |
| 5 | Deletion | 09 | `DeletionRequestEvent` |
| 6 | Repost | 18 | `RepostEvent` |
| 7 | Reaction | 25 | `ReactionEvent` |
| 14 | Chat message (sealed) | 17 | `NIP17GroupMessage` |
| 1059 | Gift wrap | 59 | `GiftWrapEvent` |
| 9734 | Zap request | 57 | `LnZapRequestEvent` |
| 9735 | Zap receipt | 57 | `LnZapEvent` |
| 9734 | Zap request | 57 | `ZapRequestEvent` |
| 9735 | Zap receipt | 57 | `ZapReceiptEvent` |
| 10002 | Relay list | 65 | `AdvertisedRelayListEvent` |
| 30023 | Long-form content | 23 | `LongTextNoteEvent` |
| 30023 | Long-form content | 23 | `LongFormContentEvent` |
## Related Skills
+3 -4
View File
@@ -45,10 +45,9 @@ Notables that surprise people:
(`zapRequest?.content.orEmpty()`) — receipts are searchable by the zapper's comment.
- **Kind 0 / 31990** index many profile fields space-joined (name, about, nip05, lud16,
website, picture URL, …).
- **Kind 30063 is claimed twice** (`ReleaseArtifactSetEvent` in nip51Lists and the experimental
`SoftwareReleaseEvent`); `EventFactory` resolves 30063 to `ReleaseArtifactSetEvent`, so
`title()\ndescription()` is what actually gets indexed — `SoftwareReleaseEvent.indexableContent()`
is dead on the store path.
- **Kind 30063** (`ReleaseArtifactSetEvent`) serves both NIP-51 release artifact sets and NIP-82
software releases: it indexes `title()\ndescription()`, plus the release notes in `content` only
for NIP-82 releases (NIP-51 `content` can be encrypted private items).
- Poll kinds (1068, 6969) append each option label on its own line.
## MANDATORY maintenance when you touch this surface
@@ -5,7 +5,7 @@ expression. **Update this file in the same PR as any change to the searchable se
`indexableContent()` body** (see SKILL.md). Verified against the code 2026-09-17.
Counts: 137 concrete classes covering 140 kind values (`GitStatusEvent` spans 4 kinds;
kind 30063 has a collision — see the footnote). File paths are under
kind 30063 is shared by two NIPs — see the footnote). File paths are under
`quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/`.
Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`.
@@ -26,7 +26,7 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`.
| 42 | ChannelMessageEvent | nip28PublicChat/message | `content` |
| 54 | PodcastEpisodeEvent | nipF4Podcasts/episode | `listOfNotNull(title(), description(), content)` NL |
| 1010 | TextNoteModificationEvent | experimental/edits | `listOfNotNull(content, summary())` NL (content first) |
| 1063 | FileHeaderEvent | nip94FileMetadata | `listOfNotNull(summary(), content)` NL |
| 1063 | FileMetadataEvent | nip94FileMetadata | `listOfNotNull(summary(), content)` NL |
| 1065 | FileStorageHeaderEvent | experimental/nip95/header | `listOfNotNull(summary())` NL |
| 1068 | PollEvent | nip88Polls/poll | `buildString { append(content); options().forEach { append('\n').append(it.label) } }` |
| 1111 | CommentEvent | nip22Comments | `(listOf(content) + tags.hashtags())` NL |
@@ -48,20 +48,20 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`.
| 2004 | TorrentCommentEvent | nip35Torrents | `content` |
| 2473 | BirdDetectionEvent | experimental/birdstar | `listOfNotNull(summary(), speciesName())` NL |
| 3302 | ConcordChatEditEvent | concord/cord03Channels | `content` |
| 5050 | NIP90TextGenerationRequestEvent | nip90Dvms/textGeneration | `inputs().filter { it.type == "prompt" \|\| it.type == "text" }.joinToString(" ") { it.value }` (SP) |
| 5100 | NIP90ImageGenerationRequestEvent | nip90Dvms/imageGeneration | `listOfNotNull(prompt(), negativePrompt()).joinToString(" ")` (SP) |
| 5050 | DvmTextGenerationRequestEvent | nip90Dvms/textGeneration | `inputs().filter { it.type == "prompt" \|\| it.type == "text" }.joinToString(" ") { it.value }` (SP) |
| 5100 | DvmImageGenerationRequestEvent | nip90Dvms/imageGeneration | `listOfNotNull(prompt(), negativePrompt()).joinToString(" ")` (SP) |
| 5129 | NappletSnapshotEvent | nip5dNapplets | `listOfNotNull(title(), description())` NL |
| 5250 | NIP90TextToSpeechRequestEvent | nip90Dvms/textToSpeech | `text() ?: ""` |
| 5302 | NIP90ContentSearchRequestEvent | nip90Dvms/contentSearch | `searchQuery() ?: ""` |
| 5303 | NIP90PeopleSearchRequestEvent | nip90Dvms/peopleSearch | `searchQuery() ?: ""` |
| 5250 | DvmTextToSpeechRequestEvent | nip90Dvms/textToSpeech | `text() ?: ""` |
| 5302 | DvmContentSearchRequestEvent | nip90Dvms/contentSearch | `searchQuery() ?: ""` |
| 5303 | DvmPeopleSearchRequestEvent | nip90Dvms/peopleSearch | `searchQuery() ?: ""` |
| 6969 | ZapPollEvent | experimental/zapPolls | `buildString { append(content); pollOptionsArray().forEach { append('\n').append(it.descriptor) } }` |
| 7516 | GeocacheFoundLogEvent | nipCCGeocaching/foundLog | `content` |
| 8333 | OnchainZapEvent | nipBCOnchainZaps/zap | `content` |
| 9002 | EditMetadataEvent | nip29RelayGroups/moderation | `(listOfNotNull(name(), about()) + hashtags())` NL |
| 9041 | GoalEvent | nip75ZapGoals | `listOfNotNull(summary(), content)` NL |
| 9002 | GroupEditMetadataEvent | nip29RelayGroups/moderation | `(listOfNotNull(name(), about()) + hashtags())` NL |
| 9041 | ZapGoalEvent | nip75ZapGoals | `listOfNotNull(summary(), content)` NL |
| 9321 | NutzapEvent | nip61Nutzaps/nutzap | `content` |
| 9734 | LnZapRequestEvent | nip57Zaps | `content` |
| 9735 | LnZapEvent | nip57Zaps | `zapRequest?.content.orEmpty()` — indexes the **embedded 9734's** content |
| 9734 | ZapRequestEvent | nip57Zaps | `content` |
| 9735 | ZapReceiptEvent | nip57Zaps | `zapRequest?.content.orEmpty()` — indexes the **embedded 9734's** content |
| 9736 | Bolt12ZapEvent | nipB1Bolt12Zaps/zap | `content` |
| 9737 | Bolt12ZapIntentEvent | nipB1Bolt12Zaps/intent | `content` |
| 9802 | HighlightEvent | nip84Highlights | `listOfNotNull(comment(), context(), content)` NL |
@@ -74,10 +74,10 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`.
| 12473 | BirdexEvent | experimental/birdstar | `(listOfNotNull(summary()) + speciesNames())` NL |
| 15128 | RootSiteEvent | nip5aStaticWebsites | `listOfNotNull(title(), description())` NL |
| 15129 | RootNappletEvent | nip5dNapplets | `listOfNotNull(title(), description())` NL |
| 30000 | PeopleListEvent | nip51Lists/peopleList | `listOfNotNull(titleOrName(), description())` NL |
| 30000 | FollowSetEvent | nip51Lists/followSet | `listOfNotNull(titleOrName(), description())` NL |
| 30001 | OldBookmarkListEvent | nip51Lists/bookmarkList | `listOfNotNull(title())` NL |
| 30002 | RelaySetEvent | nip51Lists/relaySets | `listOfNotNull(title(), description())` NL |
| 30003 | LabeledBookmarkListEvent | nip51Lists/labeledBookmarkList | `listOfNotNull(titleOrName(), description())` NL |
| 30003 | BookmarkSetEvent | nip51Lists/bookmarkSet | `listOfNotNull(titleOrName(), description())` NL |
| 30004 | ArticleCurationSetEvent | nip51Lists/articleCurationSet | `listOfNotNull(title(), description())` NL |
| 30005 | VideoCurationSetEvent | nip51Lists/videoCurationSet | `listOfNotNull(title(), description())` NL |
| 30006 | PictureCurationSetEvent | nip51Lists/pictureCurationSet | `listOfNotNull(title(), description())` NL |
@@ -87,11 +87,11 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`.
| 30018 | ProductEvent | nip15Marketplace/product | `productData()?.let { (listOfNotNull(it.name, it.description) + categories()).joinToString("\n") } ?: ""` |
| 30019 | MarketplaceEvent | nip15Marketplace/marketplace | `marketplaceData()?.let { listOfNotNull(it.name, it.about).joinToString("\n") } ?: ""` |
| 30020 | AuctionEvent | nip15Marketplace/auction | `auctionData()?.let { (listOfNotNull(it.name, it.description) + tags.hashtags()).joinToString("\n") } ?: ""` |
| 30023 | LongTextNoteEvent | nip23LongContent | `listOfNotNull(title(), summary(), content)` NL |
| 30023 | LongFormContentEvent | nip23LongContent | `listOfNotNull(title(), summary(), content)` NL |
| 30030 | EmojiPackEvent | nip30CustomEmoji/pack | `listOfNotNull(titleOrName(), description(), content)` NL |
| 30054 | Podcasting20EpisodeEvent | nipXXPodcasting20/episode | `(listOfNotNull(title(), description(), content) + topics())` NL |
| 30055 | Podcasting20TrailerEvent | nipXXPodcasting20/trailer | `listOfNotNull(title(), content)` NL |
| 30063 | ReleaseArtifactSetEvent † | nip51Lists/releaseArtifactSet | `listOfNotNull(title(), description())` NL |
| 30063 | ReleaseArtifactSetEvent † | nip51Lists/releaseArtifactSet | `listOfNotNull(title(), description(), content if NIP-82)` NL |
| 30175 | PersonaEvent | buzz/apPersonas | `personaOrNull()?.let { listOfNotNull(it.displayName, it.systemPrompt).joinToString("\n") } ?: ""` |
| 30176 | TeamEvent | buzz/teams | `teamOrNull()?.let { listOfNotNull(it.name, it.description, it.instructions).joinToString("\n") } ?: ""` |
| 30177 | ManagedAgentEvent | buzz/managedAgents | `agentOrNull()?.let { listOfNotNull(it.name, it.systemPrompt).joinToString("\n") } ?: ""` |
@@ -101,8 +101,8 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`.
| 30311 | LiveActivitiesEvent | nip53LiveActivities/streaming | `listOfNotNull(title(), summary(), content)` NL |
| 30312 | MeetingSpaceEvent | nip53LiveActivities/meetingSpaces | `listOfNotNull(room(), summary(), content)` NL |
| 30313 | MeetingRoomEvent | nip53LiveActivities/meetingSpaces | `listOfNotNull(title(), summary())` NL |
| 30315 | StatusEvent | nip38UserStatus | `content` |
| 30382 | ContactCardEvent | nip85TrustedAssertions/users | `(listOfNotNull(petName(), summary()) + topics())` NL — public tags only, never the NIP-44 content |
| 30315 | UserStatusEvent | nip38UserStatus | `content` |
| 30382 | UserAssertionEvent | nip85TrustedAssertions/users | `(listOfNotNull(petName(), summary()) + topics())` NL — public tags only, never the NIP-44 content |
| 30392 | UserTrustedListEvent | experimental/trustedLists/users | inherited `TrustedListEvent`: `title() ?: ""` — the label only; `metric`/`d` are machine ids and `content` is a JSON echo of the membership |
| 30393 | EventTrustedListEvent | experimental/trustedLists/events | inherited `TrustedListEvent`: `title() ?: ""` |
| 30394 | AddressableTrustedListEvent | experimental/trustedLists/addressables | inherited `TrustedListEvent`: `title() ?: ""` |
@@ -111,7 +111,7 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`.
| 30617 | GitRepositoryEvent | nip34Git/repository | `listOfNotNull(name(), description(), content)` NL |
| 30620 | WorkflowDefEvent | buzz/workflow | `listOfNotNull(name(), content)` NL |
| 30817 | NipTextEvent | experimental/nipsOnNostr | `listOfNotNull(title(), content)` NL |
| 30818 | WikiNoteEvent | nip54Wiki | `listOfNotNull(title(), summary(), content)` NL |
| 30818 | WikiArticleEvent | nip54Wiki | `listOfNotNull(title(), summary(), content)` NL |
| 31337 | AudioTrackEvent | experimental/audio/track | `listOfNotNull(subject())` NL |
| 31871 | AttestationEvent | experimental/attestations/attestation | `content` |
| 31872 | AttestationRequestEvent | experimental/attestations/request | `content` |
@@ -119,15 +119,15 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`.
| 31890 | FeedDefinitionEvent | feedDefinition | `title().orEmpty()` |
| 31922 | CalendarDateSlotEvent | nip52Calendar/appt/day | `listOfNotNull(title(), summary(), content)` NL |
| 31923 | CalendarTimeSlotEvent | nip52Calendar/appt/time | `listOfNotNull(title(), summary(), content)` NL |
| 31924 | CalendarEvent | nip52Calendar/calendar | `listOfNotNull(title(), content)` NL |
| 31924 | CalendarCollectionEvent | nip52Calendar/calendar | `listOfNotNull(title(), content)` NL |
| 31925 | CalendarRSVPEvent | nip52Calendar/rsvp | `content` |
| 31990 | AppDefinitionEvent | nip89AppHandlers/definition | `appMetaData()?.let { listOfNotNull(it.name, it.username, it.displayName, it.about, it.nip05, it.lud06, it.lud16, it.website, it.picture, it.banner, it.image).joinToString(" ") } ?: ""` (SP) |
| 32267 | SoftwareApplicationEvent | experimental/nip82SoftwareApps/application | `listOfNotNull(name(), summary(), content)` NL |
| 33401 | ExerciseTemplateEvent | experimental/fitness/workout | `listOfNotNull(title(), content)` NL |
| 33863 | FundraiserEvent | experimental/agora | `listOfNotNull(title(), content)` NL |
| 34139 | MusicPlaylistEvent | experimental/music/playlist | `listOfNotNull(title(), description(), content)` NL |
| 34235 | VideoHorizontalEvent | nip71Video | inherited `AddressableVideoEvent`: `listOfNotNull(title(), content)` NL |
| 34236 | VideoVerticalEvent | nip71Video | inherited `AddressableVideoEvent`: `listOfNotNull(title(), content)` NL |
| 34235 | AddressableNormalVideoEvent | nip71Video | inherited `AddressableVideoEvent`: `listOfNotNull(title(), content)` NL |
| 34236 | AddressableShortVideoEvent | nip71Video | inherited `AddressableVideoEvent`: `listOfNotNull(title(), content)` NL |
| 34550 | CommunityDefinitionEvent | nip72ModCommunities/definition | `listOfNotNull(name(), description(), rules(), content)` NL |
| 35128 | NamedSiteEvent | nip5aStaticWebsites | `listOfNotNull(title(), description())` NL |
| 35129 | NamedNappletEvent | nip5dNapplets | `listOfNotNull(title(), description())` NL |
@@ -138,7 +138,7 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`.
| 38192 | Ps1SaveEvent | experimental/ps1saves | `listOfNotNull(summary(), saveTitle(), region(), filename())` NL |
| 38383 | P2POrderEvent | nip69P2pOrderEvents | `(listOfNotNull(makerName(), currency()) + paymentMethods().orEmpty()).joinToString(" ")` (SP) |
| 39000 | GroupMetadataEvent | nip29RelayGroups/metadata | `listOfNotNull(name(), about())` NL |
| 39089 | FollowListEvent | nip51Lists/followList | `listOfNotNull(title(), description())` NL |
| 39089 | StarterPackEvent | nip51Lists/starterPack | `listOfNotNull(title(), description())` NL |
| 39092 | MediaStarterPackEvent | nip51Lists/mediaStarterPack | `listOfNotNull(title(), description())` NL |
| 39307 | TextTrackEvent | nip71Video/textTrack | `content` (the WebVTT cue text) |
| 39701 | WebBookmarkEvent | nipB0WebBookmarks | `listOfNotNull(title(), description())` NL |
@@ -150,10 +150,10 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`.
| 45003 | ForumCommentEvent | buzz/forum | `content` |
| 48106 | HuddleGuidelinesEvent | buzz/huddles | `content` |
† **Kind 30063 collision:** `experimental/nip82SoftwareApps/release/SoftwareReleaseEvent` also
declares `KIND = 30063` and implements `SearchableEvent` (`content`), but `EventFactory` maps
30063 to `ReleaseArtifactSetEvent`, so on every store path kind 30063 indexes
`title()\ndescription()`. If the factory mapping ever changes, this table changes with it.
† **Kind 30063 is shared** by NIP-51 release artifact sets and NIP-82 software releases, and
`ReleaseArtifactSetEvent` parses both. It indexes `title()` and `description()`, plus `content`
(the release notes) only when the event carries the NIP-82 `i` + `version` tags — a NIP-51 set
may hold encrypted private items in `content`, which must never be indexed.
## Abstract bases (no kind of their own)
@@ -68,7 +68,7 @@ The NIP-42 plumbing and a first-cut permission gate are already in place:
| Settings screen (global policy + per-relay list) | `amethyst/.../ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt` |
| Global policy setting, persisted local-only | `AccountSettings.defaultRelayAuthPolicy`, `LocalPreferences` key `DEFAULT_RELAY_AUTH_POLICY` |
| Blocked-relay list (kind 10006) | `amethyst/.../model/nip51Lists/blockedRelays/BlockedRelayListState.kt` (`.flow`) |
| Follow checks | `Account.isFollowing(...)`, `Account.allFollows.flow.value.authors`, `FollowListsState.isUserInFollowSets(...)` |
| Follow checks | `Account.isFollowing(...)`, `Account.allFollows.flow.value.authors`, `StarterPacksState.isUserInFollowSets(...)` |
| DM / NIP-65 relay lookups | `DmRelayListState`, `Nip65RelayListState` (+ per-user via `LocalCache`) |
## The gap
@@ -180,7 +180,7 @@ class RelayAuthContext(val relayUrl: String, val purposes: List<AuthPurpose>)
- `IF_IN_MY_LIST` → `ALLOW` if relay ∈ my relay lists, else fall through
- `TRUSTED_FOLLOWS` *(new — see idea A below)* → `ALLOW` if relay ∈ my lists
**or** any counterparty in `ctx.purposes` is followed (`Account.allFollows`
/ `FollowListsState.isUserInFollowSets`) and the purpose permits it; else
/ `StarterPacksState.isUserInFollowSets`) and the purpose permits it; else
fall through.
4. **Fall-through**: `ASK` if the purpose is attributable (we can show a reason);
otherwise `DENY` silently (don't prompt for anonymous stranger challenges).
@@ -392,11 +392,11 @@ can be a group event if it carries `h` and the relay accepts the kind."
has:
- `metadata/` — `GroupMetadataEvent` (kind **39000**), `GroupAdminsEvent`
(39001), `GroupMembersEvent` (39002), `SupportedRolesEvent` (39003).
- `moderation/` — `CreateGroupEvent` (9007), `EditMetadataEvent` (9002),
`PutUserEvent` (9000), `RemoveUserEvent` (9001), `DeleteEventEvent` (9005),
`DeleteGroupEvent` (9008), `CreateInviteEvent` (9009), plus tag helpers.
- `request/` — `JoinRequestEvent` (9021), `LeaveRequestEvent` (9022).
(39001), `GroupMembersEvent` (39002), `GroupRolesEvent` (39003).
- `moderation/` — `CreateGroupEvent` (9007), `GroupEditMetadataEvent` (9002),
`GroupPutUserEvent` (9000), `GroupRemoveUserEvent` (9001), `GroupDeleteEventEvent` (9005),
`DeleteGroupEvent` (9008), `GroupCreateInviteEvent` (9009), plus tag helpers.
- `request/` — `GroupJoinRequestEvent` (9021), `GroupLeaveRequestEvent` (9022).
- `tags/` — `GroupIdTag` (the `h` tag), `CodeTag`, `GroupAdminTag`, `RoleTag`,
and even a `PreviousTag`.
@@ -427,7 +427,7 @@ NIP-04/17 DMs, NIP-28 public channels, and NIP-C7 ephemeral chats — all under
### 5.2 The main protocol gap
There is **no kind-9 group chat message event** in `nip29RelayGroups/`
(`CreateInviteEvent` at 9009 is the highest kind present; nothing for kind 9).
(`GroupCreateInviteEvent` at 9009 is the highest kind present; nothing for kind 9).
The 9xxx moderation, 39xxx metadata, and 9021/9022 request events exist, but the
actual message carrier does not. This is the first thing to build:
@@ -71,7 +71,7 @@ Feed-level:
marker update (only the newest visible timestamp matters; the marker is
monotonic). One coroutine per scroll session instead of one per row.
6. **Jumbo without a coroutine (#4)**: only launch the decrypt effect for
encrypted kinds (`PrivateDmEvent`, sealed rumors not yet in the decrypt
encrypted kinds (`EncryptedDmEvent`, sealed rumors not yet in the decrypt
cache). Plaintext kinds (public chats, NIP-17 rumors already unwrapped —
the vast majority) take the synchronous path only.
7. **Retire settled delivery ticks (#6)**: once a message is fully accepted (or
@@ -55,7 +55,7 @@ Bitchat's Nostr side has two chat features. Amethyst is a pure-Nostr client
1. **Encrypted DMs (Phase 2).** Port the `bitchat1:` binary packet
(`BitchatPacket` TLV + `NoisePayloadType`) into quartz, wrap/unwrap it in the
existing NIP-17 stack (`GiftWrapEvent`/`SealedRumorEvent`/`ChatMessageEvent`),
existing NIP-17 stack (`GiftWrapEvent`/`SealEvent`/`ChatMessageEvent`),
handle geohash DMs (to a per-geohash pubkey) and delivery/read receipts.
Add `amy geochat dm` for interop testing.
2. **Desktop UI.** The shared pieces (quartz events, `GeoRelayDirectory`) are
@@ -178,10 +178,10 @@ This keeps the *policy* (who gets notified) exactly where it is and isolates the
| Kind(s) | Category | Style | Accent / icon | Title → Body | Actions |
| --- | --- | --- | --- | --- | --- |
| NIP-17 `ChatMessageEvent` (14), file (15), NIP-04 `PrivateDmEvent` (4), Marmot group | DIRECT_MESSAGE | `MessagingStyle` + Conversation shortcut + Bubble | blue-green / `chat` | sender → message | Reply, Mark-read |
| NIP-17 `ChatMessageEvent` (14), file (15), NIP-04 `EncryptedDmEvent` (4), Marmot group | DIRECT_MESSAGE | `MessagingStyle` + Conversation shortcut + Bubble | blue-green / `chat` | sender → message | Reply, Mark-read |
| `TextNoteEvent`(1)/`CommentEvent`(1111)/`ChannelMessageEvent`(42) **reply to me** | REPLY | `MessagingStyle` (parent as prior message) | purple / `reply` | "X replied" → excerpt (+ quoted parent) | Reply, View thread, Mute thread |
| Same kinds, **mention/quote only** | MENTION | plain (BigText) | purple / `alternate_email` | "X mentioned you" → excerpt | View, Mute thread |
| `LnZapEvent`(9735) | ZAP | **colorized** amount card (BigText) | gold / `bolt` | "⚡ 2,100 sats from X" → zap comment + zapped-note excerpt | Zap back, View |
| `ZapReceiptEvent`(9735) | ZAP | **colorized** amount card (BigText) | gold / `bolt` | "⚡ 2,100 sats from X" → zap comment + zapped-note excerpt | Zap back, View |
| `NutzapEvent`(9321) *(new)* | ZAP | colorized amount card | gold / `bolt` (cashu tint) | "🥜 X nutzapped you 2,100 sats" → … | Zap back, View |
| `OnchainZapEvent`(8333) *(new)* | ZAP | colorized amount card | gold / `bolt` | "⛓ X sent an onchain zap" → … | View |
| `ReactionEvent`(7) | REACTION | aggregated `InboxStyle` | heart-red / `favorite` | "🤙 X & N others liked your post" → note excerpt | Like back, View |
@@ -189,7 +189,7 @@ This keeps the *policy* (who gets notified) exactly where it is and isolates the
| `PictureEvent`(20)/`VideoNormal`/`Short`/`Vertical`/`Horizontal` | MEDIA | `BigPictureStyle` | purple / `image` | "X shared a photo/video" → caption, image inline | View |
| `PollEvent`/`ZapPollEvent` | MENTION | plain | purple / `ballot` | "X asked a question" → poll title | Vote, View |
| `HighlightEvent`(9802) | ARTICLE | plain | purple / `format_quote` | "X highlighted your article" → highlighted text | View |
| `LongTextNoteEvent`(30023)/`WikiNoteEvent`(30818) | ARTICLE | plain | purple / `article` | "X mentioned you in an article" → title | View |
| `LongFormContentEvent`(30023)/`WikiArticleEvent`(30818) | ARTICLE | plain | purple / `article` | "X mentioned you in an article" → title | View |
| `GitIssueEvent`/`GitPatchEvent`/`GitPullRequestEvent`/`…Update` | CODE | plain | slate / `merge` | "X opened an issue/PR" → subject | View |
| `BadgeAwardEvent`(8) *(new)* | BADGE | `BigPictureStyle` (badge art) | gold / `award_star` | "You earned a badge" → badge name, image | View |
| `LiveChessGameAcceptEvent`/`LiveChessMoveEvent` | CHESS | plain | brown / `chess` | "X accepted your challenge" / "your turn" | Open board |
+1 -1
View File
@@ -59,7 +59,7 @@ and dispatches the decrypted `Response`, so a new method needs no changes there:
decrypt-on-arrival.
- `NwcPaymentTracker` (`commons/…/service/nwc/`) — request↔response match with the
author-spoof gate.
- `LocalCache.consume(LnZapPaymentResponseEvent)` — routes 23195 back to the callback.
- `LocalCache.consume(NwcResponseEvent)` — routes 23195 back to the callback.
- Wallet storage: `AccountSettings.nwcWallets` + `defaultPaymentSourceId`;
`PaymentSourceResolver`.
- Pay rail entry: `ZapPaymentHandler.zap()` → `payViaNWC()` (the `PaymentSource.Nwc`
@@ -41,9 +41,9 @@ storm.
|---|---|
| The chip row + "Add" chip | `ui/note/creators/notify/Notifying.kt` (`Notifying`, `NotifyUserChip`, `AddUserChip`) |
| Audience state | `ShortNotePostViewModel.pTags: List<User>?`, `mutedNotifies: Set<HexKey>`, `activeNotifies()`, `addToReplyList(user)` |
| My NIP-51 people lists (kind 30000, public **and** decrypted private members) | `account.peopleLists.uiListFlow: StateFlow<List<PeopleList>>` (`model/nip51Lists/peopleList/PeopleListsState.kt`) |
| My follow packs (kind 39089, public members) | `account.followLists.uiListFlow` (`model/nip51Lists/peopleList/FollowListsState.kt`) |
| `PeopleList` UI model (`title`, `image`, `publicMembers`, `privateMembers` as `Set<User>`) | `model/nip51Lists/peopleList/PeopleList.kt` |
| My NIP-51 people lists (kind 30000, public **and** decrypted private members) | `account.peopleLists.uiListFlow: StateFlow<List<PeopleList>>` (`model/nip51Lists/followSets/FollowSetsState.kt`) |
| My follow packs (kind 39089, public members) | `account.followLists.uiListFlow` (`model/nip51Lists/followSets/StarterPacksState.kt`) |
| `PeopleList` UI model (`title`, `image`, `publicMembers`, `privateMembers` as `Set<User>`) | `model/nip51Lists/followSets/PeopleList.kt` |
| Two-column list catalog rendering | `ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserView.kt` — same "Follow sets" + "Discover follows" sectioning |
| Multi-select member review (count header, select-all checkbox, per-user row, confirm button) | `ui/screen/loggedIn/newUser/ImportFollowListPickFollowsScreen.kt` (`PreviewList` / `FollowEntryRow`) |
| Bottom-sheet picker shell w/ search field | `ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt` |
@@ -281,7 +281,7 @@ In `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt`
```kotlin
import com.vitorpamplona.quartz.nip90Dvms.dvmHeartbeat.DvmHeartbeatEvent
```
2. Add a dispatch branch inside the `when (kind)` near the other NIP-90 kinds (next to line 736 `NIP90StatusEvent.KIND -> ...`):
2. Add a dispatch branch inside the `when (kind)` near the other NIP-90 kinds (next to line 736 `DvmStatusEvent.KIND -> ...`):
```kotlin
DvmHeartbeatEvent.KIND -> DvmHeartbeatEvent(id, pubKey, createdAt, tags, content, sig)
```
@@ -304,7 +304,7 @@ private val knownDTagReaders =
- [ ] **Step 6: Update the design doc §2**
In `amethyst/plans/2026-09-10-dvm-heartbeat-liveness.md`, replace the §2 bullet that says the class "extends `BaseReplaceableEvent`" / "Overrides `dTag()`" with the as-built wording: extends `BaseAddressableEvent` (the codebase convention for 10xxx events with real `d` tags, e.g. `FollowListEvent`), so `dTag()`/`address()`/`addressTag()` come from the base; note `MAX_AGE_SECONDS`, `isFreshAt`, and the `knownDTagReaders` entry live in quartz too (commons imports them).
In `amethyst/plans/2026-09-10-dvm-heartbeat-liveness.md`, replace the §2 bullet that says the class "extends `BaseReplaceableEvent`" / "Overrides `dTag()`" with the as-built wording: extends `BaseAddressableEvent` (the codebase convention for 10xxx events with real `d` tags, e.g. `StarterPackEvent`), so `dTag()`/`address()`/`addressTag()` come from the base; note `MAX_AGE_SECONDS`, `isFreshAt`, and the `knownDTagReaders` entry live in quartz too (commons imports them).
- [ ] **Step 7: Run the tests to verify they pass**
@@ -45,7 +45,7 @@ feeds and the detail surface show an offline state instead.
New `quartz/.../nip90Dvms/dvmHeartbeat/DvmHeartbeatEvent.kt`:
- `class DvmHeartbeatEvent(...) : BaseAddressableEvent(...)`, `KIND = 11998` — the codebase
convention for 10xxx events with real `d` tags (e.g. `FollowListEvent`), so `dTag()` /
convention for 10xxx events with real `d` tags (e.g. `StarterPackEvent`), so `dTag()` /
`address()` / `addressTag()` come from the base. The cache address is
`Address(11998, dvmPubkey, dTag)`, the exact mirror of the announcement's
`Address(31990, dvmPubkey, dTag)`.
@@ -83,7 +83,7 @@ The listing event is mostly existing tags wearing a new kind number:
| `r` | `nip51Lists/tags/RelayTag` (`"r"`) |
| `a` | `nip01Core/tags/aTag/ATag` (curation list + found log) |
| `title`, `description` (37517) | `nip51Lists/tags/TitleTag`, `.../DescriptionTag` |
| embedded 7517 | `Event.fromJson(...)` (same trick `LnZapEvent.zapRequest()` uses) |
| embedded 7517 | `Event.fromJson(...)` (same trick `ZapReceiptEvent.zapRequest()` uses) |
| signature check | `Event.verifySignature()` in `nip01Core/crypto/EventExt.kt` |
| kind 1111 logs | `nip22Comments/CommentEvent` **as-is** — it already models `A/K/P` + `a/k/p` addressable root+parent (`rootAddress()`, `replyAddress()`, `hasRootAddress()`). Only the `t` log-type vocabulary is new. |
| required-tag gate | `containsAllTagNamesWithValues(REQUIRED_FIELDS)`, as `ClassifiedsEvent.isWellFormed()` does |
@@ -94,7 +94,7 @@ Section labels: "Bitcoin" and "Lightning".
## Subscription edits — extend existing kind lists
No new assemblers. Add `OnchainZapEvent.KIND` (8333) to the existing
`LnZapEvent.KIND` (9735) sites:
`ZapReceiptEvent.KIND` (9735) sites:
| File | Edit |
|---|---|
@@ -109,7 +109,7 @@ No new assemblers. Add `OnchainZapEvent.KIND` (8333) to the existing
## Display path — fold into `Note.zapsAmount`
Today: `LocalCache.consume(LnZapEvent)` → `Note.addZap()` → `Note.updateZapTotal()`
Today: `LocalCache.consume(ZapReceiptEvent)` → `Note.addZap()` → `Note.updateZapTotal()`
sums lightning amounts into `Note.zapsAmount`, which `ReactionsRow` /
`ObserveZapAmountText` / `SlidingAnimationAmount` render. We add onchain zap
sats to the same `Note.zapsAmount` — no UI changes required.
@@ -118,7 +118,7 @@ sats to the same `Note.zapsAmount` — no UI changes required.
- Add `var onchainZaps = mapOf<...>()` (separate map from `zaps`).
- Extend `updateZapTotal()` to add **verified** onchain sats. Unverified or
pending tx amounts are NOT counted.
- `amethyst/.../model/LocalCache.kt` (after the `consume(LnZapEvent)` block ~line 1667)
- `amethyst/.../model/LocalCache.kt` (after the `consume(ZapReceiptEvent)` block ~line 1667)
- New `consume(event: OnchainZapEvent)` handler.
- Reject self-zap.
- Enqueue verification against the configured `OnchainBackend`.
@@ -34,7 +34,7 @@ and what remains as future work.
2. **Private mute/block leak via `identity.getMutes`/`getBlocked`.** These read
`muteList.flow` / `blockPeopleList.flow`, which contain **decrypted private** entries.
Now they read the events' **public** tags only (`MuteListEvent.publicMutes()`,
`PeopleListEvent.publicUsersIdSet()`).
`FollowSetEvent.publicUsersIdSet()`).
3. **Silent "allow-always" actions + UI-redress.** Added persistent **trusted chrome**
(a sandbox bar the applet can't draw over: shield + name + tap-to-see "what it can
@@ -78,20 +78,20 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
@@ -952,15 +952,15 @@ object LocalPreferences {
LatestEventSlot.NIP65_RELAY_LIST to settings.backupNIP65RelayList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.SEARCH_RELAY_LIST to settings.backupSearchRelayList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.INDEX_RELAY_LIST to settings.backupIndexRelayList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.RELAY_FEEDS_LIST to settings.backupRelayFeedsList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.RELAY_FEEDS_LIST to settings.backupFavoriteRelayList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.BLOCKED_RELAY_LIST to settings.backupBlockedRelayList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.TRUSTED_RELAY_LIST to settings.backupTrustedRelayList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.MUTE_LIST to settings.backupMuteList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.PRIVATE_HOME_RELAY_LIST to settings.backupPrivateHomeRelayList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.APP_SPECIFIC_DATA to settings.backupAppSpecificData?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.CHANNEL_LIST to settings.backupChannelList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.CHANNEL_LIST to settings.backupPublicChatList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.COMMUNITY_LIST to settings.backupCommunityList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.HASHTAG_LIST to settings.backupHashtagList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.HASHTAG_LIST to settings.backupInterestList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.GEOHASH_LIST to settings.backupGeohashList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.EPHEMERAL_LIST to settings.backupEphemeralChatList?.let { OptimizedJsonMapper.toJson(it) },
LatestEventSlot.RELAY_GROUP_LIST to settings.backupRelayGroupList?.let { OptimizedJsonMapper.toJson(it) },
@@ -1222,15 +1222,15 @@ object LocalPreferences {
val latestNip65RelayListStr = stores.latestEvents[LatestEventSlot.NIP65_RELAY_LIST]
val latestSearchRelayListStr = stores.latestEvents[LatestEventSlot.SEARCH_RELAY_LIST]
val latestIndexRelayListStr = stores.latestEvents[LatestEventSlot.INDEX_RELAY_LIST]
val latestRelayFeedsListStr = stores.latestEvents[LatestEventSlot.RELAY_FEEDS_LIST]
val latestFavoriteRelayListStr = stores.latestEvents[LatestEventSlot.RELAY_FEEDS_LIST]
val latestBlockedRelayListStr = stores.latestEvents[LatestEventSlot.BLOCKED_RELAY_LIST]
val latestTrustedRelayListStr = stores.latestEvents[LatestEventSlot.TRUSTED_RELAY_LIST]
val latestMuteListStr = stores.latestEvents[LatestEventSlot.MUTE_LIST]
val latestPrivateHomeRelayListStr = stores.latestEvents[LatestEventSlot.PRIVATE_HOME_RELAY_LIST]
val latestAppSpecificDataStr = stores.latestEvents[LatestEventSlot.APP_SPECIFIC_DATA]
val latestChannelListStr = stores.latestEvents[LatestEventSlot.CHANNEL_LIST]
val latestPublicChatListStr = stores.latestEvents[LatestEventSlot.CHANNEL_LIST]
val latestCommunityListStr = stores.latestEvents[LatestEventSlot.COMMUNITY_LIST]
val latestHashtagListStr = stores.latestEvents[LatestEventSlot.HASHTAG_LIST]
val latestInterestListStr = stores.latestEvents[LatestEventSlot.HASHTAG_LIST]
val latestGeohashListStr = stores.latestEvents[LatestEventSlot.GEOHASH_LIST]
val latestEphemeralListStr = stores.latestEvents[LatestEventSlot.EPHEMERAL_LIST]
val latestRelayGroupListStr = stores.latestEvents[LatestEventSlot.RELAY_GROUP_LIST]
@@ -1282,19 +1282,19 @@ object LocalPreferences {
val pendingAttestations = async { parseOrNull<Map<HexKey, String>>(pendingAttestationsStr) ?: mapOf() }
val latestUserMetadata = async { parseEventOrNull<MetadataEvent>(latestUserMetadataStr) }
val latestContactList = async { parseEventOrNull<ContactListEvent>(latestContactListStr) }
val latestDmRelayList = async { parseEventOrNull<ChatMessageRelayListEvent>(latestDmRelayListStr) }
val latestDmRelayList = async { parseEventOrNull<DmRelayListEvent>(latestDmRelayListStr) }
val latestNip65RelayList = async { parseEventOrNull<AdvertisedRelayListEvent>(latestNip65RelayListStr) }
val latestSearchRelayList = async { parseEventOrNull<SearchRelayListEvent>(latestSearchRelayListStr) }
val latestIndexRelayList = async { parseEventOrNull<IndexerRelayListEvent>(latestIndexRelayListStr) }
val latestRelayFeedsList = async { parseEventOrNull<RelayFeedsListEvent>(latestRelayFeedsListStr) }
val latestFavoriteRelayList = async { parseEventOrNull<FavoriteRelayListEvent>(latestFavoriteRelayListStr) }
val latestBlockedRelayList = async { parseEventOrNull<BlockedRelayListEvent>(latestBlockedRelayListStr) }
val latestTrustedRelayList = async { parseEventOrNull<TrustedRelayListEvent>(latestTrustedRelayListStr) }
val latestMuteList = async { parseEventOrNull<MuteListEvent>(latestMuteListStr) }
val latestPrivateHomeRelayList = async { parseEventOrNull<PrivateOutboxRelayListEvent>(latestPrivateHomeRelayListStr) }
val latestAppSpecificData = async { parseEventOrNull<AppSpecificDataEvent>(latestAppSpecificDataStr) }
val latestChannelList = async { parseEventOrNull<ChannelListEvent>(latestChannelListStr) }
val latestPublicChatList = async { parseEventOrNull<PublicChatListEvent>(latestPublicChatListStr) }
val latestCommunityList = async { parseEventOrNull<CommunityListEvent>(latestCommunityListStr) }
val latestHashtagList = async { parseEventOrNull<HashtagListEvent>(latestHashtagListStr) }
val latestInterestList = async { parseEventOrNull<InterestListEvent>(latestInterestListStr) }
val latestGeohashList = async { parseEventOrNull<GeohashListEvent>(latestGeohashListStr) }
val latestEphemeralList = async { parseEventOrNull<EphemeralChatListEvent>(latestEphemeralListStr) }
val latestRelayGroupList = async { parseEventOrNull<SimpleGroupListEvent>(latestRelayGroupListStr) }
@@ -1342,15 +1342,15 @@ object LocalPreferences {
val latestNip65RelayListResolved = latestNip65RelayList.await()
val latestSearchRelayListResolved = latestSearchRelayList.await()
val latestIndexRelayListResolved = latestIndexRelayList.await()
val latestRelayFeedsListResolved = latestRelayFeedsList.await()
val latestFavoriteRelayListResolved = latestFavoriteRelayList.await()
val latestBlockedRelayListResolved = latestBlockedRelayList.await()
val latestTrustedRelayListResolved = latestTrustedRelayList.await()
val latestMuteListResolved = latestMuteList.await()
val latestPrivateHomeRelayListResolved = latestPrivateHomeRelayList.await()
val latestAppSpecificDataResolved = latestAppSpecificData.await()
val latestChannelListResolved = latestChannelList.await()
val latestPublicChatListResolved = latestPublicChatList.await()
val latestCommunityListResolved = latestCommunityList.await()
val latestHashtagListResolved = latestHashtagList.await()
val latestInterestListResolved = latestInterestList.await()
val latestGeohashListResolved = latestGeohashList.await()
val latestEphemeralListResolved = latestEphemeralList.await()
val latestRelayGroupListResolved = latestRelayGroupList.await()
@@ -1444,15 +1444,15 @@ object LocalPreferences {
backupDMRelayList = latestDmRelayListResolved,
backupSearchRelayList = latestSearchRelayListResolved,
backupIndexRelayList = latestIndexRelayListResolved,
backupRelayFeedsList = latestRelayFeedsListResolved,
backupFavoriteRelayList = latestFavoriteRelayListResolved,
backupBlockedRelayList = latestBlockedRelayListResolved,
backupTrustedRelayList = latestTrustedRelayListResolved,
backupPrivateHomeRelayList = latestPrivateHomeRelayListResolved,
backupMuteList = latestMuteListResolved,
backupAppSpecificData = latestAppSpecificDataResolved,
backupChannelList = latestChannelListResolved,
backupPublicChatList = latestPublicChatListResolved,
backupCommunityList = latestCommunityListResolved,
backupHashtagList = latestHashtagListResolved,
backupInterestList = latestInterestListResolved,
backupGeohashList = latestGeohashListResolved,
backupEphemeralChatList = latestEphemeralListResolved,
backupRelayGroupList = latestRelayGroupListResolved,
@@ -75,21 +75,21 @@ import com.vitorpamplona.amethyst.commons.model.nip51Lists.blockPeopleList.Block
import com.vitorpamplona.amethyst.commons.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteRelays.FavoriteRelayListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.FollowSetDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.geohashLists.GeohashListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.indexerRelays.IndexerRelayListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestLists.InterestListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.muteList.MuteListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.proxyRelays.ProxyRelayListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.relayFeeds.RelayFeedsListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.searchRelays.SearchRelayListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.trustedRelays.TrustedRelayListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip56Reports.ReportAction
import com.vitorpamplona.amethyst.commons.model.nip62Vanish.VanishRequestsState
import com.vitorpamplona.amethyst.commons.model.nip72Communities.CommunityListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.ContactCardDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.ContactCardsState
import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.TrustProviderListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.UserAssertionDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.UserAssertionsState
import com.vitorpamplona.amethyst.commons.model.nipBCOnchainZaps.OnchainWalletState
import com.vitorpamplona.amethyst.commons.model.privateChatLastReadRoute
import com.vitorpamplona.amethyst.commons.model.privateChats.hasEncryptedContent
@@ -145,18 +145,18 @@ import com.vitorpamplona.amethyst.model.nip46Signer.Nip46SignerState
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.bookmarkSets.BookmarkSetsState
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListState
import com.vitorpamplona.amethyst.model.nip51Lists.favoriteRelays.FavoriteRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetsState
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.StarterPacksState
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState
import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState
import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.interestLists.InterestListState
import com.vitorpamplona.amethyst.model.nip51Lists.interestSets.InterestSetsState
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkListsState
import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListState
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.FollowListsState
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListsState
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.relayFeeds.RelayFeedListState
import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
@@ -238,8 +238,8 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
import com.vitorpamplona.quartz.nip01Core.tags.references.references
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris
@@ -281,10 +281,10 @@ import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.PrivateZapCache
import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
@@ -320,10 +320,10 @@ import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollType
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.NostrSignerWithClientTag
import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.withoutClientTag
import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent
import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.DvmContentDiscoveryRequestEvent
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
import com.vitorpamplona.quartz.nip92IMeta.imetas
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent
import com.vitorpamplona.quartz.nip94FileMetadata.blurhash
import com.vitorpamplona.quartz.nip94FileMetadata.dimension
import com.vitorpamplona.quartz.nip94FileMetadata.fileSize
@@ -639,8 +639,8 @@ class Account(
val indexerRelayListDecryptionCache = IndexerRelayListDecryptionCache(signer)
val indexerRelayList = IndexerRelayListState(signer, cache, indexerRelayListDecryptionCache, scope, settings)
val relayFeedsListDecryptionCache = RelayFeedsListDecryptionCache(signer)
val relayFeedsList = RelayFeedListState(signer, cache, relayFeedsListDecryptionCache, scope, settings)
val favoriteRelayListDecryptionCache = FavoriteRelayListDecryptionCache(signer)
val favoriteRelayList = FavoriteRelayListState(signer, cache, favoriteRelayListDecryptionCache, scope, settings)
val blockedRelayListDecryptionCache = BlockedRelayListDecryptionCache(signer)
val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings)
@@ -758,8 +758,8 @@ class Account(
val communityListDecryptionCache = CommunityListDecryptionCache(signer)
val communityList = CommunityListState(signer, cache, communityListDecryptionCache, scope, settings)
val hashtagListDecryptionCache = HashtagListDecryptionCache(signer)
val hashtagList = HashtagListState(signer, cache, hashtagListDecryptionCache, scope, settings)
val interestListDecryptionCache = InterestListDecryptionCache(signer)
val interestList = InterestListState(signer, cache, interestListDecryptionCache, scope, settings)
val favoriteAlgoFeedsListDecryptionCache = FavoriteAlgoFeedsListDecryptionCache(signer)
val favoriteAlgoFeedsList = FavoriteAlgoFeedsListState(signer, cache, favoriteAlgoFeedsListDecryptionCache, scope, settings)
@@ -777,14 +777,14 @@ class Account(
val trustProviderListDecryptionCache = TrustProviderListDecryptionCache(signer)
val trustProviderList = TrustProviderListState(signer, cache, trustProviderListDecryptionCache, scope, settings)
val peopleListDecryptionCache = PeopleListDecryptionCache(signer)
val blockPeopleList = BlockPeopleListState(signer, cache, peopleListDecryptionCache, scope)
val peopleLists = PeopleListsState(signer, cache, peopleListDecryptionCache, scope)
val followLists = FollowListsState(signer, cache, scope)
val followSetDecryptionCache = FollowSetDecryptionCache(signer)
val blockPeopleList = BlockPeopleListState(signer, cache, followSetDecryptionCache, scope)
val followSets = FollowSetsState(signer, cache, followSetDecryptionCache, scope)
val starterPacks = StarterPacksState(signer, cache, scope)
val hiddenUsers = HiddenUsersState(muteList.flow, blockPeopleList.flow, scope, settings)
val labeledBookmarkLists = LabeledBookmarkListsState(signer, cache, scope)
val bookmarkSets = BookmarkSetsState(signer, cache, scope)
val interestSets = InterestSetsState(signer, cache, scope)
val appRecommendations = AppRecommendationsState(signer, cache, scope)
val oldBookmarkState = OldBookmarkListState(signer, cache, scope)
@@ -795,8 +795,8 @@ class Account(
val ownedEmojiPacks = OwnedEmojiPacksState(signer, cache, scope)
// needs `emoji` above: nickname edits resolve :shortcodes: against the account's packs
val contactCardDecryptionCache = ContactCardDecryptionCache(signer)
val contactCards = ContactCardsState(signer, cache, contactCardDecryptionCache, emoji)
val userAssertionDecryptionCache = UserAssertionDecryptionCache(signer)
val userAssertions = UserAssertionsState(signer, cache, userAssertionDecryptionCache, emoji)
val vanish = VanishRequestsState(signer, cache, client, scope)
@@ -893,7 +893,7 @@ class Account(
val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope).flow
// Merges all follow lists to create a single All Follows feed.
val allFollows = MergedFollowListsState(kind3FollowList, peopleLists, followLists, hashtagList, geohashList, communityList, scope)
val allFollows = MergedFollowListsState(kind3FollowList, followSets, starterPacks, interestList, geohashList, communityList, scope)
val privateDMDecryptionCache = PrivateDMCache(signer)
override val privateZapsDecryptionCache = PrivateZapCache(signer)
@@ -1030,10 +1030,10 @@ class Account(
val feedDecryptionCaches =
FeedDecryptionCaches(
peopleListCache = peopleListDecryptionCache,
peopleListCache = followSetDecryptionCache,
muteListCache = muteListDecryptionCache,
communityListCache = communityListDecryptionCache,
hashtagCache = hashtagListDecryptionCache,
hashtagCache = interestListDecryptionCache,
geohashCache = geohashListDecryptionCache,
)
@@ -1047,7 +1047,7 @@ class Account(
blockedRelays = blockedRelayList.flow,
proxyRelays = proxyRelayList.flow,
mineRelays = mineRelays.flow,
relayFeeds = relayFeedsList.flow,
relayFeeds = favoriteRelayList.flow,
caches = feedDecryptionCaches,
signer = signer,
scope = scope,
@@ -1283,7 +1283,7 @@ class Account(
suspend fun updateZapAmounts(
amountSet: List<Long>,
selectedZapType: LnZapEvent.ZapType,
selectedZapType: ZapReceiptEvent.ZapType,
nip47Update: Nip47WalletConnect.Nip47URINorm?,
) {
var changed = false
@@ -1707,7 +1707,7 @@ class Account(
if (myNotes.isNotEmpty()) {
// chunks in 200 elements to avoid going over the 65KB limit for events.
myNotes.chunked(200).forEach { chunkedList ->
val template = DeletionEvent.build(chunkedList.mapNotNull { it.event })
val template = DeletionRequestEvent.build(chunkedList.mapNotNull { it.event })
val deletionEvent = signer.sign(template)
val myRelayList = outboxRelays.flow.value.toMutableSet()
chunkedList.forEach {
@@ -1738,7 +1738,7 @@ class Account(
val recipients = (targetEvent.taggedUserIds() + targetEvent.pubKey).distinct().minus(signer.pubKey)
broadcastPrivately(
NIP17Factory().createDeletionNIP17(DeletionEvent.build(myRumors), recipients, signer),
NIP17Factory().createDeletionNIP17(DeletionRequestEvent.build(myRumors), recipients, signer),
)
}
@@ -1749,7 +1749,7 @@ class Account(
if (!isWriteable()) return
if (event.pubKey != signer.pubKey) return
val deletionEvent = signer.sign(DeletionEvent.build(listOf(event)))
val deletionEvent = signer.sign(DeletionRequestEvent.build(listOf(event)))
client.publish(deletionEvent, outboxRelays.flow.value + additionalRelays)
cache.justConsumeMyOwnEvent(deletionEvent)
}
@@ -2126,9 +2126,9 @@ class Account(
suspend fun unfollow(community: AddressableNote) = sendMyPublicAndPrivateOutbox(communityList.unfollow(community))
suspend fun followHashtag(tag: String) = sendMyPublicAndPrivateOutbox(hashtagList.follow(tag))
suspend fun followHashtag(tag: String) = sendMyPublicAndPrivateOutbox(interestList.follow(tag))
suspend fun unfollowHashtag(tag: String) = sendMyPublicAndPrivateOutbox(hashtagList.unfollow(tag))
suspend fun unfollowHashtag(tag: String) = sendMyPublicAndPrivateOutbox(interestList.unfollow(tag))
suspend fun followFavoriteAlgoFeed(dvm: AddressBookmark) = sendMyPublicAndPrivateOutbox(favoriteAlgoFeedsList.follow(dvm))
@@ -2176,7 +2176,7 @@ class Account(
suspend fun deleteWebBookmark(event: WebBookmarkEvent) {
if (!isWriteable()) return
val template = DeletionEvent.build(listOf(event))
val template = DeletionRequestEvent.build(listOf(event))
val signedEvent = signer.sign(template)
cache.justConsumeMyOwnEvent(signedEvent)
@@ -2212,7 +2212,7 @@ class Account(
if (!isWriteable()) return
if (event.pubKey != signer.pubKey) return
val template = DeletionEvent.build(listOf(event))
val template = DeletionRequestEvent.build(listOf(event))
val signedEvent = signer.sign(template)
cache.justConsumeMyOwnEvent(signedEvent)
@@ -2597,7 +2597,7 @@ class Account(
}
}
} else {
FileHeaderEvent.build(url, alt) {
FileMetadataEvent.build(url, alt) {
hash(headerInfo.hash)
fileSize(headerInfo.size)
@@ -2711,7 +2711,7 @@ class Account(
val extraRelays = draftNote.relays
val deletedDraft = DraftWrapEvent.createDeletedEvent(draftTag, signer)
val deletionEvent = signer.sign(DeletionEvent.build(listOf(deletedDraft)))
val deletionEvent = signer.sign(DeletionRequestEvent.build(listOf(deletedDraft)))
val relayList = (privateStorageRelayList.flow.value + localRelayList.flow.value + extraRelays).toSet()
@@ -2937,7 +2937,7 @@ class Account(
broadcast.forEach { client.publish(it, relayList) }
}
override suspend fun sendNip04PrivateMessage(eventTemplate: EventTemplate<PrivateDmEvent>) {
override suspend fun sendNip04PrivateMessage(eventTemplate: EventTemplate<EncryptedDmEvent>) {
if (!isWriteable()) return
val newEvent = signer.sign(eventTemplate)
@@ -3440,11 +3440,11 @@ class Account(
* encrypted in the content. `null` clears a field. Goes out through the
* account's extended outbox relays.
*/
suspend fun updateContactCardPetName(
suspend fun updateUserAssertionPetName(
pubkeyHex: HexKey,
petName: String?,
summary: String?,
) = sendMyPublicAndPrivateOutbox(contactCards.updatePetNameAndSummary(pubkeyHex, petName, summary))
) = sendMyPublicAndPrivateOutbox(userAssertions.updatePetNameAndSummary(pubkeyHex, petName, summary))
suspend fun showUser(pubkeyHex: HexKey) {
sendMyPublicAndPrivateOutbox(blockPeopleList.showUser(pubkeyHex))
@@ -3481,10 +3481,10 @@ class Account(
suspend fun requestDVMContentDiscovery(
dvmPublicKey: User,
onReady: (event: NIP90ContentDiscoveryRequestEvent, relays: Set<NormalizedRelayUrl>) -> Unit,
onReady: (event: DvmContentDiscoveryRequestEvent, relays: Set<NormalizedRelayUrl>) -> Unit,
) {
val relays = nip65RelayList.inboxFlow.value.toSet()
val request = signer.sign<NIP90ContentDiscoveryRequestEvent>(NIP90ContentDiscoveryRequestEvent.build(dvmPublicKey.pubkeyHex, signer.pubKey, relays))
val request = signer.sign<DvmContentDiscoveryRequestEvent>(DvmContentDiscoveryRequestEvent.build(dvmPublicKey.pubkeyHex, signer.pubKey, relays))
val relayList =
dvmPublicKey.inboxRelays()?.toSet()?.ifEmpty { null }
@@ -3503,8 +3503,8 @@ class Account(
return if (isWriteable()) {
when {
event is PrivateDmEvent -> privateDMDecryptionCache.cachedDM(event)
event is LnZapRequestEvent && event.isPrivateZap() -> privateZapsDecryptionCache.cachedPrivateZap(event)?.content
event is EncryptedDmEvent -> privateDMDecryptionCache.cachedDM(event)
event is ZapRequestEvent && event.isPrivateZap() -> privateZapsDecryptionCache.cachedPrivateZap(event)?.content
event is DraftWrapEvent -> draftsDecryptionCache.preCachedDraft(event)?.content
else -> event.content
}
@@ -3519,11 +3519,11 @@ class Account(
suspend fun decryptContent(note: Note): String? {
val event = note.event
return when {
event is PrivateDmEvent && isWriteable() -> {
event is EncryptedDmEvent && isWriteable() -> {
privateDMDecryptionCache.decryptDM(event)
}
event is LnZapRequestEvent && isWriteable() -> {
event is ZapRequestEvent && isWriteable() -> {
if (event.isPrivateZap()) {
if (isWriteable()) {
privateZapsDecryptionCache.decryptPrivateZap(event)?.content
@@ -3550,7 +3550,7 @@ class Account(
}
}
suspend fun decryptZapOrNull(event: LnZapRequestEvent): LnZapPrivateEvent? = if (event.isPrivateZap() && isWriteable()) privateZapsDecryptionCache.decryptPrivateZap(event) else null
suspend fun decryptZapOrNull(event: ZapRequestEvent): PrivateZapEvent? = if (event.isPrivateZap() && isWriteable()) privateZapsDecryptionCache.decryptPrivateZap(event) else null
fun isAllHidden(users: Set<HexKey>): Boolean = users.all { isHidden(it) }
@@ -3602,7 +3602,7 @@ class Account(
note.countReportAuthorsBy(followingKeySet()) < reportWarningThreshold
}
fun isDecryptedContentHidden(noteEvent: PrivateDmEvent): Boolean =
fun isDecryptedContentHidden(noteEvent: EncryptedDmEvent): Boolean =
if (hiddenUsers.flow.value.hiddenWordsCase
.isNotEmpty()
) {
@@ -3747,11 +3747,11 @@ class Account(
suspend fun saveTrustedRelayList(trustedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(trustedRelayList.saveRelayList(trustedRelays))
suspend fun saveRelayFeedsList(trustedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(relayFeedsList.saveRelayList(trustedRelays))
suspend fun saveFavoriteRelayList(trustedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(favoriteRelayList.saveRelayList(trustedRelays))
suspend fun followRelayFeed(url: NormalizedRelayUrl) = sendMyPublicAndPrivateOutbox(relayFeedsList.addRelay(url))
suspend fun followRelayFeed(url: NormalizedRelayUrl) = sendMyPublicAndPrivateOutbox(favoriteRelayList.addRelay(url))
suspend fun unfollowRelayFeed(url: NormalizedRelayUrl) = sendMyPublicAndPrivateOutbox(relayFeedsList.removeRelay(url))
suspend fun unfollowRelayFeed(url: NormalizedRelayUrl) = sendMyPublicAndPrivateOutbox(favoriteRelayList.removeRelay(url))
suspend fun saveBlockedRelayList(blockedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(blockedRelayList.saveRelayList(blockedRelays))
@@ -3783,7 +3783,7 @@ class Account(
proxyRelayList.getProxyRelayList(),
broadcastRelayList.getBroadcastRelayList(),
indexerRelayList.getIndexerRelayList(),
relayFeedsList.getRelayFeedsList(),
favoriteRelayList.getFavoriteRelayList(),
blockedRelayList.getBlockedRelayList(),
muteList.getMuteList(),
bookmarkState.getBookmarkList(),
@@ -4056,9 +4056,9 @@ class Account(
logTime("Account ${userProfile().toBestDisplayName()} newEventBundle Update with ${newNotes.size} new notes") {
upgradeAttestations()
newNotesPreProcessor.runNew(newNotes)
peopleLists.newNotes(newNotes)
followLists.newNotes(newNotes)
labeledBookmarkLists.newNotes(newNotes)
followSets.newNotes(newNotes)
starterPacks.newNotes(newNotes)
bookmarkSets.newNotes(newNotes)
interestSets.newNotes(newNotes)
ownedEmojiPacks.newNotes(newNotes)
}
@@ -4069,9 +4069,9 @@ class Account(
cache.live.deletedEventBundles.collect { deletedNotes ->
logTime("Account ${userProfile().toBestDisplayName()} deletedEventBundle Update with ${deletedNotes.size} new notes") {
newNotesPreProcessor.runDeleted(deletedNotes)
peopleLists.deletedNotes(deletedNotes)
followLists.deletedNotes(deletedNotes)
labeledBookmarkLists.deletedNotes(deletedNotes)
followSets.deletedNotes(deletedNotes)
starterPacks.deletedNotes(deletedNotes)
bookmarkSets.deletedNotes(deletedNotes)
interestSets.deletedNotes(deletedNotes)
ownedEmojiPacks.deletedNotes(deletedNotes)
}
@@ -58,15 +58,15 @@ import com.vitorpamplona.quartz.nip29RelayGroups.GroupId
import com.vitorpamplona.quartz.nip29RelayGroups.hTag
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateGroupEvent
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateInviteEvent
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteGroupEvent
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.PutUserEvent
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.RemoveUserEvent
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.UpdatePinListEvent
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupCreateInviteEvent
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupEditMetadataEvent
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupPutUserEvent
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupRemoveUserEvent
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupUpdatePinListEvent
import com.vitorpamplona.quartz.nip29RelayGroups.moderation.previous
import com.vitorpamplona.quartz.nip29RelayGroups.request.JoinRequestEvent
import com.vitorpamplona.quartz.nip29RelayGroups.request.LeaveRequestEvent
import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupJoinRequestEvent
import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupLeaveRequestEvent
import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupIdTag
import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent
import com.vitorpamplona.quartz.utils.RandomInstance
@@ -92,7 +92,7 @@ class AccountRelayGroupActions(
channel: RelayGroupChannel,
code: String? = null,
) {
val template = JoinRequestEvent.build(channel.groupId.id, inviteCode = code)
val template = GroupJoinRequestEvent.build(channel.groupId.id, inviteCode = code)
account.broadcaster.signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() }
account.follow(channel)
}
@@ -295,7 +295,7 @@ class AccountRelayGroupActions(
/** Send a kind 9022 leave request to the host relay and drop it from our list. */
suspend fun leaveRelayGroup(channel: RelayGroupChannel) {
val template = LeaveRequestEvent.build(channel.groupId.id)
val template = GroupLeaveRequestEvent.build(channel.groupId.id)
account.broadcaster.signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() }
account.unfollow(channel)
}
@@ -351,7 +351,7 @@ class AccountRelayGroupActions(
) { listOf(relay) }
val edit =
EditMetadataEvent.build(
GroupEditMetadataEvent.build(
groupId,
name = name,
about = about,
@@ -405,7 +405,7 @@ class AccountRelayGroupActions(
channel: RelayGroupChannel,
code: String,
) {
val template = CreateInviteEvent.build(channel.groupId.id, code)
val template = GroupCreateInviteEvent.build(channel.groupId.id, code)
account.broadcaster.signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() }
}
@@ -418,7 +418,7 @@ class AccountRelayGroupActions(
channel: RelayGroupChannel,
pinnedEventIds: List<HexKey>,
) {
val template = UpdatePinListEvent.build(channel.groupId.id, pinnedEventIds)
val template = GroupUpdatePinListEvent.build(channel.groupId.id, pinnedEventIds)
account.broadcaster.signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() }
}
@@ -445,7 +445,7 @@ class AccountRelayGroupActions(
channel: RelayGroupChannel,
pubkey: HexKey,
) {
val template = RemoveUserEvent.build(channel.groupId.id, listOf(pubkey))
val template = GroupRemoveUserEvent.build(channel.groupId.id, listOf(pubkey))
account.broadcaster.signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() }
}
@@ -470,7 +470,7 @@ class AccountRelayGroupActions(
} else {
null
}
val template = PutUserEvent.build(channel.groupId.id, listOf(pubkey to roles), buzzRole = buzzRole)
val template = GroupPutUserEvent.build(channel.groupId.id, listOf(pubkey to roles), buzzRole = buzzRole)
account.broadcaster.signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() }
}
@@ -525,7 +525,7 @@ class AccountRelayGroupActions(
// edit when we send that tag. A plain NIP-29 relay ignores it and honours the status flag.
val isBuzz = BuzzRelayDialect.isBuzz(channel.groupId.relayUrl)
val template =
EditMetadataEvent.build(
GroupEditMetadataEvent.build(
channel.groupId.id,
name = name,
about = about,
@@ -549,7 +549,7 @@ class AccountRelayGroupActions(
channel: RelayGroupChannel,
archived: Boolean,
) {
val template = EditMetadataEvent.build(channel.groupId.id, archived = archived)
val template = GroupEditMetadataEvent.build(channel.groupId.id, archived = archived)
account.broadcaster.signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() }
}
}
@@ -59,8 +59,8 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent
import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
@@ -68,16 +68,16 @@ import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletEvent
import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
@@ -203,20 +203,20 @@ class AccountSettings(
val showMessagesInNotifications: MutableStateFlow<Boolean> = MutableStateFlow(true),
var backupUserMetadata: MetadataEvent? = null,
var backupContactList: ContactListEvent? = null,
var backupDMRelayList: ChatMessageRelayListEvent? = null,
var backupDMRelayList: DmRelayListEvent? = null,
var backupKeyPackageRelayList: KeyPackageRelayListEvent? = null,
var backupNIP65RelayList: AdvertisedRelayListEvent? = null,
var backupSearchRelayList: SearchRelayListEvent? = null,
var backupIndexRelayList: IndexerRelayListEvent? = null,
var backupRelayFeedsList: RelayFeedsListEvent? = null,
var backupFavoriteRelayList: FavoriteRelayListEvent? = null,
var backupBlockedRelayList: BlockedRelayListEvent? = null,
var backupTrustedRelayList: TrustedRelayListEvent? = null,
var backupMuteList: MuteListEvent? = null,
var backupPrivateHomeRelayList: PrivateOutboxRelayListEvent? = null,
var backupAppSpecificData: AppSpecificDataEvent? = null,
var backupChannelList: ChannelListEvent? = null,
var backupPublicChatList: PublicChatListEvent? = null,
var backupCommunityList: CommunityListEvent? = null,
var backupHashtagList: HashtagListEvent? = null,
var backupInterestList: InterestListEvent? = null,
var backupFavoriteAlgoFeedsList: FavoriteAlgoFeedsListEvent? = null,
var backupGeohashList: GeohashListEvent? = null,
var backupEphemeralChatList: EphemeralChatListEvent? = null,
@@ -375,7 +375,7 @@ class AccountSettings(
// Zaps and Reactions
// ---
fun changeDefaultZapType(zapType: LnZapEvent.ZapType): Boolean {
fun changeDefaultZapType(zapType: ZapReceiptEvent.ZapType): Boolean {
if (syncedSettings.zaps.defaultZapType.value != zapType) {
syncedSettings.zaps.defaultZapType.tryEmit(zapType)
saveAccountSettings()
@@ -1075,7 +1075,7 @@ class AccountSettings(
when (incoming) {
is MetadataEvent -> updateUserMetadata(incoming)
is ContactListEvent -> updateContactListTo(incoming)
is ChatMessageRelayListEvent -> updateDMRelayList(incoming)
is DmRelayListEvent -> updateDMRelayList(incoming)
is KeyPackageRelayListEvent -> updateKeyPackageRelayList(incoming)
is AdvertisedRelayListEvent -> updateNIP65RelayList(incoming)
is CashuWalletEvent -> updateCashuWallet(incoming)
@@ -1084,13 +1084,13 @@ class AccountSettings(
is Bolt12OfferListEvent -> updateBolt12Offers(incoming)
is SearchRelayListEvent -> updateSearchRelayList(incoming)
is IndexerRelayListEvent -> updateIndexRelayList(incoming)
is RelayFeedsListEvent -> updateRelayFeedList(incoming)
is FavoriteRelayListEvent -> updateFavoriteRelayList(incoming)
is BlockedRelayListEvent -> updateBlockedRelayList(incoming)
is TrustedRelayListEvent -> updateTrustedRelayList(incoming)
is PrivateOutboxRelayListEvent -> updatePrivateHomeRelayList(incoming)
is ChannelListEvent -> updateChannelListTo(incoming)
is PublicChatListEvent -> updatePublicChatListTo(incoming)
is GeohashListEvent -> updateGeohashListTo(incoming)
is HashtagListEvent -> updateHashtagListTo(incoming)
is InterestListEvent -> updateInterestListTo(incoming)
is FavoriteAlgoFeedsListEvent -> updateFavoriteAlgoFeedsListTo(incoming)
is CommunityListEvent -> updateCommunityListTo(incoming)
is EphemeralChatListEvent -> updateEphemeralChatListTo(incoming)
@@ -1140,7 +1140,7 @@ class AccountSettings(
}
}
fun updateDMRelayList(newDMRelayList: ChatMessageRelayListEvent?) {
fun updateDMRelayList(newDMRelayList: DmRelayListEvent?) {
if (newDMRelayList == null) return
if (backupGuard.accept(backupDMRelayList, newDMRelayList, isEmpty = newDMRelayList.tags.isEmpty()) { updateDMRelayList(newDMRelayList) }) {
@@ -1285,11 +1285,11 @@ class AccountSettings(
}
}
fun updateRelayFeedList(newRelayFeedList: RelayFeedsListEvent?) {
if (newRelayFeedList == null) return
fun updateFavoriteRelayList(newFavoriteRelayList: FavoriteRelayListEvent?) {
if (newFavoriteRelayList == null) return
if (backupGuard.accept(backupRelayFeedsList, newRelayFeedList, isEmpty = newRelayFeedList.tags.isEmpty()) { updateRelayFeedList(newRelayFeedList) }) {
backupRelayFeedsList = newRelayFeedList
if (backupGuard.accept(backupFavoriteRelayList, newFavoriteRelayList, isEmpty = newFavoriteRelayList.tags.isEmpty()) { updateFavoriteRelayList(newFavoriteRelayList) }) {
backupFavoriteRelayList = newFavoriteRelayList
saveAccountSettings()
}
}
@@ -1321,13 +1321,13 @@ class AccountSettings(
}
}
override fun channelList() = backupChannelList
override fun publicChatList() = backupPublicChatList
override fun updateChannelListTo(newChannelList: ChannelListEvent?) {
if (newChannelList == null) return
override fun updatePublicChatListTo(newPublicChatList: PublicChatListEvent?) {
if (newPublicChatList == null) return
if (backupGuard.accept(backupChannelList, newChannelList, isEmpty = newChannelList.tags.isEmpty()) { updateChannelListTo(newChannelList) }) {
backupChannelList = newChannelList
if (backupGuard.accept(backupPublicChatList, newPublicChatList, isEmpty = newPublicChatList.tags.isEmpty()) { updatePublicChatListTo(newPublicChatList) }) {
backupPublicChatList = newPublicChatList
saveAccountSettings()
}
}
@@ -1341,11 +1341,11 @@ class AccountSettings(
}
}
fun updateHashtagListTo(newHashtagList: HashtagListEvent?) {
if (newHashtagList == null) return
fun updateInterestListTo(newInterestList: InterestListEvent?) {
if (newInterestList == null) return
if (backupGuard.accept(backupHashtagList, newHashtagList, isEmpty = newHashtagList.tags.isEmpty()) { updateHashtagListTo(newHashtagList) }) {
backupHashtagList = newHashtagList
if (backupGuard.accept(backupInterestList, newInterestList, isEmpty = newInterestList.tags.isEmpty()) { updateInterestListTo(newInterestList) }) {
backupInterestList = newInterestList
saveAccountSettings()
}
}
@@ -31,7 +31,7 @@ import com.vitorpamplona.amethyst.commons.service.pow.PoWCategory
import com.vitorpamplona.amethyst.commons.service.pow.PoWPolicy
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
@@ -265,7 +265,7 @@ class AccountVideoPlayerPreferences(
@Stable
class AccountZapPreferences(
var zapAmountChoices: MutableStateFlow<ImmutableList<Long>>,
val defaultZapType: MutableStateFlow<LnZapEvent.ZapType>,
val defaultZapType: MutableStateFlow<ZapReceiptEvent.ZapType>,
)
/**
@@ -25,7 +25,7 @@ import androidx.core.os.ConfigurationCompat
import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry
import com.vitorpamplona.amethyst.commons.service.pow.PoWCategory
import com.vitorpamplona.amethyst.ui.navigation.bottombars.DefaultBottomBarEntries
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import kotlinx.serialization.Serializable
import java.util.Locale
@@ -207,7 +207,7 @@ class AccountZapPreferencesInternal(
// and so the on-chain-eligible subset round-trips back for them; on load it
// is unioned into [zapAmountChoices]. See AccountSyncedSettings.
var onchainZapAmountChoices: List<Long> = DefaultOnchainZapAmounts,
val defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC,
val defaultZapType: ZapReceiptEvent.ZapType = ZapReceiptEvent.ZapType.PUBLIC,
)
@Serializable
@@ -48,8 +48,8 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayMethod
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaySuccessResponse
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent
import com.vitorpamplona.quartz.nipB1Bolt12Zaps.builder.Bolt12ZapBuilder
import com.vitorpamplona.quartz.nipB1Bolt12Zaps.verify.Bolt12ZapValidation
import com.vitorpamplona.quartz.utils.Log
@@ -74,12 +74,12 @@ class AccountZapActions(
event: Event,
pollOption: Int?,
message: String = "",
zapType: LnZapEvent.ZapType,
zapType: ZapReceiptEvent.ZapType,
toUser: User?,
additionalRelays: Set<NormalizedRelayUrl>? = null,
amountMillisats: Long? = null,
lnurl: String? = null,
) = LnZapRequestEvent.create(
) = ZapRequestEvent.create(
zappedEvent = event,
// Where the provider should publish the receipt. Zapping group content pins that to the room's
// host relay: the receipt belongs where the message it pays for lives, so the room can show it
@@ -205,7 +205,7 @@ class AccountZapActions(
offer: String,
amountMillisats: Long,
message: String,
zapType: LnZapEvent.ZapType,
zapType: ZapReceiptEvent.ZapType,
// (messageResId, detail) — the caller localizes; detail carries a wallet error, if any.
onError: (StringResource, String?) -> Unit,
// (code, detail) — the wallet refused or failed the payment; no funds moved.
@@ -215,7 +215,7 @@ class AccountZapActions(
) {
// NONZAP means "pay, but publish no receipt" — settle the offer without binding
// a zap intent or emitting a 9736, matching the privacy of a bolt11 NONZAP.
if (zapType == LnZapEvent.ZapType.NONZAP) {
if (zapType == ZapReceiptEvent.ZapType.NONZAP) {
sendNwcRequest(PayMethod.create("bitcoin:?lno=$offer", amountMillisats), onTimeout) { response ->
account.scope.launch {
try {
@@ -228,7 +228,7 @@ class AccountZapActions(
return
}
val anonymous = zapType == LnZapEvent.ZapType.ANONYMOUS
val anonymous = zapType == ZapReceiptEvent.ZapType.ANONYMOUS
// The 9737 intent and the 9736 zap MUST be signed by the same key. An anonymous
// zap uses a fresh ephemeral key so it carries no `P` tag and isn't traceable.
val zapSigner = if (anonymous) NostrSignerInternal(KeyPair()) else account.signer
@@ -284,12 +284,12 @@ class AccountZapActions(
suspend fun createZapRequestFor(
user: User,
message: String = "",
zapType: LnZapEvent.ZapType,
zapType: ZapReceiptEvent.ZapType,
amountMillisats: Long? = null,
lnurl: String? = null,
): LnZapRequestEvent {
): ZapRequestEvent {
val zapRequest =
LnZapRequestEvent.create(
ZapRequestEvent.create(
userHex = user.pubkeyHex,
relays = account.nip65RelayList.inboxFlow.value + (user.inboxRelays() ?: emptyList()),
signer = account.signer,
@@ -40,16 +40,16 @@ import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent
import com.vitorpamplona.quartz.nip29RelayGroups.isGroupScoped
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.BookmarkSetEvent
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletEvent
import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent
@@ -121,7 +121,7 @@ class EventBroadcaster(
event is AppSpecificDataEvent ||
event is BookmarkListEvent ||
event is OldBookmarkListEvent ||
event is LabeledBookmarkListEvent
event is BookmarkSetEvent
) {
return false
}
@@ -166,7 +166,7 @@ class EventBroadcaster(
}
// Seals, inner DM messages, and unsigned rumors never get broadcast
// relays: they only travel inside gift wraps.
if (event is SealedRumorEvent || event is BaseDMGroupEvent || event.sig.isEmpty()) {
if (event is SealEvent || event is BaseDMGroupEvent || event.sig.isEmpty()) {
return emptySet()
}
@@ -322,7 +322,7 @@ class EventBroadcaster(
.toList()
}
val filter =
if (host.kind == SealedRumorEvent.KIND) {
if (host.kind == SealEvent.KIND) {
Filter(
kinds = listOf(host.kind),
ids = listOf(host.id),
@@ -383,7 +383,7 @@ class EventBroadcaster(
fun sendRestoredVersion(event: Event) {
when (event) {
is AdvertisedRelayListEvent -> sendEverywhereAnd(event, event.relays().mapTo(mutableSetOf()) { it.relayUrl })
is ChatMessageRelayListEvent -> sendEverywhereAnd(event, event.relays().toSet())
is DmRelayListEvent -> sendEverywhereAnd(event, event.relays().toSet())
is KeyPackageRelayListEvent -> sendEverywhereAnd(event, event.relays().toSet())
is CashuWalletEvent -> sendLiterallyEverywhere(event)
// Profiles and nutzap info are normally saved everywhere so others can find them;
@@ -25,8 +25,8 @@ import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent
import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@@ -60,7 +60,7 @@ data class FavoriteAlgoFeedsSnapshot(
val responseRelays: Set<NormalizedRelayUrl> = emptySet(),
val ids: Set<HexKey> = emptySet(),
val addresses: Set<String> = emptySet(),
val latestStatus: NIP90StatusEvent? = null,
val latestStatus: DvmStatusEvent? = null,
val errorMessage: String? = null,
)
@@ -139,9 +139,9 @@ class FavoriteAlgoFeedsOrchestrator(
launch {
account.cache
.observeLatestEvent<NIP90ContentDiscoveryResponseEvent>(
.observeLatestEvent<DvmContentDiscoveryResponseEvent>(
Filter(
kinds = listOf(NIP90ContentDiscoveryResponseEvent.KIND),
kinds = listOf(DvmContentDiscoveryResponseEvent.KIND),
tags = mapOf("e" to listOf(requestId)),
limit = 1,
),
@@ -159,9 +159,9 @@ class FavoriteAlgoFeedsOrchestrator(
launch {
account.cache
.observeLatestEvent<NIP90StatusEvent>(
.observeLatestEvent<DvmStatusEvent>(
Filter(
kinds = listOf(NIP90StatusEvent.KIND),
kinds = listOf(DvmStatusEvent.KIND),
tags = mapOf("e" to listOf(requestId)),
limit = 1,
),
@@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
@@ -48,14 +48,14 @@ class DmRelayListState(
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val dmListNote = cache.getOrCreateAddressableNote(getDMRelayListAddress())
fun getDMRelayListAddress() = ChatMessageRelayListEvent.createAddress(signer.pubKey)
fun getDMRelayListAddress() = DmRelayListEvent.createAddress(signer.pubKey)
fun getDMRelayListFlow(): StateFlow<NoteState> = dmListNote.flow().metadata.stateFlow
fun getDMRelayList(): ChatMessageRelayListEvent? = dmListNote.event as? ChatMessageRelayListEvent
fun getDMRelayList(): DmRelayListEvent? = dmListNote.event as? DmRelayListEvent
fun normalizeDMRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
val event = note.event as? ChatMessageRelayListEvent ?: settings.backupDMRelayList
val event = note.event as? DmRelayListEvent ?: settings.backupDMRelayList
return event?.relays()?.toSet() ?: emptySet()
}
@@ -70,16 +70,16 @@ class DmRelayListState(
emptySet(),
)
suspend fun saveRelayList(dmRelays: List<NormalizedRelayUrl>): ChatMessageRelayListEvent {
suspend fun saveRelayList(dmRelays: List<NormalizedRelayUrl>): DmRelayListEvent {
val relayListForDMs = getDMRelayList()
return if (relayListForDMs != null && relayListForDMs.tags.isNotEmpty()) {
ChatMessageRelayListEvent.updateRelayList(
DmRelayListEvent.updateRelayList(
earlierVersion = relayListForDMs,
relays = dmRelays,
signer = signer,
)
} else {
ChatMessageRelayListEvent.create(
DmRelayListEvent.create(
relays = dmRelays,
signer = signer,
)
@@ -99,7 +99,7 @@ class DmRelayListState(
Log.d("AccountRegisterObservers", "NIP-17 Relay List Collector Start")
getDMRelayListFlow().collect {
Log.d("AccountRegisterObservers") { "Updating DM Relay List for ${signer.pubKey}" }
(it.note.event as? ChatMessageRelayListEvent)?.let {
(it.note.event as? DmRelayListEvent)?.let {
settings.updateDMRelayList(it)
}
}
@@ -33,7 +33,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip01Core.signers.update
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip30CustomEmoji.emoji
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
@@ -129,12 +129,12 @@ class OwnedEmojiPacksState(
.onStart { emit(listFeedFlow.value.getPack(dTag)) }
.flowOn(Dispatchers.IO)
fun DeletionEvent.hasAnyDeletedOwnedEmojiPacks() = deleteAddressesWithKind(EmojiPackEvent.KIND) || deletesAnyEventIn(ownedEmojiPackEventIds.value)
fun DeletionRequestEvent.hasAnyDeletedOwnedEmojiPacks() = deleteAddressesWithKind(EmojiPackEvent.KIND) || deletesAnyEventIn(ownedEmojiPackEventIds.value)
fun hasItemInNoteList(notes: Set<Note>): Boolean =
notes.anyNotNullEvent { event ->
if (event.pubKey == signer.pubKey) {
event is EmojiPackEvent || (event is DeletionEvent && event.hasAnyDeletedOwnedEmojiPacks())
event is EmojiPackEvent || (event is DeletionRequestEvent && event.hasAnyDeletedOwnedEmojiPacks())
} else {
false
}
@@ -269,7 +269,7 @@ class OwnedEmojiPacksState(
account: Account,
) {
val packEvent = getOwnedEmojiPackEvent(dTag) ?: return
val deletionEventTemplate = DeletionEvent.build(listOf(packEvent))
val deletionEventTemplate = DeletionRequestEvent.build(listOf(packEvent))
val deletionEvent = signer.sign(deletionEventTemplate)
account.sendMyPublicAndPrivateOutbox(deletionEvent)
}
@@ -36,10 +36,10 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip47WalletConnect.cache.NostrWalletConnectRequestCache
import com.vitorpamplona.quartz.nip47WalletConnect.cache.NostrWalletConnectResponseCache
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcInfoEvent
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcNotificationEvent
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaymentReceivedNotification
@@ -199,12 +199,12 @@ class NwcSignerState(
override fun isNIP47Author(pubKey: HexKey?): Boolean = nip47Signer.value.pubKey == pubKey
override suspend fun decryptRequest(event: LnZapPaymentRequestEvent): Request? {
override suspend fun decryptRequest(event: NwcRequestEvent): Request? {
if (!hasWalletConnectSetup()) return null
return zapPaymentRequestDecryptionCache.value.decryptRequest(event)
}
override suspend fun decryptResponse(event: LnZapPaymentResponseEvent): Response? {
override suspend fun decryptResponse(event: NwcResponseEvent): Response? {
if (!hasWalletConnectSetup()) return null
return zapPaymentResponseDecryptionCache.value.decryptResponse(event)
}
@@ -252,7 +252,7 @@ class NwcSignerState(
request: Request,
onTimeout: () -> Unit = {},
onResponse: (Response?) -> Unit,
): Pair<LnZapPaymentRequestEvent, NormalizedRelayUrl> = sendNwcRequestToWallet(defaultWalletUri.value, request, onTimeout, onResponse)
): Pair<NwcRequestEvent, NormalizedRelayUrl> = sendNwcRequestToWallet(defaultWalletUri.value, request, onTimeout, onResponse)
/**
* Sends a generic NIP-47 request to a specific wallet.
@@ -266,14 +266,14 @@ class NwcSignerState(
request: Request,
onTimeout: () -> Unit = {},
onResponse: (Response?) -> Unit,
): Pair<LnZapPaymentRequestEvent, NormalizedRelayUrl> {
): Pair<NwcRequestEvent, NormalizedRelayUrl> {
val walletService = walletUri ?: throw IllegalArgumentException("No NIP47 setup")
val walletSigner = buildSigner(walletService) ?: signer
val info = walletInfo(walletService)
request.dropMetadataIfUnsupported(info)
val event = LnZapPaymentRequestEvent.createRequest(request, walletService.pubKeyHex, walletSigner, useNip44 = prefersNip44(info))
val event = NwcRequestEvent.createRequest(request, walletService.pubKeyHex, walletSigner, useNip44 = prefersNip44(info))
val filter =
NWCPaymentQueryState(
@@ -314,7 +314,7 @@ class NwcSignerState(
onTimeout: () -> Unit = {},
metadata: Map<String, Any?>? = null,
onResponse: (Response?) -> Unit,
): Pair<LnZapPaymentRequestEvent, NormalizedRelayUrl> {
): Pair<NwcRequestEvent, NormalizedRelayUrl> {
val walletService = defaultWalletUri.value ?: throw IllegalArgumentException("No NIP47 setup")
val info = walletInfo(walletService)
@@ -322,7 +322,7 @@ class NwcSignerState(
request.dropMetadataIfUnsupported(info)
val event =
LnZapPaymentRequestEvent.createRequest(
NwcRequestEvent.createRequest(
request,
walletService.pubKeyHex,
nip47Signer.value,
@@ -18,7 +18,7 @@
* 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.amethyst.model.nip51Lists.labeledBookmarkLists
package com.vitorpamplona.amethyst.model.nip51Lists.bookmarkSets
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note
@@ -27,20 +27,20 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.cache.filter
import com.vitorpamplona.amethyst.commons.model.eventIdSet
import com.vitorpamplona.amethyst.commons.model.events
import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
import com.vitorpamplona.amethyst.commons.model.nip51Lists.bookmarkSets.BookmarkSet
import com.vitorpamplona.amethyst.commons.model.updateFlow
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.update
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.description
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.image
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.title
import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.BookmarkSetEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.description
import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.image
import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.title
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -55,41 +55,41 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.update
class LabeledBookmarkListsState(
class BookmarkSetsState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
) {
val user = cache.getOrCreateUser(signer.pubKey)
fun existingLabeledBookmarkNotes() = cache.addressables.filter(LabeledBookmarkListEvent.KIND, user.pubkeyHex)
fun existingBookmarkSetNotes() = cache.addressables.filter(BookmarkSetEvent.KIND, user.pubkeyHex)
val labeledBookmarkListVersions = MutableStateFlow(0)
val bookmarkSetVersions = MutableStateFlow(0)
val labeledBookmarkListNotes =
labeledBookmarkListVersions
.map { existingLabeledBookmarkNotes() }
.onStart { emit(existingLabeledBookmarkNotes()) }
val bookmarkSetNotes =
bookmarkSetVersions
.map { existingBookmarkSetNotes() }
.onStart { emit(existingBookmarkSetNotes()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptyList())
val labeledBookmarkListEventIds =
labeledBookmarkListNotes
val bookmarkSetEventIds =
bookmarkSetNotes
.map { it.eventIdSet() }
.onStart { emit(labeledBookmarkListNotes.value.eventIdSet()) }
.onStart { emit(bookmarkSetNotes.value.eventIdSet()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptySet())
@OptIn(ExperimentalCoroutinesApi::class)
val latestBookmarkLists: StateFlow<List<LabeledBookmarkListEvent>> =
labeledBookmarkListNotes
.transformLatest { emitAll(it.updateFlow<LabeledBookmarkListEvent>()) }
.onStart { emit(labeledBookmarkListNotes.value.events()) }
val latestBookmarkLists: StateFlow<List<BookmarkSetEvent>> =
bookmarkSetNotes
.transformLatest { emitAll(it.updateFlow<BookmarkSetEvent>()) }
.onStart { emit(bookmarkSetNotes.value.events()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptyList())
suspend fun LabeledBookmarkListEvent.toLabeledBookmarkList() =
LabeledBookmarkList(
suspend fun BookmarkSetEvent.toBookmarkSet() =
BookmarkSet(
identifier = dTag(),
title = titleOrName() ?: dTag(),
description = description(),
@@ -98,28 +98,28 @@ class LabeledBookmarkListsState(
publicBookmarks = publicBookmarks().toSet(),
)
suspend fun List<LabeledBookmarkListEvent>.toLabeledBookmarkListsFeed() = map { it.toLabeledBookmarkList() }.sortedBy { it.title }
suspend fun List<BookmarkSetEvent>.toBookmarkSetsFeed() = map { it.toBookmarkSet() }.sortedBy { it.title }
val listFeedFlow =
latestBookmarkLists
.map { it.toLabeledBookmarkListsFeed() }
.onStart { emit(latestBookmarkLists.value.toLabeledBookmarkListsFeed()) }
.map { it.toBookmarkSetsFeed() }
.onStart { emit(latestBookmarkLists.value.toBookmarkSetsFeed()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptyList())
fun List<LabeledBookmarkList>.getList(bookmarkListId: String) =
fun List<BookmarkSet>.getList(bookmarkListId: String) =
this.firstOrNull {
it.identifier == bookmarkListId
}
fun getBookmarkList(dTag: String) = listFeedFlow.value.getList(bookmarkListId = dTag)
fun DeletionEvent.hasAnyDeletedBookmarkLists() = deleteAddressesWithKind(LabeledBookmarkListEvent.KIND) || deletesAnyEventIn(labeledBookmarkListEventIds.value)
fun DeletionRequestEvent.hasAnyDeletedBookmarkLists() = deleteAddressesWithKind(BookmarkSetEvent.KIND) || deletesAnyEventIn(bookmarkSetEventIds.value)
fun hasItemInNoteList(notes: Set<Note>): Boolean =
notes.anyNotNullEvent { event ->
if (event.pubKey == signer.pubKey) {
event is LabeledBookmarkListEvent || (event is DeletionEvent && event.hasAnyDeletedBookmarkLists())
event is BookmarkSetEvent || (event is DeletionRequestEvent && event.hasAnyDeletedBookmarkLists())
} else {
false
}
@@ -138,14 +138,14 @@ class LabeledBookmarkListsState(
}
fun forceRefresh() {
labeledBookmarkListVersions.update { it + 1 }
bookmarkSetVersions.update { it + 1 }
}
fun getLabeledBookmarkListNote(bookmarkIdentifier: String): AddressableNote? = existingLabeledBookmarkNotes().find { it.dTag() == bookmarkIdentifier }
fun getBookmarkSetNote(bookmarkIdentifier: String): AddressableNote? = existingBookmarkSetNotes().find { it.dTag() == bookmarkIdentifier }
fun getLabeledBookmarkListEvent(bookmarkIdentifier: String): LabeledBookmarkListEvent = getLabeledBookmarkListNote(bookmarkIdentifier)?.event as LabeledBookmarkListEvent
fun getBookmarkSetEvent(bookmarkIdentifier: String): BookmarkSetEvent = getBookmarkSetNote(bookmarkIdentifier)?.event as BookmarkSetEvent
fun getLabeledBookmarkListFlow(bookmarkIdentifier: String) =
fun getBookmarkSetFlow(bookmarkIdentifier: String) =
listFeedFlow
.map { it.getList(bookmarkIdentifier) }
.onStart {
@@ -154,7 +154,7 @@ class LabeledBookmarkListsState(
)
}.flowOn(Dispatchers.IO)
suspend fun addLabeledBookmarkList(
suspend fun addBookmarkSet(
listName: String,
listDescription: String? = null,
listImage: String? = null,
@@ -163,7 +163,7 @@ class LabeledBookmarkListsState(
account: Account,
) {
val newList =
LabeledBookmarkListEvent.create(
BookmarkSetEvent.create(
title = listName,
description = listDescription,
image = listImage,
@@ -178,10 +178,10 @@ class LabeledBookmarkListsState(
listName: String?,
listDescription: String?,
listImage: String?,
bookmarkList: LabeledBookmarkList,
bookmarkList: BookmarkSet,
account: Account,
) {
val listEvent = getLabeledBookmarkListEvent(bookmarkList.identifier)
val listEvent = getBookmarkSetEvent(bookmarkList.identifier)
val template =
listEvent.update {
@@ -197,12 +197,12 @@ class LabeledBookmarkListsState(
suspend fun renameBookmarkList(
newName: String,
bookmarkList: LabeledBookmarkList,
bookmarkList: BookmarkSet,
account: Account,
) {
val listEvent = getLabeledBookmarkListEvent(bookmarkList.identifier)
val listEvent = getBookmarkSetEvent(bookmarkList.identifier)
val renamedList =
LabeledBookmarkListEvent.modifyName(
BookmarkSetEvent.modifyName(
earlierVersion = listEvent,
newTitle = newName,
signer = account.signer,
@@ -212,12 +212,12 @@ class LabeledBookmarkListsState(
suspend fun modifyListDescription(
newDescription: String?,
bookmarkList: LabeledBookmarkList,
bookmarkList: BookmarkSet,
account: Account,
) {
val listEvent = getLabeledBookmarkListEvent(bookmarkList.identifier)
val listEvent = getBookmarkSetEvent(bookmarkList.identifier)
val modifiedList =
LabeledBookmarkListEvent.modifyDescription(
BookmarkSetEvent.modifyDescription(
earlierVersion = listEvent,
newDescription = newDescription,
signer = account.signer,
@@ -226,13 +226,13 @@ class LabeledBookmarkListsState(
}
suspend fun cloneBookmarkList(
currentBookmarkList: LabeledBookmarkList,
currentBookmarkList: BookmarkSet,
customCloneName: String?,
customCloneDescription: String?,
account: Account,
) {
val clonedList =
LabeledBookmarkListEvent.create(
BookmarkSetEvent.create(
title = customCloneName ?: currentBookmarkList.title,
description = customCloneDescription ?: currentBookmarkList.description,
publicBookmarks = currentBookmarkList.publicBookmarks.toList(),
@@ -246,8 +246,8 @@ class LabeledBookmarkListsState(
bookmarkListIdentifier: String,
account: Account,
) {
val listEvent = getLabeledBookmarkListEvent(bookmarkListIdentifier)
val deletionEventTemplate = DeletionEvent.build(listOf(listEvent))
val listEvent = getBookmarkSetEvent(bookmarkListIdentifier)
val deletionEventTemplate = DeletionRequestEvent.build(listOf(listEvent))
val deletionEvent = account.signer.sign(deletionEventTemplate)
account.sendMyPublicAndPrivateOutbox(deletionEvent)
}
@@ -258,9 +258,9 @@ class LabeledBookmarkListsState(
isBookmarkPrivate: Boolean,
account: Account,
) {
val currentBookmarkList = getLabeledBookmarkListEvent(bookmarkListIdentifier)
val currentBookmarkList = getBookmarkSetEvent(bookmarkListIdentifier)
val updatedList =
LabeledBookmarkListEvent.addBookmark(
BookmarkSetEvent.addBookmark(
earlierVersion = currentBookmarkList,
bookmarkIdTag = bookmark,
isPrivate = isBookmarkPrivate,
@@ -275,9 +275,9 @@ class LabeledBookmarkListsState(
isBookmarkCurrentlyPrivate: Boolean,
account: Account,
) {
val bookmarkList = getLabeledBookmarkListEvent(bookmarkListIdentifier)
val bookmarkList = getBookmarkSetEvent(bookmarkListIdentifier)
val updatedList =
LabeledBookmarkListEvent.moveBookmark(
BookmarkSetEvent.moveBookmark(
earlierVersion = bookmarkList,
bookmarkIdTag = bookmark,
isCurrentlyPrivate = isBookmarkCurrentlyPrivate,
@@ -292,9 +292,9 @@ class LabeledBookmarkListsState(
isBookmarkPrivate: Boolean,
account: Account,
) {
val currentBookmarkList = getLabeledBookmarkListEvent(bookmarkListIdentifier)
val currentBookmarkList = getBookmarkSetEvent(bookmarkListIdentifier)
val updatedList =
LabeledBookmarkListEvent.removeBookmark(
BookmarkSetEvent.removeBookmark(
earlierVersion = currentBookmarkList,
bookmarkIdTag = bookmark,
isPrivate = isBookmarkPrivate,
@@ -311,7 +311,7 @@ class LabeledBookmarkListsState(
) {
if (deletedEventIds.isEmpty() && deletedAddresses.isEmpty()) return
val currentList = getLabeledBookmarkListNote(bookmarkListIdentifier)?.event as? LabeledBookmarkListEvent ?: return
val currentList = getBookmarkSetNote(bookmarkListIdentifier)?.event as? BookmarkSetEvent ?: return
val newPublicTags =
currentList.tags
@@ -328,7 +328,7 @@ class LabeledBookmarkListsState(
val updatedList =
if (oldPrivateTags == null) {
if (newPublicTags.size == currentList.tags.size) return
LabeledBookmarkListEvent.resign(
BookmarkSetEvent.resign(
content = currentList.content,
tags = newPublicTags,
signer = account.signer,
@@ -344,7 +344,7 @@ class LabeledBookmarkListsState(
}
}.toTypedArray()
if (newPublicTags.size == currentList.tags.size && newPrivateTags.size == oldPrivateTags.size) return
LabeledBookmarkListEvent.resign(
BookmarkSetEvent.resign(
tags = newPublicTags,
privateTags = newPrivateTags,
signer = account.signer,
@@ -18,16 +18,16 @@
* 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.amethyst.model.nip51Lists.relayFeeds
package com.vitorpamplona.amethyst.model.nip51Lists.favoriteRelays
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.NoteState
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.relayFeeds.RelayFeedsListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteRelays.FavoriteRelayListDecryptionCache
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
@@ -40,32 +40,32 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class RelayFeedListState(
class FavoriteRelayListState(
val signer: NostrSigner,
val cache: LocalCache,
val decryptionCache: RelayFeedsListDecryptionCache,
val decryptionCache: FavoriteRelayListDecryptionCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val relayFeedListNote = cache.getOrCreateAddressableNote(getRelayFeedsListAddress())
val favoriteRelayListNote = cache.getOrCreateAddressableNote(getFavoriteRelayListAddress())
fun getRelayFeedsListAddress() = RelayFeedsListEvent.createAddress(signer.pubKey)
fun getFavoriteRelayListAddress() = FavoriteRelayListEvent.createAddress(signer.pubKey)
fun getRelayFeedsListFlow(): StateFlow<NoteState> = relayFeedListNote.flow().metadata.stateFlow
fun getFavoriteRelayListFlow(): StateFlow<NoteState> = favoriteRelayListNote.flow().metadata.stateFlow
fun getRelayFeedsList(): RelayFeedsListEvent? = relayFeedListNote.event as? RelayFeedsListEvent
fun getFavoriteRelayList(): FavoriteRelayListEvent? = favoriteRelayListNote.event as? FavoriteRelayListEvent
fun relayFeedsListEvent(note: Note) = note.event as? RelayFeedsListEvent ?: settings.backupRelayFeedsList
fun favoriteRelayListEvent(note: Note) = note.event as? FavoriteRelayListEvent ?: settings.backupFavoriteRelayList
suspend fun normalizeRelayFeedsListWithBackup(note: Note): Set<NormalizedRelayUrl> = relayFeedsListEvent(note)?.let { decryptionCache.relays(it) }?.ifEmpty { null } ?: emptySet()
suspend fun normalizeFavoriteRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> = favoriteRelayListEvent(note)?.let { decryptionCache.relays(it) }?.ifEmpty { null } ?: emptySet()
suspend fun normalizeRelayFeedsListWithBackupNoDefaults(note: Note): Set<NormalizedRelayUrl> = relayFeedsListEvent(note)?.let { decryptionCache.relays(it) } ?: emptySet()
suspend fun normalizeFavoriteRelayListWithBackupNoDefaults(note: Note): Set<NormalizedRelayUrl> = favoriteRelayListEvent(note)?.let { decryptionCache.relays(it) } ?: emptySet()
val flow =
getRelayFeedsListFlow()
.map { normalizeRelayFeedsListWithBackup(it.note) }
.onStart { emit(normalizeRelayFeedsListWithBackup(relayFeedListNote)) }
getFavoriteRelayListFlow()
.map { normalizeFavoriteRelayListWithBackup(it.note) }
.onStart { emit(normalizeFavoriteRelayListWithBackup(favoriteRelayListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
@@ -74,9 +74,9 @@ class RelayFeedListState(
)
val flowNoDefaults =
getRelayFeedsListFlow()
.map { normalizeRelayFeedsListWithBackupNoDefaults(it.note) }
.onStart { emit(normalizeRelayFeedsListWithBackupNoDefaults(relayFeedListNote)) }
getFavoriteRelayListFlow()
.map { normalizeFavoriteRelayListWithBackupNoDefaults(it.note) }
.onStart { emit(normalizeFavoriteRelayListWithBackupNoDefaults(favoriteRelayListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
@@ -84,30 +84,30 @@ class RelayFeedListState(
emptySet(),
)
suspend fun addRelay(relay: NormalizedRelayUrl): RelayFeedsListEvent {
val current = normalizeRelayFeedsListWithBackupNoDefaults(relayFeedListNote).toMutableList()
suspend fun addRelay(relay: NormalizedRelayUrl): FavoriteRelayListEvent {
val current = normalizeFavoriteRelayListWithBackupNoDefaults(favoriteRelayListNote).toMutableList()
if (relay !in current) current.add(relay)
return saveRelayList(current)
}
suspend fun removeRelay(relay: NormalizedRelayUrl): RelayFeedsListEvent? {
val current = normalizeRelayFeedsListWithBackupNoDefaults(relayFeedListNote).toMutableList()
suspend fun removeRelay(relay: NormalizedRelayUrl): FavoriteRelayListEvent? {
val current = normalizeFavoriteRelayListWithBackupNoDefaults(favoriteRelayListNote).toMutableList()
if (relay !in current) return null
current.remove(relay)
return saveRelayList(current)
}
suspend fun saveRelayList(relayFeeds: List<NormalizedRelayUrl>): RelayFeedsListEvent {
val relayFeedsList = getRelayFeedsList()
suspend fun saveRelayList(relayFeeds: List<NormalizedRelayUrl>): FavoriteRelayListEvent {
val favoriteRelayList = getFavoriteRelayList()
return if (relayFeedsList != null && relayFeedsList.tags.isNotEmpty()) {
RelayFeedsListEvent.updateRelayList(
earlierVersion = relayFeedsList,
return if (favoriteRelayList != null && favoriteRelayList.tags.isNotEmpty()) {
FavoriteRelayListEvent.updateRelayList(
earlierVersion = favoriteRelayList,
relays = relayFeeds,
signer = signer,
)
} else {
RelayFeedsListEvent.create(
FavoriteRelayListEvent.create(
relays = relayFeeds,
signer = signer,
)
@@ -115,7 +115,7 @@ class RelayFeedListState(
}
init {
settings.backupRelayFeedsList?.let {
settings.backupFavoriteRelayList?.let {
Log.d("AccountRegisterObservers") { "Loading saved relay feeds list ${it.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
@@ -123,10 +123,10 @@ class RelayFeedListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Relay feeds list Collector Start")
getRelayFeedsListFlow().collect {
getFavoriteRelayListFlow().collect {
Log.d("AccountRegisterObservers") { "Updating Relay feeds list for ${signer.pubKey}" }
(it.note.event as? RelayFeedsListEvent)?.let {
settings.updateRelayFeedList(it)
(it.note.event as? FavoriteRelayListEvent)?.let {
settings.updateFavoriteRelayList(it)
}
}
}
@@ -18,7 +18,7 @@
* 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.amethyst.model.nip51Lists.peopleList
package com.vitorpamplona.amethyst.model.nip51Lists.followSets
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note
@@ -28,19 +28,19 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.cache.filter
import com.vitorpamplona.amethyst.commons.model.eventIdSet
import com.vitorpamplona.amethyst.commons.model.events
import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleList
import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.FollowSetDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.PeopleList
import com.vitorpamplona.amethyst.commons.model.updateFlow
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.update
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent
import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent
import com.vitorpamplona.quartz.nip51Lists.followSet.description
import com.vitorpamplona.quartz.nip51Lists.followSet.image
import com.vitorpamplona.quartz.nip51Lists.followSet.title
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.description
import com.vitorpamplona.quartz.nip51Lists.peopleList.image
import com.vitorpamplona.quartz.nip51Lists.peopleList.title
import com.vitorpamplona.quartz.utils.flattenToSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -62,10 +62,10 @@ import java.util.UUID
*
* This class must receive updates from Large Cache as it receives new events.
*/
class PeopleListsState(
class FollowSetsState(
val signer: NostrSigner,
val cache: LocalCache,
val decryptionCache: PeopleListDecryptionCache,
val decryptionCache: FollowSetDecryptionCache,
val scope: CoroutineScope,
) {
val user = cache.getOrCreateUser(signer.pubKey)
@@ -78,8 +78,8 @@ class PeopleListsState(
// can subscribe and fetch them from relays.
fun existingPeopleListNotes() =
cache.addressables
.filter(PeopleListEvent.KIND, user.pubkeyHex)
.filter { it.dTag() != PeopleListEvent.BLOCK_LIST_D_TAG || it.event != null }
.filter(FollowSetEvent.KIND, user.pubkeyHex)
.filter { it.dTag() != FollowSetEvent.BLOCK_LIST_D_TAG || it.event != null }
.filter { it.event != null || !cache.hasBeenDeleted(it.address) }
val peopleListVersions = MutableStateFlow(0)
@@ -99,21 +99,21 @@ class PeopleListsState(
.stateIn(scope, SharingStarted.Eagerly, emptySet())
@OptIn(ExperimentalCoroutinesApi::class)
val latestLists: StateFlow<List<PeopleListEvent>> =
val latestLists: StateFlow<List<FollowSetEvent>> =
peopleListNotes
.transformLatest { emitAll(it.updateFlow<PeopleListEvent>()) }
.transformLatest { emitAll(it.updateFlow<FollowSetEvent>()) }
.onStart { emit(peopleListNotes.value.events()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptyList())
suspend fun PeopleListEvent.userIdSet() = decryptionCache.userIdSet(this)
suspend fun FollowSetEvent.userIdSet() = decryptionCache.userIdSet(this)
suspend fun List<PeopleListEvent>.mapToUserIdSet() = this.map { it.userIdSet() }.flattenToSet()
suspend fun List<FollowSetEvent>.mapToUserIdSet() = this.map { it.userIdSet() }.flattenToSet()
suspend fun List<PeopleListEvent>.mapGoodUsersToIdSet() =
suspend fun List<FollowSetEvent>.mapGoodUsersToIdSet() =
this
.mapNotNull {
if (it.dTag() != PeopleListEvent.BLOCK_LIST_D_TAG) {
if (it.dTag() != FollowSetEvent.BLOCK_LIST_D_TAG) {
it.userIdSet()
} else {
null
@@ -127,7 +127,7 @@ class PeopleListsState(
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptySet())
suspend fun PeopleListEvent.toUI() =
suspend fun FollowSetEvent.toUI() =
PeopleList(
identifierTag = this.dTag(),
title = this.titleOrName() ?: this.dTag(),
@@ -137,7 +137,7 @@ class PeopleListsState(
publicMembers = cache.load(this.publicUsersIdSet()),
)
suspend fun List<PeopleListEvent>.toUI() = this.map { it.toUI() }.sortedBy { it.title }
suspend fun List<FollowSetEvent>.toUI() = this.map { it.toUI() }.sortedBy { it.title }
val uiListFlow =
latestLists
@@ -158,12 +158,12 @@ class PeopleListsState(
.map { it.select(dTag) }
.onStart { emit(selectList(dTag)) }
fun DeletionEvent.hasDeletedAnyPeopleList() = deleteAddressesWithKind(PeopleListEvent.KIND) || deletesAnyEventIn(peopleListsEventIds.value)
fun DeletionRequestEvent.hasDeletedAnyPeopleList() = deleteAddressesWithKind(FollowSetEvent.KIND) || deletesAnyEventIn(peopleListsEventIds.value)
fun hasItemInNoteList(notes: Set<Note>): Boolean =
notes.anyNotNullEvent { event ->
if (event.pubKey == signer.pubKey) {
event is PeopleListEvent || (event is DeletionEvent && event.hasDeletedAnyPeopleList())
event is FollowSetEvent || (event is DeletionRequestEvent && event.hasDeletedAnyPeopleList())
} else {
false
}
@@ -191,7 +191,7 @@ class PeopleListsState(
fun getPeopleListNote(noteIdentifier: String): AddressableNote? = existingPeopleListNotes().find { it.dTag() == noteIdentifier }
fun getPeopleList(noteIdentifier: String): PeopleListEvent = getPeopleListNote(noteIdentifier)?.event as PeopleListEvent
fun getPeopleList(noteIdentifier: String): FollowSetEvent = getPeopleListNote(noteIdentifier)?.event as FollowSetEvent
fun User.toUserTag() = UserTag(this.pubkeyHex, this.bestRelayHint())
@@ -207,7 +207,7 @@ class PeopleListsState(
): String {
val dTag = UUID.randomUUID().toString()
val newListTemplate =
PeopleListEvent.build(
FollowSetEvent.build(
dTag = dTag,
title = listName,
publicMembers = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
@@ -252,7 +252,7 @@ class PeopleListsState(
account: Account,
) {
val newList =
PeopleListEvent.createListWithDescription(
FollowSetEvent.createListWithDescription(
dTag = UUID.randomUUID().toString(),
title = customCloneName ?: currentPeopleList.title,
description = customCloneDescription ?: currentPeopleList.description,
@@ -268,7 +268,7 @@ class PeopleListsState(
account: Account,
) {
val followListEvent = getPeopleList(identifierTag)
val deletionEvent = account.signer.sign(DeletionEvent.build(listOf(followListEvent)))
val deletionEvent = account.signer.sign(DeletionRequestEvent.build(listOf(followListEvent)))
account.sendMyPublicAndPrivateOutbox(deletionEvent)
// Any screen whose persisted feed filter still points at this list would keep
// re-creating an empty shell for its address (and render the dTag/UUID in the
@@ -284,7 +284,7 @@ class PeopleListsState(
) {
val followListEvent = getPeopleList(identifierTag)
val newList =
PeopleListEvent.addUser(
FollowSetEvent.addUser(
earlierVersion = followListEvent,
pubKeyHex = user.pubkeyHex,
relayHint = user.bestRelayHint(),
@@ -302,7 +302,7 @@ class PeopleListsState(
) {
val followListEvent = getPeopleList(identifierTag)
val newList =
PeopleListEvent.addUserFirst(
FollowSetEvent.addUserFirst(
earlierVersion = followListEvent,
pubKeyHex = user.pubkeyHex,
relayHint = user.bestRelayHint(),
@@ -320,7 +320,7 @@ class PeopleListsState(
) {
val followListEvent = getPeopleList(identifierTag)
val newList =
PeopleListEvent.removeUser(
FollowSetEvent.removeUser(
earlierVersion = followListEvent,
pubKeyHex = user.pubkeyHex,
isUserPrivate = isPrivate,
@@ -18,7 +18,7 @@
* 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.amethyst.model.nip51Lists.peopleList
package com.vitorpamplona.amethyst.model.nip51Lists.followSets
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note
@@ -28,22 +28,22 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.cache.filter
import com.vitorpamplona.amethyst.commons.model.eventIdSet
import com.vitorpamplona.amethyst.commons.model.events
import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleList
import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.PeopleList
import com.vitorpamplona.amethyst.commons.model.updateFlow
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.update
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.followList.description
import com.vitorpamplona.quartz.nip51Lists.followList.image
import com.vitorpamplona.quartz.nip51Lists.followList.person
import com.vitorpamplona.quartz.nip51Lists.followList.personFirst
import com.vitorpamplona.quartz.nip51Lists.followList.removePerson
import com.vitorpamplona.quartz.nip51Lists.followList.title
import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent
import com.vitorpamplona.quartz.nip51Lists.starterPack.description
import com.vitorpamplona.quartz.nip51Lists.starterPack.image
import com.vitorpamplona.quartz.nip51Lists.starterPack.person
import com.vitorpamplona.quartz.nip51Lists.starterPack.personFirst
import com.vitorpamplona.quartz.nip51Lists.starterPack.removePerson
import com.vitorpamplona.quartz.nip51Lists.starterPack.title
import com.vitorpamplona.quartz.utils.flattenToSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -65,7 +65,7 @@ import java.util.UUID
*
* This class must receive updates from Large Cache as it receives new events.
*/
class FollowListsState(
class StarterPacksState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
@@ -79,34 +79,34 @@ class FollowListsState(
// them from relays.
fun existingPeopleListNotes() =
cache.addressables
.filter(FollowListEvent.KIND, user.pubkeyHex)
.filter(StarterPackEvent.KIND, user.pubkeyHex)
.filter { it.event != null || !cache.hasBeenDeleted(it.address) }
val followListVersions = MutableStateFlow(0)
val followListNotes =
val starterPackNotes =
followListVersions
.map { existingPeopleListNotes() }
.onStart { emit(existingPeopleListNotes()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptyList())
val followListsEventIds =
followListNotes
val starterPacksEventIds =
starterPackNotes
.map { it.eventIdSet() }
.onStart { emit(followListNotes.value.eventIdSet()) }
.onStart { emit(starterPackNotes.value.eventIdSet()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptySet())
@OptIn(ExperimentalCoroutinesApi::class)
val latestLists: StateFlow<List<FollowListEvent>> =
followListNotes
.transformLatest { emitAll(it.updateFlow<FollowListEvent>()) }
.onStart { emit(followListNotes.value.events()) }
val latestLists: StateFlow<List<StarterPackEvent>> =
starterPackNotes
.transformLatest { emitAll(it.updateFlow<StarterPackEvent>()) }
.onStart { emit(starterPackNotes.value.events()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptyList())
fun List<FollowListEvent>.mapToUserIdSet() = this.map { it.followIdSet() }.flattenToSet()
fun List<StarterPackEvent>.mapToUserIdSet() = this.map { it.followIdSet() }.flattenToSet()
val allPeopleListProfiles: StateFlow<Set<HexKey>> =
latestLists
@@ -115,7 +115,7 @@ class FollowListsState(
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptySet())
fun FollowListEvent.toUI() =
fun StarterPackEvent.toUI() =
PeopleList(
identifierTag = this.dTag(),
title = this.title() ?: this.dTag(),
@@ -125,7 +125,7 @@ class FollowListsState(
publicMembers = cache.load(this.followIdSet()),
)
fun List<FollowListEvent>.toUI() = this.map { it.toUI() }
fun List<StarterPackEvent>.toUI() = this.map { it.toUI() }
val uiListFlow =
latestLists
@@ -148,12 +148,12 @@ class FollowListsState(
fun isUserInFollowSets(user: User): Boolean = allPeopleListProfiles.value.contains(user.pubkeyHex)
fun DeletionEvent.hasDeletedAnyFollowList() = deleteAddressesWithKind(FollowListEvent.KIND) || deletesAnyEventIn(followListsEventIds.value)
fun DeletionRequestEvent.hasDeletedAnyStarterPack() = deleteAddressesWithKind(StarterPackEvent.KIND) || deletesAnyEventIn(starterPacksEventIds.value)
fun hasItemInNoteList(notes: Set<Note>): Boolean =
notes.anyNotNullEvent { event ->
if (event.pubKey == signer.pubKey) {
event is FollowListEvent || (event is DeletionEvent && event.hasDeletedAnyFollowList())
event is StarterPackEvent || (event is DeletionRequestEvent && event.hasDeletedAnyStarterPack())
} else {
false
}
@@ -181,7 +181,7 @@ class FollowListsState(
fun getPeopleListNote(noteIdentifier: String): AddressableNote? = existingPeopleListNotes().find { it.dTag() == noteIdentifier }
fun getPeopleList(noteIdentifier: String): FollowListEvent = getPeopleListNote(noteIdentifier)?.event as FollowListEvent
fun getPeopleList(noteIdentifier: String): StarterPackEvent = getPeopleListNote(noteIdentifier)?.event as StarterPackEvent
fun User.toUserTag() = UserTag(this.pubkeyHex, this.bestRelayHint())
@@ -198,7 +198,7 @@ class FollowListsState(
val dTag = UUID.randomUUID().toString()
val newListTemplate =
FollowListEvent.build(
StarterPackEvent.build(
name = name,
people = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
dTag = dTag,
@@ -262,7 +262,7 @@ class FollowListsState(
account: Account,
) {
val followListEvent = getPeopleList(identifierTag)
val deletionEvent = account.signer.sign(DeletionEvent.build(listOf(followListEvent)))
val deletionEvent = account.signer.sign(DeletionRequestEvent.build(listOf(followListEvent)))
account.sendMyPublicAndPrivateOutbox(deletionEvent)
// Any screen whose persisted feed filter still points at this follow pack would
// keep re-creating an empty shell for its address (and render the dTag/UUID in
@@ -18,15 +18,15 @@
* 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.amethyst.model.nip51Lists.hashtagLists
package com.vitorpamplona.amethyst.model.nip51Lists.interestLists
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.NoteState
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestLists.InterestListDecryptionCache
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
@@ -40,30 +40,30 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
class HashtagListState(
class InterestListState(
val signer: NostrSigner,
val cache: LocalCache,
val decryptionCache: HashtagListDecryptionCache,
val decryptionCache: InterestListDecryptionCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val hashtagListNote = cache.getOrCreateAddressableNote(getHashtagListAddress())
val hashtagListNote = cache.getOrCreateAddressableNote(getInterestListAddress())
fun getHashtagListAddress() = HashtagListEvent.createAddress(signer.pubKey)
fun getInterestListAddress() = InterestListEvent.createAddress(signer.pubKey)
fun getHashtagListFlow(): StateFlow<NoteState> = hashtagListNote.flow().metadata.stateFlow
fun getInterestListFlow(): StateFlow<NoteState> = hashtagListNote.flow().metadata.stateFlow
fun getHashtagList(): HashtagListEvent? = hashtagListNote.event as? HashtagListEvent
fun getInterestList(): InterestListEvent? = hashtagListNote.event as? InterestListEvent
suspend fun hashtagListWithBackup(note: Note): Set<String> {
val event = note.event as? HashtagListEvent ?: settings.backupHashtagList
val event = note.event as? InterestListEvent ?: settings.backupInterestList
return event?.let { decryptionCache.hashtags(it).mapTo(mutableSetOf()) { it.lowercase() } } ?: emptySet()
}
@OptIn(ExperimentalCoroutinesApi::class)
val flow: StateFlow<Set<String>> =
getHashtagListFlow()
getInterestListFlow()
.transformLatest { noteState ->
emit(hashtagListWithBackup(noteState.note))
}.onStart {
@@ -75,38 +75,38 @@ class HashtagListState(
emptySet(),
)
suspend fun follow(hashtags: List<String>): HashtagListEvent {
val hashtagList = getHashtagList()
suspend fun follow(hashtags: List<String>): InterestListEvent {
val hashtagList = getInterestList()
return if (hashtagList == null) {
HashtagListEvent.create(hashtags, true, signer)
InterestListEvent.create(hashtags, true, signer)
} else {
HashtagListEvent.add(hashtagList, hashtags, true, signer)
InterestListEvent.add(hashtagList, hashtags, true, signer)
}
}
suspend fun follow(hashtag: String): HashtagListEvent {
val hashtagList = getHashtagList()
suspend fun follow(hashtag: String): InterestListEvent {
val hashtagList = getInterestList()
return if (hashtagList == null) {
HashtagListEvent.create(hashtag.lowercase(), true, signer)
InterestListEvent.create(hashtag.lowercase(), true, signer)
} else {
HashtagListEvent.add(hashtagList, hashtag.lowercase(), true, signer)
InterestListEvent.add(hashtagList, hashtag.lowercase(), true, signer)
}
}
suspend fun unfollow(hashtag: String): HashtagListEvent? {
val hashtagList = getHashtagList()
suspend fun unfollow(hashtag: String): InterestListEvent? {
val hashtagList = getInterestList()
return if (hashtagList != null) {
HashtagListEvent.remove(hashtagList, hashtag, signer)
InterestListEvent.remove(hashtagList, hashtag, signer)
} else {
null
}
}
init {
settings.backupHashtagList?.let { event ->
settings.backupInterestList?.let { event ->
Log.d("AccountRegisterObservers") { "Loading saved Hashtag list ${event.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) {
@@ -116,10 +116,10 @@ class HashtagListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Hashtag List Collector Start")
getHashtagListFlow().collect {
getInterestListFlow().collect {
Log.d("AccountRegisterObservers") { "Hashtag List for ${signer.pubKey}" }
(it.note.event as? HashtagListEvent)?.let {
settings.updateHashtagListTo(it)
(it.note.event as? InterestListEvent)?.let {
settings.updateInterestListTo(it)
}
}
}
@@ -31,7 +31,7 @@ import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets.Interest
import com.vitorpamplona.amethyst.commons.model.updateFlow
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent
import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
import kotlinx.coroutines.CoroutineScope
@@ -106,12 +106,12 @@ class InterestSetsState(
fun getInterestSet(dTag: String) = listFeedFlow.value.getSet(dTag)
fun DeletionEvent.hasAnyDeletedInterestSets() = deleteAddressesWithKind(InterestSetEvent.KIND) || deletesAnyEventIn(interestSetEventIds.value)
fun DeletionRequestEvent.hasAnyDeletedInterestSets() = deleteAddressesWithKind(InterestSetEvent.KIND) || deletesAnyEventIn(interestSetEventIds.value)
fun hasItemInNoteList(notes: Set<Note>): Boolean =
notes.anyNotNullEvent { event ->
if (event.pubKey == signer.pubKey) {
event is InterestSetEvent || (event is DeletionEvent && event.hasAnyDeletedInterestSets())
event is InterestSetEvent || (event is DeletionRequestEvent && event.hasAnyDeletedInterestSets())
} else {
false
}
@@ -178,7 +178,7 @@ class InterestSetsState(
account: Account,
) {
val event = getInterestSetEvent(identifier)
val template = DeletionEvent.build(listOf(event))
val template = DeletionRequestEvent.build(listOf(event))
val deletionEvent = account.signer.sign(template)
account.sendMyPublicAndPrivateOutbox(deletionEvent)
}
@@ -41,7 +41,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAllPages
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent
import com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent
import com.vitorpamplona.quartz.nip60Cashu.mintApi.DeterministicSecretFactory
import com.vitorpamplona.quartz.nip60Cashu.mintApi.MeltQuoteBolt11ResponseDto
@@ -486,7 +486,7 @@ class CashuWalletState(
// Process our own NIP-09 deletions inline rather than
// relying on LocalCache's `deletedEventBundles`. That
// path only fires when `consume(DeletionEvent)` finds
// path only fires when `consume(DeletionRequestEvent)` finds
// the target Note still resident in `notes` — a
// LargeSoftCache backed by WeakReferences, which can
// be cleared on any GC cycle. When the weak ref is
@@ -503,7 +503,7 @@ class CashuWalletState(
val ourDeleteIds =
all
.asSequence()
.filterIsInstance<DeletionEvent>()
.filterIsInstance<DeletionRequestEvent>()
.filter { it.pubKey == pubKey }
.flatMap { it.deleteEventIds().asSequence() }
.toSet()
@@ -1447,7 +1447,7 @@ class CashuWalletState(
}
val redundantIds = redundant.map { it.event.id }.toSet()
runCatching {
val template = DeletionEvent.build(redundant.map { it.event })
val template = DeletionRequestEvent.build(redundant.map { it.event })
val signed = signer.sign(template)
publishEvent(signed)
}.onFailure {
@@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.model.serverList
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetsState
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.StarterPacksState
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState
import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.FollowListsState
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListsState
import com.vitorpamplona.amethyst.model.nip51Lists.interestLists.InterestListState
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag
@@ -44,9 +44,9 @@ import kotlinx.coroutines.flow.stateIn
@Suppress("UNCHECKED_CAST")
class MergedFollowListsState(
val kind3List: Kind3FollowListState,
val peopleList: PeopleListsState,
val followList: FollowListsState,
val hashtagList: HashtagListState,
val followSets: FollowSetsState,
val starterPacks: StarterPacksState,
val interestList: InterestListState,
val geohashList: GeohashListState,
val communityList: CommunityListState,
val scope: CoroutineScope,
@@ -85,9 +85,9 @@ class MergedFollowListsState(
combine(
listOf(
kind3List.flow,
peopleList.allGoodPeopleListProfiles,
followList.allPeopleListProfiles,
hashtagList.flow,
followSets.allGoodPeopleListProfiles,
starterPacks.allPeopleListProfiles,
interestList.flow,
geohashList.flow,
communityList.flow,
),
@@ -104,9 +104,9 @@ class MergedFollowListsState(
emit(
mergeLists(
kind3List.flow.value,
peopleList.allGoodPeopleListProfiles.value,
followList.allPeopleListProfiles.value,
hashtagList.flow.value,
followSets.allGoodPeopleListProfiles.value,
starterPacks.allPeopleListProfiles.value,
interestList.flow.value,
geohashList.flow.value,
communityList.flow.value,
),
@@ -118,9 +118,9 @@ class MergedFollowListsState(
SharingStarted.Eagerly,
mergeLists(
kind3List.flow.value,
peopleList.allGoodPeopleListProfiles.value,
followList.allPeopleListProfiles.value,
hashtagList.flow.value,
followSets.allGoodPeopleListProfiles.value,
starterPacks.allPeopleListProfiles.value,
interestList.flow.value,
geohashList.flow.value,
communityList.flow.value,
),
@@ -67,7 +67,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
import com.vitorpamplona.quartz.nip01Core.core.fastForEach
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import org.jetbrains.compose.resources.PluralStringResource
@@ -199,7 +199,7 @@ class NappletConsentSummary(
oneRemoved = Res.string.napplet_consent_diff_unmute_one,
)
// Deletions have no prior version to compare against — the tags are the whole request.
DeletionEvent.KIND ->
DeletionRequestEvent.KIND ->
pluralFor(Res.plurals.napplet_consent_effect_deletes, countTag(tags, "e") + countTag(tags, "a"))
?.let { Consequence(it) }
// Any other kind: at least tell the user tags exist and can be inspected, so an empty
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.napplet.gateways
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent
import kotlinx.serialization.json.add
@@ -137,7 +137,7 @@ class AccountIdentityReader(
val receipts =
cache
.filter(Filter(kinds = listOf(9735), tags = mapOf("p" to listOf(pubkey))))
.mapNotNull { it.event as? LnZapEvent }
.mapNotNull { it.event as? ZapReceiptEvent }
return buildJsonArray {
receipts.forEach { zap ->
addJsonObject {
@@ -40,7 +40,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendMethod
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.TlvRecord
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlForm
import com.vitorpamplona.quartz.podcasts.PodcastBoostagram
import com.vitorpamplona.quartz.podcasts.PodcastValue
@@ -91,7 +91,7 @@ class V4VPaymentHandler(
onProgress: (percent: Float) -> Unit,
onPayInvoicesViaIntent: (invoices: List<String>) -> Unit,
asZap: Boolean = false,
zapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC,
zapType: ZapReceiptEvent.ZapType = ZapReceiptEvent.ZapType.PUBLIC,
) = withContext(Dispatchers.IO) {
val shares = value.computeShares(totalMilliSats)
if (shares.isEmpty()) {
@@ -190,7 +190,7 @@ class V4VPaymentHandler(
shares: List<PodcastValueShare>,
message: String,
asZap: Boolean,
zapType: LnZapEvent.ZapType,
zapType: ZapReceiptEvent.ZapType,
zappedNote: Note?,
okHttpClient: (String) -> OkHttpClient,
context: Context,
@@ -48,8 +48,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorCode
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransactionMetadata
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup
@@ -79,7 +79,7 @@ class ZapPaymentHandler(
// The signed kind 9734 this invoice was fetched with, and the message on it.
// Carried so the NWC payment can name the payee (NWC-06 `metadata`); null for
// a NONZAP split, which has no zap request to send.
val zapRequest: LnZapRequestEvent? = null,
val zapRequest: ZapRequestEvent? = null,
val message: String = "",
)
@@ -122,7 +122,7 @@ class ZapPaymentHandler(
onError: (String, String, User?) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<Payable>) -> Unit,
zapType: LnZapEvent.ZapType,
zapType: ZapReceiptEvent.ZapType,
) = withContext(Dispatchers.IO) {
val noteEvent = note.event
val zapSplitSetup = noteEvent?.zapSplitSetup()
@@ -297,7 +297,7 @@ class ZapPaymentHandler(
note: Note,
pollOption: Int?,
message: String,
zapType: LnZapEvent.ZapType,
zapType: ZapReceiptEvent.ZapType,
totalAmountMilliSats: Long,
totalWeight: Double,
okHttpClient: (String) -> OkHttpClient,
@@ -360,14 +360,14 @@ class ZapPaymentHandler(
class ZapRequestReady(
val inputSetup: MyZapSplitSetup,
val zapRequest: LnZapRequestEvent?,
val zapRequest: ZapRequestEvent?,
)
suspend fun signAllZapRequests(
note: Note,
pollOption: Int?,
message: String,
zapType: LnZapEvent.ZapType,
zapType: ZapReceiptEvent.ZapType,
zapsToSend: List<MyZapSplitSetup>,
totalAmountMilliSats: Long,
// Shared across the lightning + BOLT12 lanes so a mixed split stays proportional.
@@ -389,7 +389,7 @@ class ZapPaymentHandler(
val splitLnurl = LnurlForm.toUrl(next.lnAddress)?.let(LnurlForm::urlToBech32)
val zapRequest =
if (zapType != LnZapEvent.ZapType.NONZAP && noteEvent != null) {
if (zapType != ZapReceiptEvent.ZapType.NONZAP && noteEvent != null) {
account.zaps.createZapRequestFor(
event = noteEvent,
pollOption = pollOption,
@@ -535,7 +535,7 @@ class ZapPaymentHandler(
totalAmountMilliSats: Long,
totalWeight: Double,
message: String,
zapType: LnZapEvent.ZapType,
zapType: ZapReceiptEvent.ZapType,
okHttpClient: (String) -> OkHttpClient,
onError: (String, String, User?) -> Unit,
onProgress: (percent: Float) -> Unit,
@@ -672,7 +672,7 @@ class ZapPaymentHandler(
private suspend fun assembleInvoice(
lud16: String,
splitSetup: MyZapSplitSetup,
nostrZapRequest: LnZapRequestEvent?,
nostrZapRequest: ZapRequestEvent?,
zapValue: Long,
message: String,
okHttpClient: (String) -> OkHttpClient,
@@ -683,7 +683,7 @@ class ZapPaymentHandler(
// Only the request the provider actually accepted may be claimed as bound to
// this invoice; see lnAddressInvoice's onZapRequestSent.
var sentZapRequest: LnZapRequestEvent? = null
var sentZapRequest: ZapRequestEvent? = null
val invoice =
LightningAddressResolver().lnAddressInvoice(
@@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.service.HttpStatusMessages
import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
import com.vitorpamplona.quartz.lightning.Lud06
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointCache
import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointInfo
import com.vitorpamplona.quartz.utils.Log
@@ -128,7 +128,7 @@ class LightningAddressResolver {
lnCallback: String,
milliSats: Long,
message: String,
nostrRequest: LnZapRequestEvent? = null,
nostrRequest: ZapRequestEvent? = null,
okHttpClient: (String) -> OkHttpClient,
context: Context,
): String {
@@ -219,11 +219,11 @@ class LightningAddressResolver {
lnAddress: String,
milliSats: Long,
message: String,
nostrRequest: LnZapRequestEvent? = null,
nostrRequest: ZapRequestEvent? = null,
okHttpClient: (String) -> OkHttpClient,
onProgress: (percent: Float) -> Unit,
context: Context,
onZapRequestSent: (LnZapRequestEvent?) -> Unit = {},
onZapRequestSent: (ZapRequestEvent?) -> Unit = {},
): String {
val mapper = ObjectMapper()
@@ -64,7 +64,7 @@ import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
@@ -72,7 +72,7 @@ import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
@@ -84,17 +84,17 @@ import com.vitorpamplona.quartz.nip34Git.status.GitStatusAppliedEvent
import com.vitorpamplona.quartz.nip34Git.status.GitStatusClosedEvent
import com.vitorpamplona.quartz.nip34Git.status.GitStatusDraftEvent
import com.vitorpamplona.quartz.nip34Git.status.GitStatusOpenEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent
import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent
import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent
import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent
import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
import com.vitorpamplona.quartz.nip71Video.VideoShortEvent
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent
@@ -234,7 +234,7 @@ class EventNotificationConsumer(
// public chat the user has silenced (matches the in-app feed, which mutes all four).
// Without the second check, muting a channel still let a like on your own message
// there notify you — the row's glyph promises silence, so it has to mean it.
if (event is ReactionEvent || event is LnZapEvent || event is RepostEvent || event is GenericRepostEvent) {
if (event is ReactionEvent || event is ZapReceiptEvent || event is RepostEvent || event is GenericRepostEvent) {
val target = LocalCache.getNoteIfExists(event)?.replyTo?.lastOrNull()
if (target != null &&
(
@@ -247,7 +247,7 @@ class EventNotificationConsumer(
}
when (event) {
is PrivateDmEvent -> DirectMessageNotification.notify(applicationContext, account, event)
is EncryptedDmEvent -> DirectMessageNotification.notify(applicationContext, account, event)
is ChatMessageEvent -> DirectMessageNotification.notify(applicationContext, account, event)
is ChatMessageEncryptedFileHeaderEvent -> DirectMessageNotification.notify(applicationContext, account, event)
@@ -256,7 +256,7 @@ class EventNotificationConsumer(
is StreamMessageV2Event -> BuzzDmNotification.notify(applicationContext, account, event)
is ChatEvent -> BuzzDmNotification.notify(applicationContext, account, event)
is LnZapEvent -> ZapNotification.notify(applicationContext, account, event)
is ZapReceiptEvent -> ZapNotification.notify(applicationContext, account, event)
is NutzapEvent -> ZapNotification.notify(applicationContext, account, event)
is OnchainZapEvent -> ZapNotification.notify(applicationContext, account, event)
@@ -274,15 +274,15 @@ class EventNotificationConsumer(
is PictureEvent,
is VideoNormalEvent,
is VideoShortEvent,
is VideoHorizontalEvent,
is VideoVerticalEvent,
is AddressableNormalVideoEvent,
is AddressableShortVideoEvent,
-> MediaNotification.notify(applicationContext, account, event)
is PollEvent -> MentionNotification.notify(applicationContext, account, event, titleRes = Res.string.app_notification_poll_channel_message)
is HighlightEvent,
is LongTextNoteEvent,
is WikiNoteEvent,
is LongFormContentEvent,
is WikiArticleEvent,
-> ArticleNotification.notify(applicationContext, account, event)
is GitIssueEvent -> CodeNotification.notify(applicationContext, account, event)
@@ -27,11 +27,11 @@ import com.vitorpamplona.amethyst.commons.model.nip71Video.selectVideoTrack
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoEvent
@@ -177,7 +177,7 @@ object NotificationContent {
}
suspend fun decryptZapContentAuthor(
event: LnZapRequestEvent,
event: ZapRequestEvent,
signer: NostrSigner,
): Event? =
if (event.isPrivateZap() && event.zappedAuthor().contains(event.pubKey)) {
@@ -191,8 +191,8 @@ object NotificationContent {
signer: NostrSigner,
): String? =
when (val event = note.event) {
is PrivateDmEvent -> event.decryptContent(signer)
is LnZapRequestEvent -> decryptZapContentAuthor(event, signer)?.content
is EncryptedDmEvent -> event.decryptContent(signer)
is ZapRequestEvent -> decryptZapContentAuthor(event, signer)?.content
else -> event?.content
}
@@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
@@ -40,7 +40,7 @@ import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
@@ -52,17 +52,17 @@ import com.vitorpamplona.quartz.nip34Git.status.GitStatusAppliedEvent
import com.vitorpamplona.quartz.nip34Git.status.GitStatusClosedEvent
import com.vitorpamplona.quartz.nip34Git.status.GitStatusDraftEvent
import com.vitorpamplona.quartz.nip34Git.status.GitStatusOpenEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent
import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent
import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent
import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent
import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
import com.vitorpamplona.quartz.nip71Video.VideoShortEvent
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent
@@ -101,7 +101,7 @@ class NotificationDispatcher(
private const val TAG = "NotificationDispatcher"
// The dispatcher observes the *final* notification payload kinds.
// GiftWrap/EphemeralGiftWrap (1059/21059) and SealedRumor (13) are NOT
// GiftWrap/EphemeralGiftWrap (1059/21059) and Seal (13) are NOT
// listed here — by the time we care, Account.newNotesPreProcessor has
// already unwrapped them and inserted the inner payload into LocalCache,
// which fires the observer a second time on the inner event.
@@ -118,8 +118,8 @@ class NotificationDispatcher(
internal val NOTIFICATION_KINDS: Set<Int> =
setOf(
// Direct-arrival
PrivateDmEvent.KIND,
LnZapEvent.KIND,
EncryptedDmEvent.KIND,
ZapReceiptEvent.KIND,
NutzapEvent.KIND,
OnchainZapEvent.KIND,
Bolt12ZapEvent.KIND,
@@ -133,8 +133,8 @@ class NotificationDispatcher(
PictureEvent.KIND,
VideoNormalEvent.KIND,
VideoShortEvent.KIND,
VideoHorizontalEvent.KIND,
VideoVerticalEvent.KIND,
AddressableNormalVideoEvent.KIND,
AddressableShortVideoEvent.KIND,
ChannelMessageEvent.KIND,
PollEvent.KIND,
GitPatchEvent.KIND,
@@ -150,8 +150,8 @@ class NotificationDispatcher(
GitStatusClosedEvent.KIND,
GitStatusDraftEvent.KIND,
HighlightEvent.KIND,
LongTextNoteEvent.KIND,
WikiNoteEvent.KIND,
LongFormContentEvent.KIND,
WikiArticleEvent.KIND,
LiveChessGameAcceptEvent.KIND,
LiveChessMoveEvent.KIND,
WakeUpEvent.KIND,
@@ -235,8 +235,8 @@ class NotificationDispatcher(
if (event is WakeUpEvent) return@predicate true
// getNoteIfExists(event) — not (event.id) — so
// AddressableEvent kinds (LongTextNote, WikiNote,
// LiveChess*, VideoHorizontal/Vertical) resolve to
// AddressableEvent kinds (LongFormContent, WikiArticle,
// LiveChess*, AddressableNormalVideo/AddressableShortVideo) resolve to
// their address-keyed replaceable note. The id-keyed
// version note has its replyTo moved away during
// insertion (LocalCache.consumeBaseReplaceable), so
@@ -36,7 +36,7 @@ import kotlinx.coroutines.CancellationException
*
* Once decrypted, the inner event is fed into [LocalCache]. From there the
* normal `Account.newNotesPreProcessor` → `GiftWrapEventHandler` →
* `SealedRumorEventHandler` chain unwraps any remaining layers, and
* `SealEventHandler` chain unwraps any remaining layers, and
* [NotificationDispatcher] picks up the final payload and notifies.
*/
object PushWrapDecryptor {
@@ -32,7 +32,7 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.Conver
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.ReplyAction
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postConversation
import com.vitorpamplona.amethyst.service.notifications.notificationManager
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
@@ -64,7 +64,7 @@ object DirectMessageNotification {
suspend fun notify(
context: Context,
account: Account,
event: PrivateDmEvent,
event: EncryptedDmEvent,
) {
if (account.signer.pubKey != event.verifiedRecipientPubKey()) return
notifyRoom(context, account, event.id, event.createdAt, event.chatroomKey(account.signer.pubKey), decrypt = true)
@@ -39,8 +39,8 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard
import com.vitorpamplona.amethyst.service.notifications.notificationManager
import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent
import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent
import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent
import org.jetbrains.compose.resources.StringResource
@@ -59,7 +59,7 @@ object ZapNotification {
suspend fun notify(
context: Context,
account: Account,
event: LnZapEvent,
event: ZapReceiptEvent,
) {
LocalCache.getNoteIfExists(event.id) ?: return
val zapRequestNote = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
@@ -67,7 +67,7 @@ object ZapNotification {
if (!account.isAcceptable(zappedNote)) return
if ((event.amount ?: BigDecimal.ZERO) < MIN_ZAP_AMOUNT) return
val zapRequestEvent = zapRequestNote.event as? LnZapRequestEvent ?: return
val zapRequestEvent = zapRequestNote.event as? ZapRequestEvent ?: return
// Resolve the (possibly private) zapper once — never re-decrypt per tick.
val decrypted = NotificationContent.decryptZapContentAuthor(zapRequestEvent, account.signer) ?: return
val sender = LocalCache.getOrCreateUser(decrypted.pubKey)
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata
import com.vitorpamplona.amethyst.commons.relayClient.assemblers.filterContactCardsByAuthorInTheRelay
import com.vitorpamplona.amethyst.commons.relayClient.assemblers.filterUserAssertionsByAuthorInTheRelay
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState.Companion.APP_SPECIFIC_DATA_D_TAG
@@ -32,18 +32,18 @@ import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent
import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent
import com.vitorpamplona.quartz.nip53LiveActivities.nestsServers.NestsServersEvent
@@ -60,9 +60,9 @@ val AccountInfoAndListsFromKeyKinds =
listOf(
MetadataEvent.KIND,
ContactListEvent.KIND,
StatusEvent.KIND,
UserStatusEvent.KIND,
AdvertisedRelayListEvent.KIND,
ChatMessageRelayListEvent.KIND,
DmRelayListEvent.KIND,
KeyPackageRelayListEvent.KIND,
SearchRelayListEvent.KIND,
FileServersEvent.KIND,
@@ -78,12 +78,12 @@ val AccountInfoAndListsFromKeyKinds2 =
BroadcastRelayListEvent.KIND,
IndexerRelayListEvent.KIND,
ProxyRelayListEvent.KIND,
HashtagListEvent.KIND,
InterestListEvent.KIND,
GeohashListEvent.KIND,
TrustProviderListEvent.KIND,
PaymentTargetsEvent.KIND,
Bolt12OfferListEvent.KIND,
RelayFeedsListEvent.KIND,
FavoriteRelayListEvent.KIND,
InterestSetEvent.KIND,
// NIP-51 "simple groups" list (kind 10009): the user's joined NIP-29 groups + servers.
// Loaded up-front so "My Groups" and group memberships resolve immediately at login,
@@ -141,7 +141,7 @@ fun filterAccountInfoAndListsFromKey(
),
// The account's own kind:30382 contact cards (nicknames, NIP-44 encrypted).
// Addressable — one card per target user — hence its own larger-limit filter.
filterContactCardsByAuthorInTheRelay(
filterUserAssertionsByAuthorInTheRelay(
relay = relay,
authors = pubkeys,
since = since,
@@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaym
import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent
@SuppressLint("StateFlowValueCalledInComposition")
@Composable
@@ -50,7 +50,7 @@ fun NWCFinderFilterAssemblerSubscription(
val states =
remember(note) {
val zapPaymentRequestNote = note
(zapPaymentRequestNote.event as? LnZapPaymentRequestEvent)?.let { noteEvent ->
(zapPaymentRequestNote.event as? NwcRequestEvent)?.let { noteEvent ->
noteEvent.walletServicePubKey()?.let {
zapPaymentRequestNote.relays.map {
NWCPaymentQueryState(
@@ -39,7 +39,7 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -64,11 +64,11 @@ fun observeUserName(
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user, accountViewModel)
val contactCards = accountViewModel.account.contactCards
val flow = remember(user) { contactCards.displayNameFlow(user) }
val userAssertions = accountViewModel.account.userAssertions
val flow = remember(user) { userAssertions.displayNameFlow(user) }
// Subscribe in the LocalCache for changes that arrive in the device
return flow.collectAsStateWithLifecycle(remember(user) { contactCards.cachedDisplayName(user) })
return flow.collectAsStateWithLifecycle(remember(user) { userAssertions.cachedDisplayName(user) })
}
/**
@@ -83,10 +83,10 @@ fun observeUserNickname(
user: User,
accountViewModel: AccountViewModel,
): State<Nickname?> {
val contactCards = accountViewModel.account.contactCards
val flow = remember(user) { contactCards.nicknameFlow(user) }
val userAssertions = accountViewModel.account.userAssertions
val flow = remember(user) { userAssertions.nicknameFlow(user) }
return flow.collectAsStateWithLifecycle(remember(user) { contactCards.cachedNickname(user) })
return flow.collectAsStateWithLifecycle(remember(user) { userAssertions.cachedNickname(user) })
}
@Composable
@@ -132,9 +132,9 @@ fun observeUserTagFollowCount(
.metadata.stateFlow
.sample(1000)
.mapLatest { noteState ->
(noteState.note.event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.size ?: 0
(noteState.note.event as? InterestListEvent)?.let { accountViewModel.account.interestListDecryptionCache.hashtags(it) }?.size ?: 0
}.onStart {
emit((accountViewModel.hashtagFollows(user).event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.size ?: 0)
emit((accountViewModel.hashtagFollows(user).event as? InterestListEvent)?.let { accountViewModel.account.interestListDecryptionCache.hashtags(it) }?.size ?: 0)
}.distinctUntilChanged()
.flowOn(Dispatchers.IO)
}
@@ -160,9 +160,9 @@ fun observeUserTagFollows(
.metadata.stateFlow
.sample(200)
.mapLatest { noteState ->
(noteState.note.event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList()
(noteState.note.event as? InterestListEvent)?.let { accountViewModel.account.interestListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList()
}.onStart {
emit((accountViewModel.hashtagFollows(user).event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList())
emit((accountViewModel.hashtagFollows(user).event as? InterestListEvent)?.let { accountViewModel.account.interestListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList())
}.distinctUntilChanged()
.flowOn(Dispatchers.IO)
}
@@ -304,16 +304,16 @@ fun observeUserIsFollowingHashtag(
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(accountViewModel) {
accountViewModel.account.hashtagList.flow
accountViewModel.account.interestList.flow
.mapLatest { hashtags ->
hashtag in hashtags
}.onStart {
emit(hashtag in accountViewModel.account.hashtagList.flow.value)
emit(hashtag in accountViewModel.account.interestList.flow.value)
}.distinctUntilChanged()
.flowOn(Dispatchers.IO)
}
return flow.collectAsStateWithLifecycle(hashtag in accountViewModel.account.hashtagList.flow.value)
return flow.collectAsStateWithLifecycle(hashtag in accountViewModel.account.interestList.flow.value)
}
@SuppressLint("StateFlowValueCalledInComposition")
@@ -373,16 +373,16 @@ fun observeUserIsFollowingRelay(
): State<Boolean> {
val flow =
remember(accountViewModel) {
accountViewModel.account.relayFeedsList.flowNoDefaults
accountViewModel.account.favoriteRelayList.flowNoDefaults
.mapLatest { relays ->
relayUrl in relays
}.onStart {
emit(relayUrl in accountViewModel.account.relayFeedsList.flowNoDefaults.value)
emit(relayUrl in accountViewModel.account.favoriteRelayList.flowNoDefaults.value)
}.distinctUntilChanged()
.flowOn(Dispatchers.IO)
}
return flow.collectAsStateWithLifecycle(relayUrl in accountViewModel.account.relayFeedsList.flowNoDefaults.value)
return flow.collectAsStateWithLifecycle(relayUrl in accountViewModel.account.favoriteRelayList.flowNoDefaults.value)
}
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@@ -439,7 +439,7 @@ fun observeUserIsFollowingChannel(
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@Composable
fun observeUserContactCardsScore(
fun observeUserAssertionsScore(
user: User,
accountViewModel: AccountViewModel,
subscribe: Boolean = true,
@@ -459,7 +459,7 @@ fun observeUserContactCardsScore(
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@Composable
fun observeUserContactCardsFollowerCount(
fun observeUserAssertionsFollowerCount(
user: User,
accountViewModel: AccountViewModel,
): State<String> {
@@ -26,8 +26,8 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip46RemoteSigner.signer.NostrSignerRemote
import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent
import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.NostrSignerWithClientTag
/**
@@ -89,7 +89,7 @@ class MeteringNostrSigner(
fromPublicKey: HexKey,
): String = metered(UsageKeys.DECRYPT_COUNT, UsageKeys.DECRYPT_US) { inner.nip44Decrypt(ciphertext, fromPublicKey) }
override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent = metered(UsageKeys.DECRYPT_COUNT, UsageKeys.DECRYPT_US) { inner.decryptZapEvent(event) }
override suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent = metered(UsageKeys.DECRYPT_COUNT, UsageKeys.DECRYPT_US) { inner.decryptZapEvent(event) }
override suspend fun deriveKey(nonce: HexKey): HexKey = inner.deriveKey(nonce)
@@ -26,9 +26,9 @@ import com.davotoula.lightcompressor.hls.HlsUploaded
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent
import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent
import com.vitorpamplona.quartz.nip71Video.VideoMeta
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
import com.vitorpamplona.quartz.nip71Video.duration
import com.vitorpamplona.quartz.nip71Video.title
import com.vitorpamplona.quartz.nip71Video.videoIMetas
@@ -61,11 +61,11 @@ data class HlsVideoPublishInput(
sealed class HlsVideoEventTemplate {
data class Horizontal(
val template: EventTemplate<VideoHorizontalEvent>,
val template: EventTemplate<AddressableNormalVideoEvent>,
) : HlsVideoEventTemplate()
data class Vertical(
val template: EventTemplate<VideoVerticalEvent>,
val template: EventTemplate<AddressableShortVideoEvent>,
) : HlsVideoEventTemplate()
}
@@ -80,7 +80,7 @@ data class HlsBuiltTemplate(
)
/**
* Assembles a NIP-71 VideoHorizontalEvent / VideoVerticalEvent template from an HLS upload
* Assembles a NIP-71 AddressableNormalVideoEvent / AddressableShortVideoEvent template from an HLS upload
* result. Orientation is decided from the first rendition's width/height: portrait
* (height > width) selects kind 34236, otherwise 34235.
*
@@ -142,7 +142,7 @@ object HlsVideoEventBuilder {
val template =
if (isVertical) {
HlsVideoEventTemplate.Vertical(
VideoVerticalEvent.build(input.description, dTag, createdAt) {
AddressableShortVideoEvent.build(input.description, dTag, createdAt) {
videoIMetas(videoMetas)
title(input.title)
input.durationSeconds?.let { duration(it) }
@@ -151,7 +151,7 @@ object HlsVideoEventBuilder {
)
} else {
HlsVideoEventTemplate.Horizontal(
VideoHorizontalEvent.build(input.description, dTag, createdAt) {
AddressableNormalVideoEvent.build(input.description, dTag, createdAt) {
videoIMetas(videoMetas)
title(input.title)
input.durationSeconds?.let { duration(it) }
@@ -54,7 +54,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.uriToRoute
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent
import com.vitorpamplona.quartz.nip92IMeta.imetasByUrl
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
@@ -425,7 +425,7 @@ fun RenderContentAsMarkdownNotePreview() {
runBlocking {
withContext(Dispatchers.IO) {
val blogPost =
LongTextNoteEvent(
LongFormContentEvent(
id = "e69fa72a221d0b7362f8e63fd6ab84cc6b5e7b505fe49cefe6823a47a6a4b583",
pubKey = "88cc134b1a65f54ef48acc1df3665063d3ea45f04eab8af4646e561c5ae99079",
createdAt = 1718785725,
@@ -32,8 +32,8 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasMoreHashtagsThan
import com.vitorpamplona.quartz.nip10Notes.threadRootIdOrSelf
import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.utils.TimeUtils
class FilterByListParams(
@@ -105,7 +105,7 @@ class FilterByListParams(
fun showHiddenKey(
selectedListName: String,
userHex: String,
) = selectedListName == PeopleListEvent.blockListFor(userHex) || selectedListName == MuteListEvent.blockListFor(userHex)
) = selectedListName == FollowSetEvent.blockListFor(userHex) || selectedListName == MuteListEvent.blockListFor(userHex)
fun create(
followLists: IFeedTopNavFilter?,
@@ -298,12 +298,12 @@ private class WarmTargets {
/**
* Adds NIP-94 file-header media: blobs declared as top-level `url` tags (not
* imeta), as used by FileHeaderEvent and the gallery/file kinds. Only image
* imeta), as used by FileMetadataEvent and the gallery/file kinds. Only image
* files are prefetched — detected by the URL extension or the event's top-level
* `m` mime type (a hashed URL may carry no extension). The decode listener
* records their dims like any other image.
*/
fun addFileHeaderMedia(event: Event) {
fun addFileMetadataMedia(event: Event) {
val urls = event.tags.mapNotNull(UrlTag::parse)
if (urls.isEmpty()) return
val imageMime = event.tags.firstNotNullOfOrNull(MimeTypeTag::parse)?.startsWith("image/") == true
@@ -333,9 +333,9 @@ private fun Note.collectWarmTargets(): WarmTargets? {
}
// NIP-94 file-header convention: the blob is a top-level `url` tag with sibling
// `m`/`dim`, not imeta (FileHeaderEvent and the gallery/file kinds). The generic
// `m`/`dim`, not imeta (FileMetadataEvent and the gallery/file kinds). The generic
// imetas() above misses these entirely.
targets.addFileHeaderMedia(ev)
targets.addFileMetadataMedia(ev)
// NIP-73 external-content scope. Only kind 1111 carries one, so the type check
// keeps every other kind out of the tag walk. Reuses scope() — the same accessor
@@ -148,7 +148,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.profileContentHeaderModifier
import com.vitorpamplona.amethyst.isDebug
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsFollowerCount
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAssertionsFollowerCount
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCatalog
@@ -588,7 +588,7 @@ fun DisplayFollowerCount(
baseAccountUser: Account,
accountViewModel: AccountViewModel,
) {
val followerCount by observeUserContactCardsFollowerCount(baseAccountUser.userProfile(), accountViewModel)
val followerCount by observeUserAssertionsFollowerCount(baseAccountUser.userProfile(), accountViewModel)
Text(
text = followerCount,
@@ -44,7 +44,7 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
@@ -57,12 +57,12 @@ import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestEvent
import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestUpdateEvent
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nip59Giftwrap.HasInnerEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
@@ -247,7 +247,7 @@ fun routeForInner(
}
}
is FollowListEvent -> {
is StarterPackEvent -> {
Route.FollowPack(noteEvent.address())
}
@@ -294,7 +294,7 @@ fun routeForInner(
Route.GeocacheHunt(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag())
}
is GiftWrapEvent, is SealedRumorEvent -> {
is GiftWrapEvent, is SealEvent -> {
val wrap = noteEvent as HasInnerEvent
wrap.innerEventId?.let {
routeFor(LocalCache.getOrCreateNote(it), loggedIn)
@@ -456,7 +456,7 @@ fun routeReplyTo(
}
is ChatroomKeyable -> {
// Covers PrivateDmEvent (NIP-04) and BaseDMGroupEvent (NIP-17) — both
// Covers EncryptedDmEvent (NIP-04) and BaseDMGroupEvent (NIP-17) — both
// implement ChatroomKeyable with identical routing semantics here.
routeToMessage(
room = noteEvent.chatroomKey(account.userProfile().pubkeyHex),
@@ -477,7 +477,7 @@ fun routeReplyTo(
}
}
is LnZapEvent -> {
is ZapReceiptEvent -> {
// A public reply can't tag a private zapper without exposing them.
// When we hold the decrypted sender (we are the zap recipient), reply
// in their DM room instead of the public comment composer.
@@ -532,7 +532,7 @@ suspend fun routeEditDraftTo(
Route.NewShortNote(draft = note.idHex)
}
is LongTextNoteEvent -> {
is LongFormContentEvent -> {
Route.NewLongFormPost(draft = note.idHex)
}
@@ -106,7 +106,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor
import com.vitorpamplona.amethyst.commons.ui.theme.overPictureBackground
import com.vitorpamplona.amethyst.commons.ui.theme.profile35dpModifier
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsScore
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAssertionsScore
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture
import com.vitorpamplona.amethyst.ui.components.CoreSecretMessage
import com.vitorpamplona.amethyst.ui.components.ExpandableRichTextViewer
@@ -841,7 +841,7 @@ fun ObserveAndRenderBoxedUserCards(
accountViewModel: AccountViewModel,
subscribe: Boolean = true,
) {
val score by observeUserContactCardsScore(user, accountViewModel, subscribe)
val score by observeUserAssertionsScore(user, accountViewModel, subscribe)
score?.let {
Box(modifier = Size35Modifier, contentAlignment = Alignment.BottomCenter) {
@@ -84,7 +84,7 @@ 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 com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
@@ -219,14 +219,14 @@ fun DisplayStatus(
nav: INav,
) {
val noteState by observeNote(addressableNote, accountViewModel)
val noteEvent = noteState.note.event as? StatusEvent ?: return
val noteEvent = noteState.note.event as? UserStatusEvent ?: return
DisplayStatus(noteEvent, accountViewModel, nav)
}
@Composable
fun DisplayStatus(
event: StatusEvent,
event: UserStatusEvent,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -266,7 +266,7 @@ fun DisplayStatusInner(
nav: INav,
) {
when (type) {
StatusEvent.MUSIC -> {
UserStatusEvent.MUSIC -> {
Icon(
imageVector = CustomHashTagIcons.Tunestr,
null,
@@ -141,18 +141,18 @@ import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayContactList
import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList
import com.vitorpamplona.amethyst.ui.note.types.DisplayFavoriteRelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayIndexerRelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList
import com.vitorpamplona.amethyst.ui.note.types.DisplayProxyRelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayRelayFeedsList
import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet
import com.vitorpamplona.amethyst.ui.note.types.DisplaySearchRelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayStarterPack
import com.vitorpamplona.amethyst.ui.note.types.DisplayTrustedRelayList
import com.vitorpamplona.amethyst.ui.note.types.EditState
import com.vitorpamplona.amethyst.ui.note.types.EmptyState
import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.FileMetadataDisplay
import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.PictureDisplay
import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition
@@ -181,6 +181,8 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderCitation
import com.vitorpamplona.amethyst.ui.note.types.RenderClassifieds
import com.vitorpamplona.amethyst.ui.note.types.RenderCommunity
import com.vitorpamplona.amethyst.ui.note.types.RenderCyberspaceBag
import com.vitorpamplona.amethyst.ui.note.types.RenderDvmContentDiscoveryResponse
import com.vitorpamplona.amethyst.ui.note.types.RenderDvmStatus
import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack
import com.vitorpamplona.amethyst.ui.note.types.RenderEntityRating
import com.vitorpamplona.amethyst.ui.note.types.RenderExternalReaction
@@ -201,14 +203,11 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveChessChallenge
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveChessGameEnd
import com.vitorpamplona.amethyst.ui.note.types.RenderLnZap
import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent
import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingRoomEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingSpaceEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderMusicPlaylist
import com.vitorpamplona.amethyst.ui.note.types.RenderMusicTrack
import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90ContentDiscoveryResponse
import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90Status
import com.vitorpamplona.amethyst.ui.note.types.RenderNamedNappletEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderNamedSiteEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderNipContent
@@ -258,21 +257,22 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderWikiMergeAcceptance
import com.vitorpamplona.amethyst.ui.note.types.RenderWikiMergeRequest
import com.vitorpamplona.amethyst.ui.note.types.RenderWikiRedirect
import com.vitorpamplona.amethyst.ui.note.types.RenderZapPoll
import com.vitorpamplona.amethyst.ui.note.types.RenderZapReceipt
import com.vitorpamplona.amethyst.ui.note.types.ReplyRenderType
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderAppCurationSet
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderArticleCurationSet
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderBookmarkList
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderBookmarkSet
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderFavoriteAlgoFeedsList
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderGeohashList
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderGitAuthorList
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderGitRepositoryList
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderGoodWikiAuthorList
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderGoodWikiRelayList
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderHashtagList
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderInterestList
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderInterestSet
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderKindMuteSet
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderLabeledBookmarkList
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderMediaFollowList
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderMediaStarterPack
import com.vitorpamplona.amethyst.ui.note.types.lists.RenderMuteList
@@ -332,16 +332,16 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull
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.nip17Dm.settings.DmRelayListEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent
import com.vitorpamplona.quartz.nip25Reactions.ExternalReactionEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
@@ -368,47 +368,47 @@ import com.vitorpamplona.quartz.nip51Lists.appCurationSet.AppCurationSetEvent
import com.vitorpamplona.quartz.nip51Lists.articleCurationSet.ArticleCurationSetEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.BookmarkSetEvent
import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
import com.vitorpamplona.quartz.nip51Lists.gitAuthorList.GitAuthorListEvent
import com.vitorpamplona.quartz.nip51Lists.gitRepositoryList.GitRepositoryListEvent
import com.vitorpamplona.quartz.nip51Lists.goodWikiAuthorList.GoodWikiAuthorListEvent
import com.vitorpamplona.quartz.nip51Lists.goodWikiRelayList.GoodWikiRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent
import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent
import com.vitorpamplona.quartz.nip51Lists.kindMuteSet.KindMuteSetEvent
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.mediaFollowList.MediaFollowListEvent
import com.vitorpamplona.quartz.nip51Lists.mediaStarterPack.MediaStarterPackEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.pictureCurationSet.PictureCurationSetEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent
import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent
import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent
import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent
import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent
import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent
import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent
import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent
import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent
import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiMergeAcceptanceEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiMergeRequestEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiRedirectEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetupBesidesAuthor
import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent
@@ -428,7 +428,7 @@ import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprov
import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip72ModCommunities.isACommunityPost
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
@@ -438,9 +438,9 @@ import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEven
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent
import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent
import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent
@@ -1146,7 +1146,7 @@ private fun RenderNoteRow(
RenderReport(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
}
is LongTextNoteEvent -> {
is LongFormContentEvent -> {
RenderLongFormContent(baseNote, accountViewModel, nav)
}
@@ -1172,7 +1172,7 @@ private fun RenderNoteRow(
RenderCodeSnippetEvent(baseNote)
}
is WikiNoteEvent -> {
is WikiArticleEvent -> {
RenderWikiContent(baseNote, accountViewModel, nav)
}
@@ -1192,8 +1192,8 @@ private fun RenderNoteRow(
BadgeDisplay(baseNote = baseNote, accountViewModel = accountViewModel, nav = nav)
}
is LnZapEvent -> {
RenderLnZap(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
is ZapReceiptEvent -> {
RenderZapReceipt(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
}
is NutzapEvent -> {
@@ -1216,12 +1216,12 @@ private fun RenderNoteRow(
RenderFhirResource(baseNote, accountViewModel, nav)
}
is PeopleListEvent -> {
is FollowSetEvent -> {
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
}
is FollowListEvent -> {
DisplayFollowList(baseNote, true, accountViewModel, nav)
is StarterPackEvent -> {
DisplayStarterPack(baseNote, true, accountViewModel, nav)
}
is ContactListEvent -> {
@@ -1232,7 +1232,7 @@ private fun RenderNoteRow(
DisplayRelaySet(baseNote, backgroundColor, accountViewModel, nav)
}
is ChatMessageRelayListEvent -> {
is DmRelayListEvent -> {
DisplayDMRelayList(baseNote, backgroundColor, accountViewModel, nav)
}
@@ -1252,8 +1252,8 @@ private fun RenderNoteRow(
DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav)
}
is RelayFeedsListEvent -> {
DisplayRelayFeedsList(baseNote, backgroundColor, accountViewModel, nav)
is FavoriteRelayListEvent -> {
DisplayFavoriteRelayList(baseNote, backgroundColor, accountViewModel, nav)
}
is IndexerRelayListEvent -> {
@@ -1385,7 +1385,7 @@ private fun RenderNoteRow(
)
}
is PrivateDmEvent -> {
is EncryptedDmEvent -> {
RenderPrivateMessage(
baseNote,
makeItShort,
@@ -1495,7 +1495,7 @@ private fun RenderNoteRow(
RenderCalendarDateSlotEvent(baseNote, accountViewModel, nav)
}
is CalendarEvent -> {
is CalendarCollectionEvent -> {
RenderCalendarCollectionEvent(baseNote, accountViewModel, nav)
}
@@ -1503,7 +1503,7 @@ private fun RenderNoteRow(
RenderCalendarRSVPEvent(baseNote, accountViewModel, nav)
}
is GoalEvent -> {
is ZapGoalEvent -> {
RenderGoal(baseNote, accountViewModel, nav)
}
@@ -1551,8 +1551,8 @@ private fun RenderNoteRow(
)
}
is NIP90ContentDiscoveryResponseEvent -> {
RenderNIP90ContentDiscoveryResponse(
is DvmContentDiscoveryResponseEvent -> {
RenderDvmContentDiscoveryResponse(
baseNote,
makeItShort,
canPreview,
@@ -1563,8 +1563,8 @@ private fun RenderNoteRow(
)
}
is NIP90StatusEvent -> {
RenderNIP90Status(
is DvmStatusEvent -> {
RenderDvmStatus(
baseNote,
accountViewModel,
nav,
@@ -1641,8 +1641,8 @@ private fun RenderNoteRow(
)
}
is FileHeaderEvent -> {
FileHeaderDisplay(baseNote, true, ContentScale.FillWidth, accountViewModel)
is FileMetadataEvent -> {
FileMetadataDisplay(baseNote, true, ContentScale.FillWidth, accountViewModel)
}
// Covers every NIP-71 video kind (21, 22, 34235, 34236) via the shared interface,
@@ -1880,8 +1880,8 @@ private fun RenderNoteRow(
RenderOldBookmarkList(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
}
is LabeledBookmarkListEvent -> {
RenderLabeledBookmarkList(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
is BookmarkSetEvent -> {
RenderBookmarkSet(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
}
is ArticleCurationSetEvent -> {
@@ -1924,8 +1924,8 @@ private fun RenderNoteRow(
RenderMediaStarterPack(baseNote, backgroundColor, accountViewModel, nav)
}
is HashtagListEvent -> {
RenderHashtagList(baseNote, backgroundColor, accountViewModel, nav)
is InterestListEvent -> {
RenderInterestList(baseNote, backgroundColor, accountViewModel, nav)
}
is InterestSetEvent -> {
@@ -2173,7 +2173,7 @@ fun FirstUserInfoRow(
// Zap receipts are signed by the recipient's lightning provider; show the
// sender from the embedded zap request instead of the service key.
val zapSender =
if (baseNote.event is LnZapEvent) {
if (baseNote.event is ZapReceiptEvent) {
observeZapSender(baseNote, accountViewModel).value
} else {
null
@@ -2412,7 +2412,7 @@ fun RenderAuthorImages(
} else {
NoteAuthorPicture(baseNote, Size55dp, accountViewModel = accountViewModel, nav = nav)
}
} else if (noteEvent is LnZapEvent) {
} else if (noteEvent is ZapReceiptEvent) {
// Zap receipts are signed by the recipient's lightning provider; show the
// sender from the embedded zap request instead of the service key, matching
// how the thread's master note resolves the author.
@@ -116,8 +116,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog
import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent
import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.StringResource
@@ -149,8 +149,8 @@ val externalLinkForNote = { note: Note ->
if (note is AddressableNote) {
when {
note.event?.bountyBaseReward() != null -> "https://nostrbounties.com/b/${note.toNAddr()}"
note.event is PeopleListEvent -> "https://listr.lol/a/${note.toNAddr()}"
note.event is FollowListEvent -> "https://following.space/d/${note.address.dTag}?p=${note.address.pubKeyHex}"
note.event is FollowSetEvent -> "https://listr.lol/a/${note.toNAddr()}"
note.event is StarterPackEvent -> "https://following.space/d/${note.address.dTag}?p=${note.address.pubKeyHex}"
else -> njumpLink(note.toNAddr())
}
} else {
@@ -95,7 +95,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiListEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -358,10 +358,10 @@ private fun EmojiSelector(
onClick: ((EmojiUrlTag) -> Unit)? = null,
) {
LoadAddressableNote(
accountViewModel.account.emoji.getEmojiPackSelectionAddress(),
accountViewModel.account.emoji.getEmojiListAddress(),
) { emptyNote ->
emptyNote?.let { usersEmojiList ->
val collections by observeNoteEventAndMapNotNull(usersEmojiList, accountViewModel) { event: EmojiPackSelectionEvent ->
val collections by observeNoteEventAndMapNotNull(usersEmojiList, accountViewModel) { event: EmojiListEvent ->
event.emojiPacks().toImmutableList()
}
@@ -121,7 +121,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.getFragmentActivity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
@@ -153,22 +153,22 @@ fun UpdateZapAmountContent(
val zapTypes =
listOf(
Triple(
LnZapEvent.ZapType.PUBLIC,
ZapReceiptEvent.ZapType.PUBLIC,
stringRes(id = Res.string.zap_type_public),
stringRes(id = Res.string.zap_type_public_explainer),
),
Triple(
LnZapEvent.ZapType.PRIVATE,
ZapReceiptEvent.ZapType.PRIVATE,
stringRes(id = Res.string.zap_type_private),
stringRes(id = Res.string.zap_type_private_explainer),
),
Triple(
LnZapEvent.ZapType.ANONYMOUS,
ZapReceiptEvent.ZapType.ANONYMOUS,
stringRes(id = Res.string.zap_type_anonymous),
stringRes(id = Res.string.zap_type_anonymous_explainer),
),
Triple(
LnZapEvent.ZapType.NONZAP,
ZapReceiptEvent.ZapType.NONZAP,
stringRes(id = Res.string.zap_type_nonzap),
stringRes(id = Res.string.zap_type_nonzap_explainer),
),
@@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip19Bech32.decodePrivateKeyAsHexOrNull
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import kotlinx.coroutines.CancellationException
@Stable
@@ -44,7 +44,7 @@ class UpdateZapAmountViewModel : ViewModel() {
var walletConnectRelay by mutableStateOf(TextFieldValue(""))
var walletConnectPubkey by mutableStateOf(TextFieldValue(""))
var walletConnectSecret by mutableStateOf(TextFieldValue(""))
var selectedZapType by mutableStateOf(LnZapEvent.ZapType.PRIVATE)
var selectedZapType by mutableStateOf(ZapReceiptEvent.ZapType.PRIVATE)
// A local UI preference rather than synced account state, but it is edited on
// this screen, so it follows this screen's Save/Cancel contract instead of
@@ -52,7 +52,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.ScoreTag
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsScore
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAssertionsScore
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -565,7 +565,7 @@ fun ObserveAndRenderUserCards(
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
) {
val score by observeUserContactCardsScore(user, accountViewModel)
val score by observeUserAssertionsScore(user, accountViewModel)
score?.let {
ScoreTag(it, size, modifier)
@@ -117,7 +117,7 @@ import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.OnchainZapSendDialog
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CancellationException
@@ -163,35 +163,35 @@ fun ZapCustomDialog(
val zapTypes =
listOf(
Triple(
LnZapEvent.ZapType.PUBLIC,
ZapReceiptEvent.ZapType.PUBLIC,
stringRes(id = Res.string.zap_type_public),
stringRes(id = Res.string.zap_type_public_explainer),
),
Triple(
LnZapEvent.ZapType.PRIVATE,
ZapReceiptEvent.ZapType.PRIVATE,
stringRes(id = Res.string.zap_type_private),
stringRes(id = Res.string.zap_type_private_explainer),
),
Triple(
LnZapEvent.ZapType.ANONYMOUS,
ZapReceiptEvent.ZapType.ANONYMOUS,
stringRes(id = Res.string.zap_type_anonymous),
stringRes(id = Res.string.zap_type_anonymous_explainer),
),
Triple(
LnZapEvent.ZapType.NONZAP,
ZapReceiptEvent.ZapType.NONZAP,
stringRes(id = Res.string.zap_type_nonzap),
stringRes(id = Res.string.zap_type_nonzap_explainer),
),
).filter {
!isPrivateTarget || it.first == LnZapEvent.ZapType.PRIVATE || it.first == LnZapEvent.ZapType.NONZAP
!isPrivateTarget || it.first == ZapReceiptEvent.ZapType.PRIVATE || it.first == ZapReceiptEvent.ZapType.NONZAP
}
var selectedZapType by
remember(accountViewModel, baseNote) {
val default = accountViewModel.defaultZapType()
mutableStateOf(
if (isPrivateTarget && default != LnZapEvent.ZapType.NONZAP) {
LnZapEvent.ZapType.PRIVATE
if (isPrivateTarget && default != ZapReceiptEvent.ZapType.NONZAP) {
ZapReceiptEvent.ZapType.PRIVATE
} else {
default
},
@@ -327,15 +327,15 @@ fun ZapCustomDialog(
OutlinedTextField(
label = {
when (selectedZapType) {
LnZapEvent.ZapType.PUBLIC, LnZapEvent.ZapType.ANONYMOUS -> {
ZapReceiptEvent.ZapType.PUBLIC, ZapReceiptEvent.ZapType.ANONYMOUS -> {
Text(text = stringRes(id = Res.string.custom_zaps_add_a_message))
}
LnZapEvent.ZapType.PRIVATE -> {
ZapReceiptEvent.ZapType.PRIVATE -> {
Text(text = stringRes(id = Res.string.custom_zaps_add_a_message_private))
}
LnZapEvent.ZapType.NONZAP -> {
ZapReceiptEvent.ZapType.NONZAP -> {
Text(text = stringRes(id = Res.string.custom_zaps_add_a_message_nonzap))
}
}
@@ -113,7 +113,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockVitorAccountViewModel
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -145,7 +145,7 @@ fun ZapZapPollNotePreview() {
)
val zapVote =
LnZapEvent(
ZapReceiptEvent(
id = "2a17fdcd0e387d1623c7313d7aa2848e18dde8a942cfe8a2d6b686ea5f68f01a",
pubKey = "79f00d3f5a19ec806189fcab03c1be4ff81d18ee4f653c88fac41fe03570f432",
createdAt = 1729186293,
@@ -30,8 +30,8 @@ import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -233,9 +233,9 @@ class PollNoteViewModel : ViewModel() {
user: User,
): Boolean =
pollNote!!.zaps.any {
val zapEvent = it.value?.event as? LnZapEvent
val zapEvent = it.value?.event as? ZapReceiptEvent
val privateZapAuthor =
(it.key.event as? LnZapRequestEvent)?.let {
(it.key.event as? ZapRequestEvent)?.let {
account.privateZapsDecryptionCache.cachedPrivateZap(it)
}
zapEvent?.zappedPollOption() == option &&
@@ -244,7 +244,7 @@ class PollNoteViewModel : ViewModel() {
private fun zappedPollOptionAmount(option: Int): BigDecimal =
pollNote?.zaps?.values?.sumOf {
val event = it?.event as? LnZapEvent
val event = it?.event as? ZapReceiptEvent
val zapAmount = event?.amount ?: BigDecimal.ZERO
val isValidAmount = isValidInputVoteAmount(event?.amount)
@@ -258,7 +258,7 @@ class PollNoteViewModel : ViewModel() {
private fun totalZapped(): BigDecimal =
pollNote?.zaps?.values?.sumOf {
val zapEvent = (it?.event as? LnZapEvent)
val zapEvent = (it?.event as? ZapReceiptEvent)
val zapAmount = zapEvent?.amount ?: BigDecimal.ZERO
val isValidAmount = isValidInputVoteAmount(zapEvent?.amount)
@@ -27,16 +27,16 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiListEvent
import kotlinx.collections.immutable.toImmutableList
@Composable
fun WatchAndLoadMyEmojiList(accountViewModel: AccountViewModel) {
LoadAddressableNote(
EmojiPackSelectionEvent.createAddress(accountViewModel.userProfile().pubkeyHex),
EmojiListEvent.createAddress(accountViewModel.userProfile().pubkeyHex),
) { emptyNote ->
emptyNote?.let { usersEmojiList ->
val collections by observeNoteEventAndMapNotNull(usersEmojiList, accountViewModel) { event: EmojiPackSelectionEvent ->
val collections by observeNoteEventAndMapNotNull(usersEmojiList, accountViewModel) { event: EmojiListEvent ->
event.taggedAddresses().toImmutableList()
}
@@ -67,7 +67,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.AudienceList
import com.vitorpamplona.amethyst.commons.model.composer.AudienceListKind
import com.vitorpamplona.amethyst.commons.model.composer.AudienceMember
import com.vitorpamplona.amethyst.commons.model.composer.AudienceSelection
import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleList
import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.PeopleList
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.audience_add_anyway
import com.vitorpamplona.amethyst.commons.resources.audience_add_people
@@ -545,9 +545,9 @@ private fun MemberBadge(
*/
@Composable
fun rememberAudienceLists(accountViewModel: AccountViewModel): List<AudienceList> {
val peopleLists by accountViewModel.account.peopleLists.uiListFlow
val peopleLists by accountViewModel.account.followSets.uiListFlow
.collectAsStateWithLifecycle()
val followPacks by accountViewModel.account.followLists.uiListFlow
val followPacks by accountViewModel.account.starterPacks.uiListFlow
.collectAsStateWithLifecycle()
return remember(peopleLists, followPacks) {
@@ -55,7 +55,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.AddDMRelayListDialog
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent
@Preview
@Composable
@@ -88,7 +88,7 @@ fun ObserveRelayListForDMsAndDisplayIfNotFound(
@Composable
fun ObserveRelayListForDMs(
accountViewModel: AccountViewModel,
inner: @Composable (relayListEvent: ChatMessageRelayListEvent?) -> Unit,
inner: @Composable (relayListEvent: DmRelayListEvent?) -> Unit,
) {
ObserveRelayListForDMs(
pubkey = accountViewModel.account.userProfile().pubkeyHex,
@@ -102,13 +102,13 @@ fun ObserveRelayListForDMs(
fun ObserveRelayListForDMs(
pubkey: HexKey,
accountViewModel: AccountViewModel,
inner: @Composable (relayListEvent: ChatMessageRelayListEvent?) -> Unit,
inner: @Composable (relayListEvent: DmRelayListEvent?) -> Unit,
) {
LoadAddressableNote(
ChatMessageRelayListEvent.createAddress(pubkey),
DmRelayListEvent.createAddress(pubkey),
) { relayList ->
if (relayList != null) {
val relayListEvent by observeNoteEvent<ChatMessageRelayListEvent>(relayList, accountViewModel)
val relayListEvent by observeNoteEvent<DmRelayListEvent>(relayList, accountViewModel)
inner(relayListEvent)
}
@@ -281,7 +281,7 @@ fun observeBookmarksFollowsAndAccount(
accountViewModel.account.bookmarkState.bookmarks,
accountViewModel.account.pinState.pinnedEventIdSet,
accountViewModel.showSensitiveContent(),
accountViewModel.account.emoji.getEmojiPackSelectionFlow(),
accountViewModel.account.emoji.getEmojiListFlow(),
) { follows, bookmarks, pinnedIds, showSensitiveContent, emojiSelectionState ->
val isEmojiPackInMyList =
if (noteIdForEmoji != null) {
@@ -312,7 +312,7 @@ fun observeBookmarksFollowsAndAccount(
isEmojiPackInMyList =
noteIdForEmoji?.let {
accountViewModel.account.emoji
.getEmojiPackSelection()
.getEmojiList()
?.isTaggedAddressableNote(it) == true
} ?: false,
),
@@ -80,7 +80,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
import kotlinx.coroutines.Dispatchers
@@ -222,7 +222,7 @@ fun noteActionSections(
// nostr source pre-tagged (`a` for an addressable article, else `e`) plus the author,
// and the passage left for the user to type or paste. Prose kinds only, and never a
// private rumor (a public highlight would e-tag the unsigned rumor onto relays).
if (!isPrivateRumor && (note.event is TextNoteEvent || note.event is LongTextNoteEvent)) {
if (!isPrivateRumor && (note.event is TextNoteEvent || note.event is LongFormContentEvent)) {
add(
NoteAction(MaterialSymbols.FormatQuote, stringRes(Res.string.highlight_action)) {
val author = note.author?.pubkeyHex
@@ -256,7 +256,7 @@ fun noteActionSections(
onClick = handlers.onEditPost,
),
)
} else if (note.event is LongTextNoteEvent && state.isLoggedUser) {
} else if (note.event is LongFormContentEvent && state.isLoggedUser) {
add(
NoteAction(MaterialSymbols.Edit, stringRes(Res.string.edit_article)) {
nav.nav { Route.NewLongFormPost(version = note.idHex) }
@@ -343,10 +343,10 @@ fun noteActionSections(
}
else -> {
val noteBookmarkType = if (note.event is LongTextNoteEvent) stringRes(Res.string.article) else stringRes(Res.string.post)
val noteBookmarkType = if (note.event is LongFormContentEvent) stringRes(Res.string.article) else stringRes(Res.string.post)
add(
NoteAction(MaterialSymbols.BookmarkAdd, stringRes(Res.string.manage_bookmark_label, noteBookmarkType)) {
if (note.event is LongTextNoteEvent) {
if (note.event is LongFormContentEvent) {
nav.nav(Route.ArticleBookmarkManagement((note as AddressableNote).address))
} else {
nav.nav(Route.PostBookmarkManagement(note.idHex))
@@ -109,7 +109,7 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
import com.vitorpamplona.quartz.nip40Expiration.expiration
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
@@ -352,7 +352,7 @@ open class CommentPostViewModel :
this.externalIdentity = (post.event as? CommentEvent)?.scope()
mutedNotifies = emptySet()
notifyProvenance = emptyMap()
(post.event as? LnZapEvent)?.let { zap ->
(post.event as? ZapReceiptEvent)?.let { zap ->
notifying = listOfNotNull(zapSenderToNotify(zap))
}
observeCommunityRules(post)
@@ -366,7 +366,7 @@ open class CommentPostViewModel :
* by an ephemeral key: skip those — tagging the throwaway key is useless, and
* tagging the decrypted sender would publicly expose a private zapper.
*/
private fun zapSenderToNotify(zapEvent: LnZapEvent): User? {
private fun zapSenderToNotify(zapEvent: ZapReceiptEvent): User? {
val request = zapEvent.zapRequest ?: return null
if (request.hasAnonTag()) return null
if (request.pubKey == account.signer.pubKey) return null
@@ -554,7 +554,7 @@ open class CommentPostViewModel :
// Replies to zaps notify the zap sender through a plain p tag (the receipt's
// author keys above are the lightning provider). The sender chip always comes
// back; a missing p tag in the draft means the user muted their bell.
(replyingTo?.event as? LnZapEvent)?.let { zap ->
(replyingTo?.event as? ZapReceiptEvent)?.let { zap ->
zapSenderToNotify(zap)?.let { sender ->
notifying = ((notifying ?: emptyList()) + sender).distinct()
if (!draftEvent.tags.mapNotNull(PTag::parseKey).contains(sender.pubkeyHex)) {
@@ -726,7 +726,7 @@ open class CommentPostViewModel :
null
}
}
} else if (replyingToEvent is LnZapEvent) {
} else if (replyingToEvent is ZapReceiptEvent) {
val sender = zapSenderToNotify(replyingToEvent)
// The sender's chip stays in the list; a muted bell means
// the user doesn't want to ping them, so don't tag them.
@@ -63,7 +63,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.GalleryThum
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.UrlImageView
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent
import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent
@@ -278,7 +278,7 @@ private fun secondaryLineFor(
return when {
event is PictureEvent -> stringRes(Res.string.share_as_qr_kind_picture)
event is VideoEvent -> stringRes(Res.string.kind_video)
event is LongTextNoteEvent -> stringRes(Res.string.article)
event is LongFormContentEvent -> stringRes(Res.string.article)
hasContentImage -> stringRes(Res.string.share_as_qr_kind_picture)
else -> ""
}
@@ -293,7 +293,7 @@ internal fun secondaryBodyTextFor(
// Gated: never surface the note's own title, content or alt text, only the kind label above.
if (isGated) return null
if (event is LongTextNoteEvent) {
if (event is LongFormContentEvent) {
val title = event.title()
if (!title.isNullOrBlank()) return title
}
@@ -103,10 +103,10 @@ private object ActivityCardPreviewData {
}
}
fun lnZapNote(): Note {
fun zapReceiptNote(): Note {
val id = "d".repeat(64)
// Receipts are signed by the lightning provider; the preview skips the
// embedded request and feeds the decrypted card to RenderLnZapCard directly.
// embedded request and feeds the decrypted card to RenderZapReceiptCard directly.
return Note(id).apply {
event = null
author = null
@@ -171,12 +171,12 @@ fun EmojiReactionActivityCardPreview() {
@Preview
@Composable
fun LnZapActivityCardPreview() {
fun ZapReceiptActivityCardPreview() {
val accountViewModel = mockAccountViewModel()
ThemeComparisonColumn {
RenderLnZapCard(
note = ActivityCardPreviewData.lnZapNote(),
RenderZapReceiptCard(
note = ActivityCardPreviewData.zapReceiptNote(),
card =
ZapAmountCommentNotification(
user = LocalCache.getOrCreateUser(ActivityCardPreviewData.senderHex),
@@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.CalendarCollectionCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent
import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent
/**
* Entry for a NIP-52 calendar collection: decodes the [Note] and renders the shared commons
@@ -38,7 +38,7 @@ fun RenderCalendarCollectionEvent(
accountViewModel: AccountViewModel,
nav: INav,
) {
val event = note.event as? CalendarEvent ?: return
val event = note.event as? CalendarCollectionEvent ?: return
CalendarCollectionCard(event)
}
@@ -181,7 +181,7 @@ private fun EmojiListOptions(
emojiPackNote: Note,
) {
LoadAddressableNote(
accountViewModel.account.emoji.getEmojiPackSelectionAddress(),
accountViewModel.account.emoji.getEmojiListAddress(),
) {
it?.let { usersEmojiList ->
val hasAddedThis by observeNoteAndMap(usersEmojiList, accountViewModel) {
@@ -35,16 +35,16 @@ import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent
@Composable
fun FileHeaderDisplay(
fun FileMetadataDisplay(
note: Note,
roundedCorner: Boolean,
contentScale: ContentScale,
accountViewModel: AccountViewModel,
) {
val event = (note.event as? FileHeaderEvent) ?: return
val event = (note.event as? FileMetadataEvent) ?: return
val fullUrl = event.url() ?: return
val mimeType = remember(note) { event.mimeType() }
val content = remember(note) { event.toMediaContent(note, fullUrl, mimeType) }
@@ -53,7 +53,7 @@ fun FileHeaderDisplay(
// which viewer happens to render it, so an NSFW-tagged archive stays behind the same gate.
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
if (content == null) {
FileHeaderAttachmentCard(event, fullUrl, mimeType)
FileMetadataAttachmentCard(event, fullUrl, mimeType)
} else {
ZoomableContentView(
content = content,
@@ -70,9 +70,9 @@ fun FileHeaderDisplay(
*
* Kind 1063 is a *generic* file container — its `m` tag can name any type, so unlike a NIP-71
* video event the kind itself asserts nothing about how to render the payload. A null here means
* the file belongs in [FileHeaderAttachmentCard] rather than being pushed into the video player.
* the file belongs in [FileMetadataAttachmentCard] rather than being pushed into the video player.
*/
internal fun FileHeaderEvent.toMediaContent(
internal fun FileMetadataEvent.toMediaContent(
note: Note,
url: String,
mimeType: String?,
@@ -128,8 +128,8 @@ internal fun FileHeaderEvent.toMediaContent(
/** The link card a kind-1063 header falls back to when [toMediaContent] returns null. */
@Composable
internal fun FileHeaderAttachmentCard(
event: FileHeaderEvent,
internal fun FileMetadataAttachmentCard(
event: FileMetadataEvent,
url: String,
mimeType: String?,
) {
@@ -145,4 +145,4 @@ internal fun FileHeaderAttachmentCard(
}
/** The human-facing name of the file: NIP-94 `content` when present, else the `alt` tag. */
private fun FileHeaderEvent.fileDescription(): String? = content.ifEmpty { null } ?: alt()
private fun FileMetadataEvent.fileDescription(): String? = content.ifEmpty { null } ?: alt()
@@ -60,7 +60,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground
import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import java.math.BigDecimal
import kotlin.math.roundToInt
@@ -71,14 +71,14 @@ fun RenderGoal(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = note.event as? GoalEvent ?: return
val noteEvent = note.event as? ZapGoalEvent ?: return
GoalHeader(noteEvent, note, accountViewModel, nav)
}
@Composable
fun GoalHeader(
noteEvent: GoalEvent,
noteEvent: ZapGoalEvent,
note: Note,
accountViewModel: AccountViewModel,
nav: INav,
@@ -65,7 +65,7 @@ import com.vitorpamplona.amethyst.ui.note.WatchAuthor
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent
import kotlinx.collections.immutable.toImmutableList
private const val WORDS_PER_MINUTE = 225
@@ -77,7 +77,7 @@ fun RenderLongFormContent(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = note.event as? LongTextNoteEvent ?: return
val noteEvent = note.event as? LongFormContentEvent ?: return
LongFormHeader(noteEvent, note, accountViewModel)
}
@@ -85,7 +85,7 @@ fun RenderLongFormContent(
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun LongFormHeader(
noteEvent: LongTextNoteEvent,
noteEvent: LongFormContentEvent,
note: Note,
accountViewModel: AccountViewModel,
) {
@@ -35,10 +35,10 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent
@Composable
fun RenderNIP90ContentDiscoveryResponse(
fun RenderDvmContentDiscoveryResponse(
note: Note,
makeItShort: Boolean,
canPreview: Boolean,
@@ -47,7 +47,7 @@ fun RenderNIP90ContentDiscoveryResponse(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = note.event as? NIP90ContentDiscoveryResponseEvent ?: return
val noteEvent = note.event as? DvmContentDiscoveryResponseEvent ?: return
val callbackUri = remember(note) { note.toNostrUri() }
SensitivityWarning(
@@ -25,15 +25,15 @@ import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent
import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent
@Composable
fun RenderNIP90Status(
fun RenderDvmStatus(
note: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = note.event as? NIP90StatusEvent ?: return
val noteEvent = note.event as? DvmStatusEvent ?: return
Text(text = noteEvent.content)
}
@@ -56,7 +56,7 @@ import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.note.getGradient
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -68,7 +68,7 @@ fun DisplayPeopleList(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = baseNote.event as? PeopleListEvent ?: return
val noteEvent = baseNote.event as? FollowSetEvent ?: return
var members by remember { mutableStateOf<ImmutableList<User>>(persistentListOf()) }
@@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.ChatroomHeader
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip19Bech32.toNpub
@@ -67,7 +67,7 @@ fun RenderPrivateMessage(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = note.event as? PrivateDmEvent ?: return
val noteEvent = note.event as? EncryptedDmEvent ?: return
val userRoom by
remember(note) {
@@ -76,7 +76,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor
import com.vitorpamplona.amethyst.commons.ui.theme.innerPostModifier
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsFollowerCount
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAssertionsFollowerCount
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
@@ -288,7 +288,7 @@ private fun ProfileCardChips(
// A self-follow in your own kind:3 is common; "Follows you" on your own card is not a fact.
val followsMe by observeUserIsFollowing(author, accountViewModel.account.userProfile(), accountViewModel)
val followsYou = followsMe && !accountViewModel.isLoggedUser(author)
val followerCount by observeUserContactCardsFollowerCount(author, accountViewModel)
val followerCount by observeUserAssertionsFollowerCount(author, accountViewModel)
// "--" is the placeholder the contact-card observer emits while no trusted
// assertion for this user has arrived. Nothing to brag about yet.
val followers = followerCount.takeIf { it != "--" }
@@ -80,8 +80,8 @@ import com.vitorpamplona.quartz.experimental.publications.PublicationContentEven
import com.vitorpamplona.quartz.experimental.publications.PublicationIndexEvent
import com.vitorpamplona.quartz.experimental.publications.PublicationSectionRef
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent
import kotlinx.collections.immutable.toImmutableList
// A 2:3 portrait cover — a book jacket, which is what NKBIP-01's default `book` type is. The
@@ -355,8 +355,8 @@ private fun ObservedSectionRow(
// list long-form, wiki and spec events as sections too.
is PublicationIndexEvent -> event.titleOrIdentifier()
is PublicationContentEvent -> event.titleOrIdentifier()
is LongTextNoteEvent -> event.title()
is WikiNoteEvent -> event.title()
is LongFormContentEvent -> event.title()
is WikiArticleEvent -> event.title()
// A bookshelf directory lists whatever it likes, including another directory, so the
// row has to name the library kinds too -- otherwise a nested entry falls through to
// the section placeholder and reads "Untitled section", which it is not.
@@ -67,7 +67,7 @@ import com.vitorpamplona.quartz.experimental.publications.PublicationIndexEvent
import com.vitorpamplona.quartz.experimental.publications.PublicationSectionRef
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent
/**
* Renders a kind-30041 NKBIP-01 publication section — a chapter, zettel or episode.
@@ -331,12 +331,12 @@ private fun wikilinkResolver(event: PublicationContentEvent): (String) -> String
link == null -> null
link.eventId != null ->
"nostr:" + NEvent.create(link.eventId!!, link.pubKey, WikiNoteEvent.KIND, link.relay)
"nostr:" + NEvent.create(link.eventId!!, link.pubKey, WikiArticleEvent.KIND, link.relay)
link.pubKey != null ->
"nostr:" +
NAddress.create(
WikiNoteEvent.KIND,
WikiArticleEvent.KIND,
link.pubKey!!,
PublicationContentEvent.normalizeWikilink(link.target),
link.relay,
@@ -70,7 +70,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.firstTagValueFor
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import kotlinx.collections.immutable.toImmutableList
@@ -158,7 +158,7 @@ fun DisplayDMRelayList(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = baseNote.event as? ChatMessageRelayListEvent ?: return
val noteEvent = baseNote.event as? DmRelayListEvent ?: return
val relays =
remember(noteEvent) {
@@ -241,14 +241,14 @@ fun DisplayTrustedRelayList(
}
@Composable
fun DisplayRelayFeedsList(
fun DisplayFavoriteRelayList(
baseNote: Note,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
val relays by accountViewModel.account.relayFeedsListDecryptionCache.observeDecryptedRelayList(baseNote).collectAsStateWithLifecycle(
accountViewModel.account.relayFeedsListDecryptionCache.fastStartValueForRelayList(baseNote),
val relays by accountViewModel.account.favoriteRelayListDecryptionCache.observeDecryptedRelayList(baseNote).collectAsStateWithLifecycle(
accountViewModel.account.favoriteRelayListDecryptionCache.fastStartValueForRelayList(baseNote),
)
DisplayRelaySet(
@@ -96,12 +96,11 @@ import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.SoftwareReleaseEvent
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.asSoftwareRelease
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.tags.AppIdTag
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -209,7 +208,7 @@ fun RenderSoftwareApplication(
}
/**
* Latest NIP-82 [SoftwareReleaseEvent] version for [app], kept live.
* Latest NIP-82 [ReleaseArtifactSetEvent] version for [app], kept live.
*
* Releases (kind 30063) are separate events that point back to the app via an
* `i` tag rather than an `a` tag, so they are never indexed as replies to the
@@ -230,7 +229,7 @@ fun produceLatestReleaseVersion(app: SoftwareApplicationEvent): State<String?> {
remember(app.pubKey, app.id) {
val filter =
Filter(
kinds = listOf(SoftwareReleaseEvent.KIND),
kinds = listOf(ReleaseArtifactSetEvent.KIND),
authors = listOf(app.pubKey),
tags = mapOf(AppIdTag.TAG_NAME to listOf(app.appId())),
)
@@ -243,13 +242,13 @@ fun produceLatestReleaseVersion(app: SoftwareApplicationEvent): State<String?> {
return flow.collectAsStateWithLifecycle(initialValue = null)
}
fun findLatestNip82Release(app: SoftwareApplicationEvent): SoftwareReleaseEvent? = latestNip82Release(nip82ReleaseNotesFor(app), app)
fun findLatestNip82Release(app: SoftwareApplicationEvent): ReleaseArtifactSetEvent? = nip82ReleasesFor(app).maxByOrNull { it.createdAt }
/** Picks the newest NIP-82 release for [app] out of an already-narrowed [notes] collection. */
private fun latestNip82Release(
notes: Collection<Note>,
app: SoftwareApplicationEvent,
): SoftwareReleaseEvent? {
): ReleaseArtifactSetEvent? {
val prefix = "${app.dTag()}@"
return notes
.mapNotNull { it.asNip82ReleaseFor(prefix) }
@@ -257,33 +256,26 @@ private fun latestNip82Release(
}
/** kind-30063 addressables authored by [app] whose `d` tag is `<app-id>@<version>`. */
private fun nip82ReleaseNotesFor(app: SoftwareApplicationEvent): Set<Note> {
private fun nip82ReleasesFor(app: SoftwareApplicationEvent): List<ReleaseArtifactSetEvent> {
val prefix = "${app.dTag()}@"
return LocalCache.addressables.filterIntoSet(SoftwareReleaseEvent.KIND, app.pubKey) { _, addr ->
val ev = addr.event ?: return@filterIntoSet false
ev.isNip82SoftwareRelease() && ev.dTag().startsWith(prefix)
}
return LocalCache.addressables
.filterIntoSet(ReleaseArtifactSetEvent.KIND, app.pubKey) { _, addr ->
val ev = addr.event ?: return@filterIntoSet false
ev.dTag().startsWith(prefix) && ev.isNip82SoftwareRelease()
}.mapNotNull { it.event as? ReleaseArtifactSetEvent }
}
/**
* Re-reads this note as the NIP-82 release for [prefix] (`<app-id>@`). kind 30063
* collides with NIP-51 `ReleaseArtifactSetEvent`, which is what `EventFactory`
* builds, so we re-parse matching tag arrays as the NIP-82 form.
* This note as the NIP-82 release for [prefix] (`<app-id>@`). kind 30063 is shared
* with NIP-51 release artifact sets, so only events carrying the NIP-82 tags qualify.
*/
private fun Note.asNip82ReleaseFor(prefix: String): SoftwareReleaseEvent? =
when (val ev = event) {
null -> null
is SoftwareReleaseEvent -> ev.takeIf { it.dTag().startsWith(prefix) }
else -> if (ev.isNip82SoftwareRelease() && ev.dTag().startsWith(prefix)) ev.asSoftwareRelease() else null
}
fun findAllNip82Releases(app: SoftwareApplicationEvent): List<SoftwareReleaseEvent> {
val prefix = "${app.dTag()}@"
return nip82ReleaseNotesFor(app)
.mapNotNull { it.asNip82ReleaseFor(prefix) }
.sortedByDescending { it.createdAt }
private fun Note.asNip82ReleaseFor(prefix: String): ReleaseArtifactSetEvent? {
val ev = event as? ReleaseArtifactSetEvent ?: return null
return ev.takeIf { it.dTag().startsWith(prefix) && it.isNip82SoftwareRelease() }
}
fun findAllNip82Releases(app: SoftwareApplicationEvent): List<ReleaseArtifactSetEvent> = nip82ReleasesFor(app).sortedByDescending { it.createdAt }
@Composable
fun AppIcon(
icon: String?,
@@ -486,25 +478,17 @@ fun RenderSoftwareRelease(
accountViewModel: AccountViewModel,
nav: INav,
) {
// kind 30063 is shared between NIP-51 ReleaseArtifactSetEvent and NIP-82 SoftwareReleaseEvent;
// EventFactory always returns the NIP-51 class, so we re-parse the same tag array as the
// NIP-82 form when the tag signature matches.
val rawEvent = note.event ?: return
val event: SoftwareReleaseEvent =
if (rawEvent is SoftwareReleaseEvent) {
rawEvent
} else if (rawEvent.isNip82SoftwareRelease()) {
rawEvent.asSoftwareRelease()
} else {
return
}
// kind 30063 is shared between NIP-51 release artifact sets and NIP-82 software
// releases; only render the NIP-82 form here.
val event = note.event as? ReleaseArtifactSetEvent ?: return
if (!event.isNip82SoftwareRelease()) return
RenderSoftwareReleaseBody(event = event, accountViewModel = accountViewModel, nav = nav)
}
@Composable
fun RenderSoftwareReleaseBody(
event: SoftwareReleaseEvent,
event: ReleaseArtifactSetEvent,
accountViewModel: AccountViewModel,
nav: INav,
showAppId: Boolean = true,
@@ -66,20 +66,20 @@ import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.FollowSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DisplayFollowList(
fun DisplayStarterPack(
baseNote: Note,
makeItShort: Boolean,
accountViewModel: AccountViewModel,
nav: INav,
) {
val card =
observeNoteEventAndMap(baseNote, accountViewModel) { event: FollowListEvent? ->
observeNoteEventAndMap(baseNote, accountViewModel) { event: StarterPackEvent? ->
if (event == null) {
FollowSetCard(
name = "",
@@ -190,7 +190,7 @@ fun RenderFollowSetThumbPreview() {
val accountViewModel = mockAccountViewModel()
val followCard =
FollowListEvent(
StarterPackEvent(
id = "eca31634fce7c9068b56fa8db9f387da70bdcceb3986a77ca1a9844f3128eb5f",
pubKey = "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da",
createdAt = 1761736286,
@@ -60,7 +60,7 @@ import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip14Subject.subject
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
enum class ReplyRenderType {
FULL,
@@ -129,7 +129,7 @@ fun RenderTextEvent(
// Zap receipts are signed by the recipient's lightning provider;
// label the reply with the zap sender instead of the service key.
val zapSender =
if (parentNote.event is LnZapEvent) {
if (parentNote.event is ZapReceiptEvent) {
observeZapSender(parentNote, accountViewModel).value
} else {
null
@@ -50,7 +50,7 @@ import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent
@Composable
fun RenderWikiContent(
@@ -58,14 +58,14 @@ fun RenderWikiContent(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = note.event as? WikiNoteEvent ?: return
val noteEvent = note.event as? WikiArticleEvent ?: return
WikiNoteHeader(noteEvent, note, accountViewModel, nav)
WikiArticleHeader(noteEvent, note, accountViewModel, nav)
}
@Composable
private fun WikiNoteHeader(
noteEvent: WikiNoteEvent,
private fun WikiArticleHeader(
noteEvent: WikiArticleEvent,
note: Note,
accountViewModel: AccountViewModel,
nav: INav,
@@ -55,9 +55,9 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiMergeAcceptanceEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiMergeRequestEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiRedirectEvent
/**
@@ -215,7 +215,7 @@ private fun WikiArticleRow(
) {
LoadAddressableNote(address) { articleNote ->
if (articleNote != null) {
val article by observeNoteEvent<WikiNoteEvent>(articleNote, accountViewModel)
val article by observeNoteEvent<WikiArticleEvent>(articleNote, accountViewModel)
WikiRow(
// The coordinate's own identifier is a readable slug, so it stands in until the
@@ -240,7 +240,7 @@ private fun WikiVersionRow(
) {
LoadNote(eventId) { versionNote ->
if (versionNote != null) {
val article by observeNoteEvent<WikiNoteEvent>(versionNote, accountViewModel)
val article by observeNoteEvent<WikiArticleEvent>(versionNote, accountViewModel)
WikiRow(
title = article?.title() ?: eventId.take(8),
@@ -64,7 +64,7 @@ import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@@ -97,18 +97,18 @@ fun RenderZappedPost(
}
@Composable
fun RenderLnZap(
fun RenderZapReceipt(
note: Note,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
val zapEvent = note.event as? LnZapEvent ?: return
val zapEvent = note.event as? ZapReceiptEvent ?: return
val card by parseAuthorCommentAndAmount(note, accountViewModel)
RenderLnZapCard(
RenderZapReceiptCard(
note = note,
card = card,
recipientKey = zapEvent.zappedAuthor().firstOrNull(),
@@ -120,7 +120,7 @@ fun RenderLnZap(
}
@Composable
fun RenderLnZapCard(
fun RenderZapReceiptCard(
note: Note,
card: ZapAmountCommentNotification,
recipientKey: String?,

Some files were not shown because too many files have changed in this diff Show More