ProfileModalLoader.svelte
1 <script lang="ts"> 2 import { Modal, Loader } from 'svelte-akui'; 3 import type { MastodonAccount } from '$lib/mastodon'; 4 import { searchAccount, getFullHandle } from '$lib/mastodon'; 5 import { friendkit } from '$lib/store.svelte'; 6 import ProfileModal from './ProfileModal.svelte'; 7 8 let { 9 handle, 10 account = null, 11 onClose 12 }: { 13 handle: string; 14 account?: MastodonAccount | null; 15 onClose: () => void; 16 } = $props(); 17 18 let isLoading = $state(false); 19 let showSpinner = $state(false); 20 let localAccount = $state<MastodonAccount | null>(null); 21 let error = $state<string | null>(null); 22 let abortController: AbortController | null = null; 23 24 function handleClose() { 25 if (abortController) { 26 abortController.abort(); 27 } 28 onClose(); 29 } 30 31 function fetchProfile(targetHandle: string) { 32 if (!friendkit.accessToken || !friendkit.instance) return; 33 34 // 1. Check local lists & memory cache first 35 const existing = friendkit.getAccountByHandle(targetHandle); 36 if (existing) { 37 localAccount = existing; 38 return; 39 } 40 41 // 2. Fetch from network 42 isLoading = true; 43 error = null; 44 45 // Don't show the modal until 300ms, because it's janky on fast connections 46 const spinnerTimeout = setTimeout(() => { 47 if (isLoading) showSpinner = true; 48 }, 300); 49 50 abortController = new AbortController(); 51 52 searchAccount( 53 targetHandle, 54 abortController.signal 55 ) 56 .then((fetched) => { 57 if (fetched) { 58 localAccount = fetched; 59 friendkit.externalUsers.set(targetHandle.toLowerCase(), fetched); 60 } else { 61 error = `Could not find user @${targetHandle}`; 62 } 63 }) 64 .catch((err: unknown) => { 65 if (err instanceof Error && err.name === 'AbortError') { 66 console.log('Fetch aborted'); 67 return; 68 } 69 console.error('Failed to fetch profile:', err); 70 error = 'An error occurred while fetching the profile.'; 71 }) 72 .finally(() => { 73 clearTimeout(spinnerTimeout); 74 isLoading = false; 75 showSpinner = false; 76 abortController = null; 77 }); 78 } 79 80 // React to handle or account prop changes 81 $effect(() => { 82 if (account) { 83 localAccount = account; 84 error = null; 85 return; 86 } 87 88 if (handle) { 89 // Check if we already have this account showing (to avoid redundant fetches) 90 if (localAccount && ( 91 localAccount.acct.toLowerCase() === handle.toLowerCase() || 92 getFullHandle(localAccount, friendkit.instance).toLowerCase() === handle.toLowerCase() 93 )) { 94 return; 95 } 96 97 // Abort any pending fetch for a different handle 98 if (abortController) { 99 abortController.abort(); 100 } 101 102 const existing = friendkit.getAccountByHandle(handle); 103 if (existing) { 104 localAccount = existing; 105 error = null; 106 } else { 107 localAccount = null; 108 fetchProfile(handle); 109 } 110 } 111 }); 112 </script> 113 114 {#if localAccount} 115 <ProfileModal account={localAccount} onClose={handleClose} /> 116 {:else if showSpinner || error} 117 <Modal onClose={handleClose} showCloseButton={false}> 118 <div class="fk-profile-loader"> 119 {#if showSpinner} 120 <div class="fk-profile-loader__spinner"> 121 <Loader size="large" /> 122 <p>Fetching profileā¦</p> 123 </div> 124 {/if} 125 126 {#if error} 127 <div class="fk-profile-loader__error"> 128 <p>{error}</p> 129 </div> 130 {/if} 131 </div> 132 </Modal> 133 {/if} 134 135 <style> 136 .fk-profile-loader { 137 min-width: 400px; 138 min-height: 200px; 139 display: flex; 140 align-items: center; 141 justify-content: center; 142 } 143 144 .fk-profile-loader__spinner, 145 .fk-profile-loader__error { 146 display: flex; 147 flex-direction: column; 148 align-items: center; 149 gap: 1rem; 150 padding: 2rem; 151 text-align: center; 152 } 153 154 .fk-profile-loader__spinner p { 155 opacity: 0.7; 156 font-size: 0.9rem; 157 } 158 159 @media (max-width: 640px) { 160 .fk-profile-loader { 161 min-width: 0; 162 width: 100%; 163 } 164 } 165 </style>