diff --git a/app/lish/Subscribe.tsx b/app/lish/Subscribe.tsx index 7c443496..118292cb 100644 --- a/app/lish/Subscribe.tsx +++ b/app/lish/Subscribe.tsx @@ -262,10 +262,10 @@ const SubscribeSuccessModal = ({ - + { > {loading ? : "Get it!"} - {error && ( -
{error}
- )} + {error &&
{error}
} @@ -91,3 +89,21 @@ export const UpgradeModal = (props: { ); }; + +export const InlineUpgrade = () => { + return ( +
+ + Upgrade to Leaflet Pro! + + } + /> +
+ Analytics for all your pubs!
Emails and membership coming soon. +
+
+ ); +}; diff --git a/app/lish/[did]/[publication]/dashboard/Actions.tsx b/app/lish/[did]/[publication]/dashboard/Actions.tsx index bac897c7..f6c8b01c 100644 --- a/app/lish/[did]/[publication]/dashboard/Actions.tsx +++ b/app/lish/[did]/[publication]/dashboard/Actions.tsx @@ -21,8 +21,6 @@ export const Actions = (props: { publication: string }) => { return ( <> - {canSeePro && !isPro && } - @@ -85,19 +83,3 @@ function PublicationShareButton() { ); } - -const MobileUpgrade = () => { - return ( - } - className={`sm:hidden block bg-[var(--accent-light)]!`} - style={{ backgroundColor: "var(--accent-light) important!" }} - /> - } - /> - ); -}; diff --git a/app/lish/[did]/[publication]/dashboard/PublicationAnalytics.tsx b/app/lish/[did]/[publication]/dashboard/PublicationAnalytics.tsx index b1c437f3..7e84ed17 100644 --- a/app/lish/[did]/[publication]/dashboard/PublicationAnalytics.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublicationAnalytics.tsx @@ -241,9 +241,9 @@ export const PublicationAnalytics = (props: { if (!isPro) return (
)}
- +
{formatDayTick(String(label))}
-
{Number(pageviews).toLocaleString()} {metricLabel}
+
+ {Number(pageviews).toLocaleString()} {metricLabel} +
); }} diff --git a/app/lish/[did]/[publication]/dashboard/PublicationSubscribers.tsx b/app/lish/[did]/[publication]/dashboard/PublicationSubscribers.tsx index aede5c80..0ccd083b 100644 --- a/app/lish/[did]/[publication]/dashboard/PublicationSubscribers.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublicationSubscribers.tsx @@ -48,7 +48,7 @@ export function PublicationSubscribers(props: { if (subscribers.length === 0) return (
void }) => { +export const ManageProSubscription = (props: {}) => { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const { identity } = useIdentityData(); @@ -34,9 +34,6 @@ export const ManageProSubscription = (props: { backToMenu: () => void }) => {
Manage Subscription -
diff --git a/components/ActionBar/ProfileButton.tsx b/components/ActionBar/ProfileButton.tsx index 58983d4e..f2e3fb3d 100644 --- a/components/ActionBar/ProfileButton.tsx +++ b/components/ActionBar/ProfileButton.tsx @@ -10,16 +10,16 @@ import { SpeedyLink } from "components/SpeedyLink"; import { Popover } from "components/Popover"; import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; import { Modal } from "components/Modal"; -import { UpgradeContent } from "app/lish/[did]/[publication]/UpgradeModal"; +import { InlineUpgrade } from "app/lish/[did]/[publication]/UpgradeModal"; import { ManageProSubscription } from "app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription"; import { useIsPro, useCanSeePro } from "src/hooks/useEntitlement"; import { useState } from "react"; +import { LeafletPro } from "components/Icons/LeafletPro"; export const ProfileButton = () => { let { identity } = useIdentityData(); let { data: record } = useRecordFromDid(identity?.atp_did); let isMobile = useIsMobile(); - let [state, setState] = useState<"menu" | "manage-subscription">("menu"); let isPro = useIsPro(); let canSeePro = useCanSeePro(); @@ -28,8 +28,7 @@ export const ProfileButton = () => { asChild side={isMobile ? "top" : "right"} align={isMobile ? "center" : "start"} - onOpenChange={() => setState("menu")} - className="w-xs" + className="w-xs py-1!" trigger={ { /> } > - {state === "manage-subscription" ? ( - setState("menu")} /> - ) : ( -
- {record && ( - <> - - - - -
- - )} - {canSeePro && ( - <> - {!isPro ? ( - - Get Leaflet Pro - -
- } - > - - - ) : ( - + + + )} + {canSeePro && isPro && ( + <> + + Manage Pro Subscription - - - )} - -
- - )} - - -
- )} +
+ } + > + + +
+ + )} + + {canSeePro && !isPro && ( + <> + {" "} +
+
+ +
+ + )} +
); }; diff --git a/components/Modal.tsx b/components/Modal.tsx index fa9def2e..c6139ee7 100644 --- a/components/Modal.tsx +++ b/components/Modal.tsx @@ -23,11 +23,11 @@ export const Modal = ({ {trigger} - + @@ -47,7 +47,9 @@ export const Modal = ({ ) : ( )} - {children} + +
{children}
+
diff --git a/components/SubscriptionSuccessModal.tsx b/components/SubscriptionSuccessModal.tsx index a2146f7e..a81db0d7 100644 --- a/components/SubscriptionSuccessModal.tsx +++ b/components/SubscriptionSuccessModal.tsx @@ -4,6 +4,8 @@ import { useSearchParams, useRouter, usePathname } from "next/navigation"; import { useEffect, useState } from "react"; import { useIdentityData } from "./IdentityProvider"; import { Modal } from "./Modal"; +import { ButtonPrimary } from "./Buttons"; +import { theme } from "tailwind.config"; export function SubscriptionSuccessModal() { let searchParams = useSearchParams(); @@ -39,8 +41,7 @@ export function SubscriptionSuccessModal() { open={open} onOpenChange={handleOpenChange} trigger={} - title="Welcome to Pro" - className="w-80" + className="sm:max-w-sm sm:w-[1000px] w-full" > {loading ? (
@@ -50,12 +51,43 @@ export function SubscriptionSuccessModal() {

) : ( -
-

- Your Pro subscription is active. Thanks for supporting Leaflet! +

+
+ +
+

Welcome to Pro!

+

+ Thank you for supporting Leaflet. +
+ We're looking forward to bringing you even cooler stuff in the near + future!

+ + Got it, Thanks! +
)} ); } + +const ProIllo = () => { + return ( + + + + + ); +}; -- 2.51.2 From c0105db036f7c0c663670f22af5233eff09b3dde Mon Sep 17 00:00:00 2001 From: Brendan Schlagel Date: Fri, 13 Mar 2026 15:35:32 -0400 Subject: [PATCH 02/22] show home filters if hasPubs or hasArchived --- components/PageLayouts/DashboardLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/PageLayouts/DashboardLayout.tsx b/components/PageLayouts/DashboardLayout.tsx index 0ab1677f..9a28fcf7 100644 --- a/components/PageLayouts/DashboardLayout.tsx +++ b/components/PageLayouts/DashboardLayout.tsx @@ -304,7 +304,7 @@ export const HomeDashboardControls = (props: { - {props.hasPubs ? ( + {props.hasPubs || props.hasArchived ? ( <> Date: Fri, 13 Mar 2026 15:49:04 -0400 Subject: [PATCH 03/22] change /discover links to /reader --- app/[leaflet_id]/actions/HelpButton.tsx | 2 +- app/lish/createPub/CreatePubForm.tsx | 10 +++++----- app/lish/createPub/UpdatePubForm.tsx | 17 ++++++----------- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/app/[leaflet_id]/actions/HelpButton.tsx b/app/[leaflet_id]/actions/HelpButton.tsx index aeab5ce5..aab289ce 100644 --- a/app/[leaflet_id]/actions/HelpButton.tsx +++ b/app/[leaflet_id]/actions/HelpButton.tsx @@ -30,7 +30,7 @@ export const HelpButton = (props: { noShortcuts?: boolean }) => { {/* contact links */} diff --git a/app/lish/createPub/CreatePubForm.tsx b/app/lish/createPub/CreatePubForm.tsx index 749b4397..c6138dac 100644 --- a/app/lish/createPub/CreatePubForm.tsx +++ b/app/lish/createPub/CreatePubForm.tsx @@ -151,11 +151,11 @@ export const CreatePubForm = () => {

Show In Discover

- Your posts will appear on our{" "} - - Discover - {" "} - page. You can change this at any time! + Your posts will appear in{" "} + + Leaflet Reader + + . You can change this at any time!

diff --git a/app/lish/createPub/UpdatePubForm.tsx b/app/lish/createPub/UpdatePubForm.tsx index b03953dd..bf5f677c 100644 --- a/app/lish/createPub/UpdatePubForm.tsx +++ b/app/lish/createPub/UpdatePubForm.tsx @@ -180,18 +180,13 @@ export const EditPubForm = (props: { onToggle={() => setShowInDiscover(!showInDiscover)} >
-

- Show In{" "} - - Discover - -

+

Show In Discover

- Your posts will appear on our{" "} - - Discover - {" "} - page. You can change this at any time! + Your posts will appear in{" "} + + Leaflet Reader + + . You can change this at any time!

-- 2.51.2 From 6a7c740b016f180f35f2610d607d8ff96e2ead28 Mon Sep 17 00:00:00 2001 From: Brendan Schlagel Date: Fri, 13 Mar 2026 15:51:01 -0400 Subject: [PATCH 04/22] remove ATP from publish button --- app/[leaflet_id]/actions/PublishButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/[leaflet_id]/actions/PublishButton.tsx b/app/[leaflet_id]/actions/PublishButton.tsx index 6ca18b02..c2497139 100644 --- a/app/[leaflet_id]/actions/PublishButton.tsx +++ b/app/[leaflet_id]/actions/PublishButton.tsx @@ -190,7 +190,7 @@ const PublishToPublicationButton = (props: { entityID: string }) => { } - label={"Publish on ATP"} + label={"Publish"} /> } > -- 2.51.2 From 7dea6c9ccabbb7fe56ef65799f4dfdaad3fac013 Mon Sep 17 00:00:00 2001 From: Brendan Schlagel Date: Fri, 13 Mar 2026 16:12:49 -0400 Subject: [PATCH 05/22] adjust margin for ordered list items in rendered PostContent --- .../[publication]/[rkey]/PostContent.tsx | 101 ++++++++++-------- 1 file changed, 58 insertions(+), 43 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx index 44394231..a06b392a 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx @@ -393,7 +393,11 @@ export let Block = ({ case PubLeafletBlocksHeader.isMain(b.block): { if (b.block.level === 1) return ( -

+

+

+

{b.block.plaintext}

; // if (b.block.level === 5) return
{b.block.plaintext}
; return ( -
+
) : null; - let orderedChildren = - props.item.orderedListChildren?.children?.length ? ( -
    - {props.item.orderedListChildren.children.map((child, index) => ( - - ))} -
- ) : null; + let orderedChildren = props.item.orderedListChildren?.children?.length ? ( +
    + {props.item.orderedListChildren.children.map((child, index) => ( + + ))} +
+ ) : null; return (
  • {props.item.children.map((child, index) => ( @@ -544,27 +560,26 @@ function OrderedListItem(props: { ))} ) : null; - let unorderedChildren = - props.item.unorderedListChildren?.children?.length ? ( -
      - {props.item.unorderedListChildren.children.map((child, index) => ( - - ))} -
    - ) : null; + let unorderedChildren = props.item.unorderedListChildren?.children?.length ? ( +
      + {props.item.unorderedListChildren.children.map((child, index) => ( + + ))} +
    + ) : null; return (
  • -
    +
    {calculatedIndex}.
    -- 2.51.2 From 282238e5e164fcac3936376ca2b18686f5260105 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sat, 14 Mar 2026 21:04:59 -0400 Subject: [PATCH 06/22] add referrer host to tinybird conf --- lib/tinybird.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/tinybird.ts b/lib/tinybird.ts index d7211a9a..9ccac361 100644 --- a/lib/tinybird.ts +++ b/lib/tinybird.ts @@ -94,6 +94,7 @@ export const publicationTraffic = defineEndpoint("publication_traffic", { date_from: p.string().optional(), date_to: p.string().optional(), path: p.string().optional(), + referrer_host: p.string().optional(), }, tokens: [PROD_READ_TOKEN], nodes: [ @@ -116,6 +117,9 @@ export const publicationTraffic = defineEndpoint("publication_traffic", { {% if defined(path) %} AND path = {{String(path)}} {% end %} + {% if defined(referrer_host) %} + AND domain(referrer) = {{String(referrer_host)}} + {% end %} GROUP BY day ORDER BY day ASC `, @@ -146,6 +150,7 @@ export const publicationTopReferrers = defineEndpoint( date_from: p.string().optional(), date_to: p.string().optional(), path: p.string().optional(), + referrer_host: p.string().optional(), limit: p.int32().optional(10), }, nodes: [ @@ -169,6 +174,9 @@ export const publicationTopReferrers = defineEndpoint( {% if defined(path) %} AND path = {{String(path)}} {% end %} + {% if defined(referrer_host) %} + AND domain(referrer) = {{String(referrer_host)}} + {% end %} GROUP BY referrer_host ORDER BY pageviews DESC LIMIT {{Int32(limit, 10)}} @@ -199,6 +207,7 @@ export const publicationTopPages = defineEndpoint("publication_top_pages", { domains: p.string(), date_from: p.string().optional(), date_to: p.string().optional(), + referrer_host: p.string().optional(), limit: p.int32().optional(10), }, nodes: [ @@ -217,6 +226,9 @@ export const publicationTopPages = defineEndpoint("publication_top_pages", { {% if defined(date_to) %} AND fromUnixTimestamp64Milli(timestamp) <= parseDateTimeBestEffort({{String(date_to)}}) {% end %} + {% if defined(referrer_host) %} + AND domain(referrer) = {{String(referrer_host)}} + {% end %} GROUP BY path ORDER BY pageviews DESC LIMIT {{Int32(limit, 10)}} -- 2.51.2 From 22370e29e3254fcab51f7272aaec85689452ca5a Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 16 Mar 2026 17:54:14 -0700 Subject: [PATCH 07/22] commit footnote lexicon src --- lexicons/src/facet.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lexicons/src/facet.ts b/lexicons/src/facet.ts index 93d4c416..b942df88 100644 --- a/lexicons/src/facet.ts +++ b/lexicons/src/facet.ts @@ -64,6 +64,19 @@ const FacetItems: LexiconDoc["defs"] = { required: [], properties: {}, }, + footnote: { + type: "object", + description: "Facet feature for a footnote reference", + required: ["footnoteId", "contentPlaintext"], + properties: { + footnoteId: { type: "string" }, + contentPlaintext: { type: "string" }, + contentFacets: { + type: "array", + items: { type: "ref", ref: "#main" }, + }, + }, + }, }; export const PubLeafletRichTextFacet = { -- 2.51.2 From 27cd0b26c0087a36abf05a4dfe04a06ae2b4de90 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 16 Mar 2026 17:55:17 -0700 Subject: [PATCH 08/22] describe lexgen workflow --- CLAUDE.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 2317a3fd..00fe4923 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,6 +45,14 @@ Documents are composed of **blocks** (text, image, embed, code, poll, etc.). Blo - **Appview** (`appview/`) consumes the firehose to index published content - **Feeds** (`feeds/`) provides subscription feeds for publications +### Lexicon Workflow + +The source of truth for lexicon schemas is the TypeScript files in `lexicons/src/` (e.g. `facet.ts`, `publication.ts`, `blocks.ts`). The JSON files in `lexicons/pub/` are **generated output** — do not edit them directly, as `npm run lexgen` will overwrite them. + +To add or modify a lexicon: +1. Edit the relevant source file in `lexicons/src/` +2. Run `npm run lexgen` to regenerate both the JSON schemas and the TypeScript API types in `lexicons/api/` + ### Key Directories - `app/` - Next.js App Router pages and API routes -- 2.51.2 From 6cb5fe0b50e96a5a32869240f42c03e0e11d2835 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 17 Mar 2026 13:07:19 -0700 Subject: [PATCH 09/22] fix rotation bug --- src/utils/addImage.ts | 47 ++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/utils/addImage.ts b/src/utils/addImage.ts index 86f26ed4..84224035 100644 --- a/src/utils/addImage.ts +++ b/src/utils/addImage.ts @@ -19,13 +19,17 @@ export async function addImage( let fileID = v7(); let url = client.storage.from("minilink-user-assets").getPublicUrl(fileID) .data.publicUrl; - let dimensions = await getImageDimensions(file); + // Re-encode through canvas to bake EXIF orientation into pixel data. + // iPhone photos have EXIF rotation metadata that browsers respect, but + // Supabase's image transformation pipeline strips without applying. + let { blob: uploadBlob, width, height } = await normalizeOrientation(file); + await cache.put( new URL(url + "?local"), - new Response(file, { + new Response(uploadBlob, { headers: { - "Content-Type": file.type, - "Content-Length": file.size.toString(), + "Content-Type": uploadBlob.type, + "Content-Length": uploadBlob.size.toString(), }, }), ); @@ -41,11 +45,11 @@ export async function addImage( type: "image", local: rep.clientID, src: url, - height: dimensions.height, - width: dimensions.width, + height, + width, }, }); - await client.storage.from("minilink-user-assets").upload(fileID, file, { + await client.storage.from("minilink-user-assets").upload(fileID, uploadBlob, { cacheControl: "public, max-age=31560000, immutable", }); await rep.mutate.assertFact({ @@ -55,8 +59,8 @@ export async function addImage( fallback: thumbhash, type: "image", src: url, - height: dimensions.height, - width: dimensions.width, + height, + width, }, }); } @@ -95,17 +99,18 @@ async function getThumbHash(file: File) { return thumbHash; } -function getImageDimensions( +async function normalizeOrientation( file: File, -): Promise<{ width: number; height: number }> { - let url = URL.createObjectURL(file); - return new Promise((resolve, reject) => { - const img = new Image(); - img.onload = function () { - resolve({ width: img.width, height: img.height }); - URL.revokeObjectURL(url); - }; - img.onerror = reject; - img.src = url; - }); +): Promise<{ blob: Blob; width: number; height: number }> { + let bitmap = await createImageBitmap(file); + let canvas = document.createElement("canvas"); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + let ctx = canvas.getContext("2d")!; + ctx.drawImage(bitmap, 0, 0); + bitmap.close(); + let blob = await new Promise((resolve) => + canvas.toBlob((b) => resolve(b!), "image/webp", 0.92), + ); + return { blob, width: canvas.width, height: canvas.height }; } -- 2.51.2 From 6bd904b9d2ba3509f8a9c0463b381fd10cd28718 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 17 Mar 2026 15:34:43 -0700 Subject: [PATCH 10/22] add href to headers --- .../[publication]/[rkey]/PostContent.tsx | 79 +++++++------------ src/utils/slugify.ts | 9 +++ 2 files changed, 39 insertions(+), 49 deletions(-) create mode 100644 src/utils/slugify.ts diff --git a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx index a06b392a..1c8f1b19 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx @@ -34,6 +34,7 @@ import { PublishedPollBlock } from "./Blocks/PublishedPollBlock"; import { PollData } from "./fetchPollData"; import { ButtonPrimary } from "components/Buttons"; import { blockTextSize } from "src/utils/blockTextSize"; +import { slugify } from "src/utils/slugify"; import { PostNotAvailable } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; export function PostContent({ @@ -391,68 +392,48 @@ export let Block = ({ ); case PubLeafletBlocksHeader.isMain(b.block): { + let slug = slugify(b.block.plaintext); + let headingProps = { + ...blockProps, + id: preview ? undefined : slug || blockProps.id, + }; + let textBlockProps = { + ...b.block, + index, + preview, + pageId, + footnoteIndexMap, + }; + let href = pageId ? `?page=${pageId}#${slug}` : `#${slug}`; + let link = (children: React.ReactNode) => + preview || !slug ? ( + children + ) : ( + + {children} + + ); if (b.block.level === 1) return ( -

    - +

    + {link()}

    ); if (b.block.level === 2) return ( -

    - +

    + {link()}

    ); if (b.block.level === 3) return ( -

    - +

    + {link()}

    ); - // if (b.block.level === 4) return

    {b.block.plaintext}

    ; - // if (b.block.level === 5) return
    {b.block.plaintext}
    ; return ( -
    - +
    + {link()}
    ); } diff --git a/src/utils/slugify.ts b/src/utils/slugify.ts new file mode 100644 index 00000000..2876a38f --- /dev/null +++ b/src/utils/slugify.ts @@ -0,0 +1,9 @@ +export function slugify(text: string): string { + return text + .toLowerCase() + .trim() + .replace(/[^\w\s-]/g, "") + .replace(/[\s_]+/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); +} -- 2.51.2 From 71c6bc04ae917a48cc7a10f6f5bed80e0ca833db Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 18 Mar 2026 15:18:12 -0700 Subject: [PATCH 11/22] fix tag search --- ...310000000_fix_search_tags_object_error.sql | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 supabase/migrations/20260310000000_fix_search_tags_object_error.sql diff --git a/supabase/migrations/20260310000000_fix_search_tags_object_error.sql b/supabase/migrations/20260310000000_fix_search_tags_object_error.sql new file mode 100644 index 00000000..1c5368a5 --- /dev/null +++ b/supabase/migrations/20260310000000_fix_search_tags_object_error.sql @@ -0,0 +1,28 @@ +-- Fix search_tags function to handle documents where data->'tags' is not a JSON array +-- This prevents "cannot extract elements from an object" errors +CREATE OR REPLACE FUNCTION search_tags(search_query text) +RETURNS TABLE (name text, document_count bigint) AS $$ +BEGIN + RETURN QUERY + SELECT + LOWER(tag::text) as name, + COUNT(DISTINCT d.uri) as document_count + FROM ( + SELECT uri, data + FROM "public"."documents" + WHERE jsonb_typeof(data->'tags') = 'array' + ) d, + jsonb_array_elements_text(d.data->'tags') as tag + WHERE + CASE + WHEN search_query = '' THEN true + ELSE LOWER(tag::text) LIKE '%' || search_query || '%' + END + GROUP BY + LOWER(tag::text) + ORDER BY + COUNT(DISTINCT d.uri) DESC, + LOWER(tag::text) ASC + LIMIT 20; +END; +$$ LANGUAGE plpgsql STABLE; -- 2.51.2 From 9e1f686cc09cd16c3e009ced9cdb78c5c5854c19 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 23 Mar 2026 14:59:16 -0700 Subject: [PATCH 12/22] fix enter breaking selected state in list item --- components/Blocks/TextBlock/keymap.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/components/Blocks/TextBlock/keymap.ts b/components/Blocks/TextBlock/keymap.ts index cef6e570..3f9c1402 100644 --- a/components/Blocks/TextBlock/keymap.ts +++ b/components/Blocks/TextBlock/keymap.ts @@ -392,6 +392,15 @@ const enter = view?: EditorView, ) => { if (state.doc.textContent.startsWith("/")) return true; + // Empty list item: just outdent, don't create a new block + if ( + propsRef.current.listData && + propsRef.current.pageType !== "canvas" && + state.doc.content.size <= 2 + ) { + shifttab(propsRef, repRef)(); + return true; + } let tr = state.tr; let newContent = tr.doc.slice(state.selection.anchor); tr.delete(state.selection.anchor, state.doc.content.size); @@ -451,9 +460,6 @@ const enter = return; } if (propsRef.current.listData) { - if (state.doc.content.size <= 2) { - return shifttab(propsRef, repRef)(); - } let createChild = propsRef.current.nextBlock?.listData && propsRef.current.nextBlock.listData.depth > -- 2.51.2 From 6eac1ce874808a1304dc2e22733813f72de1c873 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 23 Mar 2026 15:12:41 -0700 Subject: [PATCH 13/22] get block data when in lists --- .../[rkey]/DocumentPageRenderer.tsx | 74 ++++++++++++++++--- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx b/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx index 0fcd6d74..03b4fe7d 100644 --- a/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx +++ b/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx @@ -2,6 +2,8 @@ import { AtpAgent } from "@atproto/api"; import { ids } from "lexicons/api/lexicons"; import { PubLeafletBlocksBskyPost, + PubLeafletBlocksOrderedList, + PubLeafletBlocksUnorderedList, PubLeafletPagesLinearDocument, PubLeafletPagesCanvas, } from "lexicons/api"; @@ -65,9 +67,7 @@ export async function DocumentPageRenderer({ let bskyPosts = pages.flatMap((p) => { let page = p as PubLeafletPagesLinearDocument.Main; - return page.blocks?.filter( - (b) => b.block.$type === ids.PubLeafletBlocksBskyPost, - ); + return extractBlocksByType(page.blocks || [], ids.PubLeafletBlocksBskyPost); }) || []; // Batch bsky posts into groups of 25 and fetch in parallel @@ -98,10 +98,7 @@ export async function DocumentPageRenderer({ // Extract poll blocks and fetch vote data let pollBlocks = pages.flatMap((p) => { let page = p as PubLeafletPagesLinearDocument.Main; - return ( - page.blocks?.filter((b) => b.block.$type === ids.PubLeafletBlocksPoll) || - [] - ); + return extractBlocksByType(page.blocks || [], ids.PubLeafletBlocksPoll); }); let pollData = await fetchPollData( pollBlocks.map((b) => (b.block as any).pollRef.uri), @@ -123,7 +120,10 @@ export async function DocumentPageRenderer({ return ( - + ); } + +function extractBlocksByType( + blocks: PubLeafletPagesLinearDocument.Block[], + type: string, +): { block: { $type: string; [key: string]: unknown } }[] { + let results: { block: { $type: string; [key: string]: unknown } }[] = []; + for (let b of blocks) { + if (b.block.$type === type) { + results.push(b as { block: { $type: string; [key: string]: unknown } }); + } + if ( + b.block.$type === ids.PubLeafletBlocksOrderedList || + b.block.$type === ids.PubLeafletBlocksUnorderedList + ) { + let list = b.block as + | PubLeafletBlocksOrderedList.Main + | PubLeafletBlocksUnorderedList.Main; + extractFromListItems(list.children, type, results); + } + } + return results; +} + +function extractFromListItems( + items: + | PubLeafletBlocksOrderedList.ListItem[] + | PubLeafletBlocksUnorderedList.ListItem[], + type: string, + results: { block: { $type: string; [key: string]: unknown } }[], +) { + for (let item of items) { + if ((item.content as { $type?: string })?.$type === type) { + results.push({ + block: item.content as { $type: string; [key: string]: unknown }, + }); + } + if (item.children) { + extractFromListItems(item.children, type, results); + } + let orderedChildren = ( + item as PubLeafletBlocksUnorderedList.ListItem + ).orderedListChildren; + if (orderedChildren) { + extractFromListItems(orderedChildren.children, type, results); + } + let unorderedChildren = ( + item as PubLeafletBlocksOrderedList.ListItem + ).unorderedListChildren; + if (unorderedChildren) { + extractFromListItems(unorderedChildren.children, type, results); + } + } +} -- 2.51.2 From 2fa31878ed1c841df5d9990474f1cca258c92224 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 23 Mar 2026 16:04:21 -0700 Subject: [PATCH 14/22] fix types of extractBlocksByType --- .../[rkey]/DocumentPageRenderer.tsx | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx b/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx index 03b4fe7d..5ca6da7f 100644 --- a/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx +++ b/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx @@ -3,10 +3,12 @@ import { ids } from "lexicons/api/lexicons"; import { PubLeafletBlocksBskyPost, PubLeafletBlocksOrderedList, + PubLeafletBlocksPoll, PubLeafletBlocksUnorderedList, PubLeafletPagesLinearDocument, PubLeafletPagesCanvas, } from "lexicons/api"; +import { type $Typed } from "lexicons/api/util"; import { QuoteHandler } from "./QuoteHandler"; import { PublicationBackgroundProvider, @@ -67,7 +69,10 @@ export async function DocumentPageRenderer({ let bskyPosts = pages.flatMap((p) => { let page = p as PubLeafletPagesLinearDocument.Main; - return extractBlocksByType(page.blocks || [], ids.PubLeafletBlocksBskyPost); + return extractBlocksByType<$Typed>( + page.blocks || [], + ids.PubLeafletBlocksBskyPost, + ); }) || []; // Batch bsky posts into groups of 25 and fetch in parallel @@ -80,10 +85,7 @@ export async function DocumentPageRenderer({ bskyPostBatches.map((batch) => agent.getPosts( { - uris: batch.map((p) => { - let block = p?.block as PubLeafletBlocksBskyPost.Main; - return block.postRef.uri; - }), + uris: batch.map((p) => p.block.postRef.uri), }, { headers: {} }, ), @@ -98,10 +100,13 @@ export async function DocumentPageRenderer({ // Extract poll blocks and fetch vote data let pollBlocks = pages.flatMap((p) => { let page = p as PubLeafletPagesLinearDocument.Main; - return extractBlocksByType(page.blocks || [], ids.PubLeafletBlocksPoll); + return extractBlocksByType<$Typed>( + page.blocks || [], + ids.PubLeafletBlocksPoll, + ); }); let pollData = await fetchPollData( - pollBlocks.map((b) => (b.block as any).pollRef.uri), + pollBlocks.map((b) => b.block.pollRef.uri), ); const pubRecord = document.normalizedPublication; @@ -158,14 +163,14 @@ export async function DocumentPageRenderer({ ); } -function extractBlocksByType( +function extractBlocksByType( blocks: PubLeafletPagesLinearDocument.Block[], type: string, -): { block: { $type: string; [key: string]: unknown } }[] { - let results: { block: { $type: string; [key: string]: unknown } }[] = []; +): { block: T }[] { + let results: { block: T }[] = []; for (let b of blocks) { if (b.block.$type === type) { - results.push(b as { block: { $type: string; [key: string]: unknown } }); + results.push(b as unknown as { block: T }); } if ( b.block.$type === ids.PubLeafletBlocksOrderedList || @@ -180,17 +185,17 @@ function extractBlocksByType( return results; } -function extractFromListItems( +function extractFromListItems( items: | PubLeafletBlocksOrderedList.ListItem[] | PubLeafletBlocksUnorderedList.ListItem[], type: string, - results: { block: { $type: string; [key: string]: unknown } }[], + results: { block: T }[], ) { for (let item of items) { if ((item.content as { $type?: string })?.$type === type) { results.push({ - block: item.content as { $type: string; [key: string]: unknown }, + block: item.content as unknown as T, }); } if (item.children) { -- 2.51.2 From a993f16428432e620e920234fd76341e8d72d81c Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 23 Mar 2026 16:27:40 -0700 Subject: [PATCH 15/22] add alternate names for list commands --- components/Blocks/BlockCommands.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/Blocks/BlockCommands.tsx b/components/Blocks/BlockCommands.tsx index 6d766fc0..d1f9484d 100644 --- a/components/Blocks/BlockCommands.tsx +++ b/components/Blocks/BlockCommands.tsx @@ -28,7 +28,10 @@ import { } from "components/Icons/BlockTextSmall"; import { LinkSmall } from "components/Icons/LinkSmall"; import { BlockRSVPSmall } from "components/Icons/BlockRSVPSmall"; -import { ListUnorderedSmall, ListOrderedSmall } from "components/Toolbar/ListToolbar"; +import { + ListUnorderedSmall, + ListOrderedSmall, +} from "components/Toolbar/ListToolbar"; import { BlockMathSmall } from "components/Icons/BlockMathSmall"; import { BlockCodeSmall } from "components/Icons/BlockCodeSmall"; import { QuoteSmall } from "components/Icons/QuoteSmall"; @@ -153,6 +156,7 @@ export const blockCommands: Command[] = [ { name: "Unordered List", icon: , + alternateNames: ["Bulleted List"], type: "text", onSelect: async (rep, props, um) => { let entity = await createBlockWithType(rep, props, "text"); @@ -168,6 +172,7 @@ export const blockCommands: Command[] = [ name: "Ordered List", icon: , type: "text", + alternateNames: ["Numbered List"], onSelect: async (rep, props, um) => { let entity = await createBlockWithType(rep, props, "text"); await rep?.mutate.assertFact([ -- 2.51.2 From 76f0f5cb42477bdf97033c6f11dae8207f81d3c2 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 23 Mar 2026 16:40:35 -0700 Subject: [PATCH 16/22] add appview stubs for page.parts --- appview/index.ts | 38 +++++++++++++++++++ supabase/database.types.ts | 36 ++++++++++++++++++ .../20260323000000_add_mention_services.sql | 13 +++++++ 3 files changed, 87 insertions(+) create mode 100644 supabase/migrations/20260323000000_add_mention_services.sql diff --git a/appview/index.ts b/appview/index.ts index 7be9e892..0bdf2d76 100644 --- a/appview/index.ts +++ b/appview/index.ts @@ -55,6 +55,8 @@ async function main() { ids.SiteStandardDocument, ids.SiteStandardPublication, ids.SiteStandardGraphSubscription, + "parts.page.mention.service", + "parts.page.mention.config", ], handleEvent, onError: (err) => { @@ -370,6 +372,42 @@ async function handleEvent(evt: Event) { // .eq("did", evt.did); // } // } + if (evt.collection === "parts.page.mention.service") { + if (evt.event === "create" || evt.event === "update") { + let record = evt.record as any; + if (!record?.name || !record?.endpoint) return; + let { error } = await supabase.from("mention_services").upsert({ + uri: evt.uri.toString(), + identity_did: evt.did, + record: record as Json, + }); + if (error) console.log("Error upserting mention service:", error); + } + if (evt.event === "delete") { + await supabase + .from("mention_services") + .delete() + .eq("uri", evt.uri.toString()); + } + } + if (evt.collection === "parts.page.mention.config") { + if (evt.event === "create" || evt.event === "update") { + let record = evt.record as any; + if (!Array.isArray(record?.services)) return; + let { error } = await supabase.from("mention_service_configs").upsert({ + uri: evt.uri.toString(), + identity_did: evt.did, + record: record as Json, + }); + if (error) console.log("Error upserting mention config:", error); + } + if (evt.event === "delete") { + await supabase + .from("mention_service_configs") + .delete() + .eq("uri", evt.uri.toString()); + } + } if (evt.collection === "app.bsky.feed.post") { if (evt.event !== "create") return; diff --git a/supabase/database.types.ts b/supabase/database.types.ts index 5701f414..68844690 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -702,6 +702,42 @@ export type Database = { }, ] } + mention_service_configs: { + Row: { + identity_did: string + record: Json + uri: string + } + Insert: { + identity_did: string + record: Json + uri: string + } + Update: { + identity_did?: string + record?: Json + uri?: string + } + Relationships: [] + } + mention_services: { + Row: { + identity_did: string + record: Json + uri: string + } + Insert: { + identity_did: string + record: Json + uri: string + } + Update: { + identity_did?: string + record?: Json + uri?: string + } + Relationships: [] + } notifications: { Row: { created_at: string diff --git a/supabase/migrations/20260323000000_add_mention_services.sql b/supabase/migrations/20260323000000_add_mention_services.sql new file mode 100644 index 00000000..5dd6ff03 --- /dev/null +++ b/supabase/migrations/20260323000000_add_mention_services.sql @@ -0,0 +1,13 @@ +CREATE TABLE mention_services ( + uri TEXT PRIMARY KEY, + identity_did TEXT NOT NULL, + record JSONB NOT NULL +); +CREATE INDEX idx_mention_services_did ON mention_services(identity_did); + +CREATE TABLE mention_service_configs ( + uri TEXT PRIMARY KEY, + identity_did TEXT NOT NULL UNIQUE, + record JSONB NOT NULL +); +CREATE INDEX idx_mention_service_configs_did ON mention_service_configs(identity_did); -- 2.51.2 From e144bcf82d31de0908069ee813b4774a0635e46d Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 23 Mar 2026 20:58:06 -0700 Subject: [PATCH 17/22] fix mention service indexing --- appview/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/appview/index.ts b/appview/index.ts index 0bdf2d76..e795436f 100644 --- a/appview/index.ts +++ b/appview/index.ts @@ -375,7 +375,6 @@ async function handleEvent(evt: Event) { if (evt.collection === "parts.page.mention.service") { if (evt.event === "create" || evt.event === "update") { let record = evt.record as any; - if (!record?.name || !record?.endpoint) return; let { error } = await supabase.from("mention_services").upsert({ uri: evt.uri.toString(), identity_did: evt.did, -- 2.51.2 From 7cd621e634d4f459e6812c677cc863df8825233d Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 24 Mar 2026 18:28:16 -0400 Subject: [PATCH 18/22] Fix Android selection handle drag breaking selection state (#274) * Fix Android selection handle drag breaking selection state The highlightSelectionPlugin creates inline decorations (wrapping text in elements) on every selection change. On Android, these DOM mutations during native selection handle drag cause the browser to lose track of the selection handles. Since these decorations are invisible when the editor is focused (via CSS), skip decoration updates for selection-only changes on Android to prevent the DOM mutations. https://claude.ai/code/session_01Qcv5Xm4QcDnzhWdTbfgQh9 * Skip selection-only decoration updates on all platforms The DOM mutations from updating inline decorations on selection changes are unnecessary since the decorations are invisible when the editor is focused via CSS. Removing the Android-only guard avoids the overhead everywhere. https://claude.ai/code/session_01Qcv5Xm4QcDnzhWdTbfgQh9 * Update decoration on blur so toolbar highlight still works The selection highlight needs to be visible when focus moves from the editor to the toolbar (e.g. for entering a link URL). Dispatch an update on blur via handleDOMEvents so the decoration matches the current selection at that point, while still skipping DOM mutations during active selection drag. https://claude.ai/code/session_01Qcv5Xm4QcDnzhWdTbfgQh9 * Trim verbose comment in highlightSelectionPlugin https://claude.ai/code/session_01Qcv5Xm4QcDnzhWdTbfgQh9 * Fix selection highlight not showing when editor is blurred The `.selected .selection-highlight` CSS rule required a `.selected` ancestor class that only exists during multiselect, so the highlight was never visible for single-block text selection (e.g. when clicking the toolbar). Remove the `.selected` ancestor requirement so the decoration is visible whenever the editor loses focus. https://claude.ai/code/session_01Qcv5Xm4QcDnzhWdTbfgQh9 --------- Co-authored-by: Claude --- app/globals.css | 2 +- components/Blocks/TextBlock/plugins.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/globals.css b/app/globals.css index 2145c5b7..24130b0a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -338,7 +338,7 @@ pre.shiki { @apply box-decoration-clone; } -.selected .selection-highlight { +.selection-highlight { background-color: Highlight; @apply py-[1.5px]; } diff --git a/components/Blocks/TextBlock/plugins.ts b/components/Blocks/TextBlock/plugins.ts index c78fb953..256ab470 100644 --- a/components/Blocks/TextBlock/plugins.ts +++ b/components/Blocks/TextBlock/plugins.ts @@ -1,11 +1,19 @@ import { Decoration, DecorationSet } from "prosemirror-view"; import { Plugin } from "prosemirror-state"; + export const highlightSelectionPlugin = new Plugin({ state: { init(_, { doc }) { return DecorationSet.empty; }, apply(tr, oldDecorations, oldState, newState) { + // Skip selection-only changes to avoid DOM mutations that break + // native selection handle dragging. On blur, we force an update + // so the highlight is visible when focus moves to the toolbar. + if (!tr.docChanged && !tr.getMeta("updateSelectionHighlight")) { + return oldDecorations; + } + let decorations = []; // Check if there's a selection @@ -20,6 +28,14 @@ export const highlightSelectionPlugin = new Plugin({ }, }, props: { + handleDOMEvents: { + blur(view) { + view.dispatch( + view.state.tr.setMeta("updateSelectionHighlight", true), + ); + return false; + }, + }, decorations(state) { return this.getState(state); }, -- 2.51.2 From c5602126775a19a2a797506ec4dd0065a7550a00 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 24 Mar 2026 15:44:11 -0700 Subject: [PATCH 19/22] add swipe gestures on list blocks Squashed commit of the following: commit 88e9ea4d1c889a4d304438c872112658f61d2b36 Author: Jared Pereira Date: Tue Mar 24 15:38:47 2026 -0700 tweaks commit 15d34c925489742b00b7bcb4f754b90c35a2fde7 Merge: c10e691e 7cd621e6 Author: Jared Pereira Date: Tue Mar 24 15:29:08 2026 -0700 Merge branch 'main' into feature/list-gestures commit c10e691e797c86f14c25ef1a1799c40819f99d1d Author: Jared Pereira Date: Tue Mar 24 09:47:43 2026 -0700 add swipe gesture on list marker --- components/Blocks/Block.tsx | 43 +++++++++++++++++++++++++++++++++---- package-lock.json | 19 ++++++++++++++++ package.json | 1 + 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/components/Blocks/Block.tsx b/components/Blocks/Block.tsx index 1ba258be..370eafbf 100644 --- a/components/Blocks/Block.tsx +++ b/components/Blocks/Block.tsx @@ -9,7 +9,8 @@ import { useLongPress } from "src/hooks/useLongPress"; import { focusBlock } from "src/utils/focusBlock"; import { useHandleDrop } from "./useHandleDrop"; import { useEntitySetContext } from "components/EntitySetProvider"; - +import { indent, outdent } from "src/utils/list-operations"; +import { useDrag } from "@use-gesture/react"; import { TextBlock } from "./TextBlock/index"; import { ImageBlock } from "./ImageBlock"; import { PageLinkBlock } from "./PageLinkBlock"; @@ -37,6 +38,8 @@ import { Separator } from "components/Layout"; import { moveBlockUp, moveBlockDown } from "src/utils/moveBlock"; import { deleteBlock } from "src/utils/deleteBlock"; +const SWIPE_THRESHOLD = 50; + export type Block = { factID: string; parent: string; @@ -76,6 +79,8 @@ export const Block = memo(function Block( nextPosition: props.nextPosition, }); let entity_set = useEntitySetContext(); + let isMobile = useIsMobile(); + let { rep } = useReplicache(); let { isLongPress, longPressHandlers } = useLongPress(() => { if (isTextBlock[props.type]) return; @@ -115,9 +120,39 @@ export const Block = memo(function Block( // THIS IS WHERE YOU SET WHETHER OR NOT AREYOUSURE IS TRIGGERED ON THE DELETE KEY useBlockKeyboardHandlers(props, areYouSure, setAreYouSure); + const bindSwipe = useDrag( + ({ last, movement: [mx] }) => { + if (!last) return; + if (!rep || !props.listData || !entity_set.permissions.write) return; + if (Math.abs(mx) < SWIPE_THRESHOLD) return; + let { foldedBlocks, toggleFold } = useUIState.getState(); + if (mx > 0) { + if (props.previousBlock) { + indent(props, props.previousBlock, rep, { + foldedBlocks, + toggleFold, + }); + } + } else { + outdent(props, props.previousBlock, rep, { + foldedBlocks, + toggleFold, + }); + } + }, + { + axis: "x", + filterTaps: true, + pointer: { touch: true }, + enabled: isMobile && !!props.listData, + }, + ); + return (
    { - let isMobile = useIsMobile(); let checklist = useEntity(props.value, "block/check-list"); let listStyle = useEntity(props.value, "block/list-style"); let headingLevel = useEntity(props.value, "block/heading-level")?.data.value; @@ -511,7 +546,6 @@ export const ListMarker = ( let [editingNumber, setEditingNumber] = useState(false); let [numberInputValue, setNumberInputValue] = useState(""); - useEffect(() => { if (!editingNumber) { setNumberInputValue(""); @@ -545,6 +579,7 @@ export const ListMarker = ( setEditingNumber(false); }; + return (
    = 16.8.0" + } + }, "node_modules/@vercel/analytics": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.5.0.tgz", diff --git a/package.json b/package.json index c4c8f256..cccf2886 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@tinybirdco/sdk": "^0.0.55", "@tiptap/core": "^2.11.5", "@types/mdx": "^2.0.13", + "@use-gesture/react": "^10.3.1", "@vercel/analytics": "^1.5.0", "@vercel/functions": "^2.2.12", "@vercel/sdk": "^1.11.4", -- 2.51.2 From 1561298adfe0da3e6e0d4715dba024a09904e5b6 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 24 Mar 2026 16:17:09 -0700 Subject: [PATCH 20/22] prevent text selection after swipe --- components/Blocks/Block.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/Blocks/Block.tsx b/components/Blocks/Block.tsx index 370eafbf..6a9300f6 100644 --- a/components/Blocks/Block.tsx +++ b/components/Blocks/Block.tsx @@ -121,10 +121,11 @@ export const Block = memo(function Block( useBlockKeyboardHandlers(props, areYouSure, setAreYouSure); const bindSwipe = useDrag( - ({ last, movement: [mx] }) => { + ({ last, movement: [mx], event }) => { if (!last) return; if (!rep || !props.listData || !entity_set.permissions.write) return; if (Math.abs(mx) < SWIPE_THRESHOLD) return; + event?.preventDefault(); let { foldedBlocks, toggleFold } = useUIState.getState(); if (mx > 0) { if (props.previousBlock) { -- 2.51.2 From d1c1952a020e49b03911de3f924e2d952b55cc96 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 24 Mar 2026 18:56:37 -0700 Subject: [PATCH 21/22] fix bsky post ref and tag in update flow --- actions/publishToPublication.ts | 41 ++++++++++++++++------ app/[leaflet_id]/actions/PublishButton.tsx | 7 ++-- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/actions/publishToPublication.ts b/actions/publishToPublication.ts index d125d0be..288b0d91 100644 --- a/actions/publishToPublication.ts +++ b/actions/publishToPublication.ts @@ -182,7 +182,7 @@ export async function publishToPublication({ credentialSession.did!, ); - let existingRecord: Partial = {}; + let existingRecord: Partial = {}; const normalizedDoc = normalizeDocumentRecord(draft?.documents?.data); if (normalizedDoc) { // When reading existing data, use normalized format to extract fields @@ -194,6 +194,7 @@ export async function publishToPublication({ tags: normalizedDoc.tags, coverImage: normalizedDoc.coverImage, theme: normalizedDoc.theme, + bskyPostRef: normalizedDoc.bskyPostRef, }; } @@ -249,6 +250,14 @@ export async function publishToPublication({ // Determine the rkey early since we need it for the path field const rkey = existingDocUri ? new AtUri(existingDocUri).rkey : TID.nextStr(); + // Resolve fields: use new values if provided, otherwise preserve existing + const resolvedDescription = + description !== undefined ? description : existingRecord.description; + const resolvedTags = tags !== undefined ? tags : existingRecord.tags; + const resolvedCoverImage = coverImageBlob ?? existingRecord.coverImage; + const resolvedPublishedAt = + publishedAt || existingRecord.publishedAt || new Date().toISOString(); + // Create record based on the document type let record: PubLeafletDocument.Record | SiteStandardDocument.Record; @@ -263,11 +272,15 @@ export async function publishToPublication({ title: title || "", site: siteUri, path: "/" + rkey, - publishedAt: - publishedAt || existingRecord.publishedAt || new Date().toISOString(), - ...(description && { description }), - ...(tags !== undefined && { tags }), - ...(coverImageBlob && { coverImage: coverImageBlob }), + publishedAt: resolvedPublishedAt, + ...(resolvedDescription !== undefined && { + description: resolvedDescription, + }), + ...(resolvedTags !== undefined && { tags: resolvedTags }), + ...(resolvedCoverImage && { coverImage: resolvedCoverImage }), + ...(existingRecord.bskyPostRef && { + bskyPostRef: existingRecord.bskyPostRef, + }), // Include theme for standalone documents (not for publication documents) ...(!publication_uri && theme && { theme }), ...(preferences && { @@ -295,12 +308,14 @@ export async function publishToPublication({ }, }), title: title || "", - description: description || "", - ...(tags !== undefined && { tags }), - ...(coverImageBlob && { coverImage: coverImageBlob }), + description: resolvedDescription || "", + ...(resolvedTags !== undefined && { tags: resolvedTags }), + ...(resolvedCoverImage && { coverImage: resolvedCoverImage }), + ...(existingRecord.bskyPostRef && { + postRef: existingRecord.bskyPostRef, + }), pages: pagesArray, - publishedAt: - publishedAt || existingRecord.publishedAt || new Date().toISOString(), + publishedAt: resolvedPublishedAt, } satisfies PubLeafletDocument.Record; } @@ -332,6 +347,8 @@ export async function publishToPublication({ publication: publication_uri, title: title, description: description, + tags: resolvedTags ?? [], + cover_image: cover_image ?? null, }), ]); } else { @@ -341,6 +358,8 @@ export async function publishToPublication({ document: result.uri, title: title || "", description: description || "", + tags: resolvedTags ?? [], + cover_image: cover_image ?? null, }); // Heuristic: Remove title entities if this is the first time publishing standalone diff --git a/app/[leaflet_id]/actions/PublishButton.tsx b/app/[leaflet_id]/actions/PublishButton.tsx index c2497139..5991f28c 100644 --- a/app/[leaflet_id]/actions/PublishButton.tsx +++ b/app/[leaflet_id]/actions/PublishButton.tsx @@ -65,7 +65,7 @@ export const PublishButton = (props: { entityID: string }) => { const UpdateButton = () => { let [isLoading, setIsLoading] = useState(false); - let { data: pub, mutate } = useLeafletPublicationData(); + let { data: pub, mutate, normalizedDocument } = useLeafletPublicationData(); let { permission_token, rootEntity, rep } = useReplicache(); let { identity } = useIdentityData(); let toaster = useToaster(); @@ -88,8 +88,11 @@ const UpdateButton = () => { : pub?.description || ""; // Get tags from Replicache state (same as draft editor) + // Fall back to normalized document tags if Replicache hasn't pulled yet let tags = useSubscribe(rep, (tx) => tx.get("publication_tags")); - const currentTags = Array.isArray(tags) ? tags : []; + const currentTags = Array.isArray(tags) + ? tags + : normalizedDocument?.tags ?? []; // Get cover image from Replicache state let coverImage = useSubscribe(rep, (tx) => -- 2.51.2 From b1d4212d031b64404d68cead0daf93e2a7e7a068 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 25 Mar 2026 12:49:38 -0700 Subject: [PATCH 22/22] remove unsupported rss tags --- app/lish/[did]/[publication]/generateFeed.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/lish/[did]/[publication]/generateFeed.ts b/app/lish/[did]/[publication]/generateFeed.ts index 94227ea0..abe30c65 100644 --- a/app/lish/[did]/[publication]/generateFeed.ts +++ b/app/lish/[did]/[publication]/generateFeed.ts @@ -93,6 +93,17 @@ export async function generateFeed( } } + let content = chunks.join(""); + // Strip preload tags injected by React SSR — they trigger + // security warnings in RSS validators and aren't useful in feeds. + content = content.replace(/]*>/gi, ""); + // Convert relative URLs to absolute so RSS readers can resolve them. + const baseUrl = pubRecord.url.replace(/\/$/, ""); + content = content.replace( + /(src|href)="\/(?!\/)/g, + `$1="${baseUrl}/`, + ); + const docUrl = getDocumentURL(record, doc.documents.uri, pubRecord); feed.addItem({ title: record.title, @@ -100,7 +111,7 @@ export async function generateFeed( date: record.publishedAt ? new Date(record.publishedAt) : new Date(), id: docUrl, link: docUrl, - content: chunks.join(""), + content, }); }