···11+create table "public"."publication_pages" (
22+ "id" bigint generated by default as identity not null,
33+ "created_at" timestamp with time zone not null default now(),
44+ "leaflet_src" uuid not null,
55+ "document" text,
66+ "path" text,
77+ "publication" text not null,
88+ "title" text not null default '',
99+ "metadata" jsonb not null default '{}'::jsonb
1010+);
1111+1212+1313+alter table "public"."publication_pages" enable row level security;
1414+1515+CREATE UNIQUE INDEX publication_pages_pkey ON public.publication_pages USING btree (id, publication);
1616+1717+alter table "public"."publication_pages" add constraint "publication_pages_pkey" PRIMARY KEY using index "publication_pages_pkey";
1818+1919+alter table "public"."publication_pages" add constraint "publication_pages_document_fkey" FOREIGN KEY (document) REFERENCES documents(uri) not valid;
2020+2121+alter table "public"."publication_pages" validate constraint "publication_pages_document_fkey";
2222+2323+alter table "public"."publication_pages" add constraint "publication_pages_leaflet_src_fkey" FOREIGN KEY (leaflet_src) REFERENCES permission_tokens(id) not valid;
2424+2525+alter table "public"."publication_pages" validate constraint "publication_pages_leaflet_src_fkey";
2626+2727+alter table "public"."publication_pages" add constraint "publication_pages_publication_fkey" FOREIGN KEY (publication) REFERENCES publications(uri) ON UPDATE CASCADE ON DELETE CASCADE not valid;
2828+2929+alter table "public"."publication_pages" validate constraint "publication_pages_publication_fkey";
3030+3131+grant delete on table "public"."publication_pages" to "anon";
3232+3333+grant insert on table "public"."publication_pages" to "anon";
3434+3535+grant references on table "public"."publication_pages" to "anon";
3636+3737+grant select on table "public"."publication_pages" to "anon";
3838+3939+grant trigger on table "public"."publication_pages" to "anon";
4040+4141+grant truncate on table "public"."publication_pages" to "anon";
4242+4343+grant update on table "public"."publication_pages" to "anon";
4444+4545+grant delete on table "public"."publication_pages" to "authenticated";
4646+4747+grant insert on table "public"."publication_pages" to "authenticated";
4848+4949+grant references on table "public"."publication_pages" to "authenticated";
5050+5151+grant select on table "public"."publication_pages" to "authenticated";
5252+5353+grant trigger on table "public"."publication_pages" to "authenticated";
5454+5555+grant truncate on table "public"."publication_pages" to "authenticated";
5656+5757+grant update on table "public"."publication_pages" to "authenticated";
5858+5959+grant delete on table "public"."publication_pages" to "service_role";
6060+6161+grant insert on table "public"."publication_pages" to "service_role";
6262+6363+grant references on table "public"."publication_pages" to "service_role";
6464+6565+grant select on table "public"."publication_pages" to "service_role";
6666+6767+grant trigger on table "public"."publication_pages" to "service_role";
6868+6969+grant truncate on table "public"."publication_pages" to "service_role";
7070+7171+grant update on table "public"."publication_pages" to "service_role";
+17
components/Blocks/TextBlock/inputRules.ts
···33 inputRules,
44 wrappingInputRule,
55} from "prosemirror-inputrules";
66+import { EditorState } from "prosemirror-state";
67import { MutableRefObject } from "react";
78import { Replicache } from "replicache";
89import type { ReplicacheMutators } from "src/replicache";
···1415import { LAST_USED_CODE_LANGUAGE_KEY } from "src/utils/codeLanguageStorage";
1516import { insertFootnote } from "./insertFootnote";
1617import { useEditorStates } from "src/state/useEditorState";
1818+1919+const anchorInCodeMark = (state: EditorState, start: number, end: number) => {
2020+ const codeMark = state.schema.marks.code;
2121+ if (!codeMark) return false;
2222+ const startMarks = state.doc.resolve(start).marks();
2323+ const endMarks = state.doc.resolve(end).marks();
2424+ return !!codeMark.isInSet(startMarks) || !!codeMark.isInSet(endMarks);
2525+};
1726export const inputrules = (
1827 propsRef: MutableRefObject<BlockProps & { entity_set: { set: string } }>,
1928 repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>,
···2332 //Strikethrough
2433 rules: [
2534 new InputRule(/\~\~([^*]+)\~\~$/, (state, match, start, end) => {
3535+ if (anchorInCodeMark(state, start, end)) return null;
2636 const [fullMatch, content] = match;
2737 const { tr } = state;
2838 if (content) {
···40504151 //Highlight
4252 new InputRule(/\=\=([^*]+)\=\=$/, (state, match, start, end) => {
5353+ if (anchorInCodeMark(state, start, end)) return null;
4354 const [fullMatch, content] = match;
4455 const { tr } = state;
4556 if (content) {
···59706071 //Bold
6172 new InputRule(/\*\*([^*]+)\*\*$/, (state, match, start, end) => {
7373+ if (anchorInCodeMark(state, start, end)) return null;
6274 const [fullMatch, content] = match;
6375 const { tr } = state;
6476 if (content) {
···76887789 //Code
7890 new InputRule(/\`([^`]+)\`$/, (state, match, start, end) => {
9191+ if (anchorInCodeMark(state, start, end)) return null;
7992 const [fullMatch, content] = match;
8093 const { tr } = state;
8194 if (content) {
···9410795108 //Italic
96109 new InputRule(/(?:^|[^*])\*([^*]+)\*$/, (state, match, start, end) => {
110110+ if (anchorInCodeMark(state, start, end)) return null;
97111 const [fullMatch, content] = match;
98112 const { tr } = state;
99113 if (content) {
···231245232246 // Footnote - [^ triggers footnote insertion
233247 new InputRule(/\[\^$/, (state, match, start, end) => {
248248+ if (anchorInCodeMark(state, start, end)) return null;
234249 let tr = state.tr.delete(start, end);
235250 setTimeout(() => {
236251 let view = useEditorStates.getState().editorStates[propsRef.current.entityID]?.view;
···248263 // Mention - @ at start of line, after space, or after hard break
249264 new InputRule(/(?:^|\s)@$/, (state, match, start, end) => {
250265 if (!openMentionAutocomplete) return null;
266266+ if (anchorInCodeMark(state, start, end)) return null;
251267 // Schedule opening the autocomplete after the transaction is applied
252268 setTimeout(() => openMentionAutocomplete(), 0);
253269 return null; // Let the @ be inserted normally
···255271 // Mention - @ immediately after a hard break (hard breaks are nodes, not text)
256272 new InputRule(/@$/, (state, match, start, end) => {
257273 if (!openMentionAutocomplete) return null;
274274+ if (anchorInCodeMark(state, start, end)) return null;
258275 // Check if the character before @ is a hard break node
259276 const $pos = state.doc.resolve(start);
260277 const nodeBefore = $pos.nodeBefore;
···11-import { subscribeToPublication } from "app/lish/subscribeToPublication";
11+import {
22+ backfillAtprotoSubscriptionsForIdentity,
33+ subscribeToPublication,
44+} from "app/lish/subscribeToPublication";
25import { cookies } from "next/headers";
36import { redirect } from "next/navigation";
47import { NextRequest, NextResponse } from "next/server";
···1619 parseActionFromSearchParam,
1720} from "./afterSignInActions";
1821import { inngest } from "app/api/inngest/client";
2222+import { mergeEmailIdentityIntoAtpIdentity } from "src/mergeIdentity";
19232024type OauthRequestClientState = {
2125 redirect: string | null;
2226 action: ActionAfterSignIn | null;
2327 link?: boolean;
2828+ // Auto-confirm a cross-identity merge instead of routing to /merge-accounts.
2929+ // Set when the caller already showed an in-context "link this account?"
3030+ // confirmation (e.g. the subscribe-flow LinkIdentityModal).
3131+ autoMerge?: boolean;
2432};
25332634export async function GET(
···3947 const handle = searchParams.get("handle") as string;
4048 const signup = searchParams.get("signup") === "true";
4149 const link = searchParams.get("link") === "true";
5050+ const autoMerge = searchParams.get("autoMerge") === "true";
4251 // Put originating page here!
4352 let redirect = searchParams.get("redirect_url");
4453 if (redirect) redirect = decodeURIComponent(redirect);
4554 let action = parseActionFromSearchParam(searchParams.get("action"));
4646- let state: OauthRequestClientState = { redirect, action, link };
5555+ let state: OauthRequestClientState = {
5656+ redirect,
5757+ action,
5858+ link,
5959+ autoMerge,
6060+ };
47614862 // Revoke any pending authentication requests if the connection is closed (optional)
4963 const ac = new AbortController();
···9010491105 // Explicit link flow from the LoginModal for an email-only user. Never
92106 // fall through to a normal DID login — we must either attach the atp_did
9393- // to the existing email identity or route to /merge-accounts.
107107+ // to the existing email identity or route to /merge-accounts (or merge
108108+ // inline if autoMerge is set).
94109 if (
95110 s.link &&
96111 currentIdentity &&
···102117 .from("identities")
103118 .update({ atp_did: session.did })
104119 .eq("id", currentIdentity.id);
120120+ await backfillAtprotoSubscriptionsForIdentity(
121121+ currentIdentity.id,
122122+ session.did,
123123+ );
105124 return handleAction(s.action, redirectPath);
106125 }
107126 if (identity.id !== currentIdentity.id) {
108108- await stagePendingMerge(identity.id, redirectPath);
109109- // Only reached if the token insert failed. Fall through to the
110110- // normal sign-in flow rather than blocking the user.
127127+ if (s.autoMerge) {
128128+ const merged = await mergeEmailIdentityIntoAtpIdentity({
129129+ sourceId: currentIdentity.id,
130130+ targetId: identity.id,
131131+ });
132132+ if (merged.ok) {
133133+ // Source identity is gone; clear it so the cross-identity
134134+ // merge block below doesn't try to merge a deleted row.
135135+ currentIdentity = null;
136136+ } else {
137137+ console.error(
138138+ "[oauth/callback] autoMerge failed:",
139139+ merged.error,
140140+ );
141141+ await stagePendingMerge(identity.id, redirectPath);
142142+ }
143143+ } else {
144144+ await stagePendingMerge(identity.id, redirectPath);
145145+ // Only reached if the token insert failed. Fall through to the
146146+ // normal sign-in flow rather than blocking the user.
147147+ }
111148 }
112149 // Same identity already linked — fall through to refresh session.
113150 }
···118155 .from("identities")
119156 .update({ atp_did: session.did })
120157 .eq("id", currentIdentity.id);
158158+ await backfillAtprotoSubscriptionsForIdentity(
159159+ currentIdentity.id,
160160+ session.did,
161161+ );
121162 return handleAction(s.action, redirectPath);
122163 }
123164 const { data } = await supabaseServerClient
···133174 currentIdentity.email
134175 ) {
135176 // DID already has an identity row. Caller is currently signed in as a
136136- // *different* email-only identity. Stage a pending merge and let the
137137- // user confirm on /merge-accounts before we touch either account.
138138- await stagePendingMerge(identity.id, redirectPath);
139139- // Only reached if the token insert failed — fall through.
177177+ // *different* email-only identity. Either merge inline (autoMerge
178178+ // means the caller already collected the user's confirmation) or
179179+ // stage a pending merge for /merge-accounts.
180180+ if (s.autoMerge) {
181181+ const merged = await mergeEmailIdentityIntoAtpIdentity({
182182+ sourceId: currentIdentity.id,
183183+ targetId: identity.id,
184184+ });
185185+ if (!merged.ok) {
186186+ console.error(
187187+ "[oauth/callback] autoMerge failed:",
188188+ merged.error,
189189+ );
190190+ await stagePendingMerge(identity.id, redirectPath);
191191+ }
192192+ } else {
193193+ await stagePendingMerge(identity.id, redirectPath);
194194+ // Only reached if the token insert failed — fall through.
195195+ }
140196 }
141197142198 // Trigger migration if identity needs it