added expand link

account-details
Gabi Purcaru 2023-02-04 08:59:03 +00:00
rodzic 842f169f1e
commit 039e80dd8e
5 zmienionych plików z 133 dodań i 96 usunięć

Wyświetl plik

@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

Wyświetl plik

@ -1,21 +1,29 @@
import React, { useState, memo } from 'react';
import sanitizeHtml from 'sanitize-html';
import { AccountDetails } from './api';
import { AccountExpandedDetails } from './AccountExpandedDetails'
import React, { useState, memo } from 'react'
import sanitizeHtml from 'sanitize-html'
import { AccountDetails } from './api'
export const AccountDetailsRow = memo(
({
account, mainDomain,
account,
mainDomain,
}: {
account: AccountDetails;
mainDomain: string;
account: AccountDetails
mainDomain: string
}) => {
const {
avatar_static, display_name, acct, note, followers_count, followed_by,
} = account;
let formatter = Intl.NumberFormat('en', { notation: 'compact' });
let numFollowers = formatter.format(followers_count);
avatar_static,
display_name,
acct,
note,
followers_count,
followed_by,
} = account
let formatter = Intl.NumberFormat('en', { notation: 'compact' })
let numFollowers = formatter.format(followers_count)
const [expandedFollowers, setExpandedFollowers] = useState(false);
const [expandedFollowers, setExpandedFollowers] = useState(false)
const [expandedDetails, setExpandedDetails] = useState(false)
return (
<li className="px-4 py-3 pb-7 sm:px-0 sm:py-4">
@ -25,7 +33,8 @@ export const AccountDetailsRow = memo(
<img
className="w-16 h-16 sm:w-8 sm:h-8 rounded-full"
src={avatar_static}
alt={display_name} />
alt={display_name}
/>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate dark:text-white">
@ -64,6 +73,22 @@ export const AccountDetailsRow = memo(
.
</>
)}
<br />
<button
onClick={() => setExpandedDetails((d) => !d)}
className="text-blue-500 hover:text-blue-700 fill-blue-500 hover:fill-blue-700 inline-block"
>
See more details
<svg
className="w-3 h-3 inline ml-1 mb-0.5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
{/* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
<path d="M470.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L402.7 256 265.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160zm-352 160l160-160c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L210.7 256 73.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0z" />
</svg>
</button>
{expandedDetails && <AccountExpandedDetails account={account} />}
</small>
</div>
<div className="inline-flex m-auto text-base font-semibold text-gray-900 dark:text-white">
@ -81,7 +106,7 @@ export const AccountDetailsRow = memo(
</div>
</div>
</li>
);
)
}
);
AccountDetailsRow.displayName = 'AccountDetailsRow';
)
AccountDetailsRow.displayName = 'AccountDetailsRow'

Wyświetl plik

@ -0,0 +1,10 @@
import React from 'react'
import { AccountDetails } from './api'
export function AccountExpandedDetails({
account,
}: {
account: AccountDetails
}) {
return <>expanded! {account.display_name}</>
}

Wyświetl plik

@ -21,7 +21,7 @@ function matchesSearch(account: AccountDetails, search: string): boolean {
return false
}
export function Content({ }) {
export function Content({}) {
const [handle, setHandle] = useState('')
const [follows, setFollows] = useState<Array<AccountDetails>>([])
const [isLoading, setLoading] = useState(false)
@ -184,10 +184,10 @@ function ErrorLog({ errors }: { errors: Array<string> }) {
{expanded ? ':' : '.'}
{expanded
? errors.map((err) => (
<p key={err} className="text-xs">
{err}
</p>
))
<p key={err} className="text-xs">
{err}
</p>
))
: null}
</div>
) : null}

Wyświetl plik

@ -1,4 +1,4 @@
import debounce from 'debounce';
import debounce from 'debounce'
export type AccountDetails = {
/**
@ -6,41 +6,41 @@ export type AccountDetails = {
* Mastodon uses int64 so will overflow Javascript's number type
* Pleroma uses 128-bit ids. However just like Mastodon's ids they are lexically sortable strings
*/
id: string;
acct: string;
followed_by: Set<string>; // list of handles
followers_count: number;
discoverable: boolean;
display_name: string;
note: string;
avatar_static: string;
};
id: string
acct: string
followed_by: Set<string> // list of handles
followers_count: number
discoverable: boolean
display_name: string
note: string
avatar_static: string
}
async function usernameToId(
handle: string
): Promise<{ id: string; domain: string; }> {
const match = handle.match(/^(.+)@(.+)$/);
): Promise<{ id: string; domain: string }> {
const match = handle.match(/^(.+)@(.+)$/)
if (!match || match.length < 2) {
throw new Error(`Incorrect handle: ${handle}`);
throw new Error(`Incorrect handle: ${handle}`)
}
const domain = match[2];
const username = match[1];
const domain = match[2]
const username = match[1]
let response = await fetch(
`https://${domain}/api/v1/accounts/lookup?acct=${username}`
);
)
if (response.status !== 200) {
throw new Error('HTTP request failed');
throw new Error('HTTP request failed')
}
const { id } = await response.json();
return { id, domain };
const { id } = await response.json()
return { id, domain }
}
export function getDomain(handle: string) {
const match = handle.match(/^(.+)@(.+)$/);
const match = handle.match(/^(.+)@(.+)$/)
if (!match || match.length < 2) {
throw new Error(`Incorrect handle: ${handle}`);
throw new Error(`Incorrect handle: ${handle}`)
}
const domain = match[2];
return domain;
const domain = match[2]
return domain
}
async function accountFollows(
@ -48,66 +48,68 @@ async function accountFollows(
limit: number,
logError: (x: string) => void
): Promise<Array<AccountDetails>> {
let id, domain: string;
let id, domain: string
try {
; ({ id, domain } = await usernameToId(handle));
;({ id, domain } = await usernameToId(handle))
} catch (e) {
logError(`Cannot find handle ${handle}.`);
return [];
logError(`Cannot find handle ${handle}.`)
return []
}
let nextPage: string |
null = `https://${domain}/api/v1/accounts/${id}/following`;
let data: Array<AccountDetails> = [];
let nextPage:
| string
| null = `https://${domain}/api/v1/accounts/${id}/following`
let data: Array<AccountDetails> = []
while (nextPage && data.length <= limit) {
console.log(`Get page: ${nextPage}`);
let response;
let page;
console.log(`Get page: ${nextPage}`)
let response
let page
try {
response = await fetch(nextPage);
response = await fetch(nextPage)
if (response.status !== 200) {
throw new Error('HTTP request failed');
throw new Error('HTTP request failed')
}
page = await response.json();
page = await response.json()
} catch (e) {
logError(`Error while retrieving followers for ${handle}.`);
break;
logError(`Error while retrieving followers for ${handle}.`)
break
}
if (!page.map) {
break;
break
}
page = page.map((entry: AccountDetails) => {
if (entry.acct && !/@/.test(entry.acct)) {
// make sure the domain is always there
entry.acct = `${entry.acct}@${domain}`;
entry.acct = `${entry.acct}@${domain}`
}
return entry;
});
data = [...data, ...page];
nextPage = getNextPage(response.headers.get('Link'));
return entry
})
data = [...data, ...page]
nextPage = getNextPage(response.headers.get('Link'))
}
return data;
return data
}
export async function accountFofs(
handle: string,
setProgress: (x: Array<number>) => void,
setFollows: (x: Array<AccountDetails>) => void,
logError: (x: string) => void): Promise<void> {
const directFollows = await accountFollows(handle, 2000, logError);
setProgress([0, directFollows.length]);
let progress = 0;
logError: (x: string) => void
): Promise<void> {
const directFollows = await accountFollows(handle, 2000, logError)
setProgress([0, directFollows.length])
let progress = 0
const directFollowIds = new Set(directFollows.map(({ acct }) => acct));
directFollowIds.add(handle.replace(/^@/, ''));
const directFollowIds = new Set(directFollows.map(({ acct }) => acct))
directFollowIds.add(handle.replace(/^@/, ''))
const indirectFollowLists: Array<Array<AccountDetails>> = [];
const indirectFollowLists: Array<Array<AccountDetails>> = []
const updateList = debounce(() => {
let indirectFollows: Array<AccountDetails> = [].concat(
[],
...indirectFollowLists
);
const indirectFollowMap = new Map();
)
const indirectFollowMap = new Map()
indirectFollows
.filter(
@ -115,50 +117,50 @@ export async function accountFofs(
({ acct, discoverable }) => !directFollowIds.has(acct) && discoverable
)
.map((account) => {
const acct = account.acct;
const acct = account.acct
if (indirectFollowMap.has(acct)) {
const otherAccount = indirectFollowMap.get(acct);
const otherAccount = indirectFollowMap.get(acct)
account.followed_by = new Set([
...Array.from(account.followed_by.values()),
...otherAccount.followed_by,
]);
])
}
indirectFollowMap.set(acct, account);
});
indirectFollowMap.set(acct, account)
})
const list = Array.from(indirectFollowMap.values()).sort((a, b) => {
if (a.followed_by.size != b.followed_by.size) {
return b.followed_by.size - a.followed_by.size;
return b.followed_by.size - a.followed_by.size
}
return b.followers_count - a.followers_count;
});
return b.followers_count - a.followers_count
})
setFollows(list);
}, 2000);
setFollows(list)
}, 2000)
await Promise.all(
directFollows.map(async ({ acct }) => {
const follows = await accountFollows(acct, 200, logError);
progress++;
setProgress([progress, directFollows.length]);
const follows = await accountFollows(acct, 200, logError)
progress++
setProgress([progress, directFollows.length])
indirectFollowLists.push(
follows.map((account) => ({ ...account, followed_by: new Set([acct]) }))
);
updateList();
)
updateList()
})
);
)
updateList.flush();
updateList.flush()
}
function getNextPage(linkHeader: string | null): string | null {
if (!linkHeader) {
return null;
return null
}
// Example header:
// Link: <https://mastodon.example/api/v1/accounts/1/follows?limit=2&max_id=7628164>; rel="next", <https://mastodon.example/api/v1/accounts/1/follows?limit=2&since_id=7628165>; rel="prev"
const match = linkHeader.match(/<(.+)>; rel="next"/);
const match = linkHeader.match(/<(.+)>; rel="next"/)
if (match && match.length > 0) {
return match[1];
return match[1]
}
return null;
return null
}