~3,470 new keys (5,770 total, EN=ES), ~115 more files wired. - nav + chromeUi: header, More menu, burger menu, footer, map toolbar widgets (sort/trust/weather), spot-type label helper. - placesUi: events, shops, tours, food. socialUi: crews, DMs, notifications, collectives, crew chat/coordinator. settingsUi: settings, auth/signup, onboarding, profile edit, legal. walletUi: wallet/Cashu, stash, mints, app shop, newsletter, community stats, leaderboards, profile. - Fixed 738 usages where an agent dropped the socialUi. prefix (caught by check:i18n).
112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Loader2, EyeOff, Globe } from 'lucide-react';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { useCollectiveMembers } from '@/hooks/useCollectiveMembers';
|
|
import { useConfirmMembership } from '@/hooks/useMembershipAttestation';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import { useTranslation } from '@/contexts/language';
|
|
import { VisibilityPicker } from '@/components/ConfirmMembershipCard';
|
|
import { cn } from '@/lib/utils';
|
|
import type { MemberVisibility } from '@/lib/types';
|
|
|
|
interface Props {
|
|
kind: number;
|
|
founderPubkey: string;
|
|
identifier: string;
|
|
collectiveName: string;
|
|
claimedMembers: string[];
|
|
accentBg?: string;
|
|
}
|
|
|
|
/**
|
|
* Shown to a CONFIRMED member so they can change their visibility (public vs
|
|
* crew-only) at any time. Re-publishes their kind-1985 attestation with the new
|
|
* `visibility` tag. Hidden for the founder, non-members, and pending members
|
|
* (those use the ConfirmMembershipCard instead).
|
|
*/
|
|
export function MemberVisibilityControl({
|
|
kind,
|
|
founderPubkey,
|
|
identifier,
|
|
collectiveName,
|
|
claimedMembers,
|
|
accentBg = 'bg-teal-500 hover:bg-teal-600',
|
|
}: Props) {
|
|
const { t } = useTranslation();
|
|
const { user } = useCurrentUser();
|
|
const { toast } = useToast();
|
|
const { mutateAsync: republish, isPending } = useConfirmMembership();
|
|
const { data: members } = useCollectiveMembers({
|
|
kind,
|
|
founderPubkey,
|
|
identifier,
|
|
claimedMembers,
|
|
enabled: claimedMembers.length > 0,
|
|
});
|
|
|
|
const me = user?.pubkey.toLowerCase();
|
|
const mine = members?.find((m) => m.pubkey === me);
|
|
const [visibility, setVisibility] = useState<MemberVisibility>('public');
|
|
|
|
// Sync local state to the published value once it loads.
|
|
useEffect(() => {
|
|
if (mine?.visibility) setVisibility(mine.visibility);
|
|
}, [mine?.visibility]);
|
|
|
|
if (!user || !me) return null;
|
|
if (me === founderPubkey.toLowerCase()) return null;
|
|
// Only for confirmed members.
|
|
if (!mine || mine.status !== 'confirmed') return null;
|
|
|
|
const changed = visibility !== (mine.visibility ?? 'public');
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
await republish({ kind, founderPubkey, identifier, collectiveName, visibility });
|
|
toast({
|
|
title: t('socialUi.memberVisibility.updatedTitle'),
|
|
description:
|
|
visibility === 'crew'
|
|
? t('socialUi.memberVisibility.updatedCrew')
|
|
: t('socialUi.memberVisibility.updatedPublic'),
|
|
});
|
|
} catch (error) {
|
|
toast({
|
|
title: t('socialUi.memberVisibility.couldNotUpdate'),
|
|
description: error instanceof Error ? error.message : t('common.tryAgain'),
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card className="border border-white/10 bg-black/60">
|
|
<CardContent className="space-y-3 p-4">
|
|
<div className="flex items-center gap-2">
|
|
{(mine.visibility ?? 'public') === 'crew' ? (
|
|
<EyeOff className="h-4 w-4 text-gray-400" />
|
|
) : (
|
|
<Globe className="h-4 w-4 text-gray-400" />
|
|
)}
|
|
<p className="text-xs font-black uppercase tracking-widest text-gray-400">
|
|
{t('socialUi.memberVisibility.title')}
|
|
</p>
|
|
</div>
|
|
<VisibilityPicker value={visibility} onChange={setVisibility} accentBg={accentBg.split(' ')[0]} />
|
|
{changed && (
|
|
<Button
|
|
type="button"
|
|
onClick={handleSave}
|
|
disabled={isPending}
|
|
className={cn('w-full gap-2 font-bold', accentBg)}
|
|
>
|
|
{isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
|
{t('socialUi.memberVisibility.save')}
|
|
</Button>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|