Skip to content
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
59 changes: 54 additions & 5 deletions cdm.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
},
"dependencies": {
"acc2c3b5e912b762": {
"@example/playground-registry": "latest"
"@w3s/playground-registry": "latest"
}
},
"contracts": {
"acc2c3b5e912b762": {
"@example/playground-registry": {
"version": 6,
"address": "0x279585Cb8E8971e34520A3ebbda3E0C4D77C3d97",
"@w3s/playground-registry": {
"version": 0,
"address": "0xb9Aa5e8421AF2c7426a37bA10045158dDe981856",
"abi": [
{
"type": "constructor",
Expand Down Expand Up @@ -196,6 +196,55 @@
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getApps",
"inputs": [
{
"name": "start",
"type": "uint32"
},
{
"name": "count",
"type": "uint32"
}
],
"outputs": [
{
"name": "",
"type": "tuple",
"components": [
{
"name": "total",
"type": "uint32"
},
{
"name": "entries",
"type": "tuple[]",
"components": [
{
"name": "index",
"type": "uint32"
},
{
"name": "domain",
"type": "string"
},
{
"name": "metadata_uri",
"type": "string"
},
{
"name": "owner",
"type": "address"
}
]
}
]
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getMetadataUri",
Expand Down Expand Up @@ -241,7 +290,7 @@
"stateMutability": "view"
}
],
"metadataCid": "bafk2bzaceck7veaix4ttzyd6bmwlssgycrrlgilpat2c272nczzlrgnqy6fze"
"metadataCid": "bafk2bzaceb5zctlcjumwv6co6buraoqgatsss3fffmzh6bnlxgemovujkzod4"
}
}
}
Expand Down
72 changes: 42 additions & 30 deletions src/commands/mod/AppBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,37 +31,49 @@ export function AppBrowser({ registry, onSelect }: Props) {
const [cursor, setCursor] = useState(0);
const [scroll, setScroll] = useState(0);
const [fetching, setFetching] = useState(true);
const nextIdx = useRef<number | null>(null);
// Offset (in reverse-chronological order) of the next page to request.
// Contract's `getApps(start, count)` treats `start` as a REVERSE offset —
// `start=0` returns the newest batch, `start=BATCH` the next page, etc.
// `null` = no more pages.
const nextStart = useRef<number | null>(0);

const gateway = getGateway("paseo");

const loadBatch = useCallback(
async (startIdx: number) => {
async (start: number) => {
setFetching(true);
const indices = [];
for (let i = startIdx; i > startIdx - BATCH && i >= 0; i--) indices.push(i);

const lowestQueried = Math.min(...indices);
nextIdx.current = lowestQueried > 0 ? lowestQueried - 1 : null;

const results = await Promise.all(
indices.map(async (i) => {
const res = await registry.getDomainAt.query(i);
return res.value.isSome ? (res.value.value as string) : null;
}),
);

const entries: AppEntry[] = results
.filter((d): d is string => d !== null)
.map((domain) => ({ domain, name: null, description: null, repository: null }));
const res = await registry.getApps.query(start, BATCH);
const rawEntries = res.value.entries as Array<{
index: number;
domain: string;
metadata_uri: string;
owner: string;
}>;
const totalFromResp = res.value.total as number;
// Always set — React bails on same-value updates.
setTotal(totalFromResp);

// Contract returns newest-first; preserve that order for display.
nextStart.current = start + BATCH < totalFromResp ? start + BATCH : null;

const entries: AppEntry[] = rawEntries.map((e) => ({
domain: e.domain,
name: null,
description: null,
repository: null,
}));

setApps((prev) => [...prev, ...entries]);
setFetching(false);

// Metadata JSONs still have to be fetched one-at-a-time from
// the gateway — that's IPFS HTTP, not a chain query. Kick them
// off in parallel and update each row as it lands.
await Promise.allSettled(
entries.map(async (entry) => {
const metaRes = await registry.getMetadataUri.query(entry.domain);
const cid = metaRes.value.isSome ? (metaRes.value.value as string) : null;
rawEntries.map(async (raw, i) => {
const entry = entries[i];
const cid = raw.metadata_uri;
if (!cid) return;
const meta = await fetchJson<Record<string, string>>(cid, gateway);
setApps((prev) =>
Expand All @@ -83,19 +95,19 @@ export function AppBrowser({ registry, onSelect }: Props) {
);

useEffect(() => {
(async () => {
const res = await registry.getAppCount.query();
const count = res.value as number;
setTotal(count);
if (count > 0) await loadBatch(count - 1);
})();
}, []);
// `getApps(0, BATCH)` returns the newest batch plus `total`, so we
// don't need a separate `getAppCount` probe. When the registry is
// empty, the response still carries `total: 0` — we drop the spinner
// and leave `nextStart.current` at its initial 0 harmlessly (the
// scroll-trigger effect guards on `apps.length`, so it won't re-fire).
loadBatch(0);
}, [loadBatch]);

useEffect(() => {
if (cursor >= apps.length - 3 && nextIdx.current !== null && !fetching) {
loadBatch(nextIdx.current);
if (cursor >= apps.length - 3 && nextStart.current !== null && !fetching) {
loadBatch(nextStart.current);
}
}, [cursor, apps.length, fetching]);
}, [cursor, apps.length, fetching, loadBatch]);

useInput((input, key) => {
if (key.upArrow && cursor > 0) {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ export async function getRegistryContract(
defaultSigner: signer.signer,
defaultOrigin: signer.address,
});
return manager.getContract("@example/playground-registry");
return manager.getContract("@w3s/playground-registry");
}
Loading