diff --git a/public/Logos/Logo B&W 16x16.svg b/public/Logos/Logo B&W 16x16.svg new file mode 100644 index 00000000..70cdf637 --- /dev/null +++ b/public/Logos/Logo B&W 16x16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/Logos/Logo B&W 24x24.svg b/public/Logos/Logo B&W 24x24.svg new file mode 100644 index 00000000..3c5d0e52 --- /dev/null +++ b/public/Logos/Logo B&W 24x24.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/Logos/Logo B&W 32x32.svg b/public/Logos/Logo B&W 32x32.svg new file mode 100644 index 00000000..1f3d4770 --- /dev/null +++ b/public/Logos/Logo B&W 32x32.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/Logos/Logo B&W 64x64.svg b/public/Logos/Logo B&W 64x64.svg new file mode 100644 index 00000000..4698cfc9 --- /dev/null +++ b/public/Logos/Logo B&W 64x64.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/Logos/Logo Color 16x16.svg b/public/Logos/Logo Color 16x16.svg new file mode 100644 index 00000000..08f47c0d --- /dev/null +++ b/public/Logos/Logo Color 16x16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/Logos/Logo Color 24x24.svg b/public/Logos/Logo Color 24x24.svg new file mode 100644 index 00000000..3df296a0 --- /dev/null +++ b/public/Logos/Logo Color 24x24.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/Logos/Logo Color 32x32.svg b/public/Logos/Logo Color 32x32.svg new file mode 100644 index 00000000..6ab5d14e --- /dev/null +++ b/public/Logos/Logo Color 32x32.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/Logos/Logo Color 64x64.svg b/public/Logos/Logo Color 64x64.svg new file mode 100644 index 00000000..86bdc967 --- /dev/null +++ b/public/Logos/Logo Color 64x64.svg @@ -0,0 +1,4 @@ + + + + -- 2.51.2 From d2d722bdd3a095e6dcad29b3c43385392e2369ed Mon Sep 17 00:00:00 2001 From: celine Date: Thu, 13 Nov 2025 16:33:51 -0500 Subject: [PATCH 02/18] added some spacing for replies --- .../Interactions/Comments/CommentBox.tsx | 3 +- .../[rkey]/Interactions/Comments/index.tsx | 107 +++++++++++------- 2 files changed, 66 insertions(+), 44 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/CommentBox.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/CommentBox.tsx index 98b9e940..bffa821a 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/CommentBox.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/CommentBox.tsx @@ -43,6 +43,7 @@ export function CommentBox(props: { replyTo?: string; onSubmit?: () => void; autoFocus?: boolean; + className?: string; }) { let mountRef = useRef(null); let { @@ -216,7 +217,7 @@ export function CommentBox(props: { }, []); return ( -
+
{quote && (
diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx index ce5a589e..26f93164 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx @@ -162,6 +162,7 @@ const Replies = (props: { let [replyBoxOpen, setReplyBoxOpen] = useState(false); let [repliesOpen, setRepliesOpen] = useState(true); + let replies = props.comments .filter( (comment) => @@ -176,6 +177,9 @@ const Replies = (props: { new Date(aRecord.createdAt).getTime() ); }); + + let repliesOrReplyBoxOpen = + replyBoxOpen || (repliesOpen && replies.length > 0); return ( <>
@@ -203,49 +207,64 @@ const Replies = (props: { )}
-
- {replyBoxOpen && ( - { - setReplyBoxOpen(false); - }} - /> - )} - {repliesOpen && replies.length > 0 && ( -
- -
- {replies.map((reply) => { - return ( - - ); - })} + {repliesOrReplyBoxOpen && ( +
+ {replyBoxOpen && ( +
+ + { + setReplyBoxOpen(false); + }} + />
-
- )} -
+ )} + {repliesOpen && replies.length > 0 && ( +
+ +
+ {replies.map((reply) => { + return ( + + ); + })} +
+
+ )} +
+ )} ); }; @@ -263,7 +282,9 @@ const DatePopover = (props: { date: string }) => { return ( {timeAgoText}
+
+ {timeAgoText} +
} >
{fullDate}
-- 2.51.2 From 046e4251bcdd0ea3b3c5f93f184b7abcd671f771 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Thu, 13 Nov 2025 15:56:53 -0500 Subject: [PATCH 03/18] extract out prosemirror mounting logic from textblock --- components/Blocks/TextBlock/index.tsx | 214 ++---------------- .../Blocks/TextBlock/mountProsemirror.ts | 199 ++++++++++++++++ package-lock.json | 10 +- package.json | 1 + 4 files changed, 223 insertions(+), 201 deletions(-) create mode 100644 components/Blocks/TextBlock/mountProsemirror.ts diff --git a/components/Blocks/TextBlock/index.tsx b/components/Blocks/TextBlock/index.tsx index a2b9d3c6..e143ac84 100644 --- a/components/Blocks/TextBlock/index.tsx +++ b/components/Blocks/TextBlock/index.tsx @@ -1,32 +1,17 @@ -import { useRef, useEffect, useState, useLayoutEffect } from "react"; +import { useRef, useEffect, useState } from "react"; import { elementId } from "src/utils/elementId"; -import { baseKeymap } from "prosemirror-commands"; -import { keymap } from "prosemirror-keymap"; -import * as Y from "yjs"; -import * as base64 from "base64-js"; -import { useReplicache, useEntity, ReplicacheMutators } from "src/replicache"; +import { useReplicache, useEntity } from "src/replicache"; import { isVisible } from "src/utils/isVisible"; - import { EditorState, TextSelection } from "prosemirror-state"; -import { EditorView } from "prosemirror-view"; - -import { ySyncPlugin } from "y-prosemirror"; -import { Replicache } from "replicache"; import { RenderYJSFragment } from "./RenderYJSFragment"; import { useInitialPageLoad } from "components/InitialPageLoadProvider"; import { BlockProps } from "../Block"; import { focusBlock } from "src/utils/focusBlock"; -import { TextBlockKeymap } from "./keymap"; -import { multiBlockSchema, schema } from "./schema"; import { useUIState } from "src/useUIState"; import { addBlueskyPostBlock, addLinkBlock } from "src/utils/addLinkBlock"; import { BlockCommandBar } from "components/Blocks/BlockCommandBar"; import { useEditorStates } from "src/state/useEditorState"; import { useEntitySetContext } from "components/EntitySetProvider"; -import { useHandlePaste } from "./useHandlePaste"; -import { highlightSelectionPlugin } from "./plugins"; -import { inputrules } from "./inputRules"; -import { autolink } from "./autolink-plugin"; import { TooltipButton } from "components/Buttons"; import { blockCommands } from "../BlockCommands"; import { betterIsUrl } from "src/utils/isURL"; @@ -37,6 +22,7 @@ import { BlockImageSmall } from "components/Icons/BlockImageSmall"; import { isIOS } from "src/utils/isDevice"; import { useLeafletPublicationData } from "components/PageSWRDataProvider"; import { DotLoader } from "components/utils/DotLoader"; +import { useMountProsemirror } from "./mountProsemirror"; const HeadingStyle = { 1: "text-xl font-bold", @@ -177,21 +163,11 @@ export function RenderedTextBlock(props: { } export function BaseTextBlock(props: BlockProps & { className?: string }) { - let mountRef = useRef(null); - let actionTimeout = useRef(null); - let repRef = useRef>(null); let headingLevel = useEntity(props.entityID, "block/heading-level"); - let entity_set = useEntitySetContext(); let alignment = useEntity(props.entityID, "block/text-alignment")?.data.value || "left"; - let propsRef = useRef({ ...props, entity_set, alignment }); - useEffect(() => { - propsRef.current = { ...props, entity_set, alignment }; - }, [props, entity_set, alignment]); + let rep = useReplicache(); - useEffect(() => { - repRef.current = rep.rep; - }, [rep?.rep]); let selected = useUIState( (s) => !!s.selectedBlocks.find((b) => b.value === props.entityID), @@ -204,136 +180,13 @@ export function BaseTextBlock(props: BlockProps & { className?: string }) { justify: "text-justify", }[alignment]; - let value = useYJSValue(props.entityID); - let editorState = useEditorStates( (s) => s.editorStates[props.entityID], )?.editor; - let handlePaste = useHandlePaste(props.entityID, propsRef); - useLayoutEffect(() => { - if (!mountRef.current) return; - let km = TextBlockKeymap(propsRef, repRef, rep.undoManager); - let editor = EditorState.create({ - schema: schema, - plugins: [ - ySyncPlugin(value), - keymap(km), - inputrules(propsRef, repRef), - keymap(baseKeymap), - highlightSelectionPlugin, - autolink({ - type: schema.marks.link, - shouldAutoLink: () => true, - defaultProtocol: "https", - }), - ], - }); - - let unsubscribe = useEditorStates.subscribe((s) => { - let editorState = s.editorStates[props.entityID]; - if (editorState?.initial) return; - if (editorState?.editor) - editorState.view?.updateState(editorState.editor); - }); - let view = new EditorView( - { mount: mountRef.current }, - { - state: editor, - handlePaste, - handleClickOn: (view, _pos, node, _nodePos, _event, direct) => { - if (!direct) return; - if (node.nodeSize - 2 <= _pos) return; - let mark = - node - .nodeAt(_pos - 1) - ?.marks.find((f) => f.type === schema.marks.link) || - node - .nodeAt(Math.max(_pos - 2, 0)) - ?.marks.find((f) => f.type === schema.marks.link); - if (mark) { - window.open(mark.attrs.href, "_blank"); - } - }, - dispatchTransaction(tr) { - useEditorStates.setState((s) => { - let oldEditorState = this.state; - let newState = this.state.apply(tr); - let addToHistory = tr.getMeta("addToHistory"); - let isBulkOp = tr.getMeta("bulkOp"); - let docHasChanges = tr.steps.length !== 0 || tr.docChanged; - if (addToHistory !== false && docHasChanges) { - if (actionTimeout.current) { - window.clearTimeout(actionTimeout.current); - } else { - if (!isBulkOp) rep.undoManager.startGroup(); - } - - if (!isBulkOp) - actionTimeout.current = window.setTimeout(() => { - rep.undoManager.endGroup(); - actionTimeout.current = null; - }, 200); - rep.undoManager.add({ - redo: () => { - useEditorStates.setState((oldState) => { - let view = oldState.editorStates[props.entityID]?.view; - if (!view?.hasFocus() && !isBulkOp) view?.focus(); - return { - editorStates: { - ...oldState.editorStates, - [props.entityID]: { - ...oldState.editorStates[props.entityID]!, - editor: newState, - }, - }, - }; - }); - }, - undo: () => { - useEditorStates.setState((oldState) => { - let view = oldState.editorStates[props.entityID]?.view; - if (!view?.hasFocus() && !isBulkOp) view?.focus(); - return { - editorStates: { - ...oldState.editorStates, - [props.entityID]: { - ...oldState.editorStates[props.entityID]!, - editor: oldEditorState, - }, - }, - }; - }); - }, - }); - } - return { - editorStates: { - ...s.editorStates, - [props.entityID]: { - editor: newState, - view: this as unknown as EditorView, - initial: false, - keymap: km, - }, - }, - }; - }); - }, - }, - ); - return () => { - unsubscribe(); - view.destroy(); - useEditorStates.setState((s) => ({ - ...s, - editorStates: { - ...s.editorStates, - [props.entityID]: undefined, - }, - })); - }; - }, [props.entityID, props.parent, value, handlePaste, rep]); + let { mountRef, actionTimeout } = useMountProsemirror({ + props, + }); return ( <> @@ -586,45 +439,14 @@ const CommandOptions = (props: BlockProps & { className?: string }) => { ); }; -function useYJSValue(entityID: string) { - const [ydoc] = useState(new Y.Doc()); - const docStateFromReplicache = useEntity(entityID, "block/text"); - let rep = useReplicache(); - const [yText] = useState(ydoc.getXmlFragment("prosemirror")); - - if (docStateFromReplicache) { - const update = base64.toByteArray(docStateFromReplicache.data.value); - Y.applyUpdate(ydoc, update); - } - - useEffect(() => { - if (!rep.rep) return; - let timeout = null as null | number; - const updateReplicache = async () => { - const update = Y.encodeStateAsUpdate(ydoc); - await rep.rep?.mutate.assertFact({ - //These undos are handled above in the Prosemirror context - ignoreUndo: true, - entity: entityID, - attribute: "block/text", - data: { - value: base64.fromByteArray(update), - type: "text", - }, - }); - }; - const f = async (events: Y.YEvent[], transaction: Y.Transaction) => { - if (!transaction.origin) return; - if (timeout) clearTimeout(timeout); - timeout = window.setTimeout(async () => { - updateReplicache(); - }, 300); - }; - - yText.observeDeep(f); - return () => { - yText.unobserveDeep(f); - }; - }, [yText, entityID, rep, ydoc]); - return yText; -} +const useMentionState = () => { + const [editorState, setEditorState] = useState(null); + const [mentionState, setMentionState] = useState<{ + active: boolean; + range: { from: number; to: number } | null; + selectedMention: { handle: string; did: string } | null; + }>({ active: false, range: null, selectedMention: null }); + const mentionStateRef = useRef(mentionState); + mentionStateRef.current = mentionState; + return { mentionStateRef }; +}; diff --git a/components/Blocks/TextBlock/mountProsemirror.ts b/components/Blocks/TextBlock/mountProsemirror.ts new file mode 100644 index 00000000..e77b36bd --- /dev/null +++ b/components/Blocks/TextBlock/mountProsemirror.ts @@ -0,0 +1,199 @@ +import { useLayoutEffect, useRef, useEffect, useState } from "react"; +import { EditorState } from "prosemirror-state"; +import { EditorView } from "prosemirror-view"; +import { baseKeymap } from "prosemirror-commands"; +import { keymap } from "prosemirror-keymap"; +import { ySyncPlugin } from "y-prosemirror"; +import * as Y from "yjs"; +import * as base64 from "base64-js"; +import { Replicache } from "replicache"; +import { produce } from "immer"; + +import { schema } from "./schema"; +import { TextBlockKeymap } from "./keymap"; +import { inputrules } from "./inputRules"; +import { highlightSelectionPlugin } from "./plugins"; +import { autolink } from "./autolink-plugin"; +import { useEditorStates } from "src/state/useEditorState"; +import { + useEntity, + useReplicache, + type ReplicacheMutators, +} from "src/replicache"; +import { useHandlePaste } from "./useHandlePaste"; +import { BlockProps } from "../Block"; +import { useEntitySetContext } from "components/EntitySetProvider"; + +export function useMountProsemirror({ props }: { props: BlockProps }) { + let { entityID, parent } = props; + let rep = useReplicache(); + let mountRef = useRef(null); + const repRef = useRef | null>(null); + let value = useYJSValue(entityID); + let entity_set = useEntitySetContext(); + let alignment = + useEntity(entityID, "block/text-alignment")?.data.value || "left"; + let propsRef = useRef({ ...props, entity_set, alignment }); + let handlePaste = useHandlePaste(entityID, propsRef); + + const actionTimeout = useRef(null); + + propsRef.current = { ...props, entity_set, alignment }; + repRef.current = rep.rep; + + useLayoutEffect(() => { + if (!mountRef.current) return; + + const km = TextBlockKeymap(propsRef, repRef, rep.undoManager); + const editor = EditorState.create({ + schema: schema, + plugins: [ + ySyncPlugin(value), + keymap(km), + inputrules(propsRef, repRef), + keymap(baseKeymap), + highlightSelectionPlugin, + autolink({ + type: schema.marks.link, + shouldAutoLink: () => true, + defaultProtocol: "https", + }), + ], + }); + + const view = new EditorView( + { mount: mountRef.current }, + { + state: editor, + handlePaste, + handleClickOn: (_view, _pos, node, _nodePos, _event, direct) => { + if (!direct) return; + if (node.nodeSize - 2 <= _pos) return; + let mark = + node + .nodeAt(_pos - 1) + ?.marks.find((f) => f.type === schema.marks.link) || + node + .nodeAt(Math.max(_pos - 2, 0)) + ?.marks.find((f) => f.type === schema.marks.link); + if (mark) { + window.open(mark.attrs.href, "_blank"); + } + }, + dispatchTransaction, + }, + ); + + const unsubscribe = useEditorStates.subscribe((s) => { + let editorState = s.editorStates[entityID]; + if (editorState?.initial) return; + if (editorState?.editor) + editorState.view?.updateState(editorState.editor); + }); + + return () => { + unsubscribe(); + view.destroy(); + useEditorStates.setState((s) => ({ + ...s, + editorStates: { + ...s.editorStates, + [entityID]: undefined, + }, + })); + }; + + function dispatchTransaction(this: EditorView, tr: any) { + useEditorStates.setState((s) => { + let oldEditorState = this.state; + let newState = this.state.apply(tr); + let addToHistory = tr.getMeta("addToHistory"); + let isBulkOp = tr.getMeta("bulkOp"); + let docHasChanges = tr.steps.length !== 0 || tr.docChanged; + + // Handle undo/redo history with timeout-based grouping + if (addToHistory !== false && docHasChanges) { + if (actionTimeout.current) window.clearTimeout(actionTimeout.current); + else if (!isBulkOp) rep.undoManager.startGroup(); + + if (!isBulkOp) { + actionTimeout.current = window.setTimeout(() => { + rep.undoManager.endGroup(); + actionTimeout.current = null; + }, 200); + } + + let setState = (s: EditorState) => () => + useEditorStates.setState( + produce((draft) => { + let view = draft.editorStates[entityID]?.view; + if (!view?.hasFocus() && !isBulkOp) view?.focus(); + draft.editorStates[entityID]!.editor = s; + }), + ); + + rep.undoManager.add({ + redo: setState(newState), + undo: setState(oldEditorState), + }); + } + + return { + editorStates: { + ...s.editorStates, + [entityID]: { + editor: newState, + view: this as unknown as EditorView, + initial: false, + keymap: km, + }, + }, + }; + }); + } + }, [entityID, parent, value, handlePaste, rep]); + return { mountRef, actionTimeout }; +} + +function useYJSValue(entityID: string) { + const [ydoc] = useState(new Y.Doc()); + const docStateFromReplicache = useEntity(entityID, "block/text"); + let rep = useReplicache(); + const [yText] = useState(ydoc.getXmlFragment("prosemirror")); + + if (docStateFromReplicache) { + const update = base64.toByteArray(docStateFromReplicache.data.value); + Y.applyUpdate(ydoc, update); + } + + useEffect(() => { + if (!rep.rep) return; + let timeout = null as null | number; + const updateReplicache = async () => { + const update = Y.encodeStateAsUpdate(ydoc); + await rep.rep?.mutate.assertFact({ + //These undos are handled above in the Prosemirror context + ignoreUndo: true, + entity: entityID, + attribute: "block/text", + data: { + value: base64.fromByteArray(update), + type: "text", + }, + }); + }; + const f = async (events: Y.YEvent[], transaction: Y.Transaction) => { + if (!transaction.origin) return; + if (timeout) clearTimeout(timeout); + timeout = window.setTimeout(async () => { + updateReplicache(); + }, 300); + }; + + yText.observeDeep(f); + return () => { + yText.unobserveDeep(f); + }; + }, [yText, entityID, rep, ydoc]); + return yText; +} diff --git a/package-lock.json b/package-lock.json index 0d806d73..48d6531c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "feed": "^5.1.0", "fractional-indexing": "^3.2.0", "hono": "^4.7.11", + "immer": "^10.2.0", "inngest": "^3.40.1", "ioredis": "^5.6.1", "katex": "^0.16.22", @@ -10877,11 +10878,10 @@ } }, "node_modules/immer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", - "optional": true, - "peer": true, + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" diff --git a/package.json b/package.json index 19bffcf9..74fbb5ee 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "feed": "^5.1.0", "fractional-indexing": "^3.2.0", "hono": "^4.7.11", + "immer": "^10.2.0", "inngest": "^3.40.1", "ioredis": "^5.6.1", "katex": "^0.16.22", -- 2.51.2 From 3d47f0306456dad03390a5fa8608164ec24352ac Mon Sep 17 00:00:00 2001 From: celine Date: Fri, 14 Nov 2025 15:16:46 -0500 Subject: [PATCH 04/18] added better empty state message for notifications --- app/(home-pages)/notifications/NotificationList.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/(home-pages)/notifications/NotificationList.tsx b/app/(home-pages)/notifications/NotificationList.tsx index 35a974ba..bc0f054b 100644 --- a/app/(home-pages)/notifications/NotificationList.tsx +++ b/app/(home-pages)/notifications/NotificationList.tsx @@ -23,10 +23,12 @@ export function NotificationList({ }, 500); }, []); - if (notifications.length === 0) + if (notifications.length !== 0) return ( -
- no notifications yet... +
+
no notifications yet...
+ Here, you'll find notifications about new follows, comments, + mentions, and replies!
); return ( -- 2.51.2 From df74efe5d21a5f156350660894aa7680b3a888e8 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 16 Nov 2025 23:44:54 -0500 Subject: [PATCH 05/18] render empty block as
--- components/Blocks/TextBlock/RenderYJSFragment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Blocks/TextBlock/RenderYJSFragment.tsx b/components/Blocks/TextBlock/RenderYJSFragment.tsx index 132e72a7..94dc872f 100644 --- a/components/Blocks/TextBlock/RenderYJSFragment.tsx +++ b/components/Blocks/TextBlock/RenderYJSFragment.tsx @@ -27,7 +27,7 @@ export function RenderYJSFragment({ return ( {children.length === 0 ? ( -
+
) : ( node.toArray().map((node, index) => { if (node.constructor === XmlText) { -- 2.51.2 From 045e283b252eacea1f5b83406a6a0dfdf117dd12 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 16 Nov 2025 23:45:06 -0500 Subject: [PATCH 06/18] mark yjs external package --- next.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/next.config.js b/next.config.js index f54ed91f..68b3188e 100644 --- a/next.config.js +++ b/next.config.js @@ -21,6 +21,7 @@ const nextConfig = { }, ]; }, + serverExternalPackages: ["yjs"], pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"], images: { loader: "custom", -- 2.51.2 From a613e397efca2cb6b7ca41e2c8765cf80e70350c Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 17 Nov 2025 01:25:49 -0500 Subject: [PATCH 07/18] add alternate names to block commands --- components/Blocks/BlockCommandBar.tsx | 9 +++++++-- components/Blocks/BlockCommands.tsx | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/components/Blocks/BlockCommandBar.tsx b/components/Blocks/BlockCommandBar.tsx index 62bf0e87..2aaf7f99 100644 --- a/components/Blocks/BlockCommandBar.tsx +++ b/components/Blocks/BlockCommandBar.tsx @@ -47,9 +47,14 @@ export const BlockCommandBar = ({ }; let commandResults = blockCommands.filter((command) => { - const matchesSearch = command.name + const lowerSearchValue = searchValue.toLocaleLowerCase(); + const matchesName = command.name .toLocaleLowerCase() - .includes(searchValue.toLocaleLowerCase()); + .includes(lowerSearchValue); + const matchesAlternate = command.alternateNames?.some((altName) => + altName.toLocaleLowerCase().includes(lowerSearchValue) + ) ?? false; + const matchesSearch = matchesName || matchesAlternate; const isVisible = !pub || !command.hiddenInPublication; return matchesSearch && isVisible; }); diff --git a/components/Blocks/BlockCommands.tsx b/components/Blocks/BlockCommands.tsx index 9e8a78f8..326defaa 100644 --- a/components/Blocks/BlockCommands.tsx +++ b/components/Blocks/BlockCommands.tsx @@ -102,6 +102,7 @@ type Command = { name: string; icon: React.ReactNode; type: string; + alternateNames?: string[]; hiddenInPublication?: boolean; onSelect: ( rep: Replicache, @@ -125,6 +126,7 @@ export const blockCommands: Command[] = [ name: "Title", icon: , type: "text", + alternateNames: ["h1"], onSelect: async (rep, props, um) => { await setHeaderCommand(1, rep, props); }, @@ -133,6 +135,7 @@ export const blockCommands: Command[] = [ name: "Header", icon: , type: "text", + alternateNames: ["h2"], onSelect: async (rep, props, um) => { await setHeaderCommand(2, rep, props); }, @@ -141,6 +144,7 @@ export const blockCommands: Command[] = [ name: "Subheader", icon: , type: "text", + alternateNames: ["h3"], onSelect: async (rep, props, um) => { await setHeaderCommand(3, rep, props); }, -- 2.51.2 From b70eb51d3c5454edf8dd06c44541e235b565d553 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 17 Nov 2025 12:51:40 -0500 Subject: [PATCH 08/18] use cloudflare browser api instead of microlink --- app/[leaflet_id]/publish/publishBskyPost.ts | 17 +++---- app/api/link_previews/route.ts | 16 +++---- src/utils/getMicroLinkOgImage.ts | 49 ++++++++++++++++++--- 3 files changed, 57 insertions(+), 25 deletions(-) diff --git a/app/[leaflet_id]/publish/publishBskyPost.ts b/app/[leaflet_id]/publish/publishBskyPost.ts index 7fa16dd6..9fc2efb9 100644 --- a/app/[leaflet_id]/publish/publishBskyPost.ts +++ b/app/[leaflet_id]/publish/publishBskyPost.ts @@ -12,6 +12,10 @@ import { AtpBaseClient, PubLeafletDocument } from "lexicons/api"; import { createOauthClient } from "src/atproto-oauth"; import { supabaseServerClient } from "supabase/serverClient"; import { Json } from "supabase/database.types"; +import { + getMicroLinkOgImage, + getWebpageImage, +} from "src/utils/getMicroLinkOgImage"; export async function publishPostToBsky(args: { text: string; @@ -31,14 +35,11 @@ export async function publishPostToBsky(args: { credentialSession.fetchHandler.bind(credentialSession), ); let newPostUrl = args.url; - let preview_image = await fetch( - `https://pro.microlink.io/?url=${newPostUrl}&screenshot=true&viewport.width=1400&viewport.height=733&meta=false&embed=screenshot.url&force=true`, - { - headers: { - "x-api-key": process.env.MICROLINK_API_KEY!, - }, - }, - ); + let preview_image = await getWebpageImage(newPostUrl, { + width: 1400, + height: 733, + noCache: true, + }); let binary = await preview_image.blob(); let resized_preview_image = await sharp(await binary.arrayBuffer()) diff --git a/app/api/link_previews/route.ts b/app/api/link_previews/route.ts index 090674d9..9b7ad6b2 100644 --- a/app/api/link_previews/route.ts +++ b/app/api/link_previews/route.ts @@ -5,6 +5,10 @@ import { NextRequest } from "next/server"; import * as z from "zod"; import { createClient } from "@supabase/supabase-js"; import { Database } from "supabase/database.types"; +import { + getMicroLinkOgImage, + getWebpageImage, +} from "src/utils/getMicroLinkOgImage"; let supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, process.env.SUPABASE_SERVICE_ROLE_KEY as string, @@ -27,17 +31,7 @@ export type LinkPreviewMetadataResult = ReturnType; export type LinkPreviewImageResult = ReturnType; async function get_link_image_preview(url: string) { - let image = await fetch( - `https://pro.microlink.io/?url=${url}&screenshot&viewport.width=1400&viewport.height=1213&embed=screenshot.url&meta=false&force=true`, - { - headers: { - "x-api-key": process.env.MICROLINK_API_KEY!, - }, - next: { - revalidate: 600, - }, - }, - ); + let image = await getWebpageImage(url, { width: 1400, height: 1213 }); let key = await hash(url); if (image.status === 200) { await supabase.storage diff --git a/src/utils/getMicroLinkOgImage.ts b/src/utils/getMicroLinkOgImage.ts index 4ccf6ece..fac40494 100644 --- a/src/utils/getMicroLinkOgImage.ts +++ b/src/utils/getMicroLinkOgImage.ts @@ -2,21 +2,58 @@ import { headers } from "next/headers"; export async function getMicroLinkOgImage( path: string, - options?: { width?: number; height?: number; deviceScaleFactor?: number }, + options?: { + width?: number; + height?: number; + deviceScaleFactor?: number; + noCache?: boolean; + }, ) { const headersList = await headers(); const hostname = headersList.get("x-forwarded-host"); let protocol = headersList.get("x-forwarded-proto"); let full_path = `${protocol}://${hostname}${path}`; + return getWebpageImage(full_path, options); +} + +export async function getWebpageImage( + url: string, + options?: { + width?: number; + height?: number; + deviceScaleFactor?: number; + noCache?: boolean; + }, +) { let response = await fetch( - `https://pro.microlink.io/?url=${encodeURIComponent(full_path)}&screenshot=true&viewport.width=${options?.width || 1400}&viewport.height=${options?.height || 733}&viewport.deviceScaleFactor=${options?.deviceScaleFactor || 1}&meta=false&embed=screenshot.url&force=true`, + `https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_ACCOUNT}/browser-rendering/screenshot`, { + method: "POST", headers: { - "x-api-key": process.env.MICROLINK_API_KEY!, - }, - next: { - revalidate: 600, + "Content-type": "application/json", + Authorization: `Bearer ${process.env.CLOUDFLARE_API_TOKEN}`, }, + body: JSON.stringify({ + url, + addStyleTag: [ + { + content: `* {overflow: hidden !important; }`, + }, + ], + gotoOptions: { + waitUntil: "load", + }, + viewport: { + width: options?.width || 1400, + height: options?.height || 733, + deviceScaleFactor: options?.deviceScaleFactor, + }, + }), + next: !options?.noCache + ? undefined + : { + revalidate: 600, + }, }, ); const clonedResponse = response.clone(); -- 2.51.2 From 86030fa61064c9a68d99adbc49c2ed198a65d80c Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 17 Nov 2025 12:59:42 -0500 Subject: [PATCH 09/18] don't modify cache headers for cloudflare browser images --- src/utils/getMicroLinkOgImage.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/utils/getMicroLinkOgImage.ts b/src/utils/getMicroLinkOgImage.ts index fac40494..2d71e21c 100644 --- a/src/utils/getMicroLinkOgImage.ts +++ b/src/utils/getMicroLinkOgImage.ts @@ -56,17 +56,6 @@ export async function getWebpageImage( }, }, ); - const clonedResponse = response.clone(); - if (clonedResponse.status == 200) { - clonedResponse.headers.set( - "CDN-Cache-Control", - "s-maxage=600, stale-while-revalidate=3600", - ); - clonedResponse.headers.set( - "Cache-Control", - "s-maxage=600, stale-while-revalidate=3600", - ); - } - return clonedResponse; + return response; } -- 2.51.2 From 604cc827f41d311ea156e25d012de024b06a3af9 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 17 Nov 2025 16:18:39 -0500 Subject: [PATCH 10/18] remove serverExternalPackages yjs --- next.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/next.config.js b/next.config.js index 68b3188e..f54ed91f 100644 --- a/next.config.js +++ b/next.config.js @@ -21,7 +21,6 @@ const nextConfig = { }, ]; }, - serverExternalPackages: ["yjs"], pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"], images: { loader: "custom", -- 2.51.2 From 4e8d3d34b508ebaab441aa5c584bd94f9b8e5a3e Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 17 Nov 2025 16:20:36 -0500 Subject: [PATCH 11/18] Revert "render empty block as
" This reverts commit df74efe5d21a5f156350660894aa7680b3a888e8. --- components/Blocks/TextBlock/RenderYJSFragment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Blocks/TextBlock/RenderYJSFragment.tsx b/components/Blocks/TextBlock/RenderYJSFragment.tsx index 94dc872f..132e72a7 100644 --- a/components/Blocks/TextBlock/RenderYJSFragment.tsx +++ b/components/Blocks/TextBlock/RenderYJSFragment.tsx @@ -27,7 +27,7 @@ export function RenderYJSFragment({ return ( {children.length === 0 ? ( -
+
) : ( node.toArray().map((node, index) => { if (node.constructor === XmlText) { -- 2.51.2 From 26eff9f5fb032ee330badb0943f48d3185d80c90 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 17 Nov 2025 16:21:33 -0500 Subject: [PATCH 12/18] Revert "remove serverExternalPackages yjs" This reverts commit 604cc827f41d311ea156e25d012de024b06a3af9. --- next.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/next.config.js b/next.config.js index f54ed91f..68b3188e 100644 --- a/next.config.js +++ b/next.config.js @@ -21,6 +21,7 @@ const nextConfig = { }, ]; }, + serverExternalPackages: ["yjs"], pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"], images: { loader: "custom", -- 2.51.2 From 96b2c0b1485785027de30cef4d940f722ccc5e76 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 17 Nov 2025 16:27:10 -0500 Subject: [PATCH 13/18] scroll page in browser screenshot --- src/utils/getMicroLinkOgImage.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/getMicroLinkOgImage.ts b/src/utils/getMicroLinkOgImage.ts index 2d71e21c..c0b38592 100644 --- a/src/utils/getMicroLinkOgImage.ts +++ b/src/utils/getMicroLinkOgImage.ts @@ -35,6 +35,7 @@ export async function getWebpageImage( }, body: JSON.stringify({ url, + scrollPage: true, addStyleTag: [ { content: `* {overflow: hidden !important; }`, -- 2.51.2 From 9105cbfdf48231a2ededd043c988913468d80c79 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 17 Nov 2025 16:49:19 -0500 Subject: [PATCH 14/18] catch editor state done before subscribing --- components/Blocks/TextBlock/mountProsemirror.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/Blocks/TextBlock/mountProsemirror.ts b/components/Blocks/TextBlock/mountProsemirror.ts index e77b36bd..f4a59e32 100644 --- a/components/Blocks/TextBlock/mountProsemirror.ts +++ b/components/Blocks/TextBlock/mountProsemirror.ts @@ -91,6 +91,10 @@ export function useMountProsemirror({ props }: { props: BlockProps }) { editorState.view?.updateState(editorState.editor); }); + let editorState = useEditorStates.getState().editorStates[entityID]; + if (editorState?.editor && !editorState.initial) + editorState.view?.updateState(editorState.editor); + return () => { unsubscribe(); view.destroy(); -- 2.51.2 From 416b6959b833ab2af354f618a2843317d68d77d9 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 17 Nov 2025 17:10:57 -0500 Subject: [PATCH 15/18] make server time different from local --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 74fbb5ee..e7b52160 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "dev": "next dev --turbo", + "dev": "TZ=UTC next dev --turbo", "publish-lexicons": "tsx lexicons/publish.ts", "generate-db-types": "supabase gen types --local > supabase/database.types.ts && drizzle-kit introspect && rm -rf ./drizzle/*.sql ./drizzle/meta", "lexgen": "tsx ./lexicons/build.ts && lex gen-api ./lexicons/api ./lexicons/pub/leaflet/* ./lexicons/pub/leaflet/*/* ./lexicons/com/atproto/*/* ./lexicons/app/bsky/*/* --yes && find './lexicons/api' -type f -exec sed -i 's/\\.js'/'/g' {} \\;", -- 2.51.2 From cb5781ee29b7a53775612680cccf2b923064fcb1 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 17 Nov 2025 17:32:39 -0500 Subject: [PATCH 16/18] fix scrollbar styling in screenshots --- src/utils/getMicroLinkOgImage.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/utils/getMicroLinkOgImage.ts b/src/utils/getMicroLinkOgImage.ts index c0b38592..f9386088 100644 --- a/src/utils/getMicroLinkOgImage.ts +++ b/src/utils/getMicroLinkOgImage.ts @@ -10,8 +10,12 @@ export async function getMicroLinkOgImage( }, ) { const headersList = await headers(); - const hostname = headersList.get("x-forwarded-host"); + let hostname = headersList.get("x-forwarded-host"); let protocol = headersList.get("x-forwarded-proto"); + if (process.env.NODE_ENV === "development") { + protocol === "https"; + hostname = "leaflet.pub"; + } let full_path = `${protocol}://${hostname}${path}`; return getWebpageImage(full_path, options); } @@ -38,7 +42,7 @@ export async function getWebpageImage( scrollPage: true, addStyleTag: [ { - content: `* {overflow: hidden !important; }`, + content: `* {scrollbar-width:none; }`, }, ], gotoOptions: { -- 2.51.2 From 76438c3d29a837f78e6ed069a1ffbe12ba977565 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 17 Nov 2025 17:45:20 -0500 Subject: [PATCH 17/18] rename confusing hook and reverse datetime pageload logic Squashed commit of the following: commit c3deb565892c3a87102ba2ba11a698d6907e4955 Author: Jared Pereira Date: Mon Nov 17 17:45:07 2025 -0500 remove logs commit 3ff4755bd48b52cef44f1e88a3f0ffda12670f28 Author: Jared Pereira Date: Mon Nov 17 17:38:04 2025 -0500 rename has page loaded and switch logic commit 558d9b7cb8c08a07277ac14bab3c465571ff0e7e Author: Jared Pereira Date: Mon Nov 17 17:19:00 2025 -0500 log effectivetz and locale commit 2748feba61f5e495480c92e108496da427be9727 Author: Jared Pereira Date: Mon Nov 17 17:11:17 2025 -0500 are we getting tz on server/client? --- .../[publication]/[rkey]/PublishBskyPostBlock.tsx | 4 ++-- components/Blocks/BlueskyPostBlock/index.tsx | 1 - components/Blocks/DateTimeBlock.tsx | 4 ++-- components/Blocks/TextBlock/index.tsx | 4 ++-- components/InitialPageLoadProvider.tsx | 4 ++-- components/Providers/RequestHeadersProvider.tsx | 6 +++++- src/hooks/useLocalizedDate.ts | 10 +++++----- 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/PublishBskyPostBlock.tsx b/app/lish/[did]/[publication]/[rkey]/PublishBskyPostBlock.tsx index 914c082e..596e12e2 100644 --- a/app/lish/[did]/[publication]/[rkey]/PublishBskyPostBlock.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PublishBskyPostBlock.tsx @@ -7,7 +7,7 @@ import { elementId } from "src/utils/elementId"; import { focusBlock } from "src/utils/focusBlock"; import { AppBskyFeedDefs, AppBskyFeedPost, RichText } from "@atproto/api"; import { Separator } from "components/Layout"; -import { useInitialPageLoad } from "components/InitialPageLoadProvider"; +import { useHasPageLoaded } from "components/InitialPageLoadProvider"; import { BlueskyTiny } from "components/Icons/BlueskyTiny"; import { CommentTiny } from "components/Icons/CommentTiny"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; @@ -123,7 +123,7 @@ export const PubBlueskyPostBlock = (props: { }; const ClientDate = (props: { date?: string }) => { - let pageLoaded = useInitialPageLoad(); + let pageLoaded = useHasPageLoaded(); const formattedDate = useLocalizedDate( props.date || new Date().toISOString(), { diff --git a/components/Blocks/BlueskyPostBlock/index.tsx b/components/Blocks/BlueskyPostBlock/index.tsx index d9169daa..bb04a7e3 100644 --- a/components/Blocks/BlueskyPostBlock/index.tsx +++ b/components/Blocks/BlueskyPostBlock/index.tsx @@ -10,7 +10,6 @@ import { BlueskyEmbed, PostNotAvailable } from "./BlueskyEmbed"; import { BlueskyPostEmpty } from "./BlueskyEmpty"; import { BlueskyRichText } from "./BlueskyRichText"; import { Separator } from "components/Layout"; -import { useInitialPageLoad } from "components/InitialPageLoadProvider"; import { BlueskyTiny } from "components/Icons/BlueskyTiny"; import { CommentTiny } from "components/Icons/CommentTiny"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; diff --git a/components/Blocks/DateTimeBlock.tsx b/components/Blocks/DateTimeBlock.tsx index 6e202355..29430a33 100644 --- a/components/Blocks/DateTimeBlock.tsx +++ b/components/Blocks/DateTimeBlock.tsx @@ -8,14 +8,14 @@ import { useUIState } from "src/useUIState"; import { setHours, setMinutes } from "date-fns"; import { Separator } from "react-aria-components"; import { Checkbox } from "components/Checkbox"; -import { useInitialPageLoad } from "components/InitialPageLoadProvider"; +import { useHasPageLoaded } from "components/InitialPageLoadProvider"; import { useSpring, animated } from "@react-spring/web"; import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; import { BlockCalendarSmall } from "components/Icons/BlockCalendarSmall"; export function DateTimeBlock(props: BlockProps) { const [isClient, setIsClient] = useState(false); - let initialPageLoad = useInitialPageLoad(); + let initialPageLoad = useHasPageLoaded(); useEffect(() => { setIsClient(true); diff --git a/components/Blocks/TextBlock/index.tsx b/components/Blocks/TextBlock/index.tsx index e143ac84..842efd10 100644 --- a/components/Blocks/TextBlock/index.tsx +++ b/components/Blocks/TextBlock/index.tsx @@ -4,7 +4,7 @@ import { useReplicache, useEntity } from "src/replicache"; import { isVisible } from "src/utils/isVisible"; import { EditorState, TextSelection } from "prosemirror-state"; import { RenderYJSFragment } from "./RenderYJSFragment"; -import { useInitialPageLoad } from "components/InitialPageLoadProvider"; +import { useHasPageLoaded } from "components/InitialPageLoadProvider"; import { BlockProps } from "../Block"; import { focusBlock } from "src/utils/focusBlock"; import { useUIState } from "src/useUIState"; @@ -37,7 +37,7 @@ export function TextBlock( }, ) { let isLocked = useEntity(props.entityID, "block/is-locked"); - let initialized = useInitialPageLoad(); + let initialized = useHasPageLoaded(); let first = props.previousBlock === null; let permission = useEntitySetContext().permissions.write; diff --git a/components/InitialPageLoadProvider.tsx b/components/InitialPageLoadProvider.tsx index a40eb2ac..7a408740 100644 --- a/components/InitialPageLoadProvider.tsx +++ b/components/InitialPageLoadProvider.tsx @@ -2,11 +2,11 @@ import { useEffect } from "react"; import { create } from "zustand"; -export const useInitialPageLoad = create(() => false); +export const useHasPageLoaded = create(() => false); export function InitialPageLoad(props: { children: React.ReactNode }) { useEffect(() => { setTimeout(() => { - useInitialPageLoad.setState(() => true); + useHasPageLoaded.setState(() => true); }, 80); }, []); return <>{props.children}; diff --git a/components/Providers/RequestHeadersProvider.tsx b/components/Providers/RequestHeadersProvider.tsx index f85a374a..7915e569 100644 --- a/components/Providers/RequestHeadersProvider.tsx +++ b/components/Providers/RequestHeadersProvider.tsx @@ -21,7 +21,11 @@ export const RequestHeadersProvider = (props: { }) => { return ( {props.children} diff --git a/src/hooks/useLocalizedDate.ts b/src/hooks/useLocalizedDate.ts index 4fab87f3..ad81a4cc 100644 --- a/src/hooks/useLocalizedDate.ts +++ b/src/hooks/useLocalizedDate.ts @@ -2,7 +2,7 @@ import { useContext, useMemo } from "react"; import { DateTime } from "luxon"; import { RequestHeadersContext } from "components/Providers/RequestHeadersProvider"; -import { useInitialPageLoad } from "components/InitialPageLoadProvider"; +import { useHasPageLoaded } from "components/InitialPageLoadProvider"; /** * Hook that formats a date string using Luxon with timezone and locale from request headers. @@ -20,14 +20,14 @@ export function useLocalizedDate( options?: Intl.DateTimeFormatOptions, ): string { const { timezone, language } = useContext(RequestHeadersContext); - const isInitialPageLoad = useInitialPageLoad(); + const hasPageLoaded = useHasPageLoaded(); return useMemo(() => { // Parse the date string to Luxon DateTime let dateTime = DateTime.fromISO(dateString); // On initial page load, use header timezone. After hydration, use system timezone - const effectiveTimezone = isInitialPageLoad + const effectiveTimezone = !hasPageLoaded ? timezone : Intl.DateTimeFormat().resolvedOptions().timeZone; @@ -39,7 +39,7 @@ export function useLocalizedDate( // On initial page load, use header locale. After hydration, use system locale // Parse locale from accept-language header (take first locale) // accept-language format: "en-US,en;q=0.9,es;q=0.8" - const effectiveLocale = isInitialPageLoad + const effectiveLocale = !hasPageLoaded ? language?.split(",")[0]?.split(";")[0]?.trim() || "en-US" : Intl.DateTimeFormat().resolvedOptions().locale; @@ -49,5 +49,5 @@ export function useLocalizedDate( // Fallback to en-US if locale is invalid return dateTime.toLocaleString(options, { locale: "en-US" }); } - }, [dateString, options, timezone, language, isInitialPageLoad]); + }, [dateString, options, timezone, language, hasPageLoaded]); } -- 2.51.2 From 260781c6687a70653ae71d14208d0bb10adf27fb Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 18 Nov 2025 13:57:49 -0500 Subject: [PATCH 18/18] create leaflet_to_documents table --- drizzle/relations.ts | 24 ++++--- drizzle/schema.ts | 12 +++- supabase/database.types.ts | 39 ++++++++++++ ...251118185507_add_leaflets_to_documents.sql | 63 +++++++++++++++++++ 4 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 supabase/migrations/20251118185507_add_leaflets_to_documents.sql diff --git a/drizzle/relations.ts b/drizzle/relations.ts index ee948385..1e08d4fe 100644 --- a/drizzle/relations.ts +++ b/drizzle/relations.ts @@ -1,19 +1,15 @@ import { relations } from "drizzle-orm/relations"; -import { identities, publications, documents, comments_on_documents, bsky_profiles, entity_sets, entities, facts, email_auth_tokens, poll_votes_on_entity, permission_tokens, phone_rsvps_to_entity, custom_domains, custom_domain_routes, email_subscriptions_to_entity, atp_poll_records, atp_poll_votes, bsky_follows, subscribers_to_publications, permission_token_on_homepage, documents_in_publications, document_mentions_in_bsky, bsky_posts, publication_domains, leaflets_in_publications, publication_subscriptions, permission_token_rights } from "./schema"; +import { identities, notifications, publications, documents, comments_on_documents, bsky_profiles, entity_sets, entities, facts, email_auth_tokens, poll_votes_on_entity, permission_tokens, phone_rsvps_to_entity, custom_domains, custom_domain_routes, email_subscriptions_to_entity, atp_poll_records, atp_poll_votes, bsky_follows, subscribers_to_publications, permission_token_on_homepage, documents_in_publications, document_mentions_in_bsky, bsky_posts, publication_domains, leaflets_in_publications, publication_subscriptions, permission_token_rights } from "./schema"; -export const publicationsRelations = relations(publications, ({one, many}) => ({ +export const notificationsRelations = relations(notifications, ({one}) => ({ identity: one(identities, { - fields: [publications.identity_did], + fields: [notifications.recipient], references: [identities.atp_did] }), - subscribers_to_publications: many(subscribers_to_publications), - documents_in_publications: many(documents_in_publications), - publication_domains: many(publication_domains), - leaflets_in_publications: many(leaflets_in_publications), - publication_subscriptions: many(publication_subscriptions), })); export const identitiesRelations = relations(identities, ({one, many}) => ({ + notifications: many(notifications), publications: many(publications), email_auth_tokens: many(email_auth_tokens), bsky_profiles: many(bsky_profiles), @@ -39,6 +35,18 @@ export const identitiesRelations = relations(identities, ({one, many}) => ({ publication_subscriptions: many(publication_subscriptions), })); +export const publicationsRelations = relations(publications, ({one, many}) => ({ + identity: one(identities, { + fields: [publications.identity_did], + references: [identities.atp_did] + }), + subscribers_to_publications: many(subscribers_to_publications), + documents_in_publications: many(documents_in_publications), + publication_domains: many(publication_domains), + leaflets_in_publications: many(leaflets_in_publications), + publication_subscriptions: many(publication_subscriptions), +})); + export const comments_on_documentsRelations = relations(comments_on_documents, ({one}) => ({ document: one(documents, { fields: [comments_on_documents.document], diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 4745bf86..153b82e6 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -1,4 +1,4 @@ -import { pgTable, pgEnum, text, jsonb, index, foreignKey, timestamp, uuid, bigint, boolean, unique, uniqueIndex, smallint, primaryKey } from "drizzle-orm/pg-core" +import { pgTable, pgEnum, text, jsonb, foreignKey, timestamp, boolean, uuid, index, bigint, unique, uniqueIndex, smallint, primaryKey } from "drizzle-orm/pg-core" import { sql } from "drizzle-orm" export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) @@ -15,7 +15,7 @@ export const key_type = pgEnum("key_type", ['aead-ietf', 'aead-det', 'hmacsha512 export const rsvp_status = pgEnum("rsvp_status", ['GOING', 'NOT_GOING', 'MAYBE']) export const action = pgEnum("action", ['INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'ERROR']) export const equality_op = pgEnum("equality_op", ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'in']) -export const buckettype = pgEnum("buckettype", ['STANDARD', 'ANALYTICS']) +export const buckettype = pgEnum("buckettype", ['STANDARD', 'ANALYTICS', 'VECTOR']) export const oauth_state_store = pgTable("oauth_state_store", { @@ -23,6 +23,14 @@ export const oauth_state_store = pgTable("oauth_state_store", { state: jsonb("state").notNull(), }); +export const notifications = pgTable("notifications", { + recipient: text("recipient").notNull().references(() => identities.atp_did, { onDelete: "cascade", onUpdate: "cascade" } ), + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + read: boolean("read").default(false).notNull(), + data: jsonb("data").notNull(), + id: uuid("id").primaryKey().notNull(), +}); + export const publications = pgTable("publications", { uri: text("uri").primaryKey().notNull(), indexed_at: timestamp("indexed_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), diff --git a/supabase/database.types.ts b/supabase/database.types.ts index ae0c93b0..93efcbe3 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -624,6 +624,45 @@ export type Database = { }, ] } + leaflets_to_documents: { + Row: { + created_at: string + description: string + document: string + leaflet: string + title: string + } + Insert: { + created_at?: string + description?: string + document: string + leaflet: string + title?: string + } + Update: { + created_at?: string + description?: string + document?: string + leaflet?: string + title?: string + } + Relationships: [ + { + foreignKeyName: "leaflets_to_documents_document_fkey" + columns: ["document"] + isOneToOne: false + referencedRelation: "documents" + referencedColumns: ["uri"] + }, + { + foreignKeyName: "leaflets_to_documents_leaflet_fkey" + columns: ["leaflet"] + isOneToOne: false + referencedRelation: "permission_tokens" + referencedColumns: ["id"] + }, + ] + } notifications: { Row: { created_at: string diff --git a/supabase/migrations/20251118185507_add_leaflets_to_documents.sql b/supabase/migrations/20251118185507_add_leaflets_to_documents.sql new file mode 100644 index 00000000..f79aa7cd --- /dev/null +++ b/supabase/migrations/20251118185507_add_leaflets_to_documents.sql @@ -0,0 +1,63 @@ +create table "public"."leaflets_to_documents" ( + "leaflet" uuid not null, + "document" text not null, + "created_at" timestamp with time zone not null default now(), + "title" text not null default ''::text, + "description" text not null default ''::text +); + +alter table "public"."leaflets_to_documents" enable row level security; + +CREATE UNIQUE INDEX leaflets_to_documents_pkey ON public.leaflets_to_documents USING btree (leaflet, document); + +alter table "public"."leaflets_to_documents" add constraint "leaflets_to_documents_pkey" PRIMARY KEY using index "leaflets_to_documents_pkey"; + +alter table "public"."leaflets_to_documents" add constraint "leaflets_to_documents_document_fkey" FOREIGN KEY (document) REFERENCES documents(uri) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."leaflets_to_documents" validate constraint "leaflets_to_documents_document_fkey"; + +alter table "public"."leaflets_to_documents" add constraint "leaflets_to_documents_leaflet_fkey" FOREIGN KEY (leaflet) REFERENCES permission_tokens(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."leaflets_to_documents" validate constraint "leaflets_to_documents_leaflet_fkey"; + +grant delete on table "public"."leaflets_to_documents" to "anon"; + +grant insert on table "public"."leaflets_to_documents" to "anon"; + +grant references on table "public"."leaflets_to_documents" to "anon"; + +grant select on table "public"."leaflets_to_documents" to "anon"; + +grant trigger on table "public"."leaflets_to_documents" to "anon"; + +grant truncate on table "public"."leaflets_to_documents" to "anon"; + +grant update on table "public"."leaflets_to_documents" to "anon"; + +grant delete on table "public"."leaflets_to_documents" to "authenticated"; + +grant insert on table "public"."leaflets_to_documents" to "authenticated"; + +grant references on table "public"."leaflets_to_documents" to "authenticated"; + +grant select on table "public"."leaflets_to_documents" to "authenticated"; + +grant trigger on table "public"."leaflets_to_documents" to "authenticated"; + +grant truncate on table "public"."leaflets_to_documents" to "authenticated"; + +grant update on table "public"."leaflets_to_documents" to "authenticated"; + +grant delete on table "public"."leaflets_to_documents" to "service_role"; + +grant insert on table "public"."leaflets_to_documents" to "service_role"; + +grant references on table "public"."leaflets_to_documents" to "service_role"; + +grant select on table "public"."leaflets_to_documents" to "service_role"; + +grant trigger on table "public"."leaflets_to_documents" to "service_role"; + +grant truncate on table "public"."leaflets_to_documents" to "service_role"; + +grant update on table "public"."leaflets_to_documents" to "service_role";