UMBRA/frontend/src/components/connections/ConnectionSearch.tsx
Kyle Pope e27beb7736 Fix toast notifications, require accept_connections for senders
- Rewrite NotificationToaster with max-ID watermark for reliable
  new-notification detection and faster unread count polling (15s)
- Block connection search and requests when sender has
  accept_connections disabled (backend + frontend gate)
- Remove duplicate sender_settings fetch in send_connection_request
- Show actionable error messages in toast respond failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 06:21:43 +08:00

169 lines
5.6 KiB
TypeScript

import { useState } from 'react';
import { Search, UserPlus, Loader2, AlertCircle, CheckCircle, Settings } from 'lucide-react';
import { toast } from 'sonner';
import { useNavigate } from 'react-router-dom';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { useConnections } from '@/hooks/useConnections';
import { useSettings } from '@/hooks/useSettings';
import { getErrorMessage } from '@/lib/api';
interface ConnectionSearchProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export default function ConnectionSearch({ open, onOpenChange }: ConnectionSearchProps) {
const { search, isSearching, sendRequest, isSending } = useConnections();
const { settings } = useSettings();
const navigate = useNavigate();
const [umbralName, setUmbralName] = useState('');
const [found, setFound] = useState<boolean | null>(null);
const [sent, setSent] = useState(false);
const acceptConnectionsEnabled = settings?.accept_connections ?? false;
const handleSearch = async () => {
if (!umbralName.trim()) return;
setFound(null);
setSent(false);
try {
const result = await search(umbralName.trim());
setFound(result.found);
} catch {
setFound(false);
}
};
const handleSend = async () => {
try {
await sendRequest(umbralName.trim());
setSent(true);
toast.success('Connection request sent');
} catch (err) {
toast.error(getErrorMessage(err, 'Failed to send request'));
}
};
const handleClose = () => {
setUmbralName('');
setFound(null);
setSent(false);
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<UserPlus className="h-5 w-5 text-violet-400" />
Find Umbra User
</DialogTitle>
<DialogDescription>
Search for a user by their umbral name to send a connection request.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 pt-2">
{!acceptConnectionsEnabled ? (
<div className="flex flex-col items-center gap-3 py-4 text-center">
<AlertCircle className="h-8 w-8 text-amber-400" />
<p className="text-sm text-muted-foreground">
You need to enable <span className="text-foreground font-medium">Accept Connections</span> in your settings before you can send or receive connection requests.
</p>
<Button
size="sm"
variant="outline"
className="gap-1.5"
onClick={() => { handleClose(); navigate('/settings'); }}
>
<Settings className="h-3.5 w-3.5" />
Go to Settings
</Button>
</div>
) : (
<>
<div className="space-y-2">
<Label htmlFor="umbral_search">Umbral Name</Label>
<div className="flex gap-2">
<Input
id="umbral_search"
placeholder="Enter umbral name..."
value={umbralName}
onChange={(e) => {
setUmbralName(e.target.value);
setFound(null);
setSent(false);
}}
onKeyDown={(e) => { if (e.key === 'Enter') handleSearch(); }}
maxLength={50}
/>
<Button
onClick={handleSearch}
disabled={!umbralName.trim() || isSearching}
size="sm"
>
{isSearching ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Search className="h-4 w-4" />
)}
</Button>
</div>
</div>
{found === false && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<AlertCircle className="h-4 w-4" />
User not found
</div>
)}
{found === true && !sent && (
<div className="flex items-center justify-between rounded-lg border border-border p-3">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-violet-500/15 flex items-center justify-center">
<span className="text-sm font-medium text-violet-400">
{umbralName.charAt(0).toUpperCase()}
</span>
</div>
<span className="text-sm font-medium">{umbralName}</span>
</div>
<Button
onClick={handleSend}
disabled={isSending}
size="sm"
className="gap-1.5"
>
{isSending ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<UserPlus className="h-3.5 w-3.5" />
)}
Send Request
</Button>
</div>
)}
{sent && (
<div className="flex items-center gap-2 text-sm text-green-400">
<CheckCircle className="h-4 w-4" />
Connection request sent
</div>
)}
</>
)}
</div>
</DialogContent>
</Dialog>
);
}