~james/core

core/web/src/lib/avatar.ts -rw-r--r-- 2.1 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import { browser } from "$app/env";
import { SvelteMap } from "svelte/reactivity";
import { avatarWidths } from "#lib/avatarWidths.ts";

const VERSIONS_KEY = "tangled.avatarVersions";
const VERSION_TTL_MS = 12 * 60 * 60 * 1000;

const versions = new SvelteMap<string, string>();

type StoredVersions = Record<string, { version: string; at: number }>;

const readStored = (): StoredVersions => {
	try {
		const parsed: unknown = JSON.parse(localStorage.getItem(VERSIONS_KEY) ?? "{}");
		return parsed && typeof parsed === "object" ? (parsed as StoredVersions) : {};
	} catch {
		return {};
	}
};

const writeStored = (stored: StoredVersions): boolean => {
	try {
		localStorage.setItem(VERSIONS_KEY, JSON.stringify(stored));
		return true;
	} catch {
		return false;
	}
};

export const restoreAvatarVersions = (): void => {
	if (!browser) return;
	const now = Date.now();
	for (const [did, entry] of Object.entries(readStored())) {
		if (typeof entry?.version === "string" && now - entry.at < VERSION_TTL_MS) {
			versions.set(did, entry.version);
		}
	}
};

export const bumpAvatar = (did: string, version: string = String(Date.now())): void => {
	versions.set(did, version);
	if (!browser) return;
	const now = Date.now();
	const kept = Object.fromEntries(
		Object.entries(readStored()).filter(([, entry]) => now - entry.at < VERSION_TTL_MS)
	);
	writeStored({ ...kept, [did]: { version, at: now } });
};

export const avatarWidthFor = (px: number): number =>
	avatarWidths.find((width) => width >= px * 2) ?? avatarWidths[avatarWidths.length - 1];

// the service wants signed urls and the secret is server side, so this points
// at the route that signs
export const avatarUrl = (
	did: string,
	width?: number,
	format?: "webp" | "jpeg" | "png",
	cid?: string
): string => {
	// cids all start the same way. the end tells them apart
	const version = versions.get(did) ?? cid?.slice(-12);
	const query = new URLSearchParams({
		...(width ? { size: String(width) } : {}),
		...(format ? { format } : {}),
		...(version ? { v: version } : {})
	}).toString();
	return `/avatar/${encodeURIComponent(did)}${query ? `?${query}` : ""}`;
};