A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

at trunk 5.3 kB View raw
1// src/components/WritePublishFlow.tsx 2import { useMemo, useState } from 'react'; 3import type { Agent } from '@atproto/api'; 4import { publish, type Identity } from '../lib/publish/publisher'; 5import type { Publication } from '../lib/publish/publications'; 6import { computePostPreview } from './PublishPanel'; 7import PublicationForm from './PublicationForm'; 8import { uploadHeldAssets } from '../lib/write/upload-held'; 9import type { BlockNode } from '../lib/blocks/render'; 10 11interface Props { 12 agent: Agent; 13 identity: Identity; 14 pdsUrl: string; 15 blocks: BlockNode[]; 16 coverDataUrl: string | null; 17 title: string; 18 description: string; 19 /** `null` while still loading; `[]` when the writer has none yet. */ 20 publications: Publication[] | null; 21 /** Ask the parent to re-list publications (after an inline create). */ 22 onReloadPublications: () => void; 23 onPublished: ( result: { articleUrl: string } ) => void; 24 onCancel: () => void; 25} 26 27type Phase = 'pick' | 'working' | 'error'; 28 29/** 30 * The writing-first publish stepper (design 2026-06-17). Branches on how many publications the 31 * writer owns: zero → inline create; one → confirm; many → pick then confirm. The confirm step 32 * always discloses the public Bluesky post (brief §10) and blocks an over-limit post. On confirm 33 * it uploads the held images/cover, then reuses `publish()` (document + Bluesky post). 34 */ 35export default function WritePublishFlow( { 36 agent, 37 identity, 38 pdsUrl, 39 blocks, 40 coverDataUrl, 41 title, 42 description, 43 publications, 44 onReloadPublications, 45 onPublished, 46 onCancel, 47}: Props ) { 48 const pubs = publications ?? []; 49 const [ targetUri, setTargetUri ] = useState( () => pubs[ 0 ]?.uri ?? '' ); 50 const [ phase, setPhase ] = useState< Phase >( 'pick' ); 51 const [ error, setError ] = useState< string | null >( null ); 52 53 const target = pubs.find( ( p ) => p.uri === targetUri ) ?? pubs[ 0 ]; 54 55 const preview = useMemo( 56 () => 57 target 58 ? computePostPreview( { 59 title, 60 lede: description, 61 blocks, 62 handle: identity.handle ?? identity.did, 63 slug: target.slug, 64 } ) 65 : null, 66 [ target, title, description, blocks, identity ] 67 ); 68 69 if ( publications === null ) { 70 return ( 71 <section className="writeflow" aria-label="Publish"> 72 <p className="writeflow__status">Loading your publications</p> 73 </section> 74 ); 75 } 76 77 // Zero publications → inline "create your first publication". 78 if ( pubs.length === 0 ) { 79 return ( 80 <section className="writeflow" aria-label="Create your first publication"> 81 <p className="writeflow__lede"> 82 You don't have a publication yet — create one to publish into. 83 </p> 84 <PublicationForm 85 agent={ agent } 86 did={ identity.did } 87 pdsUrl={ pdsUrl } 88 handle={ identity.handle ?? identity.did } 89 onSaved={ ( pub ) => { 90 setTargetUri( pub.uri ); 91 onReloadPublications(); 92 } } 93 onCancel={ onCancel } 94 /> 95 </section> 96 ); 97 } 98 99 async function run() { 100 if ( ! target ) { 101 return; 102 } 103 setPhase( 'working' ); 104 setError( null ); 105 try { 106 const prepared = await uploadHeldAssets( agent, { 107 blocks, 108 coverDataUrl, 109 did: identity.did, 110 pdsUrl, 111 } ); 112 const res = await publish( agent, identity, { 113 title: title.trim(), 114 description, 115 blocks: prepared.blocks, 116 publicationUri: target.uri, 117 publicationCid: target.cid, 118 publicationSlug: target.slug, 119 coverImage: prepared.coverImage, 120 } ); 121 onPublished( { articleUrl: res.articleUrl } ); 122 } catch ( err ) { 123 setError( err instanceof Error ? err.message : String( err ) ); 124 setPhase( 'error' ); 125 } 126 } 127 128 const overLimit = Boolean( preview?.overLimit ); 129 130 return ( 131 <section className="writeflow" aria-label="Publish"> 132 { pubs.length > 1 && ( 133 <label className="writeflow__target"> 134 <span>Publish to</span> 135 <select 136 value={ target?.uri ?? '' } 137 onChange={ ( e ) => setTargetUri( e.target.value ) } 138 disabled={ phase === 'working' } 139 > 140 { pubs.map( ( p ) => ( 141 <option key={ p.uri } value={ p.uri }> 142 { p.name } 143 </option> 144 ) ) } 145 </select> 146 </label> 147 ) } 148 149 <p className="writeflow__warning"> 150 Publishing saves this article to <strong>your PDS</strong> and also creates a{ ' ' } 151 <strong>public Bluesky post</strong> linking to it 152 { target ? <> in <strong>{ target.name }</strong></> : null }. Everyone following you 153 will see it. 154 { preview && preview.handles.length > 0 && ( 155 <> It will notify <strong>{ preview.handles.join( ', ' ) }</strong>.</> 156 ) } 157 </p> 158 159 { overLimit && ( 160 <p className="writeflow__count" aria-live="polite"> 161 Bluesky post: { preview!.graphemes }/300 — too long to publish; shorten the 162 subtitle or remove a mention. 163 </p> 164 ) } 165 166 { phase === 'working' ? ( 167 <p className="writeflow__status">Publishing…</p> 168 ) : ( 169 <div className="writeflow__actions"> 170 <button 171 type="button" 172 className="writeflow__publish" 173 disabled={ overLimit || ! target } 174 onClick={ () => void run() } 175 > 176 Publish &amp; post to Bluesky 177 </button> 178 </div> 179 ) } 180 181 { phase === 'error' && error && ( 182 <p className="writeflow__error" role="alert"> 183 Publish failed: { error } 184 </p> 185 ) } 186 </section> 187 ); 188}