~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).
290 lines
10 KiB
TypeScript
290 lines
10 KiB
TypeScript
import { useState } from 'react';
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import { enableRelatr, getRelatrConfig, disableRelatr } from '@/hooks/useRelatrScore';
|
|
import { useNostr } from '@nostrify/react';
|
|
import { Shield, ExternalLink, Search, CheckCircle2, ChevronDown, ChevronUp } from 'lucide-react';
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
|
import { useTranslation } from '@/contexts/language';
|
|
|
|
/**
|
|
* RelatrSettings component for configuring Relatr global trust score integration
|
|
*
|
|
* Relatr publishes trust scores as NIP-85 Trusted Assertion events (kind 30382).
|
|
* Auto-enabled by default for all users with ContextVM's official service.
|
|
*/
|
|
export function RelatrSettings() {
|
|
const { toast } = useToast();
|
|
const { t } = useTranslation();
|
|
const { nostr } = useNostr();
|
|
const config = getRelatrConfig();
|
|
|
|
const [pubkey, setPubkey] = useState(config.servicePubkey);
|
|
const [relay, setRelay] = useState(config.relay);
|
|
const [isSearching, setIsSearching] = useState(false);
|
|
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);
|
|
|
|
const handleAutoDiscover = async () => {
|
|
setIsSearching(true);
|
|
toast({
|
|
title: t('settingsUi.relatr.searchingTitle'),
|
|
description: t('settingsUi.relatr.searchingDesc'),
|
|
});
|
|
|
|
try {
|
|
// Search for profiles mentioning "relatr" or "contextvm"
|
|
const profiles = await nostr.query(
|
|
[{ kinds: [0], search: 'relatr', limit: 10 }],
|
|
{ signal: AbortSignal.timeout(10000) }
|
|
);
|
|
|
|
console.log('[RelatrSettings] Found profiles:', profiles);
|
|
|
|
// Look for official Relatr service
|
|
for (const profile of profiles) {
|
|
try {
|
|
const metadata = JSON.parse(profile.content);
|
|
const name = metadata.name?.toLowerCase() || '';
|
|
const about = metadata.about?.toLowerCase() || '';
|
|
const website = metadata.website?.toLowerCase() || '';
|
|
|
|
if (
|
|
name.includes('relatr') ||
|
|
about.includes('trust score') ||
|
|
website.includes('relatr.xyz')
|
|
) {
|
|
setPubkey(profile.pubkey);
|
|
toast({
|
|
title: t('settingsUi.relatr.foundTitle'),
|
|
description: t('settingsUi.relatr.foundDesc', { name: metadata.name || t('settingsUi.relatr.unknown') }),
|
|
});
|
|
return;
|
|
}
|
|
} catch {
|
|
// Skip invalid JSON
|
|
}
|
|
}
|
|
|
|
toast({
|
|
title: t('settingsUi.relatr.notFoundTitle'),
|
|
description: t('settingsUi.relatr.notFoundDesc'),
|
|
variant: 'destructive',
|
|
});
|
|
} catch (error) {
|
|
console.error('[RelatrSettings] Auto-discovery failed:', error);
|
|
toast({
|
|
title: t('settingsUi.relatr.searchFailedTitle'),
|
|
description: t('settingsUi.relatr.searchFailedDesc'),
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setIsSearching(false);
|
|
}
|
|
};
|
|
|
|
const handleEnable = () => {
|
|
if (!pubkey || pubkey.length !== 64) {
|
|
toast({
|
|
title: t('settingsUi.relatr.invalidPubkeyTitle'),
|
|
description: t('settingsUi.relatr.invalidPubkeyDesc'),
|
|
variant: 'destructive',
|
|
});
|
|
return;
|
|
}
|
|
|
|
enableRelatr(pubkey, relay);
|
|
|
|
toast({
|
|
title: t('settingsUi.relatr.enabledTitle'),
|
|
description: t('settingsUi.relatr.enabledDesc'),
|
|
});
|
|
};
|
|
|
|
const handleDisable = () => {
|
|
disableRelatr();
|
|
|
|
toast({
|
|
title: t('settingsUi.relatr.disabledTitle'),
|
|
description: t('settingsUi.relatr.disabledDesc'),
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Card className="bg-black/90 border-2 border-[#ccff00]/30">
|
|
<CardHeader>
|
|
<CardTitle className="text-xl font-black uppercase text-[#ccff00] flex items-center gap-2">
|
|
<Shield className="h-5 w-5" />
|
|
{t('settingsUi.relatr.title')}
|
|
</CardTitle>
|
|
<CardDescription>
|
|
{t('settingsUi.relatr.desc')}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{/* Status - Always visible */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-sm font-bold text-gray-300">{t('settingsUi.relatr.status')}</Label>
|
|
<Badge variant={config.enabled ? 'default' : 'outline'} className="gap-2">
|
|
{config.enabled ? (
|
|
<>
|
|
<CheckCircle2 className="h-3 w-3" />
|
|
{t('settingsUi.relatr.active')}
|
|
</>
|
|
) : (
|
|
t('settingsUi.relatr.disabled')
|
|
)}
|
|
</Badge>
|
|
</div>
|
|
|
|
{/* Info Alert */}
|
|
<Alert className="bg-[#ccff00]/10 border-[#ccff00]/50">
|
|
<Shield className="h-4 w-4 text-[#ccff00]" />
|
|
<AlertDescription className="text-sm text-gray-300">
|
|
{config.enabled ? (
|
|
<>
|
|
<strong className="text-[#ccff00]">{t('settingsUi.relatr.activeStrong')}</strong> {t('settingsUi.relatr.activeDesc')}
|
|
</>
|
|
) : (
|
|
<>
|
|
<strong className="text-[#ccff00]">{t('settingsUi.relatr.disabledStrong')}</strong> {t('settingsUi.relatr.disabledHint')}
|
|
</>
|
|
)}
|
|
</AlertDescription>
|
|
</Alert>
|
|
|
|
{/* Quick Actions */}
|
|
{config.enabled ? (
|
|
<Button
|
|
onClick={handleDisable}
|
|
variant="outline"
|
|
className="w-full gap-2 border-red-500/50 text-red-400 hover:bg-red-500/10"
|
|
>
|
|
{t('settingsUi.relatr.disable')}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
onClick={handleEnable}
|
|
className="w-full gap-2 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold uppercase"
|
|
disabled={!pubkey || pubkey.length !== 64}
|
|
>
|
|
<Shield className="h-4 w-4" />
|
|
{t('settingsUi.relatr.enable')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Advanced Settings - Collapsible */}
|
|
<Collapsible open={isAdvancedOpen} onOpenChange={setIsAdvancedOpen}>
|
|
<CollapsibleTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
className="w-full gap-2 text-gray-400 hover:text-white"
|
|
>
|
|
{isAdvancedOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
|
{t('settingsUi.relatr.advancedSettings')}
|
|
</Button>
|
|
</CollapsibleTrigger>
|
|
|
|
<CollapsibleContent className="space-y-4 pt-4">
|
|
{/* Service Pubkey Input */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="relatr-pubkey" className="text-sm font-bold text-gray-300">
|
|
{t('settingsUi.relatr.servicePubkey')}
|
|
</Label>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
id="relatr-pubkey"
|
|
placeholder={t('settingsUi.relatr.pubkeyPlaceholder')}
|
|
value={pubkey}
|
|
onChange={(e) => setPubkey(e.target.value)}
|
|
className="font-mono text-xs"
|
|
maxLength={64}
|
|
/>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleAutoDiscover}
|
|
disabled={isSearching}
|
|
className="gap-2 whitespace-nowrap"
|
|
>
|
|
<Search className="h-4 w-4" />
|
|
{isSearching ? t('settingsUi.relatr.searching') : t('settingsUi.relatr.autoDiscover')}
|
|
</Button>
|
|
</div>
|
|
<p className="text-xs text-gray-500">
|
|
{t('settingsUi.relatr.pubkeyDefault')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Relay Input */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="relatr-relay" className="text-sm font-bold text-gray-300">
|
|
{t('settingsUi.relatr.relayLabel')}
|
|
</Label>
|
|
<Input
|
|
id="relatr-relay"
|
|
placeholder="wss://relay.example.com"
|
|
value={relay}
|
|
onChange={(e) => setRelay(e.target.value)}
|
|
className="font-mono text-xs"
|
|
/>
|
|
<p className="text-xs text-gray-500">
|
|
{t('settingsUi.relatr.relayDefault')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Apply Changes Button */}
|
|
<Button
|
|
onClick={handleEnable}
|
|
variant="outline"
|
|
className="w-full gap-2"
|
|
disabled={!pubkey || pubkey.length !== 64}
|
|
>
|
|
{t('settingsUi.relatr.applyChanges')}
|
|
</Button>
|
|
|
|
{/* Documentation Links */}
|
|
<div className="pt-4 border-t border-gray-700 space-y-2">
|
|
<p className="text-sm font-bold text-gray-300">{t('settingsUi.relatr.learnMore')}</p>
|
|
<div className="flex flex-col gap-2">
|
|
<a
|
|
href="https://www.relatr.xyz/"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-sm text-[#ccff00] hover:underline inline-flex items-center gap-1"
|
|
>
|
|
<ExternalLink className="h-3 w-3" />
|
|
{t('settingsUi.relatr.website')}
|
|
</a>
|
|
<a
|
|
href="https://github.com/ContextVM/relatr"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-sm text-[#ccff00] hover:underline inline-flex items-center gap-1"
|
|
>
|
|
<ExternalLink className="h-3 w-3" />
|
|
{t('settingsUi.relatr.github')}
|
|
</a>
|
|
<a
|
|
href="https://github.com/nostr-protocol/nips/blob/master/85.md"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-sm text-[#ccff00] hover:underline inline-flex items-center gap-1"
|
|
>
|
|
<ExternalLink className="h-3 w-3" />
|
|
{t('settingsUi.relatr.nip85Spec')}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|