From 533ae887cec2c928d7a703b15deb38c52d0d0508 Mon Sep 17 00:00:00 2001 From: celine Date: Fri, 17 Apr 2026 03:52:40 -0400 Subject: [PATCH 01/10] fix 500 issue --- app/api/oauth/[route]/oauth-metadata.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/oauth/[route]/oauth-metadata.ts b/app/api/oauth/[route]/oauth-metadata.ts index 567cbbb3..a63939b2 100644 --- a/app/api/oauth/[route]/oauth-metadata.ts +++ b/app/api/oauth/[route]/oauth-metadata.ts @@ -8,7 +8,7 @@ const hostname = : "https://leaflet.pub"; const scope = - "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview rpc:parts.page.mention.search?aud=* blob:*/*"; + "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview rpc:parts.page.mention.search?aud=* blob:*/*"; const localconfig: OAuthClientMetadataInput = { client_id: `http://localhost/?redirect_uri=${encodeURI(`http://127.0.0.1:3000/api/oauth/callback`)}&scope=${encodeURIComponent(scope)}`, client_name: `Leaflet`, -- 2.51.2 From a10bdc99d781ab398b3249c56fda085911be02f1 Mon Sep 17 00:00:00 2001 From: Hayden <22327045+hbjydev@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:37:36 +0100 Subject: [PATCH 02/10] fix(scopes): use account:email scope over transition:email (#285) * fix(scopes): use account:email scope over transition:email * fix: include new non-transitional scope in oauth meta * fix: reintroduce transition:email to scope list in metadata --- app/api/oauth/[route]/oauth-metadata.ts | 2 +- app/api/oauth/[route]/route.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/oauth/[route]/oauth-metadata.ts b/app/api/oauth/[route]/oauth-metadata.ts index a63939b2..bfec5baf 100644 --- a/app/api/oauth/[route]/oauth-metadata.ts +++ b/app/api/oauth/[route]/oauth-metadata.ts @@ -8,7 +8,7 @@ const hostname = : "https://leaflet.pub"; const scope = - "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview rpc:parts.page.mention.search?aud=* blob:*/*"; + "atproto transition:email account:email?action=read include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview rpc:parts.page.mention.search?aud=* blob:*/*"; const localconfig: OAuthClientMetadataInput = { client_id: `http://localhost/?redirect_uri=${encodeURI(`http://127.0.0.1:3000/api/oauth/callback`)}&scope=${encodeURIComponent(scope)}`, client_name: `Leaflet`, diff --git a/app/api/oauth/[route]/route.ts b/app/api/oauth/[route]/route.ts index f243f1f9..05f16579 100644 --- a/app/api/oauth/[route]/route.ts +++ b/app/api/oauth/[route]/route.ts @@ -44,7 +44,7 @@ export async function GET( const url = await client.authorize(handle || "https://bsky.social", { scope: - "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview rpc:parts.page.mention.search?aud=* blob:*/*", + "atproto account:email?action=read include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview rpc:parts.page.mention.search?aud=* blob:*/*", signal: ac.signal, state: JSON.stringify(state), ...(signup ? { prompt: "create" } : {}), -- 2.51.2 From 3ebf9823d5c995b8e2bc8240b3a11ec8cf7abbe7 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Fri, 17 Apr 2026 20:11:29 -0400 Subject: [PATCH 03/10] skip auth redirect for crawlers on custom domains (#291) The middleware redirects unauthenticated custom-domain visitors through leaflet.pub/auth_callback to hydrate an external_auth_token cookie. Bots don't hold cookies or run JS, so they never complete the handshake and end up indexing the redirect instead of the publication. Detect common crawler/social preview user agents and let them through unauthenticated. Co-authored-by: Claude --- middleware.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/middleware.ts b/middleware.ts index 27b6e977..0d0294d6 100644 --- a/middleware.ts +++ b/middleware.ts @@ -38,6 +38,15 @@ type DomainRoutes = Awaited>; const auth_callback_route = "/auth_callback"; const receive_auth_callback_route = "/receive_auth_callback"; + +const botUserAgentRegex = + /bot|crawler|spider|crawling|facebookexternalhit|facebookcatalog|whatsapp|telegram|slackbot|discordbot|linkedinbot|twitterbot|embedly|quora link preview|pinterest|redditbot|applebot|duckduckbot|baiduspider|yandex|bingpreview|vkshare|w3c_validator|mastodon|pleroma|misskey|iframely|skypeuripreview|google-inspectiontool|chrome-lighthouse/i; + +function isBot(req: NextRequest) { + let ua = req.headers.get("user-agent"); + if (!ua) return false; + return botUserAgentRegex.test(ua); +} export default async function middleware(req: NextRequest) { let hostname = req.headers.get("host")!; if (req.nextUrl.pathname === auth_callback_route) return authCallback(req); @@ -76,6 +85,7 @@ export default async function middleware(req: NextRequest) { if ( !isStaticReq && + !isBot(req) && (!cookie || req.nextUrl.searchParams.has("refreshAuth")) && !authCompleted && !hostname.includes("leaflet.pub") -- 2.51.2 From d7b5b800c749dd2d38e44996f45bec8cc6bf3351 Mon Sep 17 00:00:00 2001 From: Brendan Schlagel Date: Mon, 20 Apr 2026 12:16:21 -0400 Subject: [PATCH 04/10] set custom referrer policy for youtube in EmbedBlock --- components/Blocks/EmbedBlock.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/components/Blocks/EmbedBlock.tsx b/components/Blocks/EmbedBlock.tsx index 630853cf..d84e5aca 100644 --- a/components/Blocks/EmbedBlock.tsx +++ b/components/Blocks/EmbedBlock.tsx @@ -153,6 +153,11 @@ export const EmbedBlock = (props: BlockProps & { preview?: boolean }) => { return src.toString(); }, [url, bgPage, primary]); + // check if URL is a YouTube link, so we can set the appropriate referrer policy + // to avoid YT config "Error 153" since they're strict about requiring a valid referer header + let isYouTube = + iframeSrc?.includes("youtube.com") || iframeSrc?.includes("youtu.be"); + if (props.preview) return null; if (!url) { if (!permissions.write) return null; @@ -196,7 +201,9 @@ export const EmbedBlock = (props: BlockProps & { preview?: boolean }) => { src={iframeSrc} allow="fullscreen" loading="lazy" - referrerPolicy="no-referrer" + referrerPolicy={ + isYouTube ? "strict-origin-when-cross-origin" : "no-referrer" + } > @@ -205,8 +212,6 @@ export const EmbedBlock = (props: BlockProps & { preview?: boolean }) => {
Date: Mon, 20 Apr 2026 12:34:20 -0400 Subject: [PATCH 05/10] give footnotes a bit larger click or tap target --- app/lish/[did]/[publication]/[rkey]/Blocks/TextBlockCore.tsx | 2 +- .../[publication]/[rkey]/Footnotes/PublishedFootnotePopover.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/Blocks/TextBlockCore.tsx b/app/lish/[did]/[publication]/[rkey]/Blocks/TextBlockCore.tsx index 69a40edc..d3125de2 100644 --- a/app/lish/[did]/[publication]/[rkey]/Blocks/TextBlockCore.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Blocks/TextBlockCore.tsx @@ -59,7 +59,7 @@ export function TextBlockCore(props: TextBlockCoreProps) { footnoteId={isFootnote.footnoteId} index={fnIndex} > - + {fnIndex} , diff --git a/app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnotePopover.tsx b/app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnotePopover.tsx index a8bdb543..8fc72aba 100644 --- a/app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnotePopover.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnotePopover.tsx @@ -36,7 +36,7 @@ export function PublishedFootnoteRefRenderer(props: { return ( { e.preventDefault(); -- 2.51.2 From e5e3234eb9d19abd9b8d300a17a95cef36a8951e Mon Sep 17 00:00:00 2001 From: Brendan Schlagel Date: Mon, 20 Apr 2026 12:44:42 -0400 Subject: [PATCH 06/10] larger footnote target in editor too --- app/globals.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/globals.css b/app/globals.css index d17e1d74..a60bf02d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -591,10 +591,12 @@ pre.shiki { .footnote-ref::after { content: counter(footnote); vertical-align: super; - padding-left: 2px; + padding: 4px; + margin: -4px -4px -4px -2px; font-size: 75%; line-height: 0rem; } +/* -ml-0.5 -mr-1 -my-1 p-1 */ .footnote-ref ~ br.ProseMirror-trailingBreak { display: inline; width: 4px; -- 2.51.2 From 9f0c38255b36516bbce85de0a7fb4b3076b3c6c7 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 20 Apr 2026 13:08:07 -0400 Subject: [PATCH 07/10] fix published footnote count --- .../[rkey]/Footnotes/PublishedFootnotes.tsx | 61 ++++++++++++++++--- .../[publication]/[rkey]/PostContent.tsx | 10 +++ 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnotes.tsx b/app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnotes.tsx index deeec557..92e199f2 100644 --- a/app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnotes.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnotes.tsx @@ -5,6 +5,8 @@ import { PubLeafletBlocksText, PubLeafletBlocksHeader, PubLeafletBlocksBlockquote, + PubLeafletBlocksOrderedList, + PubLeafletBlocksUnorderedList, PubLeafletPagesLinearDocument, } from "lexicons/api"; import { TextBlockCore } from "../Blocks/TextBlockCore"; @@ -29,7 +31,6 @@ export function collectFootnotesFromBlocks( function scanFacets(facets?: PubLeafletRichtextFacet.Main[]) { if (!facets || !Array.isArray(facets)) return; - console.log(facets); for (let facet of facets) { for (let feature of facet.features) { if (PubLeafletRichtextFacet.isFootnote(feature)) { @@ -47,17 +48,59 @@ export function collectFootnotesFromBlocks( } } + function scanBlockContent( + content: + | PubLeafletBlocksText.Main + | PubLeafletBlocksHeader.Main + | PubLeafletBlocksBlockquote.Main + | { $type?: string }, + ) { + if (PubLeafletBlocksText.isMain(content)) { + scanFacets(content.facets); + } else if (PubLeafletBlocksHeader.isMain(content)) { + scanFacets(content.facets); + } else if (PubLeafletBlocksBlockquote.isMain(content)) { + scanFacets(content.facets); + } + } + + function scanOrderedListItems( + items: PubLeafletBlocksOrderedList.ListItem[], + ) { + for (let item of items) { + scanBlockContent(item.content); + if (item.children?.length) { + scanOrderedListItems(item.children); + } + if (item.unorderedListChildren?.children?.length) { + scanUnorderedListItems(item.unorderedListChildren.children); + } + } + } + + function scanUnorderedListItems( + items: PubLeafletBlocksUnorderedList.ListItem[], + ) { + for (let item of items) { + scanBlockContent(item.content); + if (item.children?.length) { + scanUnorderedListItems(item.children); + } + if (item.orderedListChildren?.children?.length) { + scanOrderedListItems(item.orderedListChildren.children); + } + } + } + for (let b of blocks) { let block = b.block; - let facets: PubLeafletRichtextFacet.Main[] | undefined; - if (PubLeafletBlocksText.isMain(block)) { - facets = block.facets; - } else if (PubLeafletBlocksHeader.isMain(block)) { - facets = block.facets; - } else if (PubLeafletBlocksBlockquote.isMain(block)) { - facets = block.facets; + if (PubLeafletBlocksOrderedList.isMain(block)) { + scanOrderedListItems(block.children); + } else if (PubLeafletBlocksUnorderedList.isMain(block)) { + scanUnorderedListItems(block.children); + } else { + scanBlockContent(block); } - if (facets) scanFacets(facets); } return footnotes; diff --git a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx index 81cc2a85..e6164175 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx @@ -256,6 +256,7 @@ export let Block = ({ key={i} className={className} pageId={pageId} + footnoteIndexMap={footnoteIndexMap} /> ))} @@ -277,6 +278,7 @@ export let Block = ({ className={className} pageId={pageId} startIndex={block.startIndex || 1} + footnoteIndexMap={footnoteIndexMap} /> ))} @@ -547,6 +549,7 @@ function ListItem(props: { bskyPostData: AppBskyFeedDefs.PostView[]; pollData: PollData[]; pageId?: string; + footnoteIndexMap?: Map; }) { let children = props.item.children?.length ? (
    @@ -561,6 +564,7 @@ function ListItem(props: { key={index} className={props.className} pageId={props.pageId} + footnoteIndexMap={props.footnoteIndexMap} /> ))}
@@ -579,6 +583,7 @@ function ListItem(props: { className={props.className} pageId={props.pageId} startIndex={props.item.orderedListChildren?.startIndex} + footnoteIndexMap={props.footnoteIndexMap} /> ))} @@ -606,6 +611,7 @@ function ListItem(props: { isList index={props.index} pageId={props.pageId} + footnoteIndexMap={props.footnoteIndexMap} /> {children} {orderedChildren} @@ -624,6 +630,7 @@ function OrderedListItem(props: { pollData: PollData[]; pageId?: string; startIndex?: number; + footnoteIndexMap?: Map; }) { const calculatedIndex = (props.startIndex || 1) + props.index[props.index.length - 1]; @@ -641,6 +648,7 @@ function OrderedListItem(props: { className={props.className} pageId={props.pageId} startIndex={props.startIndex} + footnoteIndexMap={props.footnoteIndexMap} /> ))} @@ -658,6 +666,7 @@ function OrderedListItem(props: { key={index} className={props.className} pageId={props.pageId} + footnoteIndexMap={props.footnoteIndexMap} /> ))} @@ -685,6 +694,7 @@ function OrderedListItem(props: { isList index={props.index} pageId={props.pageId} + footnoteIndexMap={props.footnoteIndexMap} /> {children} {unorderedChildren} -- 2.51.2 From 0ccd7fc63b77d46ab2ad3702e63c05359897a294 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 20 Apr 2026 21:45:27 -0400 Subject: [PATCH 08/10] rename icons --- public/{Logos => logos}/Logo B&W 16x16.svg | 0 public/{Logos => logos}/Logo B&W 24x24.svg | 0 public/{Logos => logos}/Logo B&W 32x32.svg | 0 public/{Logos => logos}/Logo B&W 64x64.svg | 0 public/{Logos => logos}/Logo Color 16x16.svg | 0 public/{Logos => logos}/Logo Color 24x24.svg | 0 public/{Logos => logos}/Logo Color 32x32.svg | 0 public/{Logos => logos}/Logo Color 64x64.svg | 0 public/{Logos => logos}/anisota.svg | 0 public/{Logos => logos}/blacksky.svg | 0 public/{Logos => logos}/blento.svg | 0 public/{Logos => logos}/bluesky.svg | 0 public/{Logos => logos}/cartridge.svg | 0 public/{Logos => logos}/eurosky.svg | 0 public/{Logos => logos}/graze.svg | 0 public/{Logos => logos}/leaflet.svg | 0 public/{Logos => logos}/margin.svg | 0 public/{Logos => logos}/offprint.svg | 0 public/{Logos => logos}/pckt.svg | 0 public/{Logos => logos}/pdsls.svg | 0 public/{Logos => logos}/plyr.fm.svg | 0 public/{Logos => logos}/popfeed.svg | 0 public/{Logos => logos}/roomy.svg | 0 public/{Logos => logos}/semble.svg | 0 public/{Logos => logos}/sill.svg | 0 public/{Logos => logos}/spark.svg | 0 public/{Logos => logos}/surf.svg | 0 public/{Logos => logos}/tangled.svg | 0 28 files changed, 0 insertions(+), 0 deletions(-) rename public/{Logos => logos}/Logo B&W 16x16.svg (100%) rename public/{Logos => logos}/Logo B&W 24x24.svg (100%) rename public/{Logos => logos}/Logo B&W 32x32.svg (100%) rename public/{Logos => logos}/Logo B&W 64x64.svg (100%) rename public/{Logos => logos}/Logo Color 16x16.svg (100%) rename public/{Logos => logos}/Logo Color 24x24.svg (100%) rename public/{Logos => logos}/Logo Color 32x32.svg (100%) rename public/{Logos => logos}/Logo Color 64x64.svg (100%) rename public/{Logos => logos}/anisota.svg (100%) rename public/{Logos => logos}/blacksky.svg (100%) rename public/{Logos => logos}/blento.svg (100%) rename public/{Logos => logos}/bluesky.svg (100%) rename public/{Logos => logos}/cartridge.svg (100%) rename public/{Logos => logos}/eurosky.svg (100%) rename public/{Logos => logos}/graze.svg (100%) rename public/{Logos => logos}/leaflet.svg (100%) rename public/{Logos => logos}/margin.svg (100%) rename public/{Logos => logos}/offprint.svg (100%) rename public/{Logos => logos}/pckt.svg (100%) rename public/{Logos => logos}/pdsls.svg (100%) rename public/{Logos => logos}/plyr.fm.svg (100%) rename public/{Logos => logos}/popfeed.svg (100%) rename public/{Logos => logos}/roomy.svg (100%) rename public/{Logos => logos}/semble.svg (100%) rename public/{Logos => logos}/sill.svg (100%) rename public/{Logos => logos}/spark.svg (100%) rename public/{Logos => logos}/surf.svg (100%) rename public/{Logos => logos}/tangled.svg (100%) diff --git a/public/Logos/Logo B&W 16x16.svg b/public/logos/Logo B&W 16x16.svg similarity index 100% rename from public/Logos/Logo B&W 16x16.svg rename to public/logos/Logo B&W 16x16.svg diff --git a/public/Logos/Logo B&W 24x24.svg b/public/logos/Logo B&W 24x24.svg similarity index 100% rename from public/Logos/Logo B&W 24x24.svg rename to public/logos/Logo B&W 24x24.svg diff --git a/public/Logos/Logo B&W 32x32.svg b/public/logos/Logo B&W 32x32.svg similarity index 100% rename from public/Logos/Logo B&W 32x32.svg rename to public/logos/Logo B&W 32x32.svg diff --git a/public/Logos/Logo B&W 64x64.svg b/public/logos/Logo B&W 64x64.svg similarity index 100% rename from public/Logos/Logo B&W 64x64.svg rename to public/logos/Logo B&W 64x64.svg diff --git a/public/Logos/Logo Color 16x16.svg b/public/logos/Logo Color 16x16.svg similarity index 100% rename from public/Logos/Logo Color 16x16.svg rename to public/logos/Logo Color 16x16.svg diff --git a/public/Logos/Logo Color 24x24.svg b/public/logos/Logo Color 24x24.svg similarity index 100% rename from public/Logos/Logo Color 24x24.svg rename to public/logos/Logo Color 24x24.svg diff --git a/public/Logos/Logo Color 32x32.svg b/public/logos/Logo Color 32x32.svg similarity index 100% rename from public/Logos/Logo Color 32x32.svg rename to public/logos/Logo Color 32x32.svg diff --git a/public/Logos/Logo Color 64x64.svg b/public/logos/Logo Color 64x64.svg similarity index 100% rename from public/Logos/Logo Color 64x64.svg rename to public/logos/Logo Color 64x64.svg diff --git a/public/Logos/anisota.svg b/public/logos/anisota.svg similarity index 100% rename from public/Logos/anisota.svg rename to public/logos/anisota.svg diff --git a/public/Logos/blacksky.svg b/public/logos/blacksky.svg similarity index 100% rename from public/Logos/blacksky.svg rename to public/logos/blacksky.svg diff --git a/public/Logos/blento.svg b/public/logos/blento.svg similarity index 100% rename from public/Logos/blento.svg rename to public/logos/blento.svg diff --git a/public/Logos/bluesky.svg b/public/logos/bluesky.svg similarity index 100% rename from public/Logos/bluesky.svg rename to public/logos/bluesky.svg diff --git a/public/Logos/cartridge.svg b/public/logos/cartridge.svg similarity index 100% rename from public/Logos/cartridge.svg rename to public/logos/cartridge.svg diff --git a/public/Logos/eurosky.svg b/public/logos/eurosky.svg similarity index 100% rename from public/Logos/eurosky.svg rename to public/logos/eurosky.svg diff --git a/public/Logos/graze.svg b/public/logos/graze.svg similarity index 100% rename from public/Logos/graze.svg rename to public/logos/graze.svg diff --git a/public/Logos/leaflet.svg b/public/logos/leaflet.svg similarity index 100% rename from public/Logos/leaflet.svg rename to public/logos/leaflet.svg diff --git a/public/Logos/margin.svg b/public/logos/margin.svg similarity index 100% rename from public/Logos/margin.svg rename to public/logos/margin.svg diff --git a/public/Logos/offprint.svg b/public/logos/offprint.svg similarity index 100% rename from public/Logos/offprint.svg rename to public/logos/offprint.svg diff --git a/public/Logos/pckt.svg b/public/logos/pckt.svg similarity index 100% rename from public/Logos/pckt.svg rename to public/logos/pckt.svg diff --git a/public/Logos/pdsls.svg b/public/logos/pdsls.svg similarity index 100% rename from public/Logos/pdsls.svg rename to public/logos/pdsls.svg diff --git a/public/Logos/plyr.fm.svg b/public/logos/plyr.fm.svg similarity index 100% rename from public/Logos/plyr.fm.svg rename to public/logos/plyr.fm.svg diff --git a/public/Logos/popfeed.svg b/public/logos/popfeed.svg similarity index 100% rename from public/Logos/popfeed.svg rename to public/logos/popfeed.svg diff --git a/public/Logos/roomy.svg b/public/logos/roomy.svg similarity index 100% rename from public/Logos/roomy.svg rename to public/logos/roomy.svg diff --git a/public/Logos/semble.svg b/public/logos/semble.svg similarity index 100% rename from public/Logos/semble.svg rename to public/logos/semble.svg diff --git a/public/Logos/sill.svg b/public/logos/sill.svg similarity index 100% rename from public/Logos/sill.svg rename to public/logos/sill.svg diff --git a/public/Logos/spark.svg b/public/logos/spark.svg similarity index 100% rename from public/Logos/spark.svg rename to public/logos/spark.svg diff --git a/public/Logos/surf.svg b/public/logos/surf.svg similarity index 100% rename from public/Logos/surf.svg rename to public/logos/surf.svg diff --git a/public/Logos/tangled.svg b/public/logos/tangled.svg similarity index 100% rename from public/Logos/tangled.svg rename to public/logos/tangled.svg -- 2.51.2 From 5bc1ab5f9c1ab0f0cdce2461df7cae6d56fb9fb8 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 20 Apr 2026 21:49:48 -0400 Subject: [PATCH 09/10] tweak handlesubscribe --- components/Subscribe/HandleSubscribe.tsx | 61 ++++++++++-------------- 1 file changed, 25 insertions(+), 36 deletions(-) diff --git a/components/Subscribe/HandleSubscribe.tsx b/components/Subscribe/HandleSubscribe.tsx index dea8b723..c2e92b86 100644 --- a/components/Subscribe/HandleSubscribe.tsx +++ b/components/Subscribe/HandleSubscribe.tsx @@ -4,28 +4,28 @@ import { BlueskyTiny } from "components/Icons/BlueskyTiny"; import { Popover } from "components/Popover"; import Link from "next/link"; import { HandleInput } from "./HandleInput"; -const logos = { - Anisota: "/logos/anisota.svg", - Blacksky: "/logos/blacksky.svg", - Blento: "/logos/blento.svg", - Bluesky: "/logos/bluesky.svg", - Cartridge: "/logos/cartridge.svg", - Eurosky: "/logos/eurosky.svg", - Graze: "/logos/graze.svg", - Leaflet: "/logos/leaflet.svg", - Margin: "/logos/margin.svg", - Offprint: "/logos/offprint.svg", - Pckt: "/logos/pckt.svg", - pdsls: "/logos/pdsls.svg", - "plyr.fm": "/logos/plyr.fm.svg", - Popfeed: "/logos/popfeed.svg", - Roomy: "/logos/roomy.svg", - Semble: "/logos/semble.svg", - Sill: "/logos/sill.svg", - Spark: "/logos/spark.svg", - Surf: "/logos/surf.svg", - Tangled: "/logos/tangled.svg", -}; +const apps = [ + { name: "Leaflet", logo: "/logos/leaflet.svg" }, + { name: "Bluesky", logo: "/logos/bluesky.svg" }, + { name: "Blacksky", logo: "/logos/blacksky.svg" }, + { name: "Eurosky", logo: "/logos/eurosky.svg" }, + { name: "Tangled", logo: "/logos/tangled.svg" }, + { name: "Semble", logo: "/logos/semble.svg" }, + { name: "Surf", logo: "/logos/surf.svg" }, + { name: "Spark", logo: "/logos/spark.svg" }, + { name: "Pckt", logo: "/logos/pckt.svg" }, + { name: "pdsls", logo: "/logos/pdsls.svg" }, + { name: "plyr.fm", logo: "/logos/plyr.fm.svg" }, + { name: "Popfeed", logo: "/logos/popfeed.svg" }, + { name: "Roomy", logo: "/logos/roomy.svg" }, + { name: "Sill", logo: "/logos/sill.svg" }, + { name: "Offprint", logo: "/logos/offprint.svg" }, + { name: "Margin", logo: "/logos/margin.svg" }, + { name: "Anisota", logo: "/logos/anisota.svg" }, + { name: "Blento", logo: "/logos/blento.svg" }, + { name: "Cartridge", logo: "/logos/cartridge.svg" }, + { name: "Graze", logo: "/logos/graze.svg" }, +]; import { Separator } from "components/Layout"; @@ -157,24 +157,13 @@ export const AtmosphericHandleInfo = (props: { trigger?: React.ReactNode }) => {
- {( - [ - "Leaflet", "Bluesky", "Blacksky", "Eurosky", "Tangled", - "Semble", "Surf", "Spark", "Pckt", "pdsls", "plyr.fm", - "Popfeed", "Roomy", "Sill", "Offprint", "Margin", - "Anisota", "Blento", "Cartridge", "Graze", - "Leaflet", "Bluesky", "Blacksky", "Eurosky", "Tangled", - "Semble", "Surf", "Spark", "Pckt", "pdsls", "plyr.fm", - "Popfeed", "Roomy", "Sill", "Offprint", "Margin", - "Anisota", "Blento", "Cartridge", "Graze", - ] as (keyof typeof logos)[] - ).map((name, i) => ( - + {[...apps, ...apps].map((app, i) => ( + ))}
- + Sign up via Bluesky! -- 2.51.2 From 53809d92e3f482498f1dcf36ac474ba04e31c763 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 21 Apr 2026 15:06:29 -0400 Subject: [PATCH 10/10] add newsletter tables --- actions/subscribeToPublicationWithEmail.ts | 14 -- actions/unsubscribeFromPublication.ts | 14 -- .../functions/migrate_user_to_standard.ts | 7 - app/api/rpc/[command]/get_publication_data.ts | 1 + app/lish/Subscribe.tsx | 1 - supabase/database.types.ts | 209 ++++++++++++++--- .../20260421000000_add_newsletter_tables.sql | 214 ++++++++++++++++++ 7 files changed, 391 insertions(+), 69 deletions(-) delete mode 100644 actions/subscribeToPublicationWithEmail.ts delete mode 100644 actions/unsubscribeFromPublication.ts create mode 100644 supabase/migrations/20260421000000_add_newsletter_tables.sql diff --git a/actions/subscribeToPublicationWithEmail.ts b/actions/subscribeToPublicationWithEmail.ts deleted file mode 100644 index ffd77c6a..00000000 --- a/actions/subscribeToPublicationWithEmail.ts +++ /dev/null @@ -1,14 +0,0 @@ -"use server"; -import { supabaseServerClient } from "supabase/serverClient"; -import { getIdentityData } from "./getIdentityData"; - -export async function subscribeToPublicationWithEmail(publication: string) { - let identity = await getIdentityData(); - if (!identity || !identity.email) return null; - //This is an email relation!! - console.log( - await supabaseServerClient - .from("subscribers_to_publications") - .insert({ publication: publication, identity: identity.email }), - ); -} diff --git a/actions/unsubscribeFromPublication.ts b/actions/unsubscribeFromPublication.ts deleted file mode 100644 index cee81156..00000000 --- a/actions/unsubscribeFromPublication.ts +++ /dev/null @@ -1,14 +0,0 @@ -"use server"; -import { supabaseServerClient } from "supabase/serverClient"; -import { getIdentityData } from "./getIdentityData"; - -export async function unsubscribeFromPublication(publication: string) { - let identity = await getIdentityData(); - if (!identity || !identity.email) return null; - //This is an email relation!! - await supabaseServerClient - .from("subscribers_to_publications") - .delete() - .eq("publication", publication) - .eq("identity", identity.email); -} diff --git a/app/api/inngest/functions/migrate_user_to_standard.ts b/app/api/inngest/functions/migrate_user_to_standard.ts index afc51931..ef5abe0b 100644 --- a/app/api/inngest/functions/migrate_user_to_standard.ts +++ b/app/api/inngest/functions/migrate_user_to_standard.ts @@ -496,13 +496,6 @@ export const migrate_user_to_standard = inngest.createFunction( .update({ document: newUri }) .eq("document", oldUri), ), - // Update subscribers_to_publications - publication references - ...pubEntries.map(([oldUri, newUri]) => - supabaseServerClient - .from("subscribers_to_publications") - .update({ publication: newUri }) - .eq("publication", oldUri), - ), // Update publication_subscriptions - publication references ...pubEntries.map(([oldUri, newUri]) => supabaseServerClient diff --git a/app/api/rpc/[command]/get_publication_data.ts b/app/api/rpc/[command]/get_publication_data.ts index 9fe23804..e0db6852 100644 --- a/app/api/rpc/[command]/get_publication_data.ts +++ b/app/api/rpc/[command]/get_publication_data.ts @@ -45,6 +45,7 @@ export const get_publication_data = makeRoute({ )), publication_subscriptions(*, identities(bsky_profiles(*))), publication_domains(*), + publication_newsletter_settings(enabled), leaflets_in_publications(*, documents(*), permission_tokens(*, diff --git a/app/lish/Subscribe.tsx b/app/lish/Subscribe.tsx index 4b75c478..3e415a28 100644 --- a/app/lish/Subscribe.tsx +++ b/app/lish/Subscribe.tsx @@ -7,7 +7,6 @@ import { confirmEmailAuthToken, requestAuthEmailToken, } from "actions/emailAuth"; -import { subscribeToPublicationWithEmail } from "actions/subscribeToPublicationWithEmail"; import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; import { ShareSmall } from "components/Icons/ShareSmall"; import { Popover } from "components/Popover"; diff --git a/supabase/database.types.ts b/supabase/database.types.ts index 68844690..4210d201 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -1057,6 +1057,182 @@ export type Database = { }, ] } + publication_email_subscriber_events: { + Row: { + event_type: string + id: string + metadata: Json | null + occurred_at: string + publication: string + subscriber: string + } + Insert: { + event_type: string + id?: string + metadata?: Json | null + occurred_at?: string + publication: string + subscriber: string + } + Update: { + event_type?: string + id?: string + metadata?: Json | null + occurred_at?: string + publication?: string + subscriber?: string + } + Relationships: [ + { + foreignKeyName: "publication_email_subscriber_events_publication_fkey" + columns: ["publication"] + isOneToOne: false + referencedRelation: "publications" + referencedColumns: ["uri"] + }, + { + foreignKeyName: "publication_email_subscriber_events_subscriber_fkey" + columns: ["subscriber"] + isOneToOne: false + referencedRelation: "publication_email_subscribers" + referencedColumns: ["id"] + }, + ] + } + publication_email_subscribers: { + Row: { + confirmation_code: string | null + confirmed_at: string | null + created_at: string + email: string + id: string + identity_id: string | null + publication: string + state: string + unsubscribe_token: string + unsubscribed_at: string | null + } + Insert: { + confirmation_code?: string | null + confirmed_at?: string | null + created_at?: string + email: string + id?: string + identity_id?: string | null + publication: string + state?: string + unsubscribe_token?: string + unsubscribed_at?: string | null + } + Update: { + confirmation_code?: string | null + confirmed_at?: string | null + created_at?: string + email?: string + id?: string + identity_id?: string | null + publication?: string + state?: string + unsubscribe_token?: string + unsubscribed_at?: string | null + } + Relationships: [ + { + foreignKeyName: "publication_email_subscribers_identity_id_fkey" + columns: ["identity_id"] + isOneToOne: false + referencedRelation: "identities" + referencedColumns: ["id"] + }, + { + foreignKeyName: "publication_email_subscribers_publication_fkey" + columns: ["publication"] + isOneToOne: false + referencedRelation: "publications" + referencedColumns: ["uri"] + }, + ] + } + publication_newsletter_settings: { + Row: { + created_at: string + enabled: boolean + publication: string + reply_to_email: string | null + reply_to_verified_at: string | null + updated_at: string + } + Insert: { + created_at?: string + enabled?: boolean + publication: string + reply_to_email?: string | null + reply_to_verified_at?: string | null + updated_at?: string + } + Update: { + created_at?: string + enabled?: boolean + publication?: string + reply_to_email?: string | null + reply_to_verified_at?: string | null + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "publication_newsletter_settings_publication_fkey" + columns: ["publication"] + isOneToOne: true + referencedRelation: "publications" + referencedColumns: ["uri"] + }, + ] + } + publication_post_sends: { + Row: { + completed_at: string | null + document: string + error: string | null + publication: string + started_at: string + status: string + subscriber_count: number | null + } + Insert: { + completed_at?: string | null + document: string + error?: string | null + publication: string + started_at?: string + status?: string + subscriber_count?: number | null + } + Update: { + completed_at?: string | null + document?: string + error?: string | null + publication?: string + started_at?: string + status?: string + subscriber_count?: number | null + } + Relationships: [ + { + foreignKeyName: "publication_post_sends_document_fkey" + columns: ["document"] + isOneToOne: false + referencedRelation: "documents" + referencedColumns: ["uri"] + }, + { + foreignKeyName: "publication_post_sends_publication_fkey" + columns: ["publication"] + isOneToOne: false + referencedRelation: "publications" + referencedColumns: ["uri"] + }, + ] + } publication_subscriptions: { Row: { created_at: string @@ -1315,39 +1491,6 @@ export type Database = { }, ] } - subscribers_to_publications: { - Row: { - created_at: string - identity: string - publication: string - } - Insert: { - created_at?: string - identity: string - publication: string - } - Update: { - created_at?: string - identity?: string - publication?: string - } - Relationships: [ - { - foreignKeyName: "subscribers_to_publications_identity_fkey" - columns: ["identity"] - isOneToOne: false - referencedRelation: "identities" - referencedColumns: ["email"] - }, - { - foreignKeyName: "subscribers_to_publications_publication_fkey" - columns: ["publication"] - isOneToOne: false - referencedRelation: "publications" - referencedColumns: ["uri"] - }, - ] - } user_entitlements: { Row: { entitlement_key: string diff --git a/supabase/migrations/20260421000000_add_newsletter_tables.sql b/supabase/migrations/20260421000000_add_newsletter_tables.sql new file mode 100644 index 00000000..9e3a463c --- /dev/null +++ b/supabase/migrations/20260421000000_add_newsletter_tables.sql @@ -0,0 +1,214 @@ +-- Newsletter mode: four new tables and drop the legacy subscribers_to_publications. +-- See specs/2026-04-20-newsletter-mode.md. + +-- Legacy table has no live rows; its writers retired with the feature branch. +drop table if exists "public"."subscribers_to_publications"; + + +-- publication_newsletter_settings --------------------------------------------- +-- Per-publication newsletter config. Separate from publications.metadata because +-- reply_to_email has a real verification lifecycle and we want cheap queries for +-- "newsletter-enabled publications". +create table "public"."publication_newsletter_settings" ( + "publication" text not null, + "enabled" boolean not null default false, + "reply_to_email" text, + "reply_to_verified_at" timestamp with time zone, + "created_at" timestamp with time zone not null default now(), + "updated_at" timestamp with time zone not null default now() +); + +alter table "public"."publication_newsletter_settings" enable row level security; + +CREATE UNIQUE INDEX publication_newsletter_settings_pkey ON public.publication_newsletter_settings USING btree (publication); +CREATE INDEX publication_newsletter_settings_enabled_idx ON public.publication_newsletter_settings USING btree (publication) WHERE enabled; + +alter table "public"."publication_newsletter_settings" add constraint "publication_newsletter_settings_pkey" PRIMARY KEY using index "publication_newsletter_settings_pkey"; + +alter table "public"."publication_newsletter_settings" add constraint "publication_newsletter_settings_publication_fkey" FOREIGN KEY (publication) REFERENCES publications(uri) ON DELETE CASCADE not valid; +alter table "public"."publication_newsletter_settings" validate constraint "publication_newsletter_settings_publication_fkey"; + +grant delete on table "public"."publication_newsletter_settings" to "anon"; +grant insert on table "public"."publication_newsletter_settings" to "anon"; +grant references on table "public"."publication_newsletter_settings" to "anon"; +grant select on table "public"."publication_newsletter_settings" to "anon"; +grant trigger on table "public"."publication_newsletter_settings" to "anon"; +grant truncate on table "public"."publication_newsletter_settings" to "anon"; +grant update on table "public"."publication_newsletter_settings" to "anon"; +grant delete on table "public"."publication_newsletter_settings" to "authenticated"; +grant insert on table "public"."publication_newsletter_settings" to "authenticated"; +grant references on table "public"."publication_newsletter_settings" to "authenticated"; +grant select on table "public"."publication_newsletter_settings" to "authenticated"; +grant trigger on table "public"."publication_newsletter_settings" to "authenticated"; +grant truncate on table "public"."publication_newsletter_settings" to "authenticated"; +grant update on table "public"."publication_newsletter_settings" to "authenticated"; +grant delete on table "public"."publication_newsletter_settings" to "service_role"; +grant insert on table "public"."publication_newsletter_settings" to "service_role"; +grant references on table "public"."publication_newsletter_settings" to "service_role"; +grant select on table "public"."publication_newsletter_settings" to "service_role"; +grant trigger on table "public"."publication_newsletter_settings" to "service_role"; +grant truncate on table "public"."publication_newsletter_settings" to "service_role"; +grant update on table "public"."publication_newsletter_settings" to "service_role"; + + +-- publication_email_subscribers ----------------------------------------------- +-- Per-publication email subscriber list. Handle subscriptions continue to live +-- in publication_subscriptions; cross-flow queries union the two. +create table "public"."publication_email_subscribers" ( + "id" uuid not null default gen_random_uuid(), + "publication" text not null, + "email" text not null, + "identity_id" uuid, + "state" text not null default 'pending', + "confirmation_code" text, + "unsubscribe_token" uuid not null default gen_random_uuid(), + "created_at" timestamp with time zone not null default now(), + "confirmed_at" timestamp with time zone, + "unsubscribed_at" timestamp with time zone +); + +alter table "public"."publication_email_subscribers" enable row level security; + +CREATE UNIQUE INDEX publication_email_subscribers_pkey ON public.publication_email_subscribers USING btree (id); +CREATE UNIQUE INDEX publication_email_subscribers_publication_email_key ON public.publication_email_subscribers USING btree (publication, email); +CREATE UNIQUE INDEX publication_email_subscribers_unsubscribe_token_key ON public.publication_email_subscribers USING btree (unsubscribe_token); +CREATE INDEX publication_email_subscribers_confirmed_idx ON public.publication_email_subscribers USING btree (publication) WHERE state = 'confirmed'; + +alter table "public"."publication_email_subscribers" add constraint "publication_email_subscribers_pkey" PRIMARY KEY using index "publication_email_subscribers_pkey"; +alter table "public"."publication_email_subscribers" add constraint "publication_email_subscribers_publication_email_key" UNIQUE using index "publication_email_subscribers_publication_email_key"; +alter table "public"."publication_email_subscribers" add constraint "publication_email_subscribers_unsubscribe_token_key" UNIQUE using index "publication_email_subscribers_unsubscribe_token_key"; + +alter table "public"."publication_email_subscribers" add constraint "publication_email_subscribers_publication_fkey" FOREIGN KEY (publication) REFERENCES publications(uri) ON DELETE CASCADE not valid; +alter table "public"."publication_email_subscribers" validate constraint "publication_email_subscribers_publication_fkey"; + +alter table "public"."publication_email_subscribers" add constraint "publication_email_subscribers_identity_id_fkey" FOREIGN KEY (identity_id) REFERENCES identities(id) ON DELETE SET NULL not valid; +alter table "public"."publication_email_subscribers" validate constraint "publication_email_subscribers_identity_id_fkey"; + +alter table "public"."publication_email_subscribers" add constraint "publication_email_subscribers_state_check" CHECK (state IN ('pending','confirmed','unsubscribed')) not valid; +alter table "public"."publication_email_subscribers" validate constraint "publication_email_subscribers_state_check"; + +grant delete on table "public"."publication_email_subscribers" to "anon"; +grant insert on table "public"."publication_email_subscribers" to "anon"; +grant references on table "public"."publication_email_subscribers" to "anon"; +grant select on table "public"."publication_email_subscribers" to "anon"; +grant trigger on table "public"."publication_email_subscribers" to "anon"; +grant truncate on table "public"."publication_email_subscribers" to "anon"; +grant update on table "public"."publication_email_subscribers" to "anon"; +grant delete on table "public"."publication_email_subscribers" to "authenticated"; +grant insert on table "public"."publication_email_subscribers" to "authenticated"; +grant references on table "public"."publication_email_subscribers" to "authenticated"; +grant select on table "public"."publication_email_subscribers" to "authenticated"; +grant trigger on table "public"."publication_email_subscribers" to "authenticated"; +grant truncate on table "public"."publication_email_subscribers" to "authenticated"; +grant update on table "public"."publication_email_subscribers" to "authenticated"; +grant delete on table "public"."publication_email_subscribers" to "service_role"; +grant insert on table "public"."publication_email_subscribers" to "service_role"; +grant references on table "public"."publication_email_subscribers" to "service_role"; +grant select on table "public"."publication_email_subscribers" to "service_role"; +grant trigger on table "public"."publication_email_subscribers" to "service_role"; +grant truncate on table "public"."publication_email_subscribers" to "service_role"; +grant update on table "public"."publication_email_subscribers" to "service_role"; + + +-- publication_email_subscriber_events ----------------------------------------- +-- Append-only event log. Timestamp columns on publication_email_subscribers +-- are convenience caches derivable from this. +create table "public"."publication_email_subscriber_events" ( + "id" uuid not null default gen_random_uuid(), + "subscriber" uuid not null, + "publication" text not null, + "event_type" text not null, + "occurred_at" timestamp with time zone not null default now(), + "metadata" jsonb +); + +alter table "public"."publication_email_subscriber_events" enable row level security; + +CREATE UNIQUE INDEX publication_email_subscriber_events_pkey ON public.publication_email_subscriber_events USING btree (id); +CREATE INDEX publication_email_subscriber_events_subscriber_idx ON public.publication_email_subscriber_events USING btree (subscriber, occurred_at DESC); +CREATE INDEX publication_email_subscriber_events_publication_type_idx ON public.publication_email_subscriber_events USING btree (publication, event_type, occurred_at DESC); + +alter table "public"."publication_email_subscriber_events" add constraint "publication_email_subscriber_events_pkey" PRIMARY KEY using index "publication_email_subscriber_events_pkey"; + +alter table "public"."publication_email_subscriber_events" add constraint "publication_email_subscriber_events_subscriber_fkey" FOREIGN KEY (subscriber) REFERENCES publication_email_subscribers(id) ON DELETE CASCADE not valid; +alter table "public"."publication_email_subscriber_events" validate constraint "publication_email_subscriber_events_subscriber_fkey"; + +alter table "public"."publication_email_subscriber_events" add constraint "publication_email_subscriber_events_publication_fkey" FOREIGN KEY (publication) REFERENCES publications(uri) ON DELETE CASCADE not valid; +alter table "public"."publication_email_subscriber_events" validate constraint "publication_email_subscriber_events_publication_fkey"; + +alter table "public"."publication_email_subscriber_events" add constraint "publication_email_subscriber_events_event_type_check" CHECK (event_type IN ('subscribe_requested','confirmation_sent','confirmed','unsubscribe_requested','resubscribed','post_sent','bounce','complaint')) not valid; +alter table "public"."publication_email_subscriber_events" validate constraint "publication_email_subscriber_events_event_type_check"; + +grant delete on table "public"."publication_email_subscriber_events" to "anon"; +grant insert on table "public"."publication_email_subscriber_events" to "anon"; +grant references on table "public"."publication_email_subscriber_events" to "anon"; +grant select on table "public"."publication_email_subscriber_events" to "anon"; +grant trigger on table "public"."publication_email_subscriber_events" to "anon"; +grant truncate on table "public"."publication_email_subscriber_events" to "anon"; +grant update on table "public"."publication_email_subscriber_events" to "anon"; +grant delete on table "public"."publication_email_subscriber_events" to "authenticated"; +grant insert on table "public"."publication_email_subscriber_events" to "authenticated"; +grant references on table "public"."publication_email_subscriber_events" to "authenticated"; +grant select on table "public"."publication_email_subscriber_events" to "authenticated"; +grant trigger on table "public"."publication_email_subscriber_events" to "authenticated"; +grant truncate on table "public"."publication_email_subscriber_events" to "authenticated"; +grant update on table "public"."publication_email_subscriber_events" to "authenticated"; +grant delete on table "public"."publication_email_subscriber_events" to "service_role"; +grant insert on table "public"."publication_email_subscriber_events" to "service_role"; +grant references on table "public"."publication_email_subscriber_events" to "service_role"; +grant select on table "public"."publication_email_subscriber_events" to "service_role"; +grant trigger on table "public"."publication_email_subscriber_events" to "service_role"; +grant truncate on table "public"."publication_email_subscriber_events" to "service_role"; +grant update on table "public"."publication_email_subscriber_events" to "service_role"; + + +-- publication_post_sends ------------------------------------------------------ +-- One row per (publication, document). Primary job is idempotency: publish +-- inserts with ON CONFLICT DO NOTHING before firing a send. +create table "public"."publication_post_sends" ( + "publication" text not null, + "document" text not null, + "status" text not null default 'pending', + "subscriber_count" integer, + "started_at" timestamp with time zone not null default now(), + "completed_at" timestamp with time zone, + "error" text +); + +alter table "public"."publication_post_sends" enable row level security; + +CREATE UNIQUE INDEX publication_post_sends_pkey ON public.publication_post_sends USING btree (publication, document); +CREATE INDEX publication_post_sends_publication_started_idx ON public.publication_post_sends USING btree (publication, started_at DESC); + +alter table "public"."publication_post_sends" add constraint "publication_post_sends_pkey" PRIMARY KEY using index "publication_post_sends_pkey"; + +alter table "public"."publication_post_sends" add constraint "publication_post_sends_publication_fkey" FOREIGN KEY (publication) REFERENCES publications(uri) ON DELETE CASCADE not valid; +alter table "public"."publication_post_sends" validate constraint "publication_post_sends_publication_fkey"; + +alter table "public"."publication_post_sends" add constraint "publication_post_sends_document_fkey" FOREIGN KEY (document) REFERENCES documents(uri) ON DELETE CASCADE not valid; +alter table "public"."publication_post_sends" validate constraint "publication_post_sends_document_fkey"; + +alter table "public"."publication_post_sends" add constraint "publication_post_sends_status_check" CHECK (status IN ('pending','sending','sent','failed')) not valid; +alter table "public"."publication_post_sends" validate constraint "publication_post_sends_status_check"; + +grant delete on table "public"."publication_post_sends" to "anon"; +grant insert on table "public"."publication_post_sends" to "anon"; +grant references on table "public"."publication_post_sends" to "anon"; +grant select on table "public"."publication_post_sends" to "anon"; +grant trigger on table "public"."publication_post_sends" to "anon"; +grant truncate on table "public"."publication_post_sends" to "anon"; +grant update on table "public"."publication_post_sends" to "anon"; +grant delete on table "public"."publication_post_sends" to "authenticated"; +grant insert on table "public"."publication_post_sends" to "authenticated"; +grant references on table "public"."publication_post_sends" to "authenticated"; +grant select on table "public"."publication_post_sends" to "authenticated"; +grant trigger on table "public"."publication_post_sends" to "authenticated"; +grant truncate on table "public"."publication_post_sends" to "authenticated"; +grant update on table "public"."publication_post_sends" to "authenticated"; +grant delete on table "public"."publication_post_sends" to "service_role"; +grant insert on table "public"."publication_post_sends" to "service_role"; +grant references on table "public"."publication_post_sends" to "service_role"; +grant select on table "public"."publication_post_sends" to "service_role"; +grant trigger on table "public"."publication_post_sends" to "service_role"; +grant truncate on table "public"."publication_post_sends" to "service_role"; +grant update on table "public"."publication_post_sends" to "service_role"; -- 2.51.2