a tool for shared writing and social publishing
0
fork

Configure Feed

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

add deleting and adding blocks!

+134 -10
+4 -2
app/[doc_id]/Blocks.tsx
··· 26 26 let blocks = useEntity(props.entityID, "card/block"); 27 27 28 28 return ( 29 - <div className="mx-auto max-w-3xl"> 29 + <div className="mx-auto max-w-3xl flex flex-col gap-1 p-2"> 30 30 {blocks 31 31 ?.sort((a, b) => { 32 32 return a.data.position > b.data.position ? 1 : -1; ··· 34 34 .map((f, index, arr) => { 35 35 return ( 36 36 <Block 37 - key={f.id} 37 + key={f.data.value} 38 38 entityID={f.data.value} 39 39 parent={props.entityID} 40 40 position={f.data.position} 41 + previousBlock={arr[index - 1]?.data || null} 41 42 nextPosition={arr[index + 1]?.data.position || null} 42 43 /> 43 44 ); ··· 50 51 entityID: string; 51 52 parent: string; 52 53 position: string; 54 + previousBlock: { position: string; value: string } | null; 53 55 nextPosition: string | null; 54 56 }) { 55 57 return (
+53 -4
components/TextBlock.tsx
··· 7 7 import * as base64 from "base64-js"; 8 8 import { useReplicache, useEntity, ReplicacheMutators } from "../replicache"; 9 9 10 - import { EditorState } from "prosemirror-state"; 10 + import { EditorState, TextSelection } from "prosemirror-state"; 11 11 import { schema } from "prosemirror-schema-basic"; 12 12 import { ySyncPlugin } from "y-prosemirror"; 13 13 import { Replicache } from "replicache"; 14 14 import { generateKeyBetween } from "fractional-indexing"; 15 + import { create } from "zustand"; 16 + 17 + let useEditorStates = create( 18 + () => 19 + ({}) as { [entity: string]: { editor: InstanceType<typeof EditorState> } }, 20 + ); 15 21 16 22 export function TextBlock(props: { 17 23 entityID: string; 18 24 parent: string; 19 25 position: string; 26 + previousBlock: { value: string; position: string } | null; 20 27 nextPosition: string | null; 21 28 }) { 22 29 const [mount, setMount] = useState<HTMLElement | null>(null); ··· 38 45 keymap({ 39 46 "Meta-b": toggleMark(schema.marks.strong), 40 47 "Meta-i": toggleMark(schema.marks.em), 41 - Enter: () => { 48 + Backspace: (state) => { 49 + if (state.doc.textContent.length === 0) { 50 + repRef.current?.mutate.removeBlock({ 51 + blockEntity: props.entityID, 52 + }); 53 + if (propsRef.current.previousBlock) { 54 + let prevBlock = propsRef.current.previousBlock.value; 55 + document 56 + .getElementById(elementId.block(prevBlock).text) 57 + ?.focus(); 58 + let previousBlockEditor = 59 + useEditorStates.getState()[prevBlock]?.editor; 60 + console.log(previousBlockEditor); 61 + if (previousBlockEditor) { 62 + let tr = previousBlockEditor.tr; 63 + let endPos = previousBlockEditor.doc.content.size; 64 + 65 + let newState = previousBlockEditor.apply( 66 + tr.setSelection( 67 + TextSelection.create(previousBlockEditor.doc, endPos), 68 + ), 69 + ); 70 + useEditorStates.setState((s) => ({ 71 + ...s, 72 + [prevBlock]: { editor: newState }, 73 + })); 74 + } 75 + } 76 + } 77 + return false; 78 + }, 79 + "Shift-Enter": () => { 42 80 let newEntityID = crypto.randomUUID(); 43 81 repRef.current?.mutate.addBlock({ 44 82 newEntityID, ··· 60 98 ], 61 99 }), 62 100 ); 101 + useEffect(() => { 102 + useEditorStates.setState((s) => { 103 + return { ...s, [props.entityID]: { editor: editorState } }; 104 + }); 105 + }, [editorState, props.entityID]); 106 + 107 + let editorStateFromZustand = useEditorStates((s) => s[props.entityID]); 108 + useEffect(() => { 109 + if (editorStateFromZustand) setEditorState(editorStateFromZustand.editor); 110 + }, [editorStateFromZustand]); 63 111 64 112 return ( 65 113 <ProseMirror ··· 71 119 > 72 120 <pre 73 121 id={elementId.block(props.entityID).text} 74 - className="w-full whitespace-pre-wrap" 122 + className="w-full whitespace-pre-wrap outline-none" 75 123 ref={setMount} 76 124 /> 77 125 </ProseMirror> 78 126 ); 79 127 } 80 128 129 + //I need to get *and* set the value to zustand? 130 + // This will mean that the value is undefined for a second... Maybe I could use a ref to figure that out? 81 131 function useYJSValue(entityID: string) { 82 132 const [ydoc] = useState(new Y.Doc()); 83 133 const docStateFromReplicache = useEntity(entityID, "block/text"); 84 - console.log(docStateFromReplicache?.data.value); 85 134 let rep = useReplicache(); 86 135 const yText = ydoc.getXmlFragment("prosemirror"); 87 136
+37 -1
package-lock.json
··· 32 32 "react-use-measure": "^2.1.1", 33 33 "replicache": "^14.2.2", 34 34 "y-prosemirror": "^1.2.5", 35 - "yjs": "^13.6.15" 35 + "yjs": "^13.6.15", 36 + "zustand": "^4.5.2" 36 37 }, 37 38 "devDependencies": { 38 39 "@cloudflare/workers-types": "^4.20240512.0", ··· 11846 11847 "dev": true, 11847 11848 "funding": { 11848 11849 "url": "https://github.com/sponsors/colinhacks" 11850 + } 11851 + }, 11852 + "node_modules/zustand": { 11853 + "version": "4.5.2", 11854 + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", 11855 + "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", 11856 + "dependencies": { 11857 + "use-sync-external-store": "1.2.0" 11858 + }, 11859 + "engines": { 11860 + "node": ">=12.7.0" 11861 + }, 11862 + "peerDependencies": { 11863 + "@types/react": ">=16.8", 11864 + "immer": ">=9.0.6", 11865 + "react": ">=16.8" 11866 + }, 11867 + "peerDependenciesMeta": { 11868 + "@types/react": { 11869 + "optional": true 11870 + }, 11871 + "immer": { 11872 + "optional": true 11873 + }, 11874 + "react": { 11875 + "optional": true 11876 + } 11877 + } 11878 + }, 11879 + "node_modules/zustand/node_modules/use-sync-external-store": { 11880 + "version": "1.2.0", 11881 + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", 11882 + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", 11883 + "peerDependencies": { 11884 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 11849 11885 } 11850 11886 } 11851 11887 }
+2 -1
package.json
··· 34 34 "react-use-measure": "^2.1.1", 35 35 "replicache": "^14.2.2", 36 36 "y-prosemirror": "^1.2.5", 37 - "yjs": "^13.6.15" 37 + "yjs": "^13.6.15", 38 + "zustand": "^4.5.2" 38 39 }, 39 40 "devDependencies": { 40 41 "@cloudflare/workers-types": "^4.20240512.0",
+18 -1
replicache/clientMutationContext.ts
··· 16 16 let existingFact = await tx 17 17 .scan<Fact<typeof attribute>>({ 18 18 indexName: "eav", 19 - prefix: `${entity}-${attribute}`, 19 + prefix: attribute ? `${entity}-${attribute}` : entity, 20 20 }) 21 21 .toArray(); 22 22 return existingFact; ··· 47 47 } 48 48 } 49 49 await tx.set(id, FactWithIndexes({ id, ...f, data })); 50 + }, 51 + async deleteEntity(entity) { 52 + let existingFacts = await tx 53 + .scan<Fact<keyof typeof Attributes>>({ 54 + indexName: "eav", 55 + prefix: `${entity}`, 56 + }) 57 + .toArray(); 58 + let references = await tx 59 + .scan<Fact<keyof typeof Attributes>>({ 60 + indexName: "vae", 61 + prefix: entity, 62 + }) 63 + .toArray(); 64 + await Promise.all( 65 + [...existingFacts, ...references].map((f) => tx.del(f.id)), 66 + ); 50 67 }, 51 68 }; 52 69 return ctx;
+7
replicache/mutations.ts
··· 10 10 attribute: A, 11 11 ) => Promise<DeepReadonly<Fact<A>[]>>; 12 12 }; 13 + deleteEntity: (entity: string) => Promise<void>; 13 14 assertFact: <A extends keyof typeof Attributes>( 14 15 f: Omit<Fact<A>, "id"> & { id?: string }, 15 16 ) => Promise<void>; ··· 34 35 }); 35 36 }; 36 37 38 + const removeBlock: Mutation<{ blockEntity: string }> = async (args, ctx) => { 39 + console.log(args); 40 + await ctx.deleteEntity(args.blockEntity); 41 + }; 42 + 37 43 const assertFact: Mutation< 38 44 Omit<Fact<keyof typeof Attributes>, "id"> & { id?: string } 39 45 > = async (args, ctx) => { ··· 43 49 export const mutations = { 44 50 addBlock, 45 51 assertFact, 52 + removeBlock, 46 53 };
+13 -1
replicache/serverMutationContext.ts
··· 60 60 const oldUpdate = base64.toByteArray( 61 61 (existingFact[0]?.data as Fact<typeof f.attribute>["data"]).value, 62 62 ); 63 - console.log("mergin updates"); 64 63 const newUpdate = base64.toByteArray(f.data.value); 65 64 const updateBytes = Y.mergeUpdatesV2([oldUpdate, newUpdate]); 66 65 data.value = base64.fromByteArray(updateBytes); ··· 84 83 .catch((e) => { 85 84 console.log(`error on inserting fact: `, JSON.stringify(e)); 86 85 }), 86 + ); 87 + }, 88 + async deleteEntity(entity) { 89 + console.log(entity); 90 + console.log( 91 + await Promise.all([ 92 + tx.delete(entities).where(driz.eq(entities.id, entity)), 93 + tx 94 + .delete(facts) 95 + .where( 96 + driz.sql`(data->>'type' = 'ordered-reference' or data ->>'type' = 'reference') and data->>'value' = ${entity}`, 97 + ), 98 + ]), 87 99 ); 88 100 }, 89 101 };