chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { Link } from '#/components/atoms/link.jsx';
import { dateTime } from '#/lib/format.js';
import { parseAtUri } from '#/lib/record-link.js';
import { cn } from '#/lib/utils.js';
// Structural bookkeeping a person never needs to read: type tags, content
// hashes, and the byte-offset ranges that map rich text back onto its string.
const HIDDEN = new Set(['$type', 'cid', 'facets']);
const LABELS = {
createdAt: 'Created',
indexedAt: 'Indexed',
updatedAt: 'Updated',
displayName: 'Display name',
avatar: 'Avatar',
banner: 'Banner',
uri: 'Link',
};
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T[\d:.]/;
function label(key) {
if (LABELS[key]) return LABELS[key];
const spaced = key
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/[._]/g, ' ');
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
}
function humanBytes(bytes) {
if (!Number.isFinite(bytes)) return null;
const units = ['B', 'KB', 'MB', 'GB'];
let value = bytes;
let unit = 0;
while (value >= 1024 && unit < units.length - 1) {
value /= 1024;
unit += 1;
}
const rounded =
value >= 10 || unit === 0 ? Math.round(value) : value.toFixed(1);
return `${rounded} ${units[unit]}`;
}
function isEmpty(value) {
if (value === null || value === undefined || value === '') return true;
if (Array.isArray(value)) return value.length === 0;
if (typeof value === 'object') return fields(value).length === 0;
return false;
}
function isBlob(value) {
return Boolean(value) && typeof value === 'object' && value.$type === 'blob';
}
function fields(object) {
return Object.entries(object).filter(
([key, value]) => !HIDDEN.has(key) && !isEmpty(value),
);
}
/**
* A record rendered as plain content rather than a code dump: text as prose,
* dates in words, image blobs as thumbnails, everything else as labeled rows.
* Structural plumbing is dropped; the raw record stays available alongside it.
*/
export function HumanValue({
value,
blobBase,
repoDid,
recordHref,
onNavigate,
top,
}) {
const opts = { blobBase, repoDid, recordHref, onNavigate };
if (value === null || value === undefined) return null;
if (typeof value === 'string') {
if (ISO_DATE.test(value))
return <span className="text-foreground">{dateTime(value)}</span>;
if (value.startsWith('at://')) {
const target = parseAtUri(value);
// Only a record in this same repo can be opened here; another account's
// records live on their own server, so those stay as plain text.
if (recordHref && target && target.did === repoDid)
return (
<Link
href={recordHref(target.collection, target.rkey)}
onNavigate={onNavigate}
className="break-all font-mono text-[12px] text-key underline-offset-2 hover:text-foreground hover:underline"
>
{value}
</Link>
);
return (
<span className="break-all font-mono text-[12px] text-muted-foreground">
{value}
</span>
);
}
if (value.startsWith('did:'))
return (
<span className="break-all font-mono text-[12px] text-muted-foreground">
{value}
</span>
);
return (
<span className="whitespace-pre-wrap break-words text-foreground">
{value}
</span>
);
}
if (typeof value === 'number')
return <span className="text-foreground">{value}</span>;
if (typeof value === 'boolean')
return <span className="text-foreground">{value ? 'Yes' : 'No'}</span>;
if (isBlob(value)) {
const link = value.ref?.$link;
if (
blobBase &&
typeof link === 'string' &&
value.mimeType?.startsWith('image/')
)
return (
<img
src={`${blobBase}&cid=${encodeURIComponent(link)}`}
alt=""
className="my-1 max-h-48 max-w-full rounded-lg border border-border"
/>
);
const size = humanBytes(value.size);
return (
<span className="text-muted-foreground">
{value.mimeType || 'File'}
{size ? ` · ${size}` : ''}
</span>
);
}
if (Array.isArray(value)) {
const items = value.filter((item) => !isEmpty(item));
if (!items.length) return null;
// A list of plain values reads as one comma-separated line; a list of
// objects stacks so each keeps its own labeled rows.
const scalar = items.every(
(item) => item === null || typeof item !== 'object',
);
if (scalar)
return (
<span className="text-foreground">
{items.map((item, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: records have no ids
<span key={index}>
{index > 0 ? ', ' : ''}
<HumanValue value={item} {...opts} />
</span>
))}
</span>
);
return (
<div className="space-y-2">
{items.map((item, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: records have no ids
<div key={index}>
<HumanValue value={item} {...opts} />
</div>
))}
</div>
);
}
const entries = fields(value);
if (!entries.length) return null;
return (
<div className={cn(top ? 'space-y-4' : 'space-y-3')}>
{entries.map(([key, inner]) => {
// Lead content — a post's text and the like — reads as the body: the
// same quiet caption as every field, but set larger.
if (top && key === 'text' && typeof inner === 'string')
return (
<Row key={key} label={label(key)}>
<p className="whitespace-pre-wrap break-words text-[15px] text-foreground leading-relaxed">
{inner}
</p>
</Row>
);
const object =
inner && typeof inner === 'object' && !Array.isArray(inner);
// Collapse a wrapper that holds a single value once its plumbing is
// hidden (a reply's `root` reduces to just its link, and so on).
if (object && !isBlob(inner)) {
const inside = fields(inner);
const flatten =
inside.length === 1 &&
!(
inside[0][1] &&
typeof inside[0][1] === 'object' &&
!isBlob(inside[0][1])
);
if (flatten)
return (
<Row key={key} label={label(key)}>
<HumanValue value={inside[0][1]} {...opts} />
</Row>
);
return (
<Group key={key} label={label(key)}>
<HumanValue value={inner} {...opts} />
</Group>
);
}
// A list of plain values (langs, tags) belongs on one line under its
// label; only a list of objects earns the indented grouping.
if (Array.isArray(inner)) {
const scalar = inner.every(
(item) => item === null || typeof item !== 'object',
);
return scalar ? (
<Row key={key} label={label(key)}>
<HumanValue value={inner} {...opts} />
</Row>
) : (
<Group key={key} label={label(key)}>
<HumanValue value={inner} {...opts} />
</Group>
);
}
return (
<Row key={key} label={label(key)}>
<HumanValue value={inner} {...opts} />
</Row>
);
})}
</div>
);
}
// A field as a caption stacked over its value: no fixed label column, so short
// values sit right under their name instead of stranding a gap across the row.
function Row({ label: text, children }) {
return (
<div className="flex flex-col gap-1">
<div className="text-[12px] text-faint">{text}</div>
<div className="min-w-0 break-words text-[14px] text-foreground">
{children}
</div>
</div>
);
}
// A caption over a bordered block, for the rare nested object or list of
// objects where the indent shows what belongs together.
function Group({ label: text, children }) {
return (
<div className="flex flex-col gap-1.5">
<div className="text-[12px] text-faint">{text}</div>
<div className="border-border border-l pl-3.5">{children}</div>
</div>
);
}