A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1import type { BlockNode } from '../blocks/render';
2import { splitAssets, mergeAssets, type AssetSkeleton } from './held-assets';
3import {
4 createIndexedDbAssetStore,
5 createMemoryAssetStore,
6 type AssetStore,
7} from './asset-store';
8
9const DRAFT_KEY = 'skypress:write:draft';
10const INTENT_KEY = 'skypress:write:publish-intent';
11
12/** The editor content the writing-first flow persists across the OAuth redirect. */
13export interface WriteDraft {
14 title: string;
15 lede: string;
16 blocks: BlockNode[];
17 coverDataUrl: string | null;
18}
19
20interface StoredMeta {
21 title: string;
22 lede: string;
23 skeleton: AssetSkeleton;
24}
25
26export interface DraftStore {
27 save( draft: WriteDraft ): Promise< void >;
28 load(): Promise< WriteDraft | null >;
29 clear(): Promise< void >;
30 setPublishIntent(): void;
31 /** Reads the intent flag and clears it — true at most once per set. */
32 consumePublishIntent(): boolean;
33}
34
35/**
36 * Persist the writing-first draft so it survives the full-page OAuth redirect. Light metadata
37 * and the token-skeletoned block tree go in `localStorage`; the heavy image bytes (data: URLs)
38 * go in the injected `AssetStore` (IndexedDB in the browser). `setPublishIntent` records that
39 * the writer hit Publish before the redirect, so the flow auto-resumes on return.
40 */
41export function createDraftStore( opts: { assets?: AssetStore; storage?: Storage } = {} ): DraftStore {
42 const storage = opts.storage ?? window.localStorage;
43 const assets = opts.assets ?? createIndexedDbAssetStore();
44
45 return {
46 async save( draft ) {
47 const { skeleton, assets: bytes } = splitAssets( draft.blocks, draft.coverDataUrl );
48 await assets.clear();
49 await assets.put( bytes );
50 const meta: StoredMeta = { title: draft.title, lede: draft.lede, skeleton };
51 storage.setItem( DRAFT_KEY, JSON.stringify( meta ) );
52 },
53 async load() {
54 const raw = storage.getItem( DRAFT_KEY );
55 if ( ! raw ) {
56 return null;
57 }
58 let meta: StoredMeta;
59 try {
60 meta = JSON.parse( raw ) as StoredMeta;
61 } catch {
62 return null;
63 }
64 const bytes = await assets.getAll();
65 const { blocks, coverDataUrl } = mergeAssets( meta.skeleton, bytes );
66 return { title: meta.title, lede: meta.lede, blocks, coverDataUrl };
67 },
68 async clear() {
69 storage.removeItem( DRAFT_KEY );
70 await assets.clear();
71 },
72 setPublishIntent() {
73 storage.setItem( INTENT_KEY, '1' );
74 },
75 consumePublishIntent() {
76 const had = storage.getItem( INTENT_KEY ) === '1';
77 storage.removeItem( INTENT_KEY );
78 return had;
79 },
80 };
81}
82
83export { createMemoryAssetStore };