What it actually took to add gallery upload: four decisions in one line of code
Adding gallery upload to DoodleTrove sounded like deleting one HTML attribute. It was four decisions about not quietly corrupting years of archive.
- nextjs
- typescript
- side-projects
- product
DoodleTrove's capture feature got built first. The camera flow is good: open the app, hit the big square, the rear camera is live, then photograph the drawing in front of you. However, "photograph it right now" was never meant to be the only way in. There's a whole camera roll of older drawings, scans, photos other people send.
So I went to add the obvious missing piece: upload from the gallery.
It sounds like deleting one HTML attribute. The big square carries
capture="environment", which tells the browser skip the picker, open the
camera. Remove it and the OS shows its normal picker โ camera, gallery, and files. One line. Ship it.
It wasn't. That one sentence hid four decisions, and three of them were about not quietly corrupting the archive I'm keeping for years.
1. Add a path, don't replace one.
The temptation is to delete capture and let the OS picker mediate everything.
That's one line, but it taxes the path that already works. Every upload,
including the camera, now stops on an OS sheet to pick its source, mid-gesture,
on a small target. The camera path is the time-sensitive one (you're standing
over a drawing); the gallery path is the calmer one (you're sitting down with the
phone). They aren't the same job, so they don't share one input.
Two buttons: the big square stays the camera, a smaller "Choose from gallery" sits below it. The camera flow keeps its speed.
2. The date that wasn't there
The form reads the date from the photo's EXIF. Camera photos always have it. Gallery photos (a forwarded image, a scan, and a screenshot) often don't.
The old code silently fell back to today.
That's the bug. A drawing your partner photographed six months ago, texted to you, uploaded today, gets logged as made today. Forever. In an archive I intend to keep for decades, a silent wrong date is worse than no date.
const taken = exif?.DateTimeOriginal ?? exif?.CreateDate;
if (taken instanceof Date && !Number.isNaN(taken.getTime())) {
setDateMade(toLocalDate(taken));
return;
}
// No EXIF. Don't default to today โ make the user confirm.
setDateMade("");
setNeedsConfirming(true);3. The orphan
The save is two steps: create the artwork record, then upload the photo. If step
two fails, step one leaves a record with no photo โ an empty grey square in the
gallery, because every tile assumes photos[0] exists.
Gallery uploads raise the odds of that failure: HEIC, forwarded files, big images. The lazy fix is to live with it. The right fix is to reorder: compress and decode-validate the image before creating the record, so an undecodable photo fails before anything is saved. The rare case where the network drops after create rolls the record back. The gallery never sees a photoless tile.
4. HEIC, or: don't optimize for a browser you don't use
iPhones shoot HEIC. The compression library draws to canvas, so whether HEIC decodes is the browser's call. On the installed iOS PWA โ the only place this app runs โ it almost certainly works. So I didn't build a server-side fallback.
I shipped the clean failure ("couldn't process this photo, try a JPEG") along
with a note to self: try a real HEIC upload on the phone, and only reach for
sharp if it actually breaks. Don't pre-optimize for a failure that probably
doesn't happen for your users.
The feature is the decisions
"Add gallery upload" is one sentence. It contained four decisions, and three of them โ the date, the orphan, the HEIC failure โ were about not quietly lying to a future version of oneself about their own kids' artworks.
That's the same lesson as the first time around. The bugs that get you aren't the ones that crash. They're the ones that look plausible. A photo dated today. A grey square you learn to ignore. A wrong record nobody checks.
capture really is one attribute. The feature isn't the code. The feature is the
decisions, and the decisions are where the integrity lives.
The drawings are safe. Now so are the ones that were already on the camera roll.