Skip to content

Commit 089f99f

Browse files
authored
bump pubky SDK (#18)
1 parent 88f3f98 commit 089f99f

File tree

8 files changed

+183
-59
lines changed

8 files changed

+183
-59
lines changed

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
24.13.0

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"dependencies": {
1313
"@faker-js/faker": "^8.4.1",
14-
"@synonymdev/pubky": "^0.4.0-rc3",
14+
"@synonymdev/pubky": "^0.6.0",
1515
"solid-js": "^1.9.9"
1616
},
1717
"devDependencies": {

src/App.tsx

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,18 @@ import {
1313
toggleDirsFirst,
1414
openPreview,
1515
closePreview,
16+
formatDisplayPath,
17+
isPubkeySegment,
18+
stripInputPrefixes,
1619
} from "./state.ts";
1720

1821
function App() {
1922
const [input, setInput] = createSignal("");
20-
const [displayPrefix, setDisplayPrefix] = createSignal<
21-
"" | "pubky://" | "pk:"
22-
>("");
2323

24-
// mirror address bar; only add a prefix if the user used one
24+
// mirror address bar using canonical pubky<z32> display format
2525
createEffect(() => {
2626
const path = store.dir;
27-
const prefix = displayPrefix();
28-
setInput(path ? (prefix ? prefix + path : path) : "");
27+
setInput(path ? formatDisplayPath(path) : "");
2928
});
3029

3130
function updateInput(value: string) {
@@ -42,22 +41,23 @@ function App() {
4241
if (!raw) return;
4342

4443
setStore("explorer", true);
44+
const stripped = stripInputPrefixes(raw);
4545

4646
// If it ends with '/', treat as directory. Otherwise treat as "file in dir".
47-
if (raw.endsWith("/")) {
48-
updateDir(raw, "none"); // keep exact hash
47+
if (stripped.endsWith("/")) {
48+
updateDir(stripped, "none"); // keep exact hash
4949
closePreview();
5050
return;
5151
}
5252

5353
// Split into dir + file; update dir without touching the URL; open preview without rewriting hash
54-
const cut = raw.lastIndexOf("/");
55-
const dir = cut >= 0 ? raw.slice(0, cut + 1) : raw; // handles bare key
56-
const file = cut >= 0 ? raw.slice(cut + 1) : "";
54+
const cut = stripped.lastIndexOf("/");
55+
const dir = cut >= 0 ? stripped.slice(0, cut + 1) : stripped; // handles bare key
56+
const file = cut >= 0 ? stripped.slice(cut + 1) : "";
5757

5858
updateDir(dir, "none");
5959
if (file)
60-
openPreview(`pubky://${store.dir}${file}`, file, { updateUrl: false });
60+
openPreview(`pubky://${dir}${file}`, file, { updateUrl: false });
6161
}
6262

6363
onMount(() => {
@@ -91,16 +91,10 @@ function App() {
9191
setStore("list", []);
9292

9393
const raw = input().trim();
94-
const m = raw.match(/^(pubky:\/\/|pk:)/i);
95-
setDisplayPrefix(
96-
m ? (m[1].toLowerCase() as "pubky://" | "pk:") : "",
97-
);
9894

9995
// Treat no trailing slash as "file in dir" for direct preview
100-
const stripped = raw
101-
.replace(/^pubky:\/\/?/i, "")
102-
.replace(/^pk:/i, "");
103-
const isBareKey = /^[A-Za-z0-9]{52}$/.test(stripped);
96+
const stripped = stripInputPrefixes(raw);
97+
const isBareKey = isPubkeySegment(stripped);
10498
const isFileIntent = !stripped.endsWith("/") && !isBareKey;
10599

106100
if (isFileIntent) {
@@ -109,7 +103,7 @@ function App() {
109103
const file = cut >= 0 ? stripped.slice(cut + 1) : "";
110104
updateDir(dir, "push");
111105
if (file)
112-
openPreview(`pubky://${store.dir}${file}`, file, {
106+
openPreview(`pubky://${dir}${file}`, file, {
113107
updateUrl: true,
114108
});
115109
setStore("explorer", true);

src/Explorer.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
loadMore,
88
prefetchDir,
99
cacheSaveScroll,
10+
isPubkeySegment,
1011
} from "./state.ts";
1112
import Preview from "./Preview";
1213

@@ -153,7 +154,9 @@ function DirectoryButtons() {
153154
for (let part of root) {
154155
if (part.length == 0) continue;
155156
previous += part + "/";
156-
buttons.push({ text: part, path: previous });
157+
const text =
158+
buttons.length === 0 && isPubkeySegment(part) ? `pubky${part}` : part;
159+
buttons.push({ text, path: previous });
157160
}
158161
return buttons;
159162
}

src/ShareButton.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createSignal } from "solid-js";
2-
import { store } from "./state";
2+
import { formatDisplayPath, store } from "./state";
33

44
type Props = {
55
/** Optional explicit path to share (no scheme). If omitted, deduced from state. */
@@ -22,9 +22,10 @@ export function ShareButton(props: Props) {
2222
(store.preview?.open
2323
? (store.dir + store.preview.name).replace(/\/+$/, "")
2424
: store.dir);
25+
const sharePath = formatDisplayPath(p);
2526

2627
const u = new URL(window.location.href);
27-
u.hash = `#p=${encodeURIComponent(p)}`;
28+
u.hash = `#p=${encodeURIComponent(sharePath)}`;
2829

2930
const text = u.toString();
3031

src/state.ts

Lines changed: 65 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
1-
import { Client } from "@synonymdev/pubky";
1+
import { Pubky } from "@synonymdev/pubky";
22
import { createStore } from "solid-js/store";
33

4-
export const client =
5-
import.meta.env.VITE_TESTNET == "true" ? Client.testnet() : new Client();
4+
export const pubky =
5+
import.meta.env.VITE_TESTNET === "true" ? Pubky.testnet() : new Pubky();
6+
const publicStorage = pubky.publicStorage;
67

78
export type ListItem = { link: string; name: string; isDirectory: boolean };
89

10+
const PUBKEY_RE = /^[a-z0-9]{52}$/i;
11+
12+
export function isPubkeySegment(value: string): boolean {
13+
return PUBKEY_RE.test(value);
14+
}
15+
16+
export function formatDisplayPath(path: string): string {
17+
return path.replace(/^([a-z0-9]{52})(?=\/|$)/i, "pubky$1");
18+
}
19+
20+
export function stripInputPrefixes(raw: string): string {
21+
let path = (raw || "").trim();
22+
path = path.replace(/^pubky:\/\/?/i, "").replace(/^pk:/i, "");
23+
if (/^pubky[a-z0-9]{52}(?:\/|$)/i.test(path)) {
24+
path = path.replace(/^pubky/i, "");
25+
}
26+
return path;
27+
}
28+
929
type PreviewState = {
1030
open: boolean;
1131
link: string;
@@ -114,15 +134,10 @@ export function loadMore() {
114134
const reqId = ++currentRequestId;
115135
isFetching = true;
116136

117-
client
118-
.list(`pubky://${path}`, cursor || "", false, limit, store.shallow)
137+
listDirectory(path, cursor || null, limit)
119138
.then((l: Array<string>) => {
120139
if (reqId !== currentRequestId) return; // stale; ignore
121-
const list = l.map((link) => {
122-
let name = link.replace("pubky://", "").replace(store.dir, "");
123-
let isDirectory = name.endsWith("/");
124-
return { link, isDirectory, name };
125-
});
140+
const list = listToItems(l, store.dir);
126141

127142
let map = new Map<string, ListItem>();
128143
for (let item of store.list) map.set(item.name, item);
@@ -204,7 +219,7 @@ export function updateDir(
204219
export async function downloadFile(link: string) {
205220
setStore("loading", true);
206221
try {
207-
const response: Response = await client.fetch(link);
222+
const response: Response = await publicStorage.get(link);
208223
if (!response.ok) {
209224
throw new Error(
210225
`Failed to fetch file: ${response.status} ${response.statusText}`,
@@ -266,7 +281,7 @@ export async function openPreview(
266281
});
267282

268283
try {
269-
const res = await client.fetch(link);
284+
const res = await publicStorage.get(link);
270285
if (!res.ok) throw new Error(`Failed to fetch file: ${res.status}`);
271286
const mime = (res.headers.get("content-type") || "").toLowerCase();
272287

@@ -369,14 +384,9 @@ export function prefetchDir(dirPath: string) {
369384
prefetchInFlight.add(key);
370385

371386
const limit = Math.ceil(window.innerHeight / 40);
372-
client
373-
.list(`pubky://${dir}`, "", false, limit, store.shallow)
387+
listDirectory(dir, null, limit)
374388
.then((l: Array<string>) => {
375-
const list = l.map((link) => {
376-
const name = link.replace("pubky://", "").replace(dir, "");
377-
const isDirectory = name.endsWith("/");
378-
return { link, isDirectory, name };
379-
});
389+
const list = listToItems(l, dir);
380390
cachePut(dir, { list, scroll: 0 });
381391
})
382392
.finally(() => prefetchInFlight.delete(key));
@@ -388,23 +398,38 @@ export function cacheSaveScroll(scroll: number = window.scrollY) {
388398
cacheSaveScrollFor(store.dir, scroll);
389399
}
390400

391-
// --- helpers: cache, sort, normalize, revalidate ---
401+
// --- helpers: listing, cache, sort, normalize, revalidate ---
402+
403+
function listDirectory(
404+
path: string,
405+
cursor: string | null,
406+
limit: number,
407+
): Promise<string[]> {
408+
const address = `pubky://${path}`;
409+
return publicStorage.list(address, cursor, false, limit, store.shallow);
410+
}
411+
412+
function listToItems(links: string[], dir: string): ListItem[] {
413+
return links.map((link) => {
414+
let name = link
415+
.replace(/^pubky:\/\//i, "")
416+
.replace(/^pubky(?=[a-z0-9]{52}(?:\/|$))/i, "");
417+
if (name.startsWith(dir)) name = name.slice(dir.length);
418+
const isDirectory = name.endsWith("/");
419+
return { link, isDirectory, name };
420+
});
421+
}
392422

393423
function backgroundRevalidate(path: string) {
394424
const limit = Math.ceil(window.innerHeight / 40);
395425
const reqId = ++currentRequestId;
396426
isFetching = true;
397427
setStore("loading", true);
398428

399-
client
400-
.list(`pubky://${path}`, "", false, limit, store.shallow)
429+
listDirectory(path, null, limit)
401430
.then((l: Array<string>) => {
402431
if (reqId !== currentRequestId) return;
403-
const head = l.map((link) => {
404-
let name = link.replace("pubky://", "").replace(path, "");
405-
let isDirectory = name.endsWith("/");
406-
return { link, isDirectory, name };
407-
});
432+
const head = listToItems(l, path);
408433

409434
// merge head with existing list
410435
const map = new Map<string, ListItem>();
@@ -437,15 +462,14 @@ function sortItems(items: ListItem[]): ListItem[] {
437462
/** Normalize a directory path (always ends with '/'). */
438463
function normalizeDir(raw: string): string {
439464
let path = stripPrefixesAndResolve(raw);
440-
if (path.length === 52) path = path + "/pub/";
465+
if (isPubkeySegment(path)) path = path + "/pub/";
441466
if (!path.endsWith("/")) path = path + "/";
442467
return path;
443468
}
444469

445470
/** Strip pubky:// or pk: and resolve ., .. and any accidental protocol/host. */
446471
function stripPrefixesAndResolve(raw: string): string {
447-
let path = (raw || "").trim();
448-
path = path.replace(/^pubky:\/\/?/i, "").replace(/^pk:/i, "");
472+
let path = stripInputPrefixes(raw);
449473
try {
450474
if (/^[a-z]+:\/\//i.test(path)) {
451475
const u = new URL(path);
@@ -472,6 +496,17 @@ function normalizeError(e: any): string {
472496
return "Network error or PK not found";
473497
return e;
474498
}
499+
const name = e?.name;
500+
const statusCode =
501+
e?.data && typeof e.data === "object" && "statusCode" in e.data
502+
? (e.data as { statusCode?: number }).statusCode
503+
: undefined;
504+
if (name === "RequestError" && typeof statusCode === "number") {
505+
if (statusCode === 404) return "Not found";
506+
if (statusCode === 403) return "Forbidden";
507+
return `Request failed (${statusCode})`;
508+
}
509+
if (name === "InvalidInput") return "Invalid input";
475510
const msg = e.message || "Unknown error";
476511
if (/abort/i.test(msg)) return "Request canceled";
477512
if (/404/.test(msg)) return "Not found";

0 commit comments

Comments
 (0)