~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
263 lines · 8.1 KB Raw History
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>
  );
}