import { download } from './imageOps'; // Posting the export to a social site. A page cannot reach into another // origin's composer: the cookie is the browser's to send, never ours to read, // and the window we open has no DOM we may touch — which is the same rule that // keeps any other site from posting as the visitor. So there is no way to drop // the picture into the site's own picker from here. Two things do work, and // this takes the best one the browser offers: // // sheet on a phone the OS share sheet opens and the target app — // Facebook, Instagram, Reddit — comes up with the picture already // in its composer. Only the caption is left to type. // clipboard the desktop path: the picture goes on the clipboard and the site // opens beside it, so one paste (Ctrl+V) puts it in the composer. // download no share sheet and no clipboard — an http origin is not a secure // context, so the API is not even there: the file is handed over the // old way and the site opens beside it. // // ponytail: the four sites are listed here rather than in a config file. Add a // network when someone asks for one; these three paths serve any site alike. export const SHARE_SITES = { facebook: 'https://www.facebook.com/', instagram: 'https://www.instagram.com/', reddit: 'https://www.reddit.com/submit', x: 'https://x.com/compose/post', } as const; export type ShareSite = keyof typeof SHARE_SITES; export type SharePath = 'sheet' | 'clipboard' | 'download'; // The clipboard takes PNG in every engine — Chromium silently refuses a JPEG — // so the file is drawn to a canvas once. No resample: the bytes handed in are // already the size the visitor chose. async function pngOf(file: File): Promise { const bitmap = await createImageBitmap(file); const canvas = document.createElement('canvas'); canvas.width = bitmap.width; canvas.height = bitmap.height; const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('no 2d context'); ctx.drawImage(bitmap, 0, 0); bitmap.close(); return await new Promise((resolve, reject) => canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png') ); } export async function sharePhoto(bytes: Uint8Array, filename: string, site: ShareSite): Promise { const file = new File([bytes as BlobPart], filename, { type: 'image/jpeg' }); // A phone is the only place the picture reaches the target app on its own: // the sheet hands the file over and that app owns its own composer. if (navigator.canShare?.({ files: [file] })) { try { await navigator.share({ files: [file] }); } catch (err) { // The sheet was closed: a "not now", not a failure, and nothing else opens // on top of the choice the visitor just made. if (!(err instanceof DOMException && err.name === 'AbortError')) throw err; } return 'sheet'; } try { await navigator.clipboard.write([new ClipboardItem({ 'image/png': await pngOf(file) })]); window.open(SHARE_SITES[site], '_blank', 'noopener'); return 'clipboard'; } catch { window.open(SHARE_SITES[site], '_blank', 'noopener'); download(bytes, filename); return 'download'; } }