diff --git a/actions/createNewLeaflet.ts b/actions/createNewLeaflet.ts
index 77d3a594..26fe608e 100644
--- a/actions/createNewLeaflet.ts
+++ b/actions/createNewLeaflet.ts
@@ -8,7 +8,7 @@ import { generateKeyBetween } from "fractional-indexing";
import { cookies } from "next/headers";
import { pool } from "supabase/pool";
-type DefaultBlockType = "h1" | "text" | "posts-list";
+export type DefaultBlockType = "h1" | "text" | "posts-list" | "signup";
type FactRow = {
id: string;
diff --git a/actions/createPublicationPage.ts b/actions/createPublicationPage.ts
index 88cac009..196d690d 100644
--- a/actions/createPublicationPage.ts
+++ b/actions/createPublicationPage.ts
@@ -1,7 +1,7 @@
"use server";
import { generateKeyBetween } from "fractional-indexing";
import { getIdentityData } from "actions/getIdentityData";
-import { createNewLeaflet } from "./createNewLeaflet";
+import { createNewLeaflet, type DefaultBlockType } from "./createNewLeaflet";
import { supabaseServerClient } from "supabase/serverClient";
export async function createPublicationPage(args: {
@@ -10,6 +10,7 @@ export async function createPublicationPage(args: {
title?: string;
sort_order?: string;
includePostsList?: boolean;
+ includeSignup?: boolean;
}) {
let identity = await getIdentityData();
if (!identity || !identity.atp_did) return null;
@@ -34,10 +35,15 @@ export async function createPublicationPage(args: {
sort_order = generateKeyBetween(last?.sort_order ?? null, null);
}
+ let firstBlocks: DefaultBlockType[] = [
+ "text",
+ ...(args.includeSignup ? (["signup"] as const) : []),
+ ...(args.includePostsList ? (["posts-list"] as const) : []),
+ ];
let leaflet_src = await createNewLeaflet({
pageType: "doc",
redirectUser: false,
- firstBlocks: args.includePostsList ? ["text", "posts-list"] : ["text"],
+ firstBlocks,
addToHomepage: false,
});
diff --git a/app/(app)/lish/[did]/[publication]/[rkey]/PostContent.tsx b/app/(app)/lish/[did]/[publication]/[rkey]/PostContent.tsx
index 1091ad44..a55bc39c 100644
--- a/app/(app)/lish/[did]/[publication]/[rkey]/PostContent.tsx
+++ b/app/(app)/lish/[did]/[publication]/[rkey]/PostContent.tsx
@@ -19,6 +19,7 @@ import {
PubLeafletBlocksPoll,
PubLeafletBlocksPostsList,
PubLeafletBlocksButton,
+ PubLeafletBlocksSignup,
} from "lexicons/api";
import {
PublicationPostsList,
@@ -44,6 +45,7 @@ import { PublishedPageLinkBlock } from "./Blocks/PublishedPageBlock";
import { PublishedPollBlock } from "./Blocks/PublishedPollBlock";
import { PollData } from "./fetchPollData";
import { ButtonPrimary } from "components/Buttons";
+import { SubscribePanel } from "components/Subscribe/SubscribeButton";
import { blockTextSize } from "src/utils/blockTextSize";
import { slugify } from "src/utils/slugify";
import { PostNotAvailable } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed";
@@ -157,8 +159,8 @@ export let Block = ({
isLast?: boolean;
}) => {
let b = block;
- let currentPublicationUri =
- useDocumentOptional()?.publication?.uri ?? null;
+ let document = useDocumentOptional();
+ let currentPublicationUri = document?.publication?.uri ?? null;
let blockProps = {
style: {
scrollMarginTop: "4rem",
@@ -280,6 +282,22 @@ export let Block = ({
case PubLeafletBlocksHorizontalRule.isMain(b.block): {
return
;
}
+ case PubLeafletBlocksSignup.isMain(b.block): {
+ if (!document?.publication?.uri) return null;
+ return (
+
+
+
+ );
+ }
case PubLeafletBlocksPostsList.isMain(b.block): {
if (!postsListData) return null;
const view: "compact" | "full" =
diff --git a/app/(app)/lish/[did]/[publication]/[rkey]/PublicationPageRenderer.tsx b/app/(app)/lish/[did]/[publication]/[rkey]/PublicationPageRenderer.tsx
index d97c4ae2..d4d77b0e 100644
--- a/app/(app)/lish/[did]/[publication]/[rkey]/PublicationPageRenderer.tsx
+++ b/app/(app)/lish/[did]/[publication]/[rkey]/PublicationPageRenderer.tsx
@@ -37,6 +37,7 @@ type PublicationRow = {
name: string;
identity_did: string;
record: unknown;
+ publication_newsletter_settings?: { enabled: boolean } | null;
publication_pages?: {
id: number;
path: string | null;
@@ -145,7 +146,7 @@ export async function PublicationPageRenderer({
DocumentContextValue["publication"]
>["record"],
publication_subscriptions: [],
- newsletterMode: false,
+ newsletterMode: !!publication.publication_newsletter_settings?.enabled,
pages: (publication.publication_pages ?? []).filter((p) => p.record_uri),
},
commentsCount: 0,
diff --git a/app/(app)/lish/[did]/[publication]/dashboard/EditPagesNavLink.tsx b/app/(app)/lish/[did]/[publication]/dashboard/EditPagesNavLink.tsx
index 2d087648..8df46369 100644
--- a/app/(app)/lish/[did]/[publication]/dashboard/EditPagesNavLink.tsx
+++ b/app/(app)/lish/[did]/[publication]/dashboard/EditPagesNavLink.tsx
@@ -32,6 +32,7 @@ export function EditPagesNavLink(props: {
path: "/",
title: "home",
includePostsList: true,
+ includeSignup: true,
});
setLoading(false);
if (!created) return;
diff --git a/components/Blocks/Block.tsx b/components/Blocks/Block.tsx
index 4244ec7b..886fdf43 100644
--- a/components/Blocks/Block.tsx
+++ b/components/Blocks/Block.tsx
@@ -32,6 +32,7 @@ import { MathBlock } from "./MathBlock";
import { CodeBlock } from "./CodeBlock";
import { HorizontalRule } from "./HorizontalRule";
import { PostsListBlock } from "./PostsListBlock";
+import { SignupBlock } from "./SignupBlock";
import { deepEquals } from "src/utils/deepEquals";
import { isTextBlock } from "src/utils/isTextBlock";
import { DeleteTiny } from "components/Icons/DeleteTiny";
@@ -363,6 +364,7 @@ const BlockTypeComponents: {
"standard-site-post": StandardSitePostBlock,
"horizontal-rule": HorizontalRule,
"posts-list": PostsListBlock,
+ signup: SignupBlock,
};
export const BlockMultiselectIndicator = (props: BlockProps) => {
diff --git a/components/Blocks/BlockCommands.tsx b/components/Blocks/BlockCommands.tsx
index 0a7dcc82..815e0135 100644
--- a/components/Blocks/BlockCommands.tsx
+++ b/components/Blocks/BlockCommands.tsx
@@ -458,11 +458,11 @@ export const blockCommands: Command[] = [
name: "Subscribe Form",
icon: ,
type: "publication",
- alternateNames: ["subscribe", "newsletter", "email"],
+ alternateNames: ["subscribe", "newsletter", "email", "signup"],
publicationOnly: true,
onSelect: async (rep, props) => {
props.entityID && clearCommandSearchText(props.entityID);
- await createBlockWithType(rep, props, "text");
+ await createBlockWithType(rep, props, "signup");
},
},
];
diff --git a/components/Blocks/SignupBlock.tsx b/components/Blocks/SignupBlock.tsx
new file mode 100644
index 00000000..404fde71
--- /dev/null
+++ b/components/Blocks/SignupBlock.tsx
@@ -0,0 +1,57 @@
+import { useState } from "react";
+import { useUIState } from "src/useUIState";
+import { useLeafletPublicationData } from "components/PageSWRDataProvider";
+import { BlockProps, BlockLayout } from "./Block";
+import { EmailInput } from "components/Subscribe/EmailSubscribe";
+import { ButtonPrimary } from "components/Buttons";
+
+// SignupBlock is a static, non-interactive preview of the publication subscribe
+// form, styled to match SubscribePanel (components/Subscribe/SubscribeButton.tsx).
+// It is shown in the editor — the real, working form is rendered on the
+// published page (see PostContent.tsx's signup case). No user data is fetched.
+export const SignupBlock = (
+ props: BlockProps & {
+ areYouSure?: boolean;
+ setAreYouSure?: (value: boolean) => void;
+ },
+) => {
+ let isSelected = useUIState((s) =>
+ s.selectedBlocks.find((b) => b.value === props.entityID),
+ );
+ let { normalizedPublication } = useLeafletPublicationData();
+ let [email, setEmail] = useState("");
+
+ let publicationName = normalizedPublication?.name || "Subscribe";
+ let publicationDescription = normalizedPublication?.description;
+
+ return (
+
+
+
{publicationName}
+ {publicationDescription && (
+
{publicationDescription}
+ )}
+
+
+ Subscribe
+
+ }
+ />
+
+
+
+ );
+};
diff --git a/lexicons/api/index.ts b/lexicons/api/index.ts
index df865afe..66d13289 100644
--- a/lexicons/api/index.ts
+++ b/lexicons/api/index.ts
@@ -40,6 +40,7 @@ import * as PubLeafletBlocksOrderedList from './types/pub/leaflet/blocks/ordered
import * as PubLeafletBlocksPage from './types/pub/leaflet/blocks/page'
import * as PubLeafletBlocksPoll from './types/pub/leaflet/blocks/poll'
import * as PubLeafletBlocksPostsList from './types/pub/leaflet/blocks/postsList'
+import * as PubLeafletBlocksSignup from './types/pub/leaflet/blocks/signup'
import * as PubLeafletBlocksStandardSitePost from './types/pub/leaflet/blocks/standardSitePost'
import * as PubLeafletBlocksText from './types/pub/leaflet/blocks/text'
import * as PubLeafletBlocksUnorderedList from './types/pub/leaflet/blocks/unorderedList'
@@ -96,6 +97,7 @@ export * as PubLeafletBlocksOrderedList from './types/pub/leaflet/blocks/ordered
export * as PubLeafletBlocksPage from './types/pub/leaflet/blocks/page'
export * as PubLeafletBlocksPoll from './types/pub/leaflet/blocks/poll'
export * as PubLeafletBlocksPostsList from './types/pub/leaflet/blocks/postsList'
+export * as PubLeafletBlocksSignup from './types/pub/leaflet/blocks/signup'
export * as PubLeafletBlocksStandardSitePost from './types/pub/leaflet/blocks/standardSitePost'
export * as PubLeafletBlocksText from './types/pub/leaflet/blocks/text'
export * as PubLeafletBlocksUnorderedList from './types/pub/leaflet/blocks/unorderedList'
diff --git a/lexicons/api/lexicons.ts b/lexicons/api/lexicons.ts
index 6cb98832..10b7ce49 100644
--- a/lexicons/api/lexicons.ts
+++ b/lexicons/api/lexicons.ts
@@ -1590,6 +1590,19 @@ export const schemaDict = {
},
},
},
+ PubLeafletBlocksSignup: {
+ lexicon: 1,
+ id: 'pub.leaflet.blocks.signup',
+ defs: {
+ main: {
+ type: 'object',
+ description:
+ "A subscribe/signup form for the publication. Renders the publication's subscribe form; carries no configurable data.",
+ required: [],
+ properties: {},
+ },
+ },
+ },
PubLeafletBlocksStandardSitePost: {
lexicon: 1,
id: 'pub.leaflet.blocks.standardSitePost',
@@ -2001,6 +2014,7 @@ export const schemaDict = {
'lex:pub.leaflet.blocks.poll',
'lex:pub.leaflet.blocks.button',
'lex:pub.leaflet.blocks.postsList',
+ 'lex:pub.leaflet.blocks.signup',
],
},
x: {
@@ -2105,6 +2119,7 @@ export const schemaDict = {
'lex:pub.leaflet.blocks.poll',
'lex:pub.leaflet.blocks.button',
'lex:pub.leaflet.blocks.postsList',
+ 'lex:pub.leaflet.blocks.signup',
],
},
alignment: {
@@ -2981,6 +2996,7 @@ export const ids = {
PubLeafletBlocksPage: 'pub.leaflet.blocks.page',
PubLeafletBlocksPoll: 'pub.leaflet.blocks.poll',
PubLeafletBlocksPostsList: 'pub.leaflet.blocks.postsList',
+ PubLeafletBlocksSignup: 'pub.leaflet.blocks.signup',
PubLeafletBlocksStandardSitePost: 'pub.leaflet.blocks.standardSitePost',
PubLeafletBlocksText: 'pub.leaflet.blocks.text',
PubLeafletBlocksUnorderedList: 'pub.leaflet.blocks.unorderedList',
diff --git a/lexicons/api/types/pub/leaflet/blocks/signup.ts b/lexicons/api/types/pub/leaflet/blocks/signup.ts
new file mode 100644
index 00000000..ea3a4ee0
--- /dev/null
+++ b/lexicons/api/types/pub/leaflet/blocks/signup.ts
@@ -0,0 +1,30 @@
+/**
+ * GENERATED CODE - DO NOT MODIFY
+ */
+import { type ValidationResult, BlobRef } from '@atproto/lexicon'
+import { CID } from 'multiformats/cid'
+import { validate as _validate } from '../../../../lexicons'
+import {
+ type $Typed,
+ is$typed as _is$typed,
+ type OmitKey,
+} from '../../../../util'
+
+const is$typed = _is$typed,
+ validate = _validate
+const id = 'pub.leaflet.blocks.signup'
+
+/** A subscribe/signup form for the publication. Renders the publication's subscribe form; carries no configurable data. */
+export interface Main {
+ $type?: 'pub.leaflet.blocks.signup'
+}
+
+const hashMain = 'main'
+
+export function isMain(v: V) {
+ return is$typed(v, id, hashMain)
+}
+
+export function validateMain(v: V) {
+ return validate(v, id, hashMain)
+}
diff --git a/lexicons/api/types/pub/leaflet/pages/canvas.ts b/lexicons/api/types/pub/leaflet/pages/canvas.ts
index e18bc2ed..8e0326fb 100644
--- a/lexicons/api/types/pub/leaflet/pages/canvas.ts
+++ b/lexicons/api/types/pub/leaflet/pages/canvas.ts
@@ -26,6 +26,7 @@ import type * as PubLeafletBlocksPage from '../blocks/page'
import type * as PubLeafletBlocksPoll from '../blocks/poll'
import type * as PubLeafletBlocksButton from '../blocks/button'
import type * as PubLeafletBlocksPostsList from '../blocks/postsList'
+import type * as PubLeafletBlocksSignup from '../blocks/signup'
const is$typed = _is$typed,
validate = _validate
@@ -67,6 +68,7 @@ export interface Block {
| $Typed
| $Typed
| $Typed
+ | $Typed
| { $type: string }
x: number
y: number
diff --git a/lexicons/api/types/pub/leaflet/pages/linearDocument.ts b/lexicons/api/types/pub/leaflet/pages/linearDocument.ts
index b35d8f98..fb2e36c6 100644
--- a/lexicons/api/types/pub/leaflet/pages/linearDocument.ts
+++ b/lexicons/api/types/pub/leaflet/pages/linearDocument.ts
@@ -26,6 +26,7 @@ import type * as PubLeafletBlocksPage from '../blocks/page'
import type * as PubLeafletBlocksPoll from '../blocks/poll'
import type * as PubLeafletBlocksButton from '../blocks/button'
import type * as PubLeafletBlocksPostsList from '../blocks/postsList'
+import type * as PubLeafletBlocksSignup from '../blocks/signup'
const is$typed = _is$typed,
validate = _validate
@@ -67,6 +68,7 @@ export interface Block {
| $Typed
| $Typed
| $Typed
+ | $Typed
| { $type: string }
alignment?:
| 'lex:pub.leaflet.pages.linearDocument#textAlignLeft'
diff --git a/lexicons/pub/leaflet/blocks/signup.json b/lexicons/pub/leaflet/blocks/signup.json
new file mode 100644
index 00000000..b5a3f594
--- /dev/null
+++ b/lexicons/pub/leaflet/blocks/signup.json
@@ -0,0 +1,12 @@
+{
+ "lexicon": 1,
+ "id": "pub.leaflet.blocks.signup",
+ "defs": {
+ "main": {
+ "type": "object",
+ "description": "A subscribe/signup form for the publication. Renders the publication's subscribe form; carries no configurable data.",
+ "required": [],
+ "properties": {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/lexicons/pub/leaflet/pages/canvas.json b/lexicons/pub/leaflet/pages/canvas.json
index 275a367e..fb465f90 100644
--- a/lexicons/pub/leaflet/pages/canvas.json
+++ b/lexicons/pub/leaflet/pages/canvas.json
@@ -48,7 +48,8 @@
"pub.leaflet.blocks.page",
"pub.leaflet.blocks.poll",
"pub.leaflet.blocks.button",
- "pub.leaflet.blocks.postsList"
+ "pub.leaflet.blocks.postsList",
+ "pub.leaflet.blocks.signup"
]
},
"x": {
diff --git a/lexicons/pub/leaflet/pages/linearDocument.json b/lexicons/pub/leaflet/pages/linearDocument.json
index 29e5e200..399c886c 100644
--- a/lexicons/pub/leaflet/pages/linearDocument.json
+++ b/lexicons/pub/leaflet/pages/linearDocument.json
@@ -45,7 +45,8 @@
"pub.leaflet.blocks.page",
"pub.leaflet.blocks.poll",
"pub.leaflet.blocks.button",
- "pub.leaflet.blocks.postsList"
+ "pub.leaflet.blocks.postsList",
+ "pub.leaflet.blocks.signup"
]
},
"alignment": {
diff --git a/lexicons/src/blocks.ts b/lexicons/src/blocks.ts
index 28baef5c..746c73e8 100644
--- a/lexicons/src/blocks.ts
+++ b/lexicons/src/blocks.ts
@@ -382,6 +382,20 @@ export const PubLeafletBlocksPostsList: LexiconDoc = {
},
};
+export const PubLeafletBlocksSignup: LexiconDoc = {
+ lexicon: 1,
+ id: "pub.leaflet.blocks.signup",
+ defs: {
+ main: {
+ type: "object",
+ description:
+ "A subscribe/signup form for the publication. Renders the publication's subscribe form; carries no configurable data.",
+ required: [],
+ properties: {},
+ },
+ },
+};
+
export const BlockLexicons = [
PubLeafletBlocksIFrame,
PubLeafletBlocksText,
@@ -400,6 +414,7 @@ export const BlockLexicons = [
PubLeafletBlocksPoll,
PubLeafletBlocksButton,
PubLeafletBlocksPostsList,
+ PubLeafletBlocksSignup,
];
export const BlockUnion: LexRefUnion = {
type: "union",
diff --git a/src/replicache/attributes.ts b/src/replicache/attributes.ts
index a3b0a0a3..142764ba 100644
--- a/src/replicache/attributes.ts
+++ b/src/replicache/attributes.ts
@@ -403,7 +403,8 @@ export type Data = {
| "code"
| "blockquote"
| "horizontal-rule"
- | "posts-list";
+ | "posts-list"
+ | "signup";
};
"canvas-pattern-union": {
type: "canvas-pattern-union";
diff --git a/src/utils/factsToPagesRecord.ts b/src/utils/factsToPagesRecord.ts
index dbc251fb..03638c5c 100644
--- a/src/utils/factsToPagesRecord.ts
+++ b/src/utils/factsToPagesRecord.ts
@@ -18,6 +18,7 @@ import {
PubLeafletBlocksPage,
PubLeafletBlocksPoll,
PubLeafletBlocksPostsList,
+ PubLeafletBlocksSignup,
PubLeafletBlocksText,
PubLeafletBlocksUnorderedList,
PubLeafletBlocksWebsite,
@@ -569,6 +570,12 @@ export async function processBlocksToPages(opts: {
};
return block;
}
+ if (b.type === "signup") {
+ const block: $Typed = {
+ $type: ids.PubLeafletBlocksSignup,
+ };
+ return block;
+ }
return;
}
diff --git a/src/utils/getBlocksAsHTML.tsx b/src/utils/getBlocksAsHTML.tsx
index c205dc08..d3559681 100644
--- a/src/utils/getBlocksAsHTML.tsx
+++ b/src/utils/getBlocksAsHTML.tsx
@@ -88,6 +88,7 @@ const BlockTypeToHTML: {
mailbox: async () => null,
poll: async () => null,
embed: async () => null,
+ signup: async () => null,
"bluesky-post": async (b, tx) => {
let [post] = await scanIndex(tx).eav(b.value, "block/bluesky-post");
if (!post) return null;