diff --git a/app/globals.css b/app/globals.css index d2872762..fb80c249 100644 --- a/app/globals.css +++ b/app/globals.css @@ -128,6 +128,10 @@ /* END GLOBAL STYLING */ } +blockquote { + margin: 0; +} + /* Hide scrollbar for Chrome, Safari and Opera */ .no-scrollbar::-webkit-scrollbar { display: none; diff --git a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx index 632386bd..73febd83 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx @@ -9,6 +9,7 @@ import { PubLeafletDocument, PubLeafletPagesLinearDocument, PubLeafletBlocksHorizontalRule, + PubLeafletBlocksBlockquote, } from "lexicons/api"; import { blobRefToSrc } from "src/utils/blobRefToSrc"; import { TextBlock } from "./TextBlock"; @@ -196,6 +197,21 @@ let Block = ({ ); } + case PubLeafletBlocksBlockquote.isMain(b.block): { + return ( +
++ ); + } case PubLeafletBlocksText.isMain(b.block): return (+
diff --git a/components/Blocks/Block.tsx b/components/Blocks/Block.tsx
index 91f4c1f1..8fa2a138 100644
--- a/components/Blocks/Block.tsx
+++ b/components/Blocks/Block.tsx
@@ -175,6 +175,7 @@ const BlockTypeComponents: {
math: MathBlock,
card: PageLinkBlock,
text: TextBlock,
+ blockquote: TextBlock,
heading: TextBlock,
image: ImageBlock,
link: ExternalLinkBlock,
diff --git a/components/Blocks/BlockCommands.tsx b/components/Blocks/BlockCommands.tsx
index 303b130b..cbd2d940 100644
--- a/components/Blocks/BlockCommands.tsx
+++ b/components/Blocks/BlockCommands.tsx
@@ -31,6 +31,8 @@ import { BlockRSVPSmall } from "components/Icons/BlockRSVPSmall";
import { ListUnorderedSmall } from "components/Toolbar/ListToolbar";
import { BlockMathSmall } from "components/Icons/BlockMathSmall";
import { BlockCodeSmall } from "components/Icons/BlockCodeSmall";
+import { QuoteTiny } from "components/Icons/QuoteTiny";
+import { QuoteSmall } from "components/Icons/QuoteSmall";
type Props = {
parent: string;
@@ -158,6 +160,16 @@ export const blockCommands: Command[] = [
clearCommandSearchText(entity);
},
},
+ {
+ name: "Block Quote",
+ icon:
]{1})\s$/, (state, match) => {
+ let tr = state.tr;
+ tr.delete(0, 2);
+ repRef.current?.mutate.assertFact({
+ entity: propsRef.current.entityID,
+ attribute: "block/type",
+ data: { type: "block-type-union", value: "blockquote" },
+ });
+ return tr;
+ }),
+
//Header
new InputRule(/^([#]{1,3})\s$/, (state, match) => {
let tr = state.tr;
diff --git a/components/Blocks/TextBlock/keymap.ts b/components/Blocks/TextBlock/keymap.ts
index f68124fd..8e123f83 100644
--- a/components/Blocks/TextBlock/keymap.ts
+++ b/components/Blocks/TextBlock/keymap.ts
@@ -2,7 +2,7 @@ import { BlockProps } from "../Block";
import { focusBlock } from "src/utils/focusBlock";
import { EditorView } from "prosemirror-view";
import { generateKeyBetween } from "fractional-indexing";
-import { setBlockType, toggleMark } from "prosemirror-commands";
+import { baseKeymap, setBlockType, toggleMark } from "prosemirror-commands";
import { keymap } from "prosemirror-keymap";
import {
Command,
@@ -30,6 +30,7 @@ export const TextBlockKeymap = (
propsRef: PropsRef,
repRef: RefObject | null>,
um: UndoManager,
+ multiLine?: boolean,
) =>
({
"Meta-b": toggleMark(schema.marks.strong),
@@ -132,11 +133,16 @@ export const TextBlockKeymap = (
),
"Shift-Backspace": backspace(propsRef, repRef),
Enter: (state, dispatch, view) => {
+ if (multiLine && state.doc.content.size - state.selection.anchor > 1)
+ return false;
return um.withUndoGroup(() =>
enter(propsRef, repRef)(state, dispatch, view),
);
},
"Shift-Enter": (state, dispatch, view) => {
+ if (multiLine) {
+ return baseKeymap.Enter(state, dispatch, view);
+ }
return um.withUndoGroup(() =>
enter(propsRef, repRef)(state, dispatch, view),
);
diff --git a/components/Blocks/TextBlock/useHandlePaste.ts b/components/Blocks/TextBlock/useHandlePaste.ts
index e6cb142d..fcd63fa6 100644
--- a/components/Blocks/TextBlock/useHandlePaste.ts
+++ b/components/Blocks/TextBlock/useHandlePaste.ts
@@ -3,7 +3,7 @@ import { Fact, ReplicacheMutators, useReplicache } from "src/replicache";
import { EditorView } from "prosemirror-view";
import { setEditorState, useEditorStates } from "src/state/useEditorState";
import { MarkType, DOMParser as ProsemirrorDOMParser } from "prosemirror-model";
-import { schema } from "./schema";
+import { multiBlockSchema, schema } from "./schema";
import { generateKeyBetween } from "fractional-indexing";
import { addImage } from "src/utils/addImage";
import { BlockProps } from "../Block";
@@ -19,6 +19,7 @@ import { addLinkBlock } from "src/utils/addLinkBlock";
import { UndoManager } from "src/undoManager";
const parser = ProsemirrorDOMParser.fromSchema(schema);
+const multilineParser = ProsemirrorDOMParser.fromSchema(multiBlockSchema);
export const useHandlePaste = (
entityID: string,
propsRef: MutableRefObject,
@@ -180,7 +181,6 @@ const createBlockFromHTML = (
getPosition: () => string;
},
) => {
- let content = parser.parse(child);
let type: Fact<"block/type">["data"]["value"] | null;
let headingLevel: number | null = null;
let hasChildren = false;
@@ -203,6 +203,10 @@ const createBlockFromHTML = (
}
}
switch (child.tagName) {
+ case "BLOCKQUOTE": {
+ type = "blockquote";
+ break;
+ }
case "LI":
case "SPAN": {
type = "text";
@@ -250,6 +254,8 @@ const createBlockFromHTML = (
default:
type = null;
}
+ let content =
+ type === "blockquote" ? multilineParser.parse(child) : parser.parse(child);
if (!type) return;
let entityID: string;
@@ -492,7 +498,9 @@ const createBlockFromHTML = (
block.editor.selection.to !== undefined
)
tr.delete(block.editor.selection.from, block.editor.selection.to);
- tr.insert(block.editor.selection.from || 1, content.content);
+ if (type === "blockquote") {
+ tr.replaceWith(0, tr.doc.content.size, content.content);
+ } else tr.insert(block.editor.selection.from || 1, content.content);
let newState = block.editor.apply(tr);
setEditorState(entityID, {
editor: newState,
@@ -559,6 +567,7 @@ function flattenHTMLToTextBlocks(element: HTMLElement): HTMLElement[] {
// Collect outer HTML for paragraph-like elements
if (
[
+ "BLOCKQUOTE",
"P",
"PRE",
"H1",
diff --git a/components/Icons/QuoteSmall.tsx b/components/Icons/QuoteSmall.tsx
new file mode 100644
index 00000000..c341fe7e
--- /dev/null
+++ b/components/Icons/QuoteSmall.tsx
@@ -0,0 +1,19 @@
+import { Props } from "./Props";
+
+export const QuoteSmall = (props: Props) => {
+ return (
+
+ );
+};
diff --git a/components/Toolbar/index.tsx b/components/Toolbar/index.tsx
index bb38a256..d9047ebc 100644
--- a/components/Toolbar/index.tsx
+++ b/components/Toolbar/index.tsx
@@ -65,7 +65,11 @@ export const Toolbar = (props: { pageID: string; blockID: string }) => {
useEffect(() => {
if (!blockType) return;
- if (blockType !== "heading" && blockType !== "text") {
+ if (
+ blockType !== "heading" &&
+ blockType !== "text" &&
+ blockType !== "blockquote"
+ ) {
setToolbarState("block");
} else {
setToolbarState("default");
diff --git a/lexicons/api/index.ts b/lexicons/api/index.ts
index 8081a202..79f28f30 100644
--- a/lexicons/api/index.ts
+++ b/lexicons/api/index.ts
@@ -7,6 +7,7 @@ import { CID } from 'multiformats/cid'
import { OmitKey, Un$Typed } from './util'
import * as PubLeafletDocument from './types/pub/leaflet/document'
import * as PubLeafletPublication from './types/pub/leaflet/publication'
+import * as PubLeafletBlocksBlockquote from './types/pub/leaflet/blocks/blockquote'
import * as PubLeafletBlocksCode from './types/pub/leaflet/blocks/code'
import * as PubLeafletBlocksHeader from './types/pub/leaflet/blocks/header'
import * as PubLeafletBlocksHorizontalRule from './types/pub/leaflet/blocks/horizontalRule'
@@ -37,6 +38,7 @@ import * as AppBskyActorProfile from './types/app/bsky/actor/profile'
export * as PubLeafletDocument from './types/pub/leaflet/document'
export * as PubLeafletPublication from './types/pub/leaflet/publication'
+export * as PubLeafletBlocksBlockquote from './types/pub/leaflet/blocks/blockquote'
export * as PubLeafletBlocksCode from './types/pub/leaflet/blocks/code'
export * as PubLeafletBlocksHeader from './types/pub/leaflet/blocks/header'
export * as PubLeafletBlocksHorizontalRule from './types/pub/leaflet/blocks/horizontalRule'
diff --git a/lexicons/api/lexicons.ts b/lexicons/api/lexicons.ts
index f54ffa62..fc429bd1 100644
--- a/lexicons/api/lexicons.ts
+++ b/lexicons/api/lexicons.ts
@@ -161,6 +161,28 @@ export const schemaDict = {
},
},
},
+ PubLeafletBlocksBlockquote: {
+ lexicon: 1,
+ id: 'pub.leaflet.blocks.blockquote',
+ defs: {
+ main: {
+ type: 'object',
+ required: ['plaintext'],
+ properties: {
+ plaintext: {
+ type: 'string',
+ },
+ facets: {
+ type: 'array',
+ items: {
+ type: 'ref',
+ ref: 'lex:pub.leaflet.richtext.facet',
+ },
+ },
+ },
+ },
+ },
+ },
PubLeafletBlocksCode: {
lexicon: 1,
id: 'pub.leaflet.blocks.code',
@@ -407,6 +429,7 @@ export const schemaDict = {
type: 'union',
refs: [
'lex:pub.leaflet.blocks.text',
+ 'lex:pub.leaflet.blocks.blockquote',
'lex:pub.leaflet.blocks.header',
'lex:pub.leaflet.blocks.image',
'lex:pub.leaflet.blocks.unorderedList',
@@ -1661,6 +1684,7 @@ export function validate(
export const ids = {
PubLeafletDocument: 'pub.leaflet.document',
PubLeafletPublication: 'pub.leaflet.publication',
+ PubLeafletBlocksBlockquote: 'pub.leaflet.blocks.blockquote',
PubLeafletBlocksCode: 'pub.leaflet.blocks.code',
PubLeafletBlocksHeader: 'pub.leaflet.blocks.header',
PubLeafletBlocksHorizontalRule: 'pub.leaflet.blocks.horizontalRule',
diff --git a/lexicons/api/types/pub/leaflet/blocks/blockquote.ts b/lexicons/api/types/pub/leaflet/blocks/blockquote.ts
new file mode 100644
index 00000000..54365f6c
--- /dev/null
+++ b/lexicons/api/types/pub/leaflet/blocks/blockquote.ts
@@ -0,0 +1,28 @@
+/**
+ * GENERATED CODE - DO NOT MODIFY
+ */
+import { ValidationResult, BlobRef } from '@atproto/lexicon'
+import { CID } from 'multiformats/cid'
+import { validate as _validate } from '../../../../lexicons'
+import { $Typed, is$typed as _is$typed, OmitKey } from '../../../../util'
+import type * as PubLeafletRichtextFacet from '../richtext/facet'
+
+const is$typed = _is$typed,
+ validate = _validate
+const id = 'pub.leaflet.blocks.blockquote'
+
+export interface Main {
+ $type?: 'pub.leaflet.blocks.blockquote'
+ plaintext: string
+ facets?: PubLeafletRichtextFacet.Main[]
+}
+
+const hashMain = 'main'
+
+export function isMain(v: V) {
+ return is$typed(v, id, hashMain)
+}
+
+export function validateMain(v: V) {
+ return validate(v, id, hashMain)
+}
diff --git a/lexicons/api/types/pub/leaflet/pages/linearDocument.ts b/lexicons/api/types/pub/leaflet/pages/linearDocument.ts
index e29bab03..110a48a1 100644
--- a/lexicons/api/types/pub/leaflet/pages/linearDocument.ts
+++ b/lexicons/api/types/pub/leaflet/pages/linearDocument.ts
@@ -6,6 +6,7 @@ import { CID } from 'multiformats/cid'
import { validate as _validate } from '../../../../lexicons'
import { $Typed, is$typed as _is$typed, OmitKey } from '../../../../util'
import type * as PubLeafletBlocksText from '../blocks/text'
+import type * as PubLeafletBlocksBlockquote from '../blocks/blockquote'
import type * as PubLeafletBlocksHeader from '../blocks/header'
import type * as PubLeafletBlocksImage from '../blocks/image'
import type * as PubLeafletBlocksUnorderedList from '../blocks/unorderedList'
@@ -37,6 +38,7 @@ export interface Block {
$type?: 'pub.leaflet.pages.linearDocument#block'
block:
| $Typed
+ | $Typed
| $Typed
| $Typed
| $Typed
diff --git a/lexicons/pub/leaflet/blocks/blockquote.json b/lexicons/pub/leaflet/blocks/blockquote.json
new file mode 100644
index 00000000..bf4e9555
--- /dev/null
+++ b/lexicons/pub/leaflet/blocks/blockquote.json
@@ -0,0 +1,24 @@
+{
+ "lexicon": 1,
+ "id": "pub.leaflet.blocks.blockquote",
+ "defs": {
+ "main": {
+ "type": "object",
+ "required": [
+ "plaintext"
+ ],
+ "properties": {
+ "plaintext": {
+ "type": "string"
+ },
+ "facets": {
+ "type": "array",
+ "items": {
+ "type": "ref",
+ "ref": "pub.leaflet.richtext.facet"
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/lexicons/pub/leaflet/pages/linearDocument.json b/lexicons/pub/leaflet/pages/linearDocument.json
index aea5936f..de763d86 100644
--- a/lexicons/pub/leaflet/pages/linearDocument.json
+++ b/lexicons/pub/leaflet/pages/linearDocument.json
@@ -24,6 +24,7 @@
"type": "union",
"refs": [
"pub.leaflet.blocks.text",
+ "pub.leaflet.blocks.blockquote",
"pub.leaflet.blocks.header",
"pub.leaflet.blocks.image",
"pub.leaflet.blocks.unorderedList",
diff --git a/lexicons/src/blocks.ts b/lexicons/src/blocks.ts
index 850f477f..c499738b 100644
--- a/lexicons/src/blocks.ts
+++ b/lexicons/src/blocks.ts
@@ -19,6 +19,24 @@ export const PubLeafletBlocksText: LexiconDoc = {
},
};
+export const PubLeafletBlocksBlockQuote: LexiconDoc = {
+ lexicon: 1,
+ id: "pub.leaflet.blocks.blockquote",
+ defs: {
+ main: {
+ type: "object",
+ required: ["plaintext"],
+ properties: {
+ plaintext: { type: "string" },
+ facets: {
+ type: "array",
+ items: { type: "ref", ref: PubLeafletRichTextFacet.id },
+ },
+ },
+ },
+ },
+};
+
export const PubLeafletBlocksHorizontalRule: LexiconDoc = {
lexicon: 1,
id: "pub.leaflet.blocks.horizontalRule",
@@ -175,6 +193,7 @@ export const PubLeafletBlocksUnorderedList: LexiconDoc = {
};
export const BlockLexicons = [
PubLeafletBlocksText,
+ PubLeafletBlocksBlockQuote,
PubLeafletBlocksHeader,
PubLeafletBlocksImage,
PubLeafletBlocksUnorderedList,
diff --git a/src/replicache/attributes.ts b/src/replicache/attributes.ts
index a574c7bc..df31d318 100644
--- a/src/replicache/attributes.ts
+++ b/src/replicache/attributes.ts
@@ -332,6 +332,7 @@ export type Data = {
| "bluesky-post"
| "math"
| "code"
+ | "blockquote"
| "horizontal-rule";
};
"canvas-pattern-union": {
diff --git a/src/utils/focusBlock.ts b/src/utils/focusBlock.ts
index ffd827ce..2dc4272a 100644
--- a/src/utils/focusBlock.ts
+++ b/src/utils/focusBlock.ts
@@ -1,4 +1,4 @@
-import { TextSelection } from "prosemirror-state";
+import { NodeSelection, TextSelection } from "prosemirror-state";
import { useUIState } from "src/useUIState";
import { Block } from "components/Blocks/Block";
import { elementId } from "src/utils/elementId";
@@ -56,7 +56,11 @@ export function focusBlock(
}
// if its not a text block, that's all we need to do
- if (block.type !== "text" && block.type !== "heading") {
+ if (
+ block.type !== "text" &&
+ block.type !== "heading" &&
+ block.type !== "blockquote"
+ ) {
return true;
}
// if its a text block, and not an empty block that is last on the page,
@@ -101,9 +105,14 @@ export function focusBlock(
}
}
- nextBlock.view.dispatch(
- tr.setSelection(TextSelection.create(tr.doc, pos?.pos || 1)),
- );
+ if (block.type === "blockquote") {
+ let sel = NodeSelection.create(tr.doc, 0);
+ nextBlock.view.dispatch(tr.setSelection(sel));
+ } else {
+ nextBlock.view.dispatch(
+ tr.setSelection(TextSelection.create(tr.doc, pos?.pos || 1)),
+ );
+ }
nextBlock.view.focus();
}
diff --git a/src/utils/getBlocksAsHTML.tsx b/src/utils/getBlocksAsHTML.tsx
index c0970ce4..cb89a099 100644
--- a/src/utils/getBlocksAsHTML.tsx
+++ b/src/utils/getBlocksAsHTML.tsx
@@ -162,6 +162,31 @@ async function renderBlock(
${nodes + .map((node) => { + if (node.constructor === Y.XmlElement) { + let children = node.toArray(); + if (children.length === 0) return ""; + return renderToStaticMarkup( +`; + } let value = (await scanIndex(tx).eav(b.value, "block/text"))[0]; if (!value) return ignoreWrapper ? "" : `<${wrapper || "p"}>${wrapper || "p"}>`; diff --git a/supabase/migrations/20250809164020_add_comments_table.sql b/supabase/migrations/20250809164020_add_comments_table.sql new file mode 100644 index 00000000..bd32145d --- /dev/null +++ b/supabase/migrations/20250809164020_add_comments_table.sql @@ -0,0 +1,68 @@ +create table "public"."comments_on_documents" ( + "uri" text not null, + "record" jsonb not null, + "document" text, + "indexed_at" timestamp with time zone not null default now(), + "profile" text +); + + +alter table "public"."comments_on_documents" enable row level security; + +CREATE UNIQUE INDEX comments_on_documents_pkey ON public.comments_on_documents USING btree (uri); + +alter table "public"."comments_on_documents" add constraint "comments_on_documents_pkey" PRIMARY KEY using index "comments_on_documents_pkey"; + +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; + +alter table "public"."comments_on_documents" validate constraint "comments_on_documents_document_fkey"; + +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; + +alter table "public"."comments_on_documents" validate constraint "comments_on_documents_profile_fkey"; + +alter table "public"."publications" add constraint "publications_identity_did_fkey" FOREIGN KEY (identity_did) REFERENCES identities(atp_did) ON DELETE CASCADE not valid; + +alter table "public"."publications" validate constraint "publications_identity_did_fkey"; + +grant delete on table "public"."comments_on_documents" to "anon"; + +grant insert on table "public"."comments_on_documents" to "anon"; + +grant references on table "public"."comments_on_documents" to "anon"; + +grant select on table "public"."comments_on_documents" to "anon"; + +grant trigger on table "public"."comments_on_documents" to "anon"; + +grant truncate on table "public"."comments_on_documents" to "anon"; + +grant update on table "public"."comments_on_documents" to "anon"; + +grant delete on table "public"."comments_on_documents" to "authenticated"; + +grant insert on table "public"."comments_on_documents" to "authenticated"; + +grant references on table "public"."comments_on_documents" to "authenticated"; + +grant select on table "public"."comments_on_documents" to "authenticated"; + +grant trigger on table "public"."comments_on_documents" to "authenticated"; + +grant truncate on table "public"."comments_on_documents" to "authenticated"; + +grant update on table "public"."comments_on_documents" to "authenticated"; + +grant delete on table "public"."comments_on_documents" to "service_role"; + +grant insert on table "public"."comments_on_documents" to "service_role"; + +grant references on table "public"."comments_on_documents" to "service_role"; + +grant select on table "public"."comments_on_documents" to "service_role"; + +grant trigger on table "public"."comments_on_documents" to "service_role"; + +grant truncate on table "public"."comments_on_documents" to "service_role"; + +grant update on table "public"."comments_on_documents" to "service_role";, + ); + } + }) + .join("\n")}