import React, { useState } from "react"; import sanitizeHtml from 'sanitize-html'; type AccountDetails = { id: string, // IMPORTANT: this is int64 so will overflow Javascript's number type acct: string, followed_by: Array, // list of handles }; async function usernameToId(handle: string): Promise<{ id: number, domain: string }> { const match = handle.match(/^(.+)@(.+)$/); if (!match || match.length < 2) { throw new Error(`Incorrect handle: ${handle}`); } const domain = match[2]; const username = match[1]; let response = await fetch(`https://${domain}/api/v1/accounts/lookup?acct=${username}`); const { id } = await response.json(); return { id, domain }; } function getDomain(handle: string) { const match = handle.match(/^(.+)@(.+)$/); if (!match || match.length < 2) { throw new Error(`Incorrect handle: ${handle}`); } const domain = match[2]; return domain; } async function accountFollows(handle: string): Promise> { let id, domain: string; try { ({ id, domain } = await usernameToId(handle)); } catch (e) { return []; } let nextPage: string | null = `https://${domain}/api/v1/accounts/${id}/following`; let data: Array = []; while (nextPage && data.length <= 50) { console.log(`Get page: ${nextPage}`); let response; let page; try { response = await fetch(nextPage); page = await response.json(); } catch (e) { console.log(e); break; } if (!page.map) { break; } page = page.map((entry: AccountDetails) => { if (entry.acct && !/@/.test(entry.acct)) { // make sure the domain is always there entry.acct = `${entry.acct}@${domain}`; }; return entry; }) data = [...data, ...page]; nextPage = getNextPage(response.headers.get('Link')); } return data; } async function accountFofs(handle: string, setProgress: (x: Array) => void): Promise> { console.log('Start'); const directFollows = await accountFollows(handle); setProgress([0, directFollows.length]); console.log(`Direct follows: ${directFollows.length}`); let progress = 0; const indirectFollowLists = await Promise.all( directFollows.map( async ({ acct }) => { const follows = await accountFollows(acct); progress++; setProgress([progress, directFollows.length]); return follows.map(account => ({...account, followed_by: [acct]})); } ), ); let indirectFollows: Array = [].concat([], ...indirectFollowLists); const indirectFollowMap = new Map(); const directFollowIds = new Set(directFollows.map(({ acct }) => acct)); indirectFollows.filter( // exclude direct follows ({ acct }) => !directFollowIds.has(acct) ).map(account => { const acct = account.acct; if (indirectFollowMap.has(acct)) { const otherAccount = indirectFollowMap.get(acct); account.followed_by = [...account.followed_by, ...otherAccount.followed_by]; } indirectFollowMap.set(acct, account); }); return Array.from(indirectFollowMap.values()).sort((a, b) => { if (a.followed_by.length != b.followed_by.length) { return b.followed_by.length - a.followed_by.length; } return b.followers_count - a.followers_count; }); } function getNextPage(linkHeader: string | null): string | null { if (!linkHeader) { return null; } // Example header: // Link: ; rel="next", ; rel="prev" const match = linkHeader.match(/<(.+)>; rel="next"/); if (match && match.length > 0) { return match[1]; } return null; } export function Content({ }) { const [handle, setHandle] = useState(""); const [follows, setfollows] = useState>([]); const [isLoading, setLoading] = useState(false); const [isDone, setDone] = useState(false); const [domain, setDomain] = useState(""); const [[numLoaded, totalToLoad], setProgress] = useState>([0, 0]); console.log(follows.length); async function search(handle: string) { if (!/@/.test(handle)) { return; } setLoading(true); setDomain(getDomain(handle)); setfollows(await accountFofs(handle, setProgress)); setLoading(false); setDone(true); } return
{ search(handle); e.preventDefault(); return false; }}>
setHandle(e.target.value)} className="form-control block w-80 px-3 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-900 focus:bg-white focus:border-green-600 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-gray-200 dark:focus:bg-gray-900 dark:focus:text-gray-200 " id="mastodonHandle" aria-describedby="mastodonHandleHelp" placeholder="johnmastodon@mas.to" /> Be sure to include the full handle, including the domain. {isLoading ?

Loaded {numLoaded} from {totalToLoad}...

: null}
{isDone ?
    {follows.slice(0, 100).map(account => )}
: null}
; } function AccountDetails({ account, mainDomain }) { const { avatar_static, display_name, acct, note, followers_count, followed_by } = account; let formatter = Intl.NumberFormat('en', { notation: 'compact' }); let numfollows = formatter.format(followers_count); console.log(account); return (
  • {display_name}

    {display_name}

    {acct} | {numfollows} follows



    Followed by{' '} {followed_by.map((handle, idx) => ( <>{handle.replace(/@.+/, '')}{idx === followed_by.length - 1 ? '' : ', '} ))}
  • ); }