/ src / lib / components / ProfileModalLoader.svelte
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>