Files
eranos/src/components/MembersOnlyToggle.tsx
T
lemon e21ee2e4fc Enforce report pubkey match and add members-only filter toggle
Two NIP-alignment fixes:

Gap 1 — Report warnings now require `p` match (correctness).

Previously `CommunityContentWarning` looked up reports by event id only,
so any community member could publish a kind 1984 pairing a victim
event's id with their own pubkey on the `p` tag to force a warning
overlay onto an arbitrary event. Added `getApplicableReports` in
communityUtils mirroring `hasApplicableContentBan`, and use it to
require `report.targetPubkey === event.pubkey` before the warning
renders. Matches NIP.md §Reports — Content Warnings: "report warnings
MUST only attach to content when the target event's id matches the
report's `e` tag and the target event's pubkey matches the report's
`p` tag."

Gap 2 — Members-only filter toggle.

The NIP recommends canonical community feeds discard non-member
content by default. Added a shield-icon toggle that controls this as
a presentation-layer filter, defaulting on. When active, community
feeds (Activities feed, per-community Comments tab, and any future
community-scoped content surfaces) only show events authored by
chain-validated members. When off, everything scoped to the
community is shown regardless of authorship.

- `useMembersOnlyFilter` — localStorage-backed hook with cross-tab
  sync; one preference shared across all community surfaces.
- `MembersOnlyToggle` — shield / shield-off icon button with tooltip
  explaining current state.
- Filtering is applied post-query in the consumer pages, so toggling
  is instant and doesn't invalidate the query cache.
- Community definition events (kind 34550) are never filtered — they
  represent the community itself, not user-generated content.
- Toggle placement: in `CommunitiesPage` header (scopes the global
  Activities feed); in `CommunityDetailPage` alongside the tabs
  (scopes every content feed in that community, now and future).
- Empty-state copy hints at the filter when a list is empty only
  because of it.
2026-04-27 09:22:08 -07:00

60 lines
2.2 KiB
TypeScript

import { Shield, ShieldOff } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { useMembersOnlyFilter } from '@/hooks/useMembersOnlyFilter';
import { cn } from '@/lib/utils';
interface MembersOnlyToggleProps {
/** Additional classes for the trigger button. */
className?: string;
}
/**
* Shield-icon toggle that controls the "members only" filter for community
* surfaces. When active (default), community feeds only show content authored
* by chain-validated members — matching the NIP's canonical-author
* recommendation. When inactive, the feed shows every event scoped to the
* community regardless of author.
*
* The preference is persisted in localStorage via `useMembersOnlyFilter` and
* is global across community surfaces (Activities feed, per-community
* Comments tab, etc.).
*/
export function MembersOnlyToggle({ className }: MembersOnlyToggleProps) {
const { membersOnly, toggle } = useMembersOnlyFilter();
const label = membersOnly ? 'Showing members only' : 'Showing everyone';
const hint = membersOnly
? 'Click to show posts from anyone scoped to this community.'
: 'Click to limit posts to validated community members.';
return (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={toggle}
aria-pressed={membersOnly}
aria-label={label}
className={cn(
'p-2 rounded-full transition-colors',
membersOnly
? 'text-primary hover:bg-primary/10'
: 'text-muted-foreground hover:bg-secondary',
className,
)}
>
{membersOnly
? <Shield className="size-5" />
: <ShieldOff className="size-5" />}
</button>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-[220px] text-center">
<p className="text-xs font-medium">{label}</p>
<p className="text-xs text-muted-foreground mt-0.5">{hint}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}