Skip to content

Moderation U1: disable composer in a moderation DM (member surface)#1614

Open
tlongwell-block wants to merge 2 commits into
eva/moderation-uifrom
dawn/moderation-ui-u1
Open

Moderation U1: disable composer in a moderation DM (member surface)#1614
tlongwell-block wants to merge 2 commits into
eva/moderation-uifrom
dawn/moderation-ui-u1

Conversation

@tlongwell-block

Copy link
Copy Markdown
Collaborator

Lane U1 (member surface) for Phase-2 community moderation, stacked on the integration branch eva/moderation-ui.

The bulk of U1 — report modal, tombstone render, timeout parser + chip, and the shared type-flip fix — already reached integration (commits 06572210, 7295cff2, 6ebe3b91). This PR is the last U1 piece: disabling the composer in a moderation DM.

What this adds

A moderation DM is the 1:1 thread between a member and the relay's own identity, where moderators explain an action. The member must not be able to reply into it, so the composer is read-only there — and only there, never on ordinary DMs.

  • get_relay_self command — the relay identity is the NIP-11 self pubkey. A private fetch_relay_self helper already existed in commands/identity_archive.rs (unauthenticated NIP-11 GET, 64-hex validation, fail-open); rather than a second copy, it's promoted to pub(crate) and exposed through one thin command, so the archive read and the moderation read share a single source of truth.
  • getRelaySelf / useRelaySelfQuery — workspace-scoped, cached indefinitely; null is a valid fail-open answer, not an error to retry.
  • isModerationDm (+ 7 tests) — pure predicate, true only for a DM whose sole other participant is the relay self. Case-insensitive; fails open on a null/absent self.
  • ChannelPane — queries self only for DM channels, folds the result into the shared isComposerDisabled (thread panel inherits the block), and shows a read-only placeholder. The moderator's message stays visible; only the reply is blocked.

Fail-open, by design

It's an affordance, not enforcement — the relay is authoritative over writes. A false negative (composer stays enabled on a real moderation DM) is the safe failure; the relay rejects the write anyway. A false positive (disabling a normal DM) is the worse outcome, so every uncertain path resolves to "enabled".

Validation

tsc --noEmit clean · biome clean · moderation tests 15/15 green · src-tauri cargo check clean · all pre-push legs (rust-tests, desktop-test, mobile-test, desktop-tauri-test) green.

DCO: authored by agent npub, Signed-off-by + Co-authored-by: Tyler Longwell <tlongwell@block.xyz>.

npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 and others added 2 commits July 7, 2026 18:49
A moderation DM is the 1:1 thread between a member and the relay's own
identity, where moderators explain an action. The member must not be
able to reply into it, so the composer is read-only there — and only
there, never on ordinary DMs.

The relay identity is the NIP-11 `self` pubkey. A private helper for
this already existed in commands/identity_archive.rs (fetch_relay_self:
unauthenticated NIP-11 GET, 64-hex validation, fail-open); rather than
add a second copy, promote it to pub(crate) and expose it through one
thin `get_relay_self` command, so the archive read and the moderation
read share a single source of truth.

- commands/identity_archive.rs: fetch_relay_self → pub(crate); add the
  get_relay_self command (+ register in lib.rs).
- features/moderation/lib/relaySelf.ts: getRelaySelf() invoke wrapper;
  hooks.ts: useRelaySelfQuery (workspace-scoped, cached indefinitely,
  null is a valid fail-open answer).
- features/moderation/lib/moderationDm.ts (+ tests): isModerationDm —
  pure predicate, true only for a DM whose sole other participant is the
  relay self. Case-insensitive; fails open on a null/absent self.
- ChannelPane: query self only for DM channels, fold isModerationDm into
  the shared isComposerDisabled (thread panel inherits), and show a
  read-only placeholder. The moderator's message stays visible; only the
  reply is blocked.

Affordance, not enforcement: the relay is authoritative over writes, so
a false negative (composer stays enabled on a real moderation DM) is the
safe failure — the relay rejects the write anyway. A false positive
(disabling a normal DM) is the worse outcome, so every uncertain path
resolves to "enabled".

Validated: tsc --noEmit clean, biome clean, moderation tests 15/15
green, src-tauri cargo check clean.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
The report-queue timeout resolution (U2) needs the same 1h/24h/7d
duration choice the per-message author cluster already offers, and the
two must never drift. Provide the shared piece from the timeout lib that
owns the parse/format side.

- lib/timeout.ts: TIMEOUT_PRESETS (the single 1h/24h/7d source of truth)
  and timeoutExpiresAt(seconds) → now + seconds in epoch seconds, the
  shape useTimeoutMemberMutation expects. (+ tests.)
- ui/TimeoutDurationSubmenu.tsx: a presentational DropdownMenuSub of
  those presets. Duration-only — it knows nothing about the target or
  which command fires — so it drops into both the per-message cluster
  and the queue's timeout resolution unchanged, calling onSelect with
  the absolute expiry.

No consumer on this branch yet; U2 wires it into the resolve menu's
timeout branch. Validated: tsc --noEmit clean, biome clean, moderation
tests 17/17 green.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant