Retro Bulletin Board Systems on atproto. Web app and TUI. lazy mirror of alyraffauf/atbbs atbbs.xyz
forums python tui atproto bbs
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

web/DialBBS: resolve identities while typing

+62 -19
+62 -19
web/src/components/DialBBS.tsx
··· 1 - import { useRef, useState, type SyntheticEvent } from "react"; 1 + import { useEffect, useRef, useState, type SyntheticEvent } from "react"; 2 2 import { Link, useNavigate } from "react-router-dom"; 3 3 import HandleInput from "./form/HandleInput"; 4 + import { resolveIdentity, getRecord } from "../lib/atproto"; 5 + import { SITE } from "../lib/lexicon"; 4 6 import type { DiscoveredBBS } from "../hooks/useDiscovery"; 5 7 6 8 export interface Suggestion { ··· 13 15 discovered?: DiscoveredBBS[]; 14 16 suggestions?: Suggestion[]; 15 17 } 18 + 19 + const RESOLVE_DEBOUNCE_MS = 300; 16 20 17 21 export default function DialBBS({ discovered, suggestions }: DialBBSProps) { 18 22 const navigate = useNavigate(); 19 - const [handle, setHandle] = useState(""); 23 + const [inputValue, setInputValue] = useState(""); 20 24 const [focused, setFocused] = useState(false); 25 + const [resolvedSuggestion, setResolvedSuggestion] = 26 + useState<Suggestion | null>(null); 21 27 const blurTimeout = useRef<ReturnType<typeof setTimeout>>(undefined); 22 28 23 29 function onSubmit(event: SyntheticEvent) { 24 30 event.preventDefault(); 25 - const trimmed = handle.trim(); 31 + const trimmed = inputValue.trim(); 26 32 if (trimmed) navigate(`/bbs/${encodeURIComponent(trimmed)}`); 27 33 } 28 34 ··· 45 51 blurTimeout.current = setTimeout(() => setFocused(false), 150); 46 52 } 47 53 48 - const hasSuggestions = suggestions && suggestions.length > 0; 54 + useEffect(() => { 55 + const query = inputValue.trim(); 56 + if (!query || !query.includes(".")) { 57 + setResolvedSuggestion(null); 58 + return; 59 + } 60 + 61 + let cancelled = false; 62 + const timeout = setTimeout(async () => { 63 + try { 64 + const identity = await resolveIdentity(query); 65 + const siteRecord = await getRecord(identity.did, SITE, "self"); 66 + const siteValue = siteRecord.value as { name?: string }; 67 + if (!cancelled) { 68 + setResolvedSuggestion({ 69 + to: `/bbs/${encodeURIComponent(identity.handle)}`, 70 + name: siteValue.name ?? identity.handle, 71 + handle: identity.handle, 72 + }); 73 + } 74 + } catch { 75 + if (!cancelled) setResolvedSuggestion(null); 76 + } 77 + }, RESOLVE_DEBOUNCE_MS); 78 + 79 + return () => { 80 + cancelled = true; 81 + clearTimeout(timeout); 82 + }; 83 + }, [inputValue]); 49 84 50 - const query = handle.trim().toLowerCase(); 51 - const filteredSuggestions = hasSuggestions 52 - ? query 53 - ? suggestions.filter( 54 - (entry) => 55 - entry.name.toLowerCase().includes(query) || 56 - entry.handle.toLowerCase().includes(query), 57 - ) 58 - : suggestions 59 - : []; 85 + const filterQuery = inputValue.trim().toLowerCase(); 86 + const staticMatches = (suggestions ?? []).filter( 87 + (entry) => 88 + !filterQuery || 89 + entry.name.toLowerCase().includes(filterQuery) || 90 + entry.handle.toLowerCase().includes(filterQuery), 91 + ); 92 + 93 + const visibleSuggestions = 94 + resolvedSuggestion && 95 + !staticMatches.some( 96 + (entry) => entry.handle === resolvedSuggestion.handle, 97 + ) 98 + ? [resolvedSuggestion, ...staticMatches] 99 + : staticMatches; 60 100 61 101 return ( 62 - <div onFocus={hasSuggestions ? onFocus : undefined} onBlur={hasSuggestions ? onBlur : undefined}> 102 + <div onFocus={onFocus} onBlur={onBlur}> 63 103 <form onSubmit={onSubmit} className="flex flex-col sm:flex-row gap-2"> 64 104 <HandleInput 65 105 name="handle" 66 - value={handle} 67 - onChange={setHandle} 106 + value={inputValue} 107 + onChange={setInputValue} 68 108 required 69 109 className="sm:flex-1" 70 110 /> ··· 82 122 random 83 123 </button> 84 124 </form> 85 - {focused && filteredSuggestions.length > 0 && ( 125 + {focused && visibleSuggestions.length > 0 && ( 86 126 <div className="relative"> 87 127 <div className="absolute left-0 right-0 mt-1 bg-neutral-900 border border-neutral-800 rounded shadow-lg z-10"> 88 - {filteredSuggestions.map((entry) => ( 128 + {visibleSuggestions.map((entry) => ( 89 129 <Link 90 130 key={entry.to} 91 131 to={entry.to} 92 132 className="block px-3 py-2 text-sm text-neutral-300 hover:bg-neutral-800 first:rounded-t last:rounded-b" 93 133 > 94 134 {entry.name} 135 + {entry.name !== entry.handle && ( 136 + <span className="text-neutral-500 ml-2">{entry.handle}</span> 137 + )} 95 138 </Link> 96 139 ))} 97 140 </div>