Follow-up to the event class renames: the names around those classes now
match too.
Quartz (published API, every old name kept as a @Deprecated alias/forwarder):
- nip51Lists packages moved: followList -> starterPack, hashtagList -> interestList,
peopleList -> followSet, labeledBookmarkList -> bookmarkSet. The old packages
keep a Deprecated.kt with typealiases and forwarding extension functions
(ReplaceWith points at the new package).
- LnZapPrivateEvent -> PrivateZapEvent, LnZapReceiptValidator -> ZapReceiptValidator,
ChannelListDiff -> PublicChatListDiff, HashtagListDiff -> InterestListDiff.
- Kind 30063 now indexes NIP-82 release notes for search (never NIP-51 content,
which can hold encrypted private items). searchable-events docs updated.
App code (amethyst, commons, desktopApp, cli; no aliases needed):
- model packages nip51Lists.{hashtagLists,peopleList,labeledBookmarkLists,relayFeeds}
-> {interestLists,followSets,bookmarkSets,favoriteRelays}.
- State/cache/UI names derived from the old event names: HashtagListState ->
InterestListState, PeopleListsState -> FollowSetsState, FollowListsState ->
StarterPacksState, LabeledBookmarkList -> BookmarkSet, RelayFeedListState ->
FavoriteRelayListState, ContactCardsState -> UserAssertionsState, NIP90* view
models/renderers -> Dvm*, LnZap* handlers -> Zap*, SealedRumor* -> Seal*, and
Account.peopleLists/followLists/hashtagList -> followSets/starterPacks/interestList.
- String literals were left untouched, so preference keys and @SerialName values
stored on disk are unchanged.
- The generic PeopleList UI model (shared by follow sets and starter packs) and the
EmojiPackSelection route (which shows a kind 30030 pack) keep their names.
Docs: plans, brainstorms, changelogs, skills and READMEs use the new names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VvJ8XpeYqBe8bYT3bJ7UD
9.8 KiB
title, type, status, date, parent
| title | type | status | date | parent |
|---|---|---|---|---|
| Desktop Profile — Phase 3 (Header Actions) | feat | drafting | 2026-07-27 | docs/plans/2026-07-27-feat-desktop-profile-parity-plan.md |
Desktop Profile — Phase 3: Header Actions
Sub-plan expanding Phase 3 of the parent parity plan. Adds DM, Share, Add-to-list, and Block to the profile header.
Depends on
feat/desktop-moderation-safety(Block extends its header overflow menu +DesktopIAccountwrite path + hidden-set). Resolve the base first — see parent "Base Strategy" (recommended: wait for merge + rebase ontomain). DM/Share/Add-to-list do NOT need it.
Actor/subject guards (apply to every write action)
From the parent's actor×subject matrix. Before any publish:
require(pubKeyHex != account.signer.pubKey)— no self-DM/block/report.if (!account.isWriteable()) return— read-only/watch-only; UI already hides the button.- Buttons hidden (not just disabled) per the matrix when not applicable.
Action 1 — DM (all REUSE; only button + callback are new)
Open Desktop Messages for this pubkey via the existing deck-column pattern.
- Expose
onStartDm: (pubKey: String) -> UnitfromUserProfileScreen; wire inMain.kt:onStartDm = { pubkey -> scope.launch { chatListState.fetchMetadataIfNeeded(listOf(pubkey)) // ChatroomListState.kt:158 — handles no-relay chatListState.selectRoom(ChatroomKey(setOf(pubkey))) // ChatroomListState.kt:121 (ctor, NOT build1on1) /* focus existing Messages column or addColumn(DeckColumnType.Messages) — reuse onOpenMessages() Main.kt:2145 */ } } ChatroomKey(users: Set<HexKey>)(quartznip17Dm/base/ChatroomKey.kt:27); 1:1 =setOf(pubkey).fetchMetadataIfNeededalready handles the no-inbox-relay case; guard read-only per the matrix. No crash.
Action 2 — Share (all REUSE)
copyToClipboard(text)(AWTToolkit…systemClipboard,ShareMenu.kt:113; also inline inUserProfileScreen.kt:721).- npub:
pubKeyHex.hexToByteArrayOrNull()?.toNpub()(quartznip19Bech32/ByteArrayExt.kt:27). - nprofile (with relay hints):
UserTag(pubKey, relayHint).toNProfile()(UserTag.kt:39). UserProfileScreen.kt:699-738already shows the copy-npub + "Copied" feedback pattern — extend it to a Share menu item (npub / nostr: toggle). Optionally reuse theShareMenucomposable (ShareMenu.kt:56) shape. No auth required; own + others.
Action 3 — Add to follow-pack (kind 39089) — mostly REUSE
- Picker source:
FollowPacksState.allPacks: StateFlow<List<StarterPackEvent>>(followpacks/FollowPacksState.kt:84) — zero-packs returnsemptyList(). - Add member:
StarterPackEvent.add(earlierVersion, person = UserTag(pubKey, relayHint), signer)(quartz nip51Lists/starterPack/StarterPackEvent.kt:116;UserTagfrommuteList/tags/UserTag.kt:35). Publish viarelayManager.broadcastToAll(event)+cache.consume(event, relay, wasVerified=false)(pattern inFollowPackDetailScreen.kt:178-192). - Thin
commons/actions/FollowPackActions.kt= pure builder wrappingStarterPackEvent.add(returns the signed event; desktop layer publishes). No hand-rolled tag assembly. - Zero-packs → offer "Create new pack" (reuse
FollowPackEditor); no dead-end modal. - Stale-list guard: disable until the target pack's current 39089 has loaded (
addonto a staleearlierVersiondrops members). - Modal caveat: the picker must NOT use
rememberSubscription(broken in AlertDialog); theallPacksStateFlow is already hoisted, so read it directly.
Action 4 — Block (FollowSetEvent kind 30000, d=mute)
Much of this already exists on moderation-safety — Block is mostly wiring, mirroring mute.
Revision of parent's "extract to commons" default: moderation-safety keeps the mute write inline on
DesktopIAccount.updateMuteList(not a commons builder). To avoid the architecture reviewer's "two parallel patterns" smell, Block should be inline and symmetric with mute — NOT acommons/actions/BlockActions.kt. The quartzFollowSetEvent.addUserIS the shared builder; a commons wrapper adds nothing. (If a commons home is later wanted, extract mute + block together.)
Already present (REUSE, no changes):
FollowSetEvent(quartznip51Lists/followSet/FollowSetEvent.kt):KIND=30000,BLOCK_LIST_D_TAG="mute",createBlockAddress(pubKey);addUser(earlierVersion, pubKeyHex, relayHint, isPrivate, signer),removeUser(earlierVersion, pubKeyHex, isUserPrivate, signer),remove/removeAll.FollowSetDecryptionCache(signer)(commons) —userIdSet(event)merges public+private, fail-closed (privateTags→ null /UnauthorizedDecryptionExceptionon read-only).DesktopHiddenUsersStatealready loads the own kind-30000 block list (blockListNote = cache.getOrCreateAddressableNote(FollowSetEvent.createBlockAddress(signer.pubKey))).publishAndConfirmDetailed(event, relays, timeout): Map<relay,Boolean>(quartzNostrClientPublishExt.kt);DmSendTracker.sendBatchshows the confirmed-publish + status pattern.
WIRE (new, small):
DesktopHiddenUsersState.currentBlockList(): FollowSetEvent?— mirror the existingcurrentMuteList()(blockListNote.event as? FollowSetEvent).DesktopIAccount.blockUser(pubkeyHex)/unblockUser(pubkeyHex)+ privateupdateBlockList(tag, isPrivate, add)— mirrorupdateMuteListexactly: loadcurrentBlockList(),FollowSetEvent.addUser/removeUser(orcreateonly if null),localCache.justConsumeMyOwnEvent(event)(optimistic), then publish.- Publish Block via
publishAndConfirmDetailed(Block is security-relevant; Desktop NIP-42 AUTH is partial so fire-and-forgetbroadcastToAll— whatpublishModerationuses for mute — risks accepted-but-not-persisted). Snackbar on result; rollback optimistic apply on total failure. - Never
create()an existing list (replaceable → silently un-blocks everyone); disable Block untilcurrentBlockList()has loaded (stale-earlierVersionclobber guard). - Header menu: add "Block user"/"Unblock user" to the moderation overflow dropdown built at
UserProfileScreen.kt:~350-390(shown wheniAccount.isWriteable() && pubKeyHex != iAccount.pubKey— the self/read-only guards already live there).
Unified enforcement — ALREADY DONE on the base branch ✅
moderation-safety already merges mute ∪ block: DesktopHiddenUsersState.assemble() folds
blockCache.userIdSet(blockEvent) into LiveHiddenUsers, and
LiveHiddenUsers.isUserHidden(hex) = hiddenUsers.contains(hex) || spammers.contains(hex) where
hiddenUsers = mute ∪ block. The six new Phase-2 tab filters simply read the existing
iAccount.hiddenUsers.value.hiddenUsersHashCodes / .hiddenUsers — no new enforcement code, no
commons extraction needed. (This retires parent Open Q #3 and the "do it now" enforcement task.)
Acceptance criteria
- DM opens the correct 1:1 room (
ChatroomKey(setOf(pubkey))+selectRoom); graceful when target has no inbox relays / read-only. - Share yields a valid
npub/nostr:link viatoNpub()/toNProfile()+copyToClipboard. - Add-to-list adds to a chosen pack (39089,
StarterPackEvent.add) and persists; zero-packs offers create-new; disabled until pack loaded. - Block (30000, d=
mute) hides user across feeds/threads/replies/profile (via existingLiveHiddenUsers); Unblock reverses. - Block round-trip preserves prior private entries (test mirrors
MuteListEventTest.add_eventTagPreservesPriorUserAndWordTags); disabled untilcurrentBlockList()loaded; decrypt-fail is fail-closed. - Block publish confirmed via
publishAndConfirmDetailed(not fire-and-forget); optimistic UI rolls back on total failure. - Self-actions impossible; read-only hides write actions (existing header guard covers this).
- Block write is inline on
DesktopIAccount, symmetric withupdateMuteList(not a commons builder);FollowPackActionsmay be a thin commons builder; modals don't userememberSubscription. - spotless clean; commons + desktopApp compile; unit tests green.
Open questions
- Block write location: inline on
DesktopIAccountsymmetric with mute (recommended, overrides parent's "extract to commons") — confirm, or still want acommons/actionshome (then extract mute+block together)? - Fold Block into a combined "block & mute" (Android-style dialog) or keep standalone? (parent Open Q #1) — note enforcement effect is identical since
LiveHiddenUsersalready unions both. - Add-member to a 39089 pack:
StarterPackEvent.addinline inFollowPackActionsbuilder — any existing desktop add-member path to prefer? (agent found none.)
Resolved by research
- ✅ moderation-safety already loads the own kind-30000 list and unions mute ∪ block in
LiveHiddenUsers— enforcement needs no new code (retires parent Open Q #3 / #6). - ✅ Confirmed publish =
publishAndConfirmDetailed(reuseDmSendTrackerpattern).
Sources (verified)
- Parent plan. Branch
origin/feat/desktop-moderation-safety:DesktopIAccount.kt(updateMuteList,hideUser/showUser,publishModeration, header menu ~L350-390),DesktopHiddenUsersState.kt(currentMuteList, loads block viacreateBlockAddress,assemble()). - quartz
nip51Lists/followSet/FollowSetEvent.kt(KIND 30000, d=mute,addUser/removeUser), commonsFollowSetDecryptionCache.kt, commonsIAccount.kt(LiveHiddenUsers.isUserHidden), quartzNostrClientPublishExt.kt(publishAndConfirm/Detailed),DmSendTracker.kt. - Packs/DM/share:
followpacks/FollowPacksState.kt(allPacks), quartzStarterPackEvent.kt(add),ChatroomKey.kt,ui/chats/ChatroomListState.kt(selectRoom,fetchMetadataIfNeeded),ShareMenu.kt(copyToClipboard),nip19Bech32/ByteArrayExt.kt(toNpub/toNProfile).