Skip to content

Without virtualisation #266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 44 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@
"@mui/icons-material": "^5.14.16",
"@mui/material": "^5.14.20",
"@mui/styles": "^5.14.20",
"@tanstack/react-query": "^5.61.4",
"@tanstack/react-query": "^5.64.1",
"@yornaath/batshit": "^0.10.1",
"ag-grid-community": "^31.0.2",
"ag-grid-react": "^31.0.3",
"fuse.js": "^7.0.0",
"iso-web": "^1.0.6",
"p-queue": "npm:@nmann/p-queue@^8.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.28.0"
Expand Down
34 changes: 27 additions & 7 deletions src/api/lists.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { unwrapShortHandle } from '.';
import { fetchClearskyApi, unwrapClearskyURL } from './core';
import { useResolveHandleOrDid } from './resolve-handle-or-did';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import PQueue from 'p-queue';

const PAGE_SIZE = 100;

Expand All @@ -26,16 +27,18 @@ export function useList(handleOrDID) {
* Look up the total number of lists to which a given handle/DID belongs
* @param {string} handleOrDID
*/
export function useListTotal(handleOrDID) {
export function useListCount(handleOrDID) {
const profileQuery = useResolveHandleOrDid(handleOrDID);
const shortHandle = profileQuery.data?.shortHandle;
return useQuery({
enabled: !!shortHandle,
queryKey: ['list-total', shortHandle],
queryFn: () => getListTotal(shortHandle),
queryFn: () => getListCount(shortHandle),
});
}

const TWELVE_HOURS = 1000 * 60 * 60 * 12;

/**
* Gets the size (length) of a given user list
* @param {string} listUrl
Expand All @@ -44,7 +47,9 @@ export function useListSize(listUrl) {
return useQuery({
enabled: !!listUrl,
queryKey: ['list-size', listUrl],
queryFn: () => getListSize(listUrl),
queryFn: ({ signal }) => getListSize(listUrl, signal),
staleTime: TWELVE_HOURS,
gcTime: TWELVE_HOURS,
});
}

Expand Down Expand Up @@ -84,9 +89,8 @@ async function getList(shortHandle, currentPage = 1) {
* Gets the total number of lists to which a given handle belongs
* @param {string} shortHandle
*/
async function getListTotal(shortHandle) {
async function getListCount(shortHandle) {
const handleURL = 'get-list/total/' + unwrapShortHandle(shortHandle);

/** @type {{ data: { count: number; pages: number } }} */
const re = await fetchClearskyApi('v1', handleURL);
return re.data;
Expand All @@ -95,13 +99,18 @@ async function getListTotal(shortHandle) {
/**
* Gets the size (length) of a given user list
* @param {string} listUrl
* @param {AbortSignal} signal
* @returns {Promise<{ count: number } | null>} null if response is a 400/404
*/
async function getListSize(listUrl) {
async function getListSize(listUrl, signal) {
const apiUrl = unwrapClearskyURL(
`/api/v1/anon/get-list/specific/total/${encodeURIComponent(listUrl)}`
);
const resp = await fetch(apiUrl);
signal.throwIfAborted;
const resp = await listSizeQueue.add(() => fetch(apiUrl, { signal }), {
signal,
throwOnTimeout: true,
});
if (resp.ok) {
/** @type {{ data: { count: number }, list_uri: string }} */
const respData = await resp.json();
Expand All @@ -112,3 +121,14 @@ async function getListSize(listUrl) {
}
throw new Error('getListSize error: ' + resp.statusText);
}

/**
* create a queue where only one request can be in flight at a time,
* and at most 1 may be sent in any 250 millisecond interval
*/
const listSizeQueue = new PQueue({
concurrency: 1,
intervalCap: 1,
interval: 200,
timeout: 500,
});
2 changes: 1 addition & 1 deletion src/common-components/account-short-entry.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ function getAvatarDelay(account) {
const rnd = Math.abs(hash) - Math.floor(Math.abs(hash));
delay = (rnd * 40).toFixed(3) + 's';
avatarDelays[avatarUrl] = delay;
console.log('Avatar delay', account.shortHandle, { delay, hash, rnd });
//console.log('Avatar delay', account.shortHandle, { delay, hash, rnd });
return delay;
}

Expand Down
45 changes: 45 additions & 0 deletions src/common-components/progressive-render.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// @ts-check
import { useState, cloneElement } from 'react';
import { Visible } from './visible';

const INITIAL_SIZE = 20;
const GROW_BLOCK_SIZE = 29;

/**
* @template ItemType
* @param {{ items: Array<ItemType>, renderItem(item: ItemType): import('react').ReactElement }} props
* Given a list of items, only renders the first 20 by default,
* and then more whenever the last item is close to being on screen
*/
export function ProgressiveRender(props) {
const [listSize, setListSize] = useState(INITIAL_SIZE);
const showSize = Math.min(props.items.length, listSize);

return (
<>
{props.items.slice(0, showSize).map((item, index) => {
const entry = cloneElement(props.renderItem(item), { key: index });

return index < showSize - 1 ? (
entry
) : (
<Visible
key={index}
rootMargin="0px 0px 300px 0px"
onVisible={handleBottomVisible}
>
{entry}
</Visible>
);
})}
</>
);

function handleBottomVisible() {
const incrementListSize = listSize + GROW_BLOCK_SIZE;
setListSize(incrementListSize);
if (incrementListSize > props.items.length) {
// TODO: control fetch more from here?
}
}
}
36 changes: 5 additions & 31 deletions src/detail-panels/block-panel-generic/list-view.jsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,24 @@
// @ts-check
/// <reference path="../../types.d.ts" />
import { FormatTimestamp } from '../../common-components/format-timestamp';
import { Visible } from '../../common-components/visible';
import { useState } from 'react';
import { ProgressiveRender } from '../../common-components/progressive-render';
import { AccountShortEntry } from '../../common-components/account-short-entry';
import { localise } from '../../localisation';

const INITIAL_SIZE = 20;
const GROW_BLOCK_SIZE = 29;

/**
* @param {{
* blocklist: (BlockedByRecord | { did: string; blocked_date: string })[];
* }} _
*/
export function ListView({ blocklist }) {
const [listSize, setListSize] = useState(INITIAL_SIZE);
const showSize = Math.min(blocklist.length, listSize);

return (
<ul className="block-list">
{blocklist.slice(0, showSize).map((block, index) => {
const entry = <ListViewEntry key={index} {...block} />;

return index < showSize - 1 ? (
entry
) : (
<Visible
key={index}
rootMargin="0px 0px 300px 0px"
onVisible={handleBottomVisible}
>
{entry}
</Visible>
);
})}
<ProgressiveRender
items={blocklist}
renderItem={(item) => <ListViewEntry {...item} />}
/>
</ul>
);

function handleBottomVisible() {
const incrementListSize = listSize + GROW_BLOCK_SIZE;
setListSize(incrementListSize);
if (incrementListSize > blocklist.length) {
// TODO: fetch more
}
}
}

/**
Expand Down
20 changes: 13 additions & 7 deletions src/detail-panels/lists/list-view.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
.lists-as-list-view {
margin: 0;
padding: 0
padding: 0;
}

.lists-as-list-view .lists-entry {
list-style-type: none;
height: 3.5em;
height: auto;
border-top: solid 1px #dbdbdb;
padding-top: 0.4em;
padding-left: 0.7em;
Expand Down Expand Up @@ -39,15 +39,21 @@
.lists-as-list-view .lists-entry .list-name {
grid-row: 1/ 2;
grid-column: 1 / 2;
padding-left: 1.7em;
padding-left: 0.5em;
font-weight: bold;
}

.lists-as-list-view .lists-entry .list-description {
grid-row: 1 / 2;
grid-column: 2 / 3;
opacity: 0.6;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 0.5em;
}
padding-left: 1.7em;
white-space: normal;
word-wrap: break-word;
}

.lists-as-list-view .lists-entry .list-count {
padding-left: 0.3em;
color: green;
}

Loading