Files
eranos/src/components/FeedSettingsForm.tsx
T
shakespeare.diy 9c79578d7a Sync feed and relay settings to encrypted Nostr storage
- Update FeedSettingsForm to save to encrypted settings when toggling
- Update RelayListManager to sync app relay toggle to encrypted storage
- Feed preferences and relay toggle now prompt for signer when changed
- Settings sync across devices via NIP-78 encrypted events
- Local storage still used for immediate UI updates (dual-write pattern)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-18 03:23:42 -06:00

166 lines
5.8 KiB
TypeScript

import { Clapperboard, BarChart3, Palette, PartyPopper } from 'lucide-react';
import { ChestIcon } from '@/components/icons/ChestIcon';
import { Switch } from '@/components/ui/switch';
import { useFeedSettings } from '@/hooks/useFeedSettings';
import { useEncryptedSettings } from '@/hooks/useEncryptedSettings';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { EXTRA_KINDS } from '@/lib/extraKinds';
import type { ExtraKindDef, SubKindDef } from '@/lib/extraKinds';
import { cn } from '@/lib/utils';
/** Map route name → lucide icon. */
const ICONS: Record<string, React.ReactNode> = {
vines: <Clapperboard className="size-5" />,
polls: <BarChart3 className="size-5" />,
treasures: <ChestIcon className="size-5" />,
colors: <Palette className="size-5" />,
packs: <PartyPopper className="size-5" />,
};
function KindBadge({ kind }: { kind: number }) {
return (
<span className="text-[10px] font-mono text-muted-foreground/60 shrink-0">
[{kind}]
</span>
);
}
function SubKindRow({ sub, parentEnabled }: { sub: SubKindDef; parentEnabled: boolean }) {
const { feedSettings, updateFeedSettings } = useFeedSettings();
const { updateSettings } = useEncryptedSettings();
const { user } = useCurrentUser();
const handleToggle = async (key: string, value: boolean) => {
// Update local settings immediately
updateFeedSettings({ [key]: value });
// Sync to encrypted storage if logged in
if (user) {
const updatedFeedSettings = { ...feedSettings, [key]: value };
await updateSettings.mutateAsync({ feedSettings: updatedFeedSettings });
}
};
return (
<div className={cn(
'flex items-center justify-between py-2.5 pl-12 pr-3 transition-colors',
!parentEnabled && 'opacity-40 pointer-events-none',
)}>
<div className="min-w-0">
<span className="text-sm">{sub.label}</span>
<p className="text-xs text-muted-foreground mt-0.5">
<KindBadge kind={sub.kind} />{' '}{sub.description}
</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<div className="w-[52px] flex justify-center">
<Switch
checked={feedSettings[sub.showKey]}
onCheckedChange={(checked) => handleToggle(sub.showKey, checked)}
className="scale-90"
/>
</div>
<div className="w-[52px] flex justify-center">
<Switch
checked={feedSettings[sub.feedKey]}
onCheckedChange={(checked) => handleToggle(sub.feedKey, checked)}
className="scale-90"
/>
</div>
</div>
</div>
);
}
function ContentTypeRow({ def }: { def: ExtraKindDef }) {
const { feedSettings, updateFeedSettings } = useFeedSettings();
const { updateSettings } = useEncryptedSettings();
const { user } = useCurrentUser();
const icon = ICONS[def.route] ?? <Palette className="size-5" />;
const hasSubKinds = !!def.subKinds;
const handleToggle = async (key: string, value: boolean) => {
// Update local settings immediately
updateFeedSettings({ [key]: value });
// Sync to encrypted storage if logged in
if (user) {
const updatedFeedSettings = { ...feedSettings, [key]: value };
await updateSettings.mutateAsync({ feedSettings: updatedFeedSettings });
}
};
return (
<div className="border-b border-border last:border-b-0">
<div className="flex items-center justify-between py-3.5 px-3">
<div className="flex items-center gap-3 min-w-0">
<span className="text-muted-foreground shrink-0">{icon}</span>
<div className="min-w-0">
<span className="text-sm font-medium">{def.label}</span>
<p className="text-xs text-muted-foreground mt-0.5">
<KindBadge kind={def.kind} />{' '}{def.description}
</p>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<div className="w-[52px] flex justify-center">
<Switch
checked={feedSettings[def.showKey]}
onCheckedChange={(checked) => handleToggle(def.showKey, checked)}
/>
</div>
<div className="w-[52px] flex justify-center">
{!hasSubKinds && def.feedKey ? (
<Switch
checked={feedSettings[def.feedKey]}
onCheckedChange={(checked) => handleToggle(def.feedKey, checked)}
/>
) : null}
</div>
</div>
</div>
{/* Sub-kind toggles */}
{hasSubKinds && def.subKinds!.map((sub) => (
<SubKindRow
key={sub.showKey}
sub={sub}
parentEnabled={feedSettings[def.showKey]}
/>
))}
</div>
);
}
export function FeedSettingsForm() {
return (
<div>
{/* Intro */}
<div className="flex items-center gap-4 px-3 pt-2 pb-4">
<img
src="/feed-intro.png"
alt=""
className="w-40 shrink-0 mix-blend-difference opacity-80"
/>
<div className="min-w-0">
<h2 className="text-sm font-semibold">Other Stuff</h2>
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
Nostr isn't just text posts people publish all kinds of things. Pick what shows up in your sidebar and feed.
</p>
</div>
</div>
{/* Column headers */}
<div className="flex items-center justify-end gap-2 px-3 pb-2 border-b border-border">
<span className="text-[11px] font-medium text-muted-foreground w-[52px] text-center">Sidebar</span>
<span className="text-[11px] font-medium text-muted-foreground w-[52px] text-center">Feed</span>
</div>
{/* Content type rows */}
{EXTRA_KINDS.map((def) => (
<ContentTypeRow key={def.showKey} def={def} />
))}
</div>
);
}