ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

fix login avatar display by fetching from Bluesky API

actor-typeahead component doesn't expose avatar data via events or attributes.
Added debounced API fetch (300ms) to searchActorsTypeahead endpoint when
handle is entered. Avatar now displays for both typeahead selections and
manually entered handles.

byarielm.fyi 9bdca934 fdbab043

verified
Changed files
+41 -26
src
pages
+41 -26
src/pages/Login.tsx
··· 32 32 handle: "", 33 33 }); 34 34 35 - // Sync typeahead selection with form state and extract avatar 35 + // Sync typeahead selection with form state and fetch avatar 36 36 useEffect(() => { 37 37 const input = inputRef.current; 38 38 if (!input) return; 39 39 40 + let debounceTimer: ReturnType<typeof setTimeout>; 41 + 42 + const fetchAvatar = async (handle: string) => { 43 + if (!handle || handle.length < 3) { 44 + setSelectedAvatar(null); 45 + return; 46 + } 47 + 48 + try { 49 + const url = new URL( 50 + "xrpc/app.bsky.actor.searchActorsTypeahead", 51 + "https://public.api.bsky.app" 52 + ); 53 + url.searchParams.set("q", handle); 54 + url.searchParams.set("limit", "1"); 55 + 56 + const res = await fetch(url); 57 + const json = await res.json(); 58 + 59 + if (json.actors?.[0]?.avatar) { 60 + setSelectedAvatar(json.actors[0].avatar); 61 + } else { 62 + setSelectedAvatar(null); 63 + } 64 + } catch (error) { 65 + // Silently fail - avatar is optional 66 + setSelectedAvatar(null); 67 + } 68 + }; 69 + 40 70 const handleInputChange = () => { 41 71 let value = input.value.trim(); 42 72 ··· 52 82 } 53 83 } 54 84 55 - // Check if typeahead has selection data (avatar) 56 - const typeaheadElement = input.closest("actor-typeahead"); 57 - if (typeaheadElement) { 58 - const avatar = typeaheadElement.getAttribute("data-avatar"); 59 - if (avatar) { 60 - setSelectedAvatar(avatar); 61 - } else if (value === "") { 62 - // Clear avatar when input is cleared 63 - setSelectedAvatar(null); 64 - } 65 - } 66 - 67 85 // Update form state 68 86 setValue("handle", value); 87 + 88 + // Debounce avatar fetch 89 + clearTimeout(debounceTimer); 90 + if (value === "") { 91 + setSelectedAvatar(null); 92 + } else { 93 + debounceTimer = setTimeout(() => fetchAvatar(value), 300); 94 + } 69 95 }; 70 96 71 - // Listen for input, change, and blur events to catch typeahead selections 97 + // Listen for input and change events 72 98 input.addEventListener("input", handleInputChange); 73 99 input.addEventListener("change", handleInputChange); 74 - input.addEventListener("blur", handleInputChange); 75 - 76 - // Also listen for custom typeahead selection event if it exists 77 - const handleSelection = (e: Event) => { 78 - const customEvent = e as CustomEvent; 79 - if (customEvent.detail?.avatar) { 80 - setSelectedAvatar(customEvent.detail.avatar); 81 - } 82 - }; 83 - input.addEventListener("actor-select", handleSelection as EventListener); 84 100 85 101 return () => { 86 102 input.removeEventListener("input", handleInputChange); 87 103 input.removeEventListener("change", handleInputChange); 88 - input.removeEventListener("blur", handleInputChange); 89 - input.removeEventListener("actor-select", handleSelection as EventListener); 104 + clearTimeout(debounceTimer); 90 105 }; 91 106 }, [setValue, strippedAtMessage]); 92 107