
beyond-text-3
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Viewing readonly version of add-image-upload branch: v14View latest version
Lets users take a selfie (or pick any image) on mobile and drop it into the
chat. Designed to merge cleanly alongside concurrent work on main.
To avoid stepping on another agent working on main, nearly all of the code
lives in new files. Edits to existing files are kept intentionally tiny
and non-overlapping with the message-rendering path.
api/
images.ts ← Hono sub-app: POST /api/images, GET /api/images/:id
database/
images.ts ← chat_images_v1 table + blob storage helpers
shared/
image-types.ts ← ChatImage type, upload limits (separate from types.ts)
frontend/
components/CameraButton.tsx ← Mobile camera button (icon + label)
hooks/useImageUpload.ts ← Capture → compress → upload flow
index.ts— +3 lines: importimagesRoutes, mount withapp.route("/", imagesRoutes).frontend/components/App.tsx— +1 import, +6 JSX lines: render<CameraButton />above<MessageInput />. Does not touchMessageList,MessageInput,useMessages,db.ts,types.ts, orindex.html.
Rendering mermaid diagram...
- Capture — a hidden
<input type="file" accept="image/*" capture="user">element opens the front-facing camera on iOS and Android. - Compress in-browser — images are downscaled to max 1600px and re-encoded as JPEG at quality 0.82 before upload. Keeps uploads fast on cellular.
- Upload —
POST /api/imageswith a multipart form. Server validates MIME type, stores bytes instd/blobunderchat-images/v1/{id}, stores metadata in a newchat_images_v1SQLite table. - Post to chat — the server then calls the existing
insertMessage()to append a text message of the form📷 /api/images/{id}. This means the image is delivered to every other client through the existing SSE stream — zero changes to the message pipeline. - View —
GET /api/images/:idstreams the bytes back with animmutablecache header.
capture="user"→ front camera on iPhone/Android.accept="image/*"→ also allows picking from the gallery if the user prefers.font-size: 16pxon any input (inherited from the app) avoids iOS zoom-on-focus.- Button is large (
min-h-10, rounded pill) for easy thumb-tapping. - EXIF orientation handled via
createImageBitmap({ imageOrientation: 'from-image' }). - Error state shows as a small inline toast above the button so it doesn't shift the layout.
Currently, images show up in the chat as a text line 📷 /api/images/123. To
render them as real <img> tags, MessageList.tsx could be taught to detect
this prefix and render accordingly. That change is intentionally not made
here so this branch stays orthogonal to any concurrent work on MessageList.
A one-liner enhancement once merged:
// In MessageList.tsx, inside the message map:
const imageMatch = msg.text.match(/^📷\s+(\/api\/images\/\d+)/);
if (imageMatch) {
return <img src={imageMatch[1]} class="max-w-xs rounded-lg" />;
}
- Bytes:
std/blobatchat-images/v1/{id} - Metadata: SQLite table
chat_images_v1(brand new — no migration of existing tables) - Limits: 8 MB max upload (enforced on both client post-compression and
server). MIME allowlist: jpeg, png, webp, heic, heif.
application/octet-streamfrom iOS camera is normalized toimage/jpeg.