-
-
-
+
+
+
+
+
+ }
+ >
+
- }
- >
-
-
-
{props.children}
+
+
{props.children}
+
-
-
+
+
);
}
diff --git a/app/(app)/lish/[did]/[publication]/edit/[[...route]]/page.tsx b/app/(app)/lish/[did]/[publication]/edit/[[...route]]/page.tsx
index d95c482c..05fc9988 100644
--- a/app/(app)/lish/[did]/[publication]/edit/[[...route]]/page.tsx
+++ b/app/(app)/lish/[did]/[publication]/edit/[[...route]]/page.tsx
@@ -71,6 +71,9 @@ export default async function PublicationEditPage(props: Props) {
token={res.data}
publicationRecord={publication.record}
publicationCreator={publication.identity_did}
+ publicationUri={publication.uri}
+ pagePath={path}
+ pageTitle={page.title ?? ""}
/>
diff --git a/src/utils/publicationPageDiff.ts b/src/utils/publicationPageDiff.ts
new file mode 100644
index 00000000..09c5c679
--- /dev/null
+++ b/src/utils/publicationPageDiff.ts
@@ -0,0 +1,85 @@
+import { BlobRef } from "@atproto/lexicon";
+import type { PubLeafletPublicationPage } from "lexicons/api";
+import type { ProcessBlocksToPagesHooks } from "src/utils/factsToPagesRecord";
+
+// The published record stores BlobRef CIDs for images and at-uri/cid for polls,
+// neither of which we can recompute client-side without uploading. For the
+// dirty check we generate the would-be-record using the placeholder hooks
+// below, then strip both records of those opaque fields before comparing.
+
+export const dirtyCheckHooks: ProcessBlocksToPagesHooks = {
+ uploadImage: async (src) =>
+ ({
+ ref: { $link: src },
+ mimeType: "image/*",
+ size: 0,
+ }) as unknown as BlobRef,
+ uploadPoll: async (entityId) => ({
+ uri: `at://dirty-check/${entityId}`,
+ cid: "dirty-check",
+ }),
+};
+
+function stripVolatile(value: unknown): unknown {
+ if (Array.isArray(value)) return value.map(stripVolatile);
+ if (value && typeof value === "object") {
+ const obj = value as Record
;
+ const type = obj["$type"];
+ if (type === "pub.leaflet.blocks.image" || type === "pub.leaflet.blocks.website") {
+ const { image: _image, previewImage: _previewImage, ...rest } = obj as {
+ image?: unknown;
+ previewImage?: unknown;
+ } & Record;
+ return Object.fromEntries(
+ Object.entries(rest).map(([k, v]) => [k, stripVolatile(v)]),
+ );
+ }
+ if (type === "pub.leaflet.blocks.poll") {
+ const { pollRef: _pollRef, ...rest } = obj as {
+ pollRef?: unknown;
+ } & Record;
+ return Object.fromEntries(
+ Object.entries(rest).map(([k, v]) => [k, stripVolatile(v)]),
+ );
+ }
+ return Object.fromEntries(
+ Object.entries(obj).map(([k, v]) => [k, stripVolatile(v)]),
+ );
+ }
+ return value;
+}
+
+// `publishedAt` is set to "now" on every publish, so a freshly generated
+// record will always differ from the stored one on that field alone.
+export function normalizePageRecordForDiff(
+ record: PubLeafletPublicationPage.Record | Record | null | undefined,
+): unknown {
+ if (!record) return null;
+ const { publishedAt: _publishedAt, ...rest } = record as Record;
+ return stripVolatile(rest);
+}
+
+export function deepEqual(a: unknown, b: unknown): boolean {
+ if (a === b) return true;
+ if (typeof a !== typeof b) return false;
+ if (a === null || b === null) return false;
+ if (typeof a !== "object") return false;
+ if (Array.isArray(a) || Array.isArray(b)) {
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
+ if (a.length !== b.length) return false;
+ for (let i = 0; i < a.length; i++) {
+ if (!deepEqual(a[i], b[i])) return false;
+ }
+ return true;
+ }
+ const ao = a as Record;
+ const bo = b as Record;
+ const ak = Object.keys(ao);
+ const bk = Object.keys(bo);
+ if (ak.length !== bk.length) return false;
+ for (const k of ak) {
+ if (!Object.prototype.hasOwnProperty.call(bo, k)) return false;
+ if (!deepEqual(ao[k], bo[k])) return false;
+ }
+ return true;
+}