Files
eranos/.agents/skills/file-uploads/SKILL.md
T
Alex Gleason bd68a32708 Split AGENTS.md into skills; compress to 358 lines
Extract eleven topic areas into loadable skills so AGENTS.md can serve
as a scannable overview instead of a specification dump. The file
shrinks from 1480 to 358 lines (~76%) while keeping every concrete
rule, critical code pattern, and pointer that an agent needs on first
read.

New Ditto-specific skills:
- nostr-kinds: NIP-vs-custom-kind decision framework, kind ranges,
  tag design, content-vs-tags, NIP.md update rule, and Ditto's
  seven-location UI registration checklist for new kinds (NoteCard,
  PostDetailPage, extraKinds.ts, KIND_LABELS/KIND_ICONS in
  CommentContext, WELL_KNOWN_KIND_LABELS in ExternalContentHeader,
  EmbeddedNote/EmbeddedNaddr, ReplyComposeModal).
- nostr-publishing: useNostrPublish, the read-modify-write pattern
  via fetchFreshEvent + prev for replaceable/addressable events,
  published_at contract, and d-tag collision prevention.
- nostr-queries: the standard useNostr + useQuery pattern,
  combining kinds into one filter to avoid rate limits, and the
  NIP-52 validator walkthrough.
- theming: @fontsource install flow, the Ditto runtime font-loader
  path (sanitizeUrl + sanitizeCssString), color scheme variables,
  useTheme toggle, and the isolate + negative-z-index gotcha.
- ci-cd-publishing: Zapstore NIP-46 bunker auth (zsp +
  nip46-auth.mjs), nsite deploys (nsyte nbunksec + configured
  relays/servers), and Google Play AAB uploads via fastlane supply
  (service-account JSON base64 encoding and rotation).
- capacitor-compat: WKWebView/WebView limitations, the
  downloadTextFile / openUrl helpers in src/lib/downloadFile.ts,
  platform detection, and the full plugin list.
- git-workflow: pre-commit validation order and the Regression-of:
  trailer convention used by the release skill's changelog
  generator.

Ported from mkstack, lightly adapted where needed:
- nip19-routing: root-level /:nip19 routing and filter construction
  patterns (adapted to reference Ditto's existing NIP19Page).
- nostr-relay-pools: nostr.relay() and nostr.group() for targeted
  queries.
- nostr-encryption: NIP-44 / NIP-04 via the user's signer.
- file-uploads: useUploadFile + Blossom + NIP-94 imeta tag
  construction.

AGENTS.md itself now follows mkstack's density — concrete rules inline,
one code example per section, pointer to the matching skill for details.
The enumerations that previously bloated it (every shadcn primitive,
every hook, every Capacitor plugin, the full NostrMetadata type dump,
the NIP-19 prefix reference table, etc.) are either removed in favor
of "ls the directory" or moved into their skill.
2026-04-26 23:13:30 -05:00

3.1 KiB

name, description
name description
file-uploads Upload files (images, media, attachments) from the browser to a Blossom server via the useUploadFile hook, and attach them to Nostr events with NIP-94 imeta tags.

File Uploads on Nostr

This project includes a useUploadFile hook that uploads files to Blossom servers and returns NIP-94-compatible tags. Use it whenever a feature needs to accept a user-provided file (avatars, banners, post attachments, etc.).

The useUploadFile Hook

import { useUploadFile } from "@/hooks/useUploadFile";

function MyComponent() {
  const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();

  const handleUpload = async (file: File) => {
    try {
      // Returns an array of NIP-94-compatible tags.
      // The first tag is the `url` tag; its second element is the file URL.
      const tags = await uploadFile(file);
      const url = tags[0][1];
      // ...use the url
    } catch (error) {
      // ...handle errors (show a toast, etc.)
    }
  };

  // ...rest of component
}

The hook is a TanStack Query mutation, so isPending can drive loading UI and mutateAsync integrates cleanly with async/await flows.

Attaching Files to Events

Kind 0 (profile metadata)

Use the plain URL in the relevant JSON field:

const tags = await uploadFile(file);
const url = tags[0][1];

createEvent({
  kind: 0,
  content: JSON.stringify({ ...existingMetadata, picture: url }),
});

Kind 1 (text notes) and other content events

Append the URL to content, and add one imeta tag per file. imeta carries the NIP-94 metadata (mime type, dimensions, blurhash, etc.) that the uploader returned:

const tags = await uploadFile(file); // e.g. [["url", "https://..."], ["m", "image/png"], ["dim", "1024x768"], ...]
const url = tags[0][1];

// Flatten the NIP-94 tags into a single imeta tag value.
const imeta = tags.map(([name, value]) => `${name} ${value}`);

createEvent({
  kind: 1,
  content: `Check this out ${url}`,
  tags: [["imeta", ...imeta]],
});

Repeat the pattern (one imeta tag per file) for multiple attachments.

Common Patterns

  • Avatar / banner pickers: wrap an <input type="file" accept="image/*"> and call uploadFile on change; on success, update the relevant profile field and publish a kind 0 event.
  • Post composers: call uploadFile for each selected file before publishing the note, then build imeta tags alongside content.
  • Progress UI: use isPending from the mutation to disable the submit button and show a spinner or skeleton.
  • Error handling: wrap uploadFile in try/catch and surface failures via useToast — network and Blossom-server errors are common and should never break the UI.

Constraints

  • The hook requires a logged-in user (Blossom auth is signed by the user's signer). Guard uploads behind useCurrentUser.
  • Don't store or display raw File objects after upload — always use the returned URL.
  • Large files may take time; prefer mutateAsync over mutate so the caller can await completion before publishing an event that references the URL.