a tool for shared writing and social publishing
0
fork

Configure Feed

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

Feature/blockquote (#177)

* add block quote blocktype

* handle copying and pasting blockquotes

authored by

Jared Pereira and committed by
GitHub
75c5cf54 141390fd

+313 -16
+4
app/globals.css
··· 128 128 /* END GLOBAL STYLING */ 129 129 } 130 130 131 + blockquote { 132 + margin: 0; 133 + } 134 + 131 135 /* Hide scrollbar for Chrome, Safari and Opera */ 132 136 .no-scrollbar::-webkit-scrollbar { 133 137 display: none;
+16
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 9 9 PubLeafletDocument, 10 10 PubLeafletPagesLinearDocument, 11 11 PubLeafletBlocksHorizontalRule, 12 + PubLeafletBlocksBlockquote, 12 13 } from "lexicons/api"; 13 14 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 14 15 import { TextBlock } from "./TextBlock"; ··· 194 195 </div> 195 196 )} 196 197 </div> 198 + ); 199 + } 200 + case PubLeafletBlocksBlockquote.isMain(b.block): { 201 + return ( 202 + <blockquote 203 + className={`border-l-2 border-border pl-2 ${className}`} 204 + {...blockProps} 205 + > 206 + <TextBlock 207 + facets={b.block.facets} 208 + plaintext={b.block.plaintext} 209 + index={index} 210 + preview={preview} 211 + /> 212 + </blockquote> 197 213 ); 198 214 } 199 215 case PubLeafletBlocksText.isMain(b.block):
+1
components/Blocks/Block.tsx
··· 175 175 math: MathBlock, 176 176 card: PageLinkBlock, 177 177 text: TextBlock, 178 + blockquote: TextBlock, 178 179 heading: TextBlock, 179 180 image: ImageBlock, 180 181 link: ExternalLinkBlock,
+12
components/Blocks/BlockCommands.tsx
··· 31 31 import { ListUnorderedSmall } from "components/Toolbar/ListToolbar"; 32 32 import { BlockMathSmall } from "components/Icons/BlockMathSmall"; 33 33 import { BlockCodeSmall } from "components/Icons/BlockCodeSmall"; 34 + import { QuoteTiny } from "components/Icons/QuoteTiny"; 35 + import { QuoteSmall } from "components/Icons/QuoteSmall"; 34 36 35 37 type Props = { 36 38 parent: string; ··· 155 157 attribute: "block/is-list", 156 158 data: { value: true, type: "boolean" }, 157 159 }); 160 + clearCommandSearchText(entity); 161 + }, 162 + }, 163 + { 164 + name: "Block Quote", 165 + icon: <QuoteSmall />, 166 + type: "text", 167 + onSelect: async (rep, props, um) => { 168 + if (props.entityID) clearCommandSearchText(props.entityID); 169 + let entity = await createBlockWithType(rep, props, "blockquote"); 158 170 clearCommandSearchText(entity); 159 171 }, 160 172 },
+17 -6
components/Blocks/TextBlock/index.tsx
··· 17 17 import { BlockProps } from "../Block"; 18 18 import { focusBlock } from "src/utils/focusBlock"; 19 19 import { TextBlockKeymap } from "./keymap"; 20 - import { schema } from "./schema"; 20 + import { multiBlockSchema, schema } from "./schema"; 21 21 import { useUIState } from "src/useUIState"; 22 22 import { addBlueskyPostBlock, addLinkBlock } from "src/utils/addLinkBlock"; 23 23 import { BlockCommandBar } from "components/Blocks/BlockCommandBar"; ··· 44 44 } as { [level: number]: string }; 45 45 46 46 export function TextBlock( 47 - props: BlockProps & { className?: string; preview?: boolean }, 47 + props: BlockProps & { 48 + className?: string; 49 + preview?: boolean; 50 + }, 48 51 ) { 49 52 let isLocked = useEntity(props.entityID, "block/is-locked"); 50 53 let initialized = useInitialPageLoad(); ··· 213 216 let handlePaste = useHandlePaste(props.entityID, propsRef); 214 217 useLayoutEffect(() => { 215 218 if (!mountRef.current) return; 216 - let km = TextBlockKeymap(propsRef, repRef, rep.undoManager); 219 + let km = TextBlockKeymap( 220 + propsRef, 221 + repRef, 222 + rep.undoManager, 223 + props.type === "blockquote", 224 + ); 217 225 let editor = EditorState.create({ 218 - schema, 226 + schema: props.type === "blockquote" ? multiBlockSchema : schema, 219 227 plugins: [ 220 228 ySyncPlugin(value), 221 229 keymap(km), ··· 332 340 }, 333 341 })); 334 342 }; 335 - }, [props.entityID, props.parent, value, handlePaste, rep]); 343 + }, [props.entityID, props.parent, value, handlePaste, rep, props.type]); 336 344 337 345 return ( 338 346 <> 339 347 <div 340 - className={`flex items-center justify-between w-full ${selected && props.pageType === "canvas" && "bg-bg-page rounded-md"} `} 348 + className={`flex items-center justify-between w-full 349 + ${selected && props.pageType === "canvas" && "bg-bg-page rounded-md"} 350 + ${props.type === "blockquote" ? "border-l-2 border-border pl-2 " : ""} 351 + `} 341 352 > 342 353 <pre 343 354 data-entityid={props.entityID}
+12
components/Blocks/TextBlock/inputRules.ts
··· 152 152 return tr; 153 153 }), 154 154 155 + //Blockquote 156 + new InputRule(/^([>]{1})\s$/, (state, match) => { 157 + let tr = state.tr; 158 + tr.delete(0, 2); 159 + repRef.current?.mutate.assertFact({ 160 + entity: propsRef.current.entityID, 161 + attribute: "block/type", 162 + data: { type: "block-type-union", value: "blockquote" }, 163 + }); 164 + return tr; 165 + }), 166 + 155 167 //Header 156 168 new InputRule(/^([#]{1,3})\s$/, (state, match) => { 157 169 let tr = state.tr;
+7 -1
components/Blocks/TextBlock/keymap.ts
··· 2 2 import { focusBlock } from "src/utils/focusBlock"; 3 3 import { EditorView } from "prosemirror-view"; 4 4 import { generateKeyBetween } from "fractional-indexing"; 5 - import { setBlockType, toggleMark } from "prosemirror-commands"; 5 + import { baseKeymap, setBlockType, toggleMark } from "prosemirror-commands"; 6 6 import { keymap } from "prosemirror-keymap"; 7 7 import { 8 8 Command, ··· 30 30 propsRef: PropsRef, 31 31 repRef: RefObject<Replicache<ReplicacheMutators> | null>, 32 32 um: UndoManager, 33 + multiLine?: boolean, 33 34 ) => 34 35 ({ 35 36 "Meta-b": toggleMark(schema.marks.strong), ··· 132 133 ), 133 134 "Shift-Backspace": backspace(propsRef, repRef), 134 135 Enter: (state, dispatch, view) => { 136 + if (multiLine && state.doc.content.size - state.selection.anchor > 1) 137 + return false; 135 138 return um.withUndoGroup(() => 136 139 enter(propsRef, repRef)(state, dispatch, view), 137 140 ); 138 141 }, 139 142 "Shift-Enter": (state, dispatch, view) => { 143 + if (multiLine) { 144 + return baseKeymap.Enter(state, dispatch, view); 145 + } 140 146 return um.withUndoGroup(() => 141 147 enter(propsRef, repRef)(state, dispatch, view), 142 148 );
+12 -3
components/Blocks/TextBlock/useHandlePaste.ts
··· 3 3 import { EditorView } from "prosemirror-view"; 4 4 import { setEditorState, useEditorStates } from "src/state/useEditorState"; 5 5 import { MarkType, DOMParser as ProsemirrorDOMParser } from "prosemirror-model"; 6 - import { schema } from "./schema"; 6 + import { multiBlockSchema, schema } from "./schema"; 7 7 import { generateKeyBetween } from "fractional-indexing"; 8 8 import { addImage } from "src/utils/addImage"; 9 9 import { BlockProps } from "../Block"; ··· 19 19 import { UndoManager } from "src/undoManager"; 20 20 21 21 const parser = ProsemirrorDOMParser.fromSchema(schema); 22 + const multilineParser = ProsemirrorDOMParser.fromSchema(multiBlockSchema); 22 23 export const useHandlePaste = ( 23 24 entityID: string, 24 25 propsRef: MutableRefObject<BlockProps>, ··· 180 181 getPosition: () => string; 181 182 }, 182 183 ) => { 183 - let content = parser.parse(child); 184 184 let type: Fact<"block/type">["data"]["value"] | null; 185 185 let headingLevel: number | null = null; 186 186 let hasChildren = false; ··· 203 203 } 204 204 } 205 205 switch (child.tagName) { 206 + case "BLOCKQUOTE": { 207 + type = "blockquote"; 208 + break; 209 + } 206 210 case "LI": 207 211 case "SPAN": { 208 212 type = "text"; ··· 250 254 default: 251 255 type = null; 252 256 } 257 + let content = 258 + type === "blockquote" ? multilineParser.parse(child) : parser.parse(child); 253 259 if (!type) return; 254 260 255 261 let entityID: string; ··· 492 498 block.editor.selection.to !== undefined 493 499 ) 494 500 tr.delete(block.editor.selection.from, block.editor.selection.to); 495 - tr.insert(block.editor.selection.from || 1, content.content); 501 + if (type === "blockquote") { 502 + tr.replaceWith(0, tr.doc.content.size, content.content); 503 + } else tr.insert(block.editor.selection.from || 1, content.content); 496 504 let newState = block.editor.apply(tr); 497 505 setEditorState(entityID, { 498 506 editor: newState, ··· 559 567 // Collect outer HTML for paragraph-like elements 560 568 if ( 561 569 [ 570 + "BLOCKQUOTE", 562 571 "P", 563 572 "PRE", 564 573 "H1",
+19
components/Icons/QuoteSmall.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const QuoteSmall = (props: Props) => { 4 + return ( 5 + <svg 6 + width="24" 7 + height="24" 8 + viewBox="0 0 24 24" 9 + fill="none" 10 + xmlns="http://www.w3.org/2000/svg" 11 + {...props} 12 + > 13 + <path 14 + d="M16.7949 7.38542C17.9558 7.38542 18.8084 7.67608 19.3525 8.25651C19.8603 8.87319 20.1143 9.65325 20.1143 10.5964V11.3581C20.1142 12.8453 19.6792 14.4781 18.8086 16.2555C17.0255 19.9698 12.2552 20.9527 12.0156 20.238C11.8204 19.6524 13.4757 19.2384 14.6699 17.5368C15.3351 16.5889 15.5976 15.7254 15.5977 14.2419C14.8724 14.0605 14.3469 13.7154 14.0205 13.2077C13.6578 12.6999 13.4756 12.0835 13.4756 11.3581V10.5964C13.4756 9.65333 13.748 8.87317 14.292 8.25651C14.7998 7.67613 15.6342 7.38547 16.7949 7.38542ZM5.19141 7.74479C6.97464 4.02989 11.7458 3.04715 11.9844 3.76237C12.1796 4.34789 10.5242 4.76176 9.33008 6.46354C8.6649 7.4115 8.40234 8.27486 8.40234 9.75847C9.12765 9.93988 9.65307 10.2849 9.97949 10.7926C10.3422 11.3005 10.5244 11.9168 10.5244 12.6423V13.404C10.5244 14.3471 10.2521 15.1271 9.70801 15.7438C9.20018 16.3242 8.36578 16.6149 7.20508 16.6149C6.04423 16.6149 5.19162 16.3242 4.64746 15.7438C4.13958 15.1271 3.88574 14.3472 3.88574 13.404V12.6423C3.88575 11.1549 4.32077 9.52235 5.19141 7.74479Z" 15 + fill="black" 16 + /> 17 + </svg> 18 + ); 19 + };
+5 -1
components/Toolbar/index.tsx
··· 65 65 66 66 useEffect(() => { 67 67 if (!blockType) return; 68 - if (blockType !== "heading" && blockType !== "text") { 68 + if ( 69 + blockType !== "heading" && 70 + blockType !== "text" && 71 + blockType !== "blockquote" 72 + ) { 69 73 setToolbarState("block"); 70 74 } else { 71 75 setToolbarState("default");
+2
lexicons/api/index.ts
··· 7 7 import { OmitKey, Un$Typed } from './util' 8 8 import * as PubLeafletDocument from './types/pub/leaflet/document' 9 9 import * as PubLeafletPublication from './types/pub/leaflet/publication' 10 + import * as PubLeafletBlocksBlockquote from './types/pub/leaflet/blocks/blockquote' 10 11 import * as PubLeafletBlocksCode from './types/pub/leaflet/blocks/code' 11 12 import * as PubLeafletBlocksHeader from './types/pub/leaflet/blocks/header' 12 13 import * as PubLeafletBlocksHorizontalRule from './types/pub/leaflet/blocks/horizontalRule' ··· 37 38 38 39 export * as PubLeafletDocument from './types/pub/leaflet/document' 39 40 export * as PubLeafletPublication from './types/pub/leaflet/publication' 41 + export * as PubLeafletBlocksBlockquote from './types/pub/leaflet/blocks/blockquote' 40 42 export * as PubLeafletBlocksCode from './types/pub/leaflet/blocks/code' 41 43 export * as PubLeafletBlocksHeader from './types/pub/leaflet/blocks/header' 42 44 export * as PubLeafletBlocksHorizontalRule from './types/pub/leaflet/blocks/horizontalRule'
+24
lexicons/api/lexicons.ts
··· 161 161 }, 162 162 }, 163 163 }, 164 + PubLeafletBlocksBlockquote: { 165 + lexicon: 1, 166 + id: 'pub.leaflet.blocks.blockquote', 167 + defs: { 168 + main: { 169 + type: 'object', 170 + required: ['plaintext'], 171 + properties: { 172 + plaintext: { 173 + type: 'string', 174 + }, 175 + facets: { 176 + type: 'array', 177 + items: { 178 + type: 'ref', 179 + ref: 'lex:pub.leaflet.richtext.facet', 180 + }, 181 + }, 182 + }, 183 + }, 184 + }, 185 + }, 164 186 PubLeafletBlocksCode: { 165 187 lexicon: 1, 166 188 id: 'pub.leaflet.blocks.code', ··· 407 429 type: 'union', 408 430 refs: [ 409 431 'lex:pub.leaflet.blocks.text', 432 + 'lex:pub.leaflet.blocks.blockquote', 410 433 'lex:pub.leaflet.blocks.header', 411 434 'lex:pub.leaflet.blocks.image', 412 435 'lex:pub.leaflet.blocks.unorderedList', ··· 1661 1684 export const ids = { 1662 1685 PubLeafletDocument: 'pub.leaflet.document', 1663 1686 PubLeafletPublication: 'pub.leaflet.publication', 1687 + PubLeafletBlocksBlockquote: 'pub.leaflet.blocks.blockquote', 1664 1688 PubLeafletBlocksCode: 'pub.leaflet.blocks.code', 1665 1689 PubLeafletBlocksHeader: 'pub.leaflet.blocks.header', 1666 1690 PubLeafletBlocksHorizontalRule: 'pub.leaflet.blocks.horizontalRule',
+28
lexicons/api/types/pub/leaflet/blocks/blockquote.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 PubLeafletRichtextFacet from '../richtext/facet' 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate 12 + const id = 'pub.leaflet.blocks.blockquote' 13 + 14 + export interface Main { 15 + $type?: 'pub.leaflet.blocks.blockquote' 16 + plaintext: string 17 + facets?: PubLeafletRichtextFacet.Main[] 18 + } 19 + 20 + const hashMain = 'main' 21 + 22 + export function isMain<V>(v: V) { 23 + return is$typed(v, id, hashMain) 24 + } 25 + 26 + export function validateMain<V>(v: V) { 27 + return validate<Main & V>(v, id, hashMain) 28 + }
+2
lexicons/api/types/pub/leaflet/pages/linearDocument.ts
··· 6 6 import { validate as _validate } from '../../../../lexicons' 7 7 import { $Typed, is$typed as _is$typed, OmitKey } from '../../../../util' 8 8 import type * as PubLeafletBlocksText from '../blocks/text' 9 + import type * as PubLeafletBlocksBlockquote from '../blocks/blockquote' 9 10 import type * as PubLeafletBlocksHeader from '../blocks/header' 10 11 import type * as PubLeafletBlocksImage from '../blocks/image' 11 12 import type * as PubLeafletBlocksUnorderedList from '../blocks/unorderedList' ··· 37 38 $type?: 'pub.leaflet.pages.linearDocument#block' 38 39 block: 39 40 | $Typed<PubLeafletBlocksText.Main> 41 + | $Typed<PubLeafletBlocksBlockquote.Main> 40 42 | $Typed<PubLeafletBlocksHeader.Main> 41 43 | $Typed<PubLeafletBlocksImage.Main> 42 44 | $Typed<PubLeafletBlocksUnorderedList.Main>
+24
lexicons/pub/leaflet/blocks/blockquote.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "pub.leaflet.blocks.blockquote", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "required": [ 8 + "plaintext" 9 + ], 10 + "properties": { 11 + "plaintext": { 12 + "type": "string" 13 + }, 14 + "facets": { 15 + "type": "array", 16 + "items": { 17 + "type": "ref", 18 + "ref": "pub.leaflet.richtext.facet" 19 + } 20 + } 21 + } 22 + } 23 + } 24 + }
+1
lexicons/pub/leaflet/pages/linearDocument.json
··· 24 24 "type": "union", 25 25 "refs": [ 26 26 "pub.leaflet.blocks.text", 27 + "pub.leaflet.blocks.blockquote", 27 28 "pub.leaflet.blocks.header", 28 29 "pub.leaflet.blocks.image", 29 30 "pub.leaflet.blocks.unorderedList",
+19
lexicons/src/blocks.ts
··· 19 19 }, 20 20 }; 21 21 22 + export const PubLeafletBlocksBlockQuote: LexiconDoc = { 23 + lexicon: 1, 24 + id: "pub.leaflet.blocks.blockquote", 25 + defs: { 26 + main: { 27 + type: "object", 28 + required: ["plaintext"], 29 + properties: { 30 + plaintext: { type: "string" }, 31 + facets: { 32 + type: "array", 33 + items: { type: "ref", ref: PubLeafletRichTextFacet.id }, 34 + }, 35 + }, 36 + }, 37 + }, 38 + }; 39 + 22 40 export const PubLeafletBlocksHorizontalRule: LexiconDoc = { 23 41 lexicon: 1, 24 42 id: "pub.leaflet.blocks.horizontalRule", ··· 175 193 }; 176 194 export const BlockLexicons = [ 177 195 PubLeafletBlocksText, 196 + PubLeafletBlocksBlockQuote, 178 197 PubLeafletBlocksHeader, 179 198 PubLeafletBlocksImage, 180 199 PubLeafletBlocksUnorderedList,
+1
src/replicache/attributes.ts
··· 332 332 | "bluesky-post" 333 333 | "math" 334 334 | "code" 335 + | "blockquote" 335 336 | "horizontal-rule"; 336 337 }; 337 338 "canvas-pattern-union": {
+14 -5
src/utils/focusBlock.ts
··· 1 - import { TextSelection } from "prosemirror-state"; 1 + import { NodeSelection, TextSelection } from "prosemirror-state"; 2 2 import { useUIState } from "src/useUIState"; 3 3 import { Block } from "components/Blocks/Block"; 4 4 import { elementId } from "src/utils/elementId"; ··· 56 56 } 57 57 58 58 // if its not a text block, that's all we need to do 59 - if (block.type !== "text" && block.type !== "heading") { 59 + if ( 60 + block.type !== "text" && 61 + block.type !== "heading" && 62 + block.type !== "blockquote" 63 + ) { 60 64 return true; 61 65 } 62 66 // if its a text block, and not an empty block that is last on the page, ··· 101 105 } 102 106 } 103 107 104 - nextBlock.view.dispatch( 105 - tr.setSelection(TextSelection.create(tr.doc, pos?.pos || 1)), 106 - ); 108 + if (block.type === "blockquote") { 109 + let sel = NodeSelection.create(tr.doc, 0); 110 + nextBlock.view.dispatch(tr.setSelection(sel)); 111 + } else { 112 + nextBlock.view.dispatch( 113 + tr.setSelection(TextSelection.create(tr.doc, pos?.pos || 1)), 114 + ); 115 + } 107 116 nextBlock.view.focus(); 108 117 } 109 118
+25
src/utils/getBlocksAsHTML.tsx
··· 162 162 </div>, 163 163 ); 164 164 } 165 + if (b.type === "blockquote") { 166 + let value = (await scanIndex(tx).eav(b.value, "block/text"))[0]; 167 + if (!value) return "<blockquote></blockquote>"; 168 + let doc = new Y.Doc(); 169 + const update = base64.toByteArray(value.data.value); 170 + Y.applyUpdate(doc, update); 171 + let nodes = doc.getXmlElement("prosemirror").toArray(); 172 + //Have to handle this specially because it's a multi-line block 173 + return `<blockquote>${nodes 174 + .map((node) => { 175 + if (node.constructor === Y.XmlElement) { 176 + let children = node.toArray(); 177 + if (children.length === 0) return "<p></p>"; 178 + return renderToStaticMarkup( 179 + <RenderYJSFragment 180 + attrs={{ 181 + "data-alignment": alignment?.data.value, 182 + }} 183 + node={node} 184 + />, 185 + ); 186 + } 187 + }) 188 + .join("\n")}</blockquote>`; 189 + } 165 190 let value = (await scanIndex(tx).eav(b.value, "block/text"))[0]; 166 191 if (!value) 167 192 return ignoreWrapper ? "" : `<${wrapper || "p"}></${wrapper || "p"}>`;
+68
supabase/migrations/20250809164020_add_comments_table.sql
··· 1 + create table "public"."comments_on_documents" ( 2 + "uri" text not null, 3 + "record" jsonb not null, 4 + "document" text, 5 + "indexed_at" timestamp with time zone not null default now(), 6 + "profile" text 7 + ); 8 + 9 + 10 + alter table "public"."comments_on_documents" enable row level security; 11 + 12 + CREATE UNIQUE INDEX comments_on_documents_pkey ON public.comments_on_documents USING btree (uri); 13 + 14 + alter table "public"."comments_on_documents" add constraint "comments_on_documents_pkey" PRIMARY KEY using index "comments_on_documents_pkey"; 15 + 16 + alter table "public"."comments_on_documents" add constraint "comments_on_documents_document_fkey" FOREIGN KEY (document) REFERENCES documents(uri) ON UPDATE CASCADE ON DELETE CASCADE not valid; 17 + 18 + alter table "public"."comments_on_documents" validate constraint "comments_on_documents_document_fkey"; 19 + 20 + alter table "public"."comments_on_documents" add constraint "comments_on_documents_profile_fkey" FOREIGN KEY (profile) REFERENCES bsky_profiles(did) ON UPDATE CASCADE ON DELETE SET NULL not valid; 21 + 22 + alter table "public"."comments_on_documents" validate constraint "comments_on_documents_profile_fkey"; 23 + 24 + alter table "public"."publications" add constraint "publications_identity_did_fkey" FOREIGN KEY (identity_did) REFERENCES identities(atp_did) ON DELETE CASCADE not valid; 25 + 26 + alter table "public"."publications" validate constraint "publications_identity_did_fkey"; 27 + 28 + grant delete on table "public"."comments_on_documents" to "anon"; 29 + 30 + grant insert on table "public"."comments_on_documents" to "anon"; 31 + 32 + grant references on table "public"."comments_on_documents" to "anon"; 33 + 34 + grant select on table "public"."comments_on_documents" to "anon"; 35 + 36 + grant trigger on table "public"."comments_on_documents" to "anon"; 37 + 38 + grant truncate on table "public"."comments_on_documents" to "anon"; 39 + 40 + grant update on table "public"."comments_on_documents" to "anon"; 41 + 42 + grant delete on table "public"."comments_on_documents" to "authenticated"; 43 + 44 + grant insert on table "public"."comments_on_documents" to "authenticated"; 45 + 46 + grant references on table "public"."comments_on_documents" to "authenticated"; 47 + 48 + grant select on table "public"."comments_on_documents" to "authenticated"; 49 + 50 + grant trigger on table "public"."comments_on_documents" to "authenticated"; 51 + 52 + grant truncate on table "public"."comments_on_documents" to "authenticated"; 53 + 54 + grant update on table "public"."comments_on_documents" to "authenticated"; 55 + 56 + grant delete on table "public"."comments_on_documents" to "service_role"; 57 + 58 + grant insert on table "public"."comments_on_documents" to "service_role"; 59 + 60 + grant references on table "public"."comments_on_documents" to "service_role"; 61 + 62 + grant select on table "public"."comments_on_documents" to "service_role"; 63 + 64 + grant trigger on table "public"."comments_on_documents" to "service_role"; 65 + 66 + grant truncate on table "public"."comments_on_documents" to "service_role"; 67 + 68 + grant update on table "public"."comments_on_documents" to "service_role";