a tool for shared writing and social publishing

add lists to pubs!

+372 -143
+99 -68
actions/publishToPublication.ts
··· 7 7 import { 8 8 AtpBaseClient, 9 9 PubLeafletBlocksHeader, 10 + PubLeafletBlocksImage, 10 11 PubLeafletBlocksText, 12 + PubLeafletBlocksUnorderedList, 11 13 PubLeafletDocument, 12 14 PubLeafletPagesLinearDocument, 13 15 PubLeafletRichtextFacet, ··· 23 25 YJSFragmentToString, 24 26 } from "components/Blocks/TextBlock/RenderYJSFragment"; 25 27 import { ids } from "lexicons/api/lexicons"; 26 - import { OmitKey } from "lexicons/api/util"; 27 28 import { BlobRef } from "@atproto/lexicon"; 28 29 import { IdResolver } from "@atproto/identity"; 29 30 import { AtUri } from "@atproto/syntax"; 30 31 import { Json } from "supabase/database.types"; 31 - import { UnicodeString } from "@atproto/api"; 32 + import { $Typed, UnicodeString } from "@atproto/api"; 33 + import { List, parseBlocksToList } from "src/utils/parseBlocksToList"; 32 34 33 35 const idResolver = new IdResolver(); 34 36 export async function publishToPublication({ ··· 138 140 blocks: Block[], 139 141 imageMap: Map<string, BlobRef>, 140 142 scan: ReturnType<typeof scanIndexLocal>, 143 + ): PubLeafletPagesLinearDocument.Block[] { 144 + let parsedBlocks = parseBlocksToList(blocks); 145 + return parsedBlocks.flatMap((blockOrList) => { 146 + if (blockOrList.type === "block") { 147 + let alignmentValue = 148 + scan.eav(blockOrList.block.value, "block/text-alignment")[0]?.data 149 + .value || "left"; 150 + let alignment = 151 + alignmentValue === "center" 152 + ? "lex:pub.leaflet.pages.linearDocument#textAlignCenter" 153 + : alignmentValue === "right" 154 + ? "lex:pub.leaflet.pages.linearDocument#textAlignRight" 155 + : undefined; 156 + let b = blockToRecord(blockOrList.block, imageMap, scan); 157 + if (!b) return []; 158 + let block: PubLeafletPagesLinearDocument.Block = { 159 + $type: "pub.leaflet.pages.linearDocument#block", 160 + alignment, 161 + block: b, 162 + }; 163 + return [block]; 164 + } else { 165 + let block: PubLeafletPagesLinearDocument.Block = { 166 + $type: "pub.leaflet.pages.linearDocument#block", 167 + block: { 168 + $type: "pub.leaflet.blocks.unorderedList", 169 + children: childrenToRecord(blockOrList.children, imageMap, scan), 170 + }, 171 + }; 172 + return [block]; 173 + } 174 + }); 175 + } 176 + 177 + function childrenToRecord( 178 + children: List[], 179 + imageMap: Map<string, BlobRef>, 180 + scan: ReturnType<typeof scanIndexLocal>, 181 + ) { 182 + return children.flatMap((child) => { 183 + let content = blockToRecord(child.block, imageMap, scan); 184 + if (!content) return []; 185 + let record: PubLeafletBlocksUnorderedList.ListItem = { 186 + $type: "pub.leaflet.blocks.unorderedList#listItem", 187 + content, 188 + children: childrenToRecord(child.children, imageMap, scan), 189 + }; 190 + return record; 191 + }); 192 + } 193 + function blockToRecord( 194 + b: Block, 195 + imageMap: Map<string, BlobRef>, 196 + scan: ReturnType<typeof scanIndexLocal>, 141 197 ) { 142 198 const getBlockContent = (b: string) => { 143 199 let [content] = scan.eav(b, "block/text"); 144 - if (!content) return ""; 200 + if (!content) return ["", [] as PubLeafletRichtextFacet.Main[]] as const; 145 201 let doc = new Y.Doc(); 146 202 const update = base64.toByteArray(content.data.value); 147 203 Y.applyUpdate(doc, update); 148 204 let nodes = doc.getXmlElement("prosemirror").toArray(); 149 205 let stringValue = YJSFragmentToString(nodes[0]); 150 206 let facets = YJSFragmentToFacets(nodes[0]); 151 - return [stringValue, facets]; 207 + return [stringValue, facets] as const; 152 208 }; 153 - return blocks.flatMap((b) => { 154 - if (b.type !== "text" && b.type !== "heading" && b.type !== "image") 155 - return []; 156 - let alignmentValue = 157 - scan.eav(b.value, "block/text-alignment")[0]?.data.value || "left"; 209 + if (b.type !== "text" && b.type !== "heading" && b.type !== "image") return; 210 + let alignmentValue = 211 + scan.eav(b.value, "block/text-alignment")[0]?.data.value || "left"; 158 212 159 - let alignment = 160 - alignmentValue === "center" 161 - ? "lex:pub.leaflet.pages.linearDocument#textAlignCenter" 162 - : alignmentValue === "right" 163 - ? "lex:pub.leaflet.pages.linearDocument#textAlignRight" 164 - : undefined; 213 + if (b.type === "heading") { 214 + let [headingLevel] = scan.eav(b.value, "block/heading-level"); 165 215 166 - if (b.type === "heading") { 167 - let [headingLevel] = scan.eav(b.value, "block/heading-level"); 216 + let [stringValue, facets] = getBlockContent(b.value); 217 + let block: $Typed<PubLeafletBlocksHeader.Main> = { 218 + $type: "pub.leaflet.blocks.header", 219 + level: headingLevel?.data.value || 1, 220 + plaintext: stringValue, 221 + facets, 222 + }; 223 + return block; 224 + } 168 225 169 - let [stringValue, facets] = getBlockContent(b.value); 170 - return [ 171 - { 172 - $type: "pub.leaflet.pages.linearDocument#block", 173 - alignment, 174 - block: { 175 - $type: "pub.leaflet.blocks.header", 176 - level: headingLevel?.data.value || 1, 177 - plaintext: stringValue, 178 - facets, 179 - }, 180 - } as PubLeafletPagesLinearDocument.Block, 181 - ]; 182 - } 183 - 184 - if (b.type == "text") { 185 - let [stringValue, facets] = getBlockContent(b.value); 186 - return [ 187 - { 188 - $type: "pub.leaflet.pages.linearDocument#block", 189 - alignment, 190 - block: { 191 - $type: ids.PubLeafletBlocksText, 192 - plaintext: stringValue, 193 - facets, 194 - }, 195 - } as PubLeafletPagesLinearDocument.Block, 196 - ]; 197 - } 198 - if (b.type == "image") { 199 - let [image] = scan.eav(b.value, "block/image"); 200 - if (!image) return []; 201 - let blobref = imageMap.get(image.data.src); 202 - if (!blobref) return []; 203 - return [ 204 - { 205 - $type: "pub.leaflet.pages.linearDocument#block", 206 - alignment, 207 - block: { 208 - $type: "pub.leaflet.blocks.image", 209 - image: blobref, 210 - aspectRatio: { 211 - height: image.data.height, 212 - width: image.data.width, 213 - }, 214 - }, 215 - } as PubLeafletPagesLinearDocument.Block, 216 - ]; 217 - } 218 - return []; 219 - }); 226 + if (b.type == "text") { 227 + let [stringValue, facets] = getBlockContent(b.value); 228 + let block: $Typed<PubLeafletBlocksText.Main> = { 229 + $type: ids.PubLeafletBlocksText, 230 + plaintext: stringValue, 231 + facets, 232 + }; 233 + return block; 234 + } 235 + if (b.type == "image") { 236 + let [image] = scan.eav(b.value, "block/image"); 237 + if (!image) return; 238 + let blobref = imageMap.get(image.data.src); 239 + if (!blobref) return; 240 + let block: $Typed<PubLeafletBlocksImage.Main> = { 241 + $type: "pub.leaflet.blocks.image", 242 + image: blobref, 243 + aspectRatio: { 244 + height: image.data.height, 245 + width: image.data.width, 246 + }, 247 + }; 248 + return block; 249 + } 250 + return; 220 251 } 221 252 222 253 async function sendPostToEmailSubscribers(
+28
app/lish/[did]/[publication]/[rkey]/page.tsx
··· 6 6 PubLeafletBlocksHeader, 7 7 PubLeafletBlocksImage, 8 8 PubLeafletBlocksText, 9 + PubLeafletBlocksUnorderedList, 9 10 PubLeafletDocument, 10 11 PubLeafletPagesLinearDocument, 11 12 } from "lexicons/api"; ··· 109 110 let className = `${b.alignment === "lex:pub.leaflet.pages.linearDocument#textAlignRight" ? "text-right" : b.alignment === "lex:pub.leaflet.pages.linearDocument#textAlignCenter" ? "text-center" : ""}`; 110 111 console.log(b.alignment); 111 112 switch (true) { 113 + case PubLeafletBlocksUnorderedList.isMain(b.block): { 114 + return ( 115 + <ul> 116 + {b.block.children.map((child, index) => ( 117 + <ListItem item={child} did={did} key={index} /> 118 + ))} 119 + </ul> 120 + ); 121 + } 112 122 case PubLeafletBlocksImage.isMain(b.block): { 113 123 return ( 114 124 <img ··· 156 166 return null; 157 167 } 158 168 }; 169 + 170 + function ListItem(props: { 171 + item: PubLeafletBlocksUnorderedList.ListItem; 172 + did: string; 173 + }) { 174 + return ( 175 + <li> 176 + <Block block={{ block: props.item.content }} did={props.did} /> 177 + {props.item.children?.length ? ( 178 + <ul> 179 + {props.item.children.map((child, index) => ( 180 + <ListItem item={child} did={props.did} key={index} /> 181 + ))} 182 + </ul> 183 + ) : null} 184 + </li> 185 + ); 186 + }
+2
lexicons/api/index.ts
··· 10 10 import * as PubLeafletBlocksHeader from './types/pub/leaflet/blocks/header' 11 11 import * as PubLeafletBlocksImage from './types/pub/leaflet/blocks/image' 12 12 import * as PubLeafletBlocksText from './types/pub/leaflet/blocks/text' 13 + import * as PubLeafletBlocksUnorderedList from './types/pub/leaflet/blocks/unorderedList' 13 14 import * as PubLeafletPagesLinearDocument from './types/pub/leaflet/pages/linearDocument' 14 15 import * as PubLeafletRichtextFacet from './types/pub/leaflet/richtext/facet' 15 16 import * as ComAtprotoLabelDefs from './types/com/atproto/label/defs' ··· 31 32 export * as PubLeafletBlocksHeader from './types/pub/leaflet/blocks/header' 32 33 export * as PubLeafletBlocksImage from './types/pub/leaflet/blocks/image' 33 34 export * as PubLeafletBlocksText from './types/pub/leaflet/blocks/text' 35 + export * as PubLeafletBlocksUnorderedList from './types/pub/leaflet/blocks/unorderedList' 34 36 export * as PubLeafletPagesLinearDocument from './types/pub/leaflet/pages/linearDocument' 35 37 export * as PubLeafletRichtextFacet from './types/pub/leaflet/richtext/facet' 36 38 export * as ComAtprotoLabelDefs from './types/com/atproto/label/defs'
+42
lexicons/api/lexicons.ts
··· 179 179 }, 180 180 }, 181 181 }, 182 + PubLeafletBlocksUnorderedList: { 183 + lexicon: 1, 184 + id: 'pub.leaflet.blocks.unorderedList', 185 + defs: { 186 + main: { 187 + type: 'object', 188 + required: ['children'], 189 + properties: { 190 + children: { 191 + type: 'array', 192 + items: { 193 + type: 'ref', 194 + ref: 'lex:pub.leaflet.blocks.unorderedList#listItem', 195 + }, 196 + }, 197 + }, 198 + }, 199 + listItem: { 200 + type: 'object', 201 + required: ['content'], 202 + properties: { 203 + content: { 204 + type: 'union', 205 + refs: [ 206 + 'lex:pub.leaflet.blocks.text', 207 + 'lex:pub.leaflet.blocks.header', 208 + 'lex:pub.leaflet.blocks.image', 209 + ], 210 + }, 211 + children: { 212 + type: 'array', 213 + items: { 214 + type: 'ref', 215 + ref: 'lex:pub.leaflet.blocks.unorderedList#listItem', 216 + }, 217 + }, 218 + }, 219 + }, 220 + }, 221 + }, 182 222 PubLeafletPagesLinearDocument: { 183 223 lexicon: 1, 184 224 id: 'pub.leaflet.pages.linearDocument', ··· 205 245 'lex:pub.leaflet.blocks.text', 206 246 'lex:pub.leaflet.blocks.header', 207 247 'lex:pub.leaflet.blocks.image', 248 + 'lex:pub.leaflet.blocks.unorderedList', 208 249 ], 209 250 }, 210 251 alignment: { ··· 1301 1342 PubLeafletBlocksHeader: 'pub.leaflet.blocks.header', 1302 1343 PubLeafletBlocksImage: 'pub.leaflet.blocks.image', 1303 1344 PubLeafletBlocksText: 'pub.leaflet.blocks.text', 1345 + PubLeafletBlocksUnorderedList: 'pub.leaflet.blocks.unorderedList', 1304 1346 PubLeafletPagesLinearDocument: 'pub.leaflet.pages.linearDocument', 1305 1347 PubLeafletRichtextFacet: 'pub.leaflet.richtext.facet', 1306 1348 ComAtprotoLabelDefs: 'com.atproto.label.defs',
+49
lexicons/api/types/pub/leaflet/blocks/unorderedList.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../../lexicons' 7 + import { $Typed, is$typed as _is$typed, OmitKey } from '../../../../util' 8 + import type * as PubLeafletBlocksText from './text' 9 + import type * as PubLeafletBlocksHeader from './header' 10 + import type * as PubLeafletBlocksImage from './image' 11 + 12 + const is$typed = _is$typed, 13 + validate = _validate 14 + const id = 'pub.leaflet.blocks.unorderedList' 15 + 16 + export interface Main { 17 + $type?: 'pub.leaflet.blocks.unorderedList' 18 + children: ListItem[] 19 + } 20 + 21 + const hashMain = 'main' 22 + 23 + export function isMain<V>(v: V) { 24 + return is$typed(v, id, hashMain) 25 + } 26 + 27 + export function validateMain<V>(v: V) { 28 + return validate<Main & V>(v, id, hashMain) 29 + } 30 + 31 + export interface ListItem { 32 + $type?: 'pub.leaflet.blocks.unorderedList#listItem' 33 + content: 34 + | $Typed<PubLeafletBlocksText.Main> 35 + | $Typed<PubLeafletBlocksHeader.Main> 36 + | $Typed<PubLeafletBlocksImage.Main> 37 + | { $type: string } 38 + children?: ListItem[] 39 + } 40 + 41 + const hashListItem = 'listItem' 42 + 43 + export function isListItem<V>(v: V) { 44 + return is$typed(v, id, hashListItem) 45 + } 46 + 47 + export function validateListItem<V>(v: V) { 48 + return validate<ListItem & V>(v, id, hashListItem) 49 + }
+2
lexicons/api/types/pub/leaflet/pages/linearDocument.ts
··· 8 8 import type * as PubLeafletBlocksText from '../blocks/text' 9 9 import type * as PubLeafletBlocksHeader from '../blocks/header' 10 10 import type * as PubLeafletBlocksImage from '../blocks/image' 11 + import type * as PubLeafletBlocksUnorderedList from '../blocks/unorderedList' 11 12 12 13 const is$typed = _is$typed, 13 14 validate = _validate ··· 34 35 | $Typed<PubLeafletBlocksText.Main> 35 36 | $Typed<PubLeafletBlocksHeader.Main> 36 37 | $Typed<PubLeafletBlocksImage.Main> 38 + | $Typed<PubLeafletBlocksUnorderedList.Main> 37 39 | { $type: string } 38 40 alignment?: 39 41 | 'lex:pub.leaflet.pages.linearDocument#textAlignLeft'
+44
lexicons/pub/leaflet/blocks/unorderedList.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "pub.leaflet.blocks.unorderedList", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "required": [ 8 + "children" 9 + ], 10 + "properties": { 11 + "children": { 12 + "type": "array", 13 + "items": { 14 + "type": "ref", 15 + "ref": "#listItem" 16 + } 17 + } 18 + } 19 + }, 20 + "listItem": { 21 + "type": "object", 22 + "required": [ 23 + "content" 24 + ], 25 + "properties": { 26 + "content": { 27 + "type": "union", 28 + "refs": [ 29 + "pub.leaflet.blocks.text", 30 + "pub.leaflet.blocks.header", 31 + "pub.leaflet.blocks.image" 32 + ] 33 + }, 34 + "children": { 35 + "type": "array", 36 + "items": { 37 + "type": "ref", 38 + "ref": "#listItem" 39 + } 40 + } 41 + } 42 + } 43 + } 44 + }
+2 -1
lexicons/pub/leaflet/pages/linearDocument.json
··· 25 25 "refs": [ 26 26 "pub.leaflet.blocks.text", 27 27 "pub.leaflet.blocks.header", 28 - "pub.leaflet.blocks.image" 28 + "pub.leaflet.blocks.image", 29 + "pub.leaflet.blocks.unorderedList" 29 30 ] 30 31 }, 31 32 "alignment": {
+30 -1
lexicons/src/blocks.ts
··· 68 68 }, 69 69 }; 70 70 71 + export const PubLeafletBlocksUnorderedList: LexiconDoc = { 72 + lexicon: 1, 73 + id: "pub.leaflet.blocks.unorderedList", 74 + defs: { 75 + main: { 76 + type: "object", 77 + required: ["children"], 78 + properties: { 79 + children: { type: "array", items: { type: "ref", ref: "#listItem" } }, 80 + }, 81 + }, 82 + listItem: { 83 + type: "object", 84 + required: ["content"], 85 + properties: { 86 + content: { 87 + type: "union", 88 + refs: [ 89 + PubLeafletBlocksText, 90 + PubLeafletBlocksHeader, 91 + PubLeafletBlocksImage, 92 + ].map((l) => l.id), 93 + }, 94 + children: { type: "array", items: { type: "ref", ref: "#listItem" } }, 95 + }, 96 + }, 97 + }, 98 + }; 71 99 export const BlockLexicons = [ 72 100 PubLeafletBlocksText, 73 101 PubLeafletBlocksHeader, 74 102 PubLeafletBlocksImage, 103 + PubLeafletBlocksUnorderedList, 75 104 ]; 76 105 export const BlockUnion: LexRefUnion = { 77 106 type: "union", 78 - refs: BlockLexicons.map((lexicon) => lexicon.id), 107 + refs: [...BlockLexicons.map((lexicon) => lexicon.id)], 79 108 };
+2 -73
src/utils/getBlocksAsHTML.tsx
··· 6 6 import * as base64 from "base64-js"; 7 7 import { RenderYJSFragment } from "components/Blocks/TextBlock/RenderYJSFragment"; 8 8 import { Block } from "components/Blocks/Block"; 9 - import { getBlocksWithType } from "src/hooks/queries/useBlocks"; 9 + import { List, parseBlocksToList } from "./parseBlocksToList"; 10 10 11 11 export async function getBlocksAsHTML( 12 12 rep: Replicache<ReplicacheMutators>, ··· 14 14 ) { 15 15 let data = await rep?.query(async (tx) => { 16 16 let result: string[] = []; 17 - let parsed = parseBlocks(selectedBlocks); 17 + let parsed = parseBlocksToList(selectedBlocks); 18 18 for (let pb of parsed) { 19 19 if (pb.type === "block") result.push(await renderBlock(pb.block, tx)); 20 20 else ··· 150 150 />, 151 151 ); 152 152 } 153 - 154 - function parseBlocks(blocks: Block[]) { 155 - let parsed: ParsedBlocks = []; 156 - for (let i = 0; i < blocks.length; i++) { 157 - let b = blocks[i]; 158 - if (!b.listData) parsed.push({ type: "block", block: b }); 159 - else { 160 - let previousBlock = parsed[parsed.length - 1]; 161 - if ( 162 - !previousBlock || 163 - previousBlock.type !== "list" || 164 - previousBlock.depth > b.listData.depth 165 - ) 166 - parsed.push({ 167 - type: "list", 168 - depth: b.listData.depth, 169 - children: [ 170 - { 171 - type: "list", 172 - block: b, 173 - depth: b.listData.depth, 174 - children: [], 175 - }, 176 - ], 177 - }); 178 - else { 179 - let depth = b.listData.depth; 180 - if (depth === previousBlock.depth) 181 - previousBlock.children.push({ 182 - type: "list", 183 - block: b, 184 - depth: b.listData.depth, 185 - children: [], 186 - }); 187 - else { 188 - let parent = 189 - previousBlock.children[previousBlock.children.length - 1]; 190 - while (depth > 1) { 191 - if ( 192 - parent.children[parent.children.length - 1] && 193 - parent.children[parent.children.length - 1].depth < 194 - b.listData.depth 195 - ) { 196 - parent = parent.children[parent.children.length - 1]; 197 - } 198 - depth -= 1; 199 - } 200 - parent.children.push({ 201 - type: "list", 202 - block: b, 203 - depth: b.listData.depth, 204 - children: [], 205 - }); 206 - } 207 - } 208 - } 209 - } 210 - return parsed; 211 - } 212 - 213 - type ParsedBlocks = Array< 214 - | { type: "block"; block: Block } 215 - | { type: "list"; depth: number; children: List[] } 216 - >; 217 - 218 - type List = { 219 - type: "list"; 220 - block: Block; 221 - depth: number; 222 - children: List[]; 223 - };
+72
src/utils/parseBlocksToList.ts
··· 1 + import type { Block } from "components/Blocks/Block"; 2 + 3 + export function parseBlocksToList(blocks: Block[]) { 4 + let parsed: ParsedBlocks = []; 5 + for (let i = 0; i < blocks.length; i++) { 6 + let b = blocks[i]; 7 + if (!b.listData) parsed.push({ type: "block", block: b }); 8 + else { 9 + let previousBlock = parsed[parsed.length - 1]; 10 + if ( 11 + !previousBlock || 12 + previousBlock.type !== "list" || 13 + previousBlock.depth > b.listData.depth 14 + ) 15 + parsed.push({ 16 + type: "list", 17 + depth: b.listData.depth, 18 + children: [ 19 + { 20 + type: "list", 21 + block: b, 22 + depth: b.listData.depth, 23 + children: [], 24 + }, 25 + ], 26 + }); 27 + else { 28 + let depth = b.listData.depth; 29 + if (depth === previousBlock.depth) 30 + previousBlock.children.push({ 31 + type: "list", 32 + block: b, 33 + depth: b.listData.depth, 34 + children: [], 35 + }); 36 + else { 37 + let parent = 38 + previousBlock.children[previousBlock.children.length - 1]; 39 + while (depth > 1) { 40 + if ( 41 + parent.children[parent.children.length - 1] && 42 + parent.children[parent.children.length - 1].depth < 43 + b.listData.depth 44 + ) { 45 + parent = parent.children[parent.children.length - 1]; 46 + } 47 + depth -= 1; 48 + } 49 + parent.children.push({ 50 + type: "list", 51 + block: b, 52 + depth: b.listData.depth, 53 + children: [], 54 + }); 55 + } 56 + } 57 + } 58 + } 59 + return parsed; 60 + } 61 + 62 + type ParsedBlocks = Array< 63 + | { type: "block"; block: Block } 64 + | { type: "list"; depth: number; children: List[] } 65 + >; 66 + 67 + export type List = { 68 + type: "list"; 69 + block: Block; 70 + depth: number; 71 + children: List[]; 72 + };