- 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>
169 lines
5.6 KiB
TypeScript
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>
|
|
);
|
|
}
|