Files
amethyst/desktopApp/plans/2026-07-27-feat-desktop-profile-actions-plan.md
Claude f72388a559 refactor: rename packages, helpers and app code after the NIP event renames
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
2026-09-26 20:56:07 +00:00

136 lines
9.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Desktop Profile — Phase 3 (Header Actions)
type: feat
status: drafting
date: 2026-07-27
parent: 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](../../docs/plans/2026-07-27-feat-desktop-profile-parity-plan.md).
Adds DM, Share, Add-to-list, and Block to the profile header.
> **Depends on `feat/desktop-moderation-safety`** (Block extends its header overflow menu +
> `DesktopIAccount` write path + hidden-set). Resolve the base first — see parent "Base Strategy"
> (recommended: wait for merge + rebase onto `main`). 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) -> Unit` from `UserProfileScreen`; wire in `Main.kt`:
```kotlin
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>)` (quartz `nip17Dm/base/ChatroomKey.kt:27`); 1:1 = `setOf(pubkey)`.
- `fetchMetadataIfNeeded` already handles the no-inbox-relay case; guard read-only per the matrix. No crash.
## Action 2 — Share (all REUSE)
- `copyToClipboard(text)` (AWT `Toolkit…systemClipboard`, `ShareMenu.kt:113`; also inline in
`UserProfileScreen.kt:721`).
- npub: `pubKeyHex.hexToByteArrayOrNull()?.toNpub()` (quartz `nip19Bech32/ByteArrayExt.kt:27`).
- nprofile (with relay hints): `UserTag(pubKey, relayHint).toNProfile()` (`UserTag.kt:39`).
- `UserProfileScreen.kt:699-738` already shows the copy-npub + "Copied" feedback pattern — extend it
to a Share menu item (npub / nostr: toggle). Optionally reuse the `ShareMenu` composable
(`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 returns `emptyList()`.
- **Add member:** `StarterPackEvent.add(earlierVersion, person = UserTag(pubKey, relayHint), signer)`
(`quartz nip51Lists/starterPack/StarterPackEvent.kt:116`; `UserTag` from `muteList/tags/UserTag.kt:35`).
Publish via `relayManager.broadcastToAll(event)` + `cache.consume(event, relay, wasVerified=false)`
(pattern in `FollowPackDetailScreen.kt:178-192`).
- **Thin `commons/actions/FollowPackActions.kt`** = pure builder wrapping `StarterPackEvent.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 (`add` onto a stale
`earlierVersion` drops members).
- **Modal caveat:** the picker must NOT use `rememberSubscription` (broken in AlertDialog); the
`allPacks` StateFlow 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
> a `commons/actions/BlockActions.kt`. The quartz `FollowSetEvent.addUser` IS 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` (quartz `nip51Lists/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 / `UnauthorizedDecryptionException` on read-only).
- `DesktopHiddenUsersState` **already loads the own kind-30000 block list**
(`blockListNote = cache.getOrCreateAddressableNote(FollowSetEvent.createBlockAddress(signer.pubKey))`).
- `publishAndConfirmDetailed(event, relays, timeout): Map<relay,Boolean>` (quartz
`NostrClientPublishExt.kt`); `DmSendTracker.sendBatch` shows the confirmed-publish + status pattern.
**WIRE (new, small):**
- `DesktopHiddenUsersState.currentBlockList(): FollowSetEvent?` — mirror the existing
`currentMuteList()` (`blockListNote.event as? FollowSetEvent`).
- `DesktopIAccount.blockUser(pubkeyHex)` / `unblockUser(pubkeyHex)` + private
`updateBlockList(tag, isPrivate, add)` — **mirror `updateMuteList` exactly**: load
`currentBlockList()`, `FollowSetEvent.addUser/removeUser` (or `create` **only 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-forget `broadcastToAll` — what `publishModeration` uses 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
until `currentBlockList()` has loaded** (stale-`earlierVersion` clobber guard).
- Header menu: add "Block user"/"Unblock user" to the moderation overflow dropdown built at
`UserProfileScreen.kt:~350-390` (shown when `iAccount.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 via `toNpub()`/`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 existing `LiveHiddenUsers`); Unblock reverses.
- [ ] Block round-trip preserves prior private entries (test mirrors `MuteListEventTest.add_eventTagPreservesPriorUserAndWordTags`); disabled until `currentBlockList()` 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 with `updateMuteList` (not a commons builder); `FollowPackActions` may be a thin commons builder; modals don't use `rememberSubscription`.
- [ ] spotless clean; commons + desktopApp compile; unit tests green.
## Open questions
1. **Block write location:** inline on `DesktopIAccount` symmetric with mute (recommended, overrides parent's "extract to commons") — confirm, or still want a `commons/actions` home (then extract mute+block together)?
2. Fold Block into a combined "block & mute" (Android-style dialog) or keep standalone? (parent Open Q #1) — note enforcement effect is identical since `LiveHiddenUsers` already unions both.
3. Add-member to a 39089 pack: `StarterPackEvent.add` inline in `FollowPackActions` builder — 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` (reuse `DmSendTracker` pattern).
## 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 via `createBlockAddress`, `assemble()`).
- quartz `nip51Lists/followSet/FollowSetEvent.kt` (KIND 30000, d=`mute`, `addUser/removeUser`),
commons `FollowSetDecryptionCache.kt`, commons `IAccount.kt` (`LiveHiddenUsers.isUserHidden`),
quartz `NostrClientPublishExt.kt` (`publishAndConfirm`/`Detailed`), `DmSendTracker.kt`.
- Packs/DM/share: `followpacks/FollowPacksState.kt` (`allPacks`), quartz `StarterPackEvent.kt`
(`add`), `ChatroomKey.kt`, `ui/chats/ChatroomListState.kt` (`selectRoom`, `fetchMetadataIfNeeded`),
`ShareMenu.kt` (`copyToClipboard`), `nip19Bech32/ByteArrayExt.kt` (`toNpub`/`toNProfile`).