a tool for shared writing and social publishing

added small text attribute and button to leaflet

+162 -108
+16 -8
actions/publishToPublication.ts
··· 2 2 3 3 import * as Y from "yjs"; 4 4 import * as base64 from "base64-js"; 5 - import { 6 - restoreOAuthSession, 7 - OAuthSessionError, 8 - } from "src/atproto-oauth"; 5 + import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; 9 6 import { getIdentityData } from "actions/getIdentityData"; 10 7 import { 11 8 AtpBaseClient, ··· 50 47 ColorToRGBA, 51 48 } from "components/ThemeManager/colorToLexicons"; 52 49 import { parseColor } from "@react-stately/color"; 53 - import { Notification, pingIdentityToUpdateNotification } from "src/notifications"; 50 + import { 51 + Notification, 52 + pingIdentityToUpdateNotification, 53 + } from "src/notifications"; 54 54 import { v7 } from "uuid"; 55 55 56 56 type PublishResult = ··· 253 253 254 254 // Create notifications for mentions (only on first publish) 255 255 if (!existingDocUri) { 256 - await createMentionNotifications(result.uri, record, credentialSession.did!); 256 + await createMentionNotifications( 257 + result.uri, 258 + record, 259 + credentialSession.did!, 260 + ); 257 261 } 258 262 259 263 return { success: true, rkey, record: JSON.parse(JSON.stringify(record)) }; ··· 865 869 .single(); 866 870 867 871 if (publication && publication.identity_did !== authorDid) { 868 - mentionedPublications.set(publication.identity_did, feature.atURI); 872 + mentionedPublications.set( 873 + publication.identity_did, 874 + feature.atURI, 875 + ); 869 876 } 870 877 } else if (uri.collection === "pub.leaflet.document") { 871 878 // Get the document owner's DID ··· 876 883 .single(); 877 884 878 885 if (document) { 879 - const docRecord = document.data as PubLeafletDocument.Record; 886 + const docRecord = 887 + document.data as PubLeafletDocument.Record; 880 888 if (docRecord.author !== authorDid) { 881 889 mentionedDocuments.set(docRecord.author, feature.atURI); 882 890 }
+1 -1
components/Blocks/Block.tsx
··· 10 10 import { useHandleDrop } from "./useHandleDrop"; 11 11 import { useEntitySetContext } from "components/EntitySetProvider"; 12 12 13 - import { TextBlock } from "components/Blocks/TextBlock"; 13 + import { TextBlock } from "./TextBlock/index"; 14 14 import { ImageBlock } from "./ImageBlock"; 15 15 import { PageLinkBlock } from "./PageLinkBlock"; 16 16 import { ExternalLinkBlock } from "./ExternalLinkBlock";
+11 -4
components/Blocks/TextBlock/index.tsx
··· 120 120 }) { 121 121 let initialFact = useEntity(props.entityID, "block/text"); 122 122 let headingLevel = useEntity(props.entityID, "block/heading-level"); 123 + let textSize = useEntity(props.entityID, "block/text-size"); 123 124 let alignment = 124 125 useEntity(props.entityID, "block/text-alignment")?.data.value || "left"; 125 126 let alignmentClass = { ··· 128 129 center: "text-center", 129 130 justify: "text-justify", 130 131 }[alignment]; 132 + let textStyle = textSize?.data.value === "small" ? "text-sm" : ""; 131 133 let { permissions } = useEntitySetContext(); 132 134 133 135 let content = <br />; ··· 159 161 className={` 160 162 ${alignmentClass} 161 163 ${props.type === "blockquote" ? (props.previousBlock?.type === "blockquote" ? `blockquote pt-3 ` : "blockquote") : ""} 162 - ${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : ""} 164 + ${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : textStyle} 163 165 w-full whitespace-pre-wrap outline-hidden ${props.className} `} 164 166 > 165 167 {content} ··· 169 171 170 172 export function BaseTextBlock(props: BlockProps & { className?: string }) { 171 173 let headingLevel = useEntity(props.entityID, "block/heading-level"); 174 + let textSize = useEntity(props.entityID, "block/text-size"); 172 175 let alignment = 173 176 useEntity(props.entityID, "block/text-alignment")?.data.value || "left"; 174 177 ··· 184 187 center: "text-center", 185 188 justify: "text-justify", 186 189 }[alignment]; 190 + let textStyle = 191 + textSize?.data.value === "small" 192 + ? "text-sm text-secondary" 193 + : "text-base text-primary"; 187 194 188 195 let editorState = useEditorStates( 189 196 (s) => s.editorStates[props.entityID], ··· 258 265 grow resize-none align-top whitespace-pre-wrap bg-transparent 259 266 outline-hidden 260 267 261 - ${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : ""} 268 + ${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : textStyle} 262 269 ${props.className}`} 263 270 ref={mountRef} 264 271 /> ··· 277 284 // if this is the only block on the page and is empty or is a canvas, show placeholder 278 285 <div 279 286 className={`${props.className} ${alignmentClass} w-full pointer-events-none absolute top-0 left-0 italic text-tertiary flex flex-col 280 - ${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : ""} 287 + ${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : textStyle} 281 288 `} 282 289 > 283 290 {props.type === "text" ··· 496 503 497 504 // Find the relative positioned parent container 498 505 const editorEl = view.dom; 499 - const container = editorEl.closest('.relative') as HTMLElement | null; 506 + const container = editorEl.closest(".relative") as HTMLElement | null; 500 507 501 508 if (container) { 502 509 const containerRect = container.getBoundingClientRect();
+124 -95
components/Toolbar/TextBlockTypeToolbar.tsx
··· 4 4 Header3Small, 5 5 } from "components/Icons/BlockTextSmall"; 6 6 import { Props } from "components/Icons/Props"; 7 - import { ShortcutKey } from "components/Layout"; 7 + import { ShortcutKey, Separator } from "components/Layout"; 8 8 import { ToolbarButton } from "components/Toolbar"; 9 9 import { TextSelection } from "prosemirror-state"; 10 10 import { useCallback } from "react"; ··· 22 22 focusedBlock?.entityID || null, 23 23 "block/heading-level", 24 24 ); 25 + 26 + let textSize = useEntity(focusedBlock?.entityID || null, "block/text-size"); 25 27 let { rep } = useReplicache(); 26 28 27 29 let setLevel = useCallback( ··· 51 53 ); 52 54 return ( 53 55 // This Toolbar should close once the user starts typing again 54 - <div className="flex w-full justify-between items-center gap-4"> 55 - <div className="flex items-center gap-[6px]"> 56 - <ToolbarButton 57 - className={props.className} 58 - onClick={() => { 59 - setLevel(1); 60 - }} 61 - active={ 62 - blockType?.data.value === "heading" && 63 - headingLevel?.data.value === 1 64 - } 65 - tooltipContent={ 66 - <div className="flex flex-col justify-center"> 67 - <div className="font-bold text-center">Title</div> 68 - <div className="flex gap-1 font-normal"> 69 - start line with 70 - <ShortcutKey>#</ShortcutKey> 71 - </div> 56 + <> 57 + <ToolbarButton 58 + className={props.className} 59 + onClick={() => { 60 + setLevel(1); 61 + }} 62 + active={ 63 + blockType?.data.value === "heading" && headingLevel?.data.value === 1 64 + } 65 + tooltipContent={ 66 + <div className="flex flex-col justify-center"> 67 + <div className="font-bold text-center">Title</div> 68 + <div className="flex gap-1 font-normal"> 69 + start line with 70 + <ShortcutKey>#</ShortcutKey> 72 71 </div> 73 - } 74 - > 75 - <Header1Small /> 76 - </ToolbarButton> 77 - <ToolbarButton 78 - className={props.className} 79 - onClick={() => { 80 - setLevel(2); 81 - }} 82 - active={ 83 - blockType?.data.value === "heading" && 84 - headingLevel?.data.value === 2 85 - } 86 - tooltipContent={ 87 - <div className="flex flex-col justify-center"> 88 - <div className="font-bold text-center">Heading</div> 89 - <div className="flex gap-1 font-normal"> 90 - start line with 91 - <ShortcutKey>##</ShortcutKey> 92 - </div> 72 + </div> 73 + } 74 + > 75 + <Header1Small /> 76 + </ToolbarButton> 77 + <ToolbarButton 78 + className={props.className} 79 + onClick={() => { 80 + setLevel(2); 81 + }} 82 + active={ 83 + blockType?.data.value === "heading" && headingLevel?.data.value === 2 84 + } 85 + tooltipContent={ 86 + <div className="flex flex-col justify-center"> 87 + <div className="font-bold text-center">Heading</div> 88 + <div className="flex gap-1 font-normal"> 89 + start line with 90 + <ShortcutKey>##</ShortcutKey> 93 91 </div> 94 - } 95 - > 96 - <Header2Small /> 97 - </ToolbarButton> 98 - <ToolbarButton 99 - className={props.className} 100 - onClick={() => { 101 - setLevel(3); 102 - }} 103 - active={ 104 - blockType?.data.value === "heading" && 105 - headingLevel?.data.value === 3 106 - } 107 - tooltipContent={ 108 - <div className="flex flex-col justify-center"> 109 - <div className="font-bold text-center">Subheading</div> 110 - <div className="flex gap-1 font-normal"> 111 - start line with 112 - <ShortcutKey>###</ShortcutKey> 113 - </div> 92 + </div> 93 + } 94 + > 95 + <Header2Small /> 96 + </ToolbarButton> 97 + <ToolbarButton 98 + className={props.className} 99 + onClick={() => { 100 + setLevel(3); 101 + }} 102 + active={ 103 + blockType?.data.value === "heading" && headingLevel?.data.value === 3 104 + } 105 + tooltipContent={ 106 + <div className="flex flex-col justify-center"> 107 + <div className="font-bold text-center">Subheading</div> 108 + <div className="flex gap-1 font-normal"> 109 + start line with 110 + <ShortcutKey>###</ShortcutKey> 114 111 </div> 112 + </div> 113 + } 114 + > 115 + <Header3Small /> 116 + </ToolbarButton> 117 + <Separator classname="h-6!!" /> 118 + <ToolbarButton 119 + className={`px-[6px] ${props.className}`} 120 + onClick={async () => { 121 + if (headingLevel) 122 + await rep?.mutate.retractFact({ factID: headingLevel.id }); 123 + if (textSize) await rep?.mutate.retractFact({ factID: textSize.id }); 124 + if (!focusedBlock || !blockType) return; 125 + if (blockType.data.value !== "text") { 126 + let existingEditor = 127 + useEditorStates.getState().editorStates[focusedBlock.entityID]; 128 + let selection = existingEditor?.editor.selection; 129 + await rep?.mutate.assertFact({ 130 + entity: focusedBlock?.entityID, 131 + attribute: "block/type", 132 + data: { type: "block-type-union", value: "text" }, 133 + }); 134 + 135 + let newEditor = 136 + useEditorStates.getState().editorStates[focusedBlock.entityID]; 137 + if (!newEditor || !selection) return; 138 + newEditor.view?.dispatch( 139 + newEditor.editor.tr.setSelection( 140 + TextSelection.create(newEditor.editor.doc, selection.anchor), 141 + ), 142 + ); 143 + 144 + newEditor.view?.focus(); 115 145 } 116 - > 117 - <Header3Small /> 118 - </ToolbarButton> 119 - <ToolbarButton 120 - className={`px-[6px] ${props.className}`} 121 - onClick={async () => { 146 + }} 147 + active={ 148 + blockType?.data.value === "text" && textSize?.data.value !== "small" 149 + } 150 + tooltipContent={<div>Normal Text</div>} 151 + > 152 + Text 153 + </ToolbarButton> 154 + <ToolbarButton 155 + className={`px-[6px] text-sm text-secondary ${props.className}`} 156 + onClick={async () => { 157 + if (!focusedBlock || !blockType) return; 158 + if (blockType.data.value !== "text") { 159 + // Convert to text block first if it's a heading 122 160 if (headingLevel) 123 161 await rep?.mutate.retractFact({ factID: headingLevel.id }); 124 - if (!focusedBlock || !blockType) return; 125 - if (blockType.data.value !== "text") { 126 - let existingEditor = 127 - useEditorStates.getState().editorStates[focusedBlock.entityID]; 128 - let selection = existingEditor?.editor.selection; 129 - await rep?.mutate.assertFact({ 130 - entity: focusedBlock?.entityID, 131 - attribute: "block/type", 132 - data: { type: "block-type-union", value: "text" }, 133 - }); 134 - 135 - let newEditor = 136 - useEditorStates.getState().editorStates[focusedBlock.entityID]; 137 - if (!newEditor || !selection) return; 138 - newEditor.view?.dispatch( 139 - newEditor.editor.tr.setSelection( 140 - TextSelection.create(newEditor.editor.doc, selection.anchor), 141 - ), 142 - ); 143 - 144 - newEditor.view?.focus(); 145 - } 146 - }} 147 - active={blockType?.data.value === "text"} 148 - tooltipContent={<div>Paragraph</div>} 149 - > 150 - Paragraph 151 - </ToolbarButton> 152 - </div> 153 - </div> 162 + await rep?.mutate.assertFact({ 163 + entity: focusedBlock.entityID, 164 + attribute: "block/type", 165 + data: { type: "block-type-union", value: "text" }, 166 + }); 167 + } 168 + // Set text size to small 169 + await rep?.mutate.assertFact({ 170 + entity: focusedBlock.entityID, 171 + attribute: "block/text-size", 172 + data: { type: "text-size-union", value: "small" }, 173 + }); 174 + }} 175 + active={ 176 + blockType?.data.value === "text" && textSize?.data.value === "small" 177 + } 178 + tooltipContent={<div>Small Text</div>} 179 + > 180 + Small 181 + </ToolbarButton> 182 + </> 154 183 ); 155 184 }; 156 185
+8
src/replicache/attributes.ts
··· 71 71 type: "number", 72 72 cardinality: "one", 73 73 }, 74 + "block/text-size": { 75 + type: "text-size-union", 76 + cardinality: "one", 77 + }, 74 78 "block/image": { 75 79 type: "image", 76 80 cardinality: "one", ··· 317 321 "text-alignment-type-union": { 318 322 type: "text-alignment-type-union"; 319 323 value: "right" | "left" | "center" | "justify"; 324 + }; 325 + "text-size-union": { 326 + type: "text-size-union"; 327 + value: "default" | "small"; 320 328 }; 321 329 "page-type-union": { type: "page-type-union"; value: "doc" | "canvas" }; 322 330 "block-type-union": {
+2
src/utils/getBlocksAsHTML.tsx
··· 171 171 }, 172 172 text: async (b, tx, a) => { 173 173 let [value] = await scanIndex(tx).eav(b.value, "block/text"); 174 + let [textSize] = await scanIndex(tx).eav(b.value, "block/text-size"); 174 175 return ( 175 176 <RenderYJSFragment 176 177 value={value?.data.value} 177 178 attrs={{ 178 179 "data-alignment": a, 180 + "data-text-size": textSize?.data.value, 179 181 }} 180 182 wrapper="p" 181 183 />