From 9b47b512a937e3df720964e3f125393b79e66872 Mon Sep 17 00:00:00 2001 From: celine Date: Sat, 7 Feb 2026 04:23:32 -1000 Subject: [PATCH 1/7] Feature/recommend (#264) * just like a bunch of stuff sorry * cleaned up naming * refactor the bskypostcontent to use booleans for reply and quote rather than functions, include its own reply line, some renaming of variables to be more consistant * styling the grandparents in the threadviewer * styling the quotepage * adjustments to make everything more consistent * use BskyPostContent in the leaflet as well * fixing the width of thread and quote pages so they don't get too wide * make the reply line emcompass it's replies as well * little tweaks to the embed block * fixed a bunch of small things * init the recommend button, some stuff is broken tho * oops forgot the new files * add local like data fetching w/ batcher * adjusting interaction button styles for consistency * add user preference to show/hide recommends * made some chnages to canvas layout to better match * `add a little smoker to the recommend acion to keep it cute * copy fixes * spacing fixes --------- Co-authored-by: Jared Pereira --- .claude/settings.local.json | 15 + .claude/skills/lexicons.md | 298 ------------------ .../p/[didOrHandle]/getProfilePosts.ts | 27 +- app/(home-pages)/reader/getReaderFeed.ts | 3 + .../tag/[tag]/getDocumentsByTag.ts | 2 + app/api/oauth/[route]/route.ts | 3 +- app/api/rpc/[command]/get_publication_data.ts | 5 +- .../rpc/[command]/get_user_recommendations.ts | 40 +++ app/api/rpc/[command]/route.ts | 2 + app/lish/Subscribe.tsx | 4 +- .../[did]/[publication]/[rkey]/CanvasPage.tsx | 6 + .../[rkey]/Interactions/Interactions.tsx | 124 +++++--- .../[rkey]/Interactions/recommendAction.ts | 135 ++++++++ .../[rkey]/LinearDocumentPage.tsx | 6 +- .../[rkey]/PostHeader/PostHeader.tsx | 27 +- .../[did]/[publication]/[rkey]/PostPages.tsx | 2 + .../[publication]/[rkey]/getPostPageData.ts | 43 ++- .../dashboard/PublishedPostsLists.tsx | 16 +- .../dashboard/settings/PostOptions.tsx | 22 +- app/lish/[did]/[publication]/page.tsx | 22 +- app/lish/createPub/CreatePubForm.tsx | 1 + app/lish/createPub/UpdatePubForm.tsx | 3 +- app/lish/createPub/createPublication.ts | 32 +- app/lish/createPub/updatePublication.ts | 265 ++++++++++------ components/Canvas.tsx | 21 +- components/Icons/RecommendTiny.tsx | 37 +++ components/InteractionsPreview.tsx | 34 +- components/Pages/PublicationMetadata.tsx | 68 ++-- components/PostListing.tsx | 10 +- components/RecommendButton.tsx | 173 ++++++++++ contexts/DocumentContext.tsx | 1 + lexicons/api/types/pub/leaflet/publication.ts | 83 ++--- .../api/types/site/standard/publication.ts | 61 ++-- lexicons/pub/leaflet/publication.json | 4 + lexicons/site/standard/publication.json | 4 + lexicons/src/normalize.ts | 27 +- lexicons/src/publication.ts | 1 + package-lock.json | 16 + package.json | 1 + 39 files changed, 1023 insertions(+), 621 deletions(-) create mode 100644 .claude/settings.local.json delete mode 100644 .claude/skills/lexicons.md create mode 100644 app/api/rpc/[command]/get_user_recommendations.ts create mode 100644 app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts create mode 100644 components/Icons/RecommendTiny.tsx create mode 100644 components/RecommendButton.tsx diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..9371392c --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "mcp__acp__Edit", + "mcp__acp__Write", + "mcp__acp__Bash", + "mcp__primitive__say_hello", + "mcp__primitive__pending_delegations", + "mcp__primitive__claim_delegation", + "mcp__primitive__tasks_update", + "mcp__primitive__contexts_update", + "mcp__primitive__contexts_list" + ] + } +} diff --git a/.claude/skills/lexicons.md b/.claude/skills/lexicons.md deleted file mode 100644 index bc14e81b..00000000 --- a/.claude/skills/lexicons.md +++ /dev/null @@ -1,298 +0,0 @@ -# Lexicon System - -## Overview - -Lexicons define the schema for AT Protocol records. This project has two namespaces: -- **`pub.leaflet.*`** - Leaflet-specific lexicons (documents, publications, blocks, etc.) -- **`site.standard.*`** - Standard site lexicons for interoperability - -The lexicons are defined as TypeScript in `lexicons/src/`, built to JSON in `lexicons/pub/leaflet/` and `lexicons/site/standard/`, and TypeScript types are generated in `lexicons/api/`. - -## Key Files - -- **`lexicons/src/*.ts`** - Source definitions for `pub.leaflet.*` lexicons -- **`lexicons/site/standard/**/*.json`** - JSON definitions for `site.standard.*` lexicons (manually maintained) -- **`lexicons/build.ts`** - Builds TypeScript sources to JSON -- **`lexicons/api/`** - Generated TypeScript types and client -- **`package.json`** - Contains `lexgen` script - -## Running Lexicon Generation - -```bash -npm run lexgen -``` - -This runs: -1. `tsx ./lexicons/build.ts` - Builds `pub.leaflet.*` JSON from TypeScript -2. `lex gen-api` - Generates TypeScript types from all JSON lexicons -3. `tsx ./lexicons/fix-extensions.ts` - Fixes import extensions - -## Adding a New pub.leaflet Lexicon - -### 1. Create the Source Definition - -Create a file in `lexicons/src/` (e.g., `lexicons/src/myLexicon.ts`): - -```typescript -import { LexiconDoc } from "@atproto/lexicon"; - -export const PubLeafletMyLexicon: LexiconDoc = { - lexicon: 1, - id: "pub.leaflet.myLexicon", - defs: { - main: { - type: "record", // or "object" for non-record types - key: "tid", - record: { - type: "object", - required: ["field1"], - properties: { - field1: { type: "string", maxLength: 1000 }, - field2: { type: "integer", minimum: 0 }, - optionalRef: { type: "ref", ref: "other.lexicon#def" }, - }, - }, - }, - // Additional defs for sub-objects - subType: { - type: "object", - properties: { - nested: { type: "string" }, - }, - }, - }, -}; -``` - -### 2. Add to Build - -Update `lexicons/build.ts`: - -```typescript -import { PubLeafletMyLexicon } from "./src/myLexicon"; - -const lexicons = [ - // ... existing lexicons - PubLeafletMyLexicon, -]; -``` - -### 3. Update lexgen Command (if needed) - -If your lexicon is at the top level of `pub/leaflet/` (not in a subdirectory), add it to the `lexgen` script in `package.json`: - -```json -"lexgen": "tsx ./lexicons/build.ts && lex gen-api ./lexicons/api ./lexicons/pub/leaflet/document.json ./lexicons/pub/leaflet/myLexicon.json ./lexicons/pub/leaflet/*/* ..." -``` - -Note: Files in subdirectories (`pub/leaflet/*/*`) are automatically included. - -### 4. Add to authFullPermissions (for record types) - -If your lexicon is a record type that users should be able to create/update/delete, add it to the `authFullPermissions` permission set in `lexicons/src/authFullPermissions.ts`: - -```typescript -import { PubLeafletMyLexicon } from "./myLexicon"; - -// In the permissions collection array: -collection: [ - // ... existing lexicons - PubLeafletMyLexicon.id, -], -``` - -### 5. Regenerate Types - -```bash -npm run lexgen -``` - -### 6. Use the Generated Types - -```typescript -import { PubLeafletMyLexicon } from "lexicons/api"; - -// Type for the record -type MyRecord = PubLeafletMyLexicon.Record; - -// Validation -const result = PubLeafletMyLexicon.validateRecord(data); -if (result.success) { - // result.value is typed -} - -// Type guard -if (PubLeafletMyLexicon.isRecord(data)) { - // data is typed as Record -} -``` - -## Adding a New site.standard Lexicon - -### 1. Create the JSON Definition - -Create a file in `lexicons/site/standard/` (e.g., `lexicons/site/standard/myType.json`): - -```json -{ - "lexicon": 1, - "id": "site.standard.myType", - "defs": { - "main": { - "type": "record", - "key": "tid", - "record": { - "type": "object", - "required": ["field1"], - "properties": { - "field1": { - "type": "string", - "maxLength": 1000 - } - } - } - } - } -} -``` - -### 2. Regenerate Types - -```bash -npm run lexgen -``` - -The `site/*/* site/*/*/*` globs in the lexgen command automatically pick up new files. - -## Common Lexicon Patterns - -### Referencing Other Lexicons - -```typescript -// Reference another lexicon's main def -{ type: "ref", ref: "pub.leaflet.publication" } - -// Reference a specific def within a lexicon -{ type: "ref", ref: "pub.leaflet.publication#theme" } - -// Reference within the same lexicon -{ type: "ref", ref: "#myDef" } -``` - -### Union Types - -```typescript -{ - type: "union", - refs: [ - "pub.leaflet.pages.linearDocument", - "pub.leaflet.pages.canvas", - ], -} - -// Open union (allows unknown types) -{ - type: "union", - closed: false, // default is true - refs: ["pub.leaflet.content"], -} -``` - -### Blob Types (for images/files) - -```typescript -{ - type: "blob", - accept: ["image/*"], // or specific types like ["image/png", "image/jpeg"] - maxSize: 1000000, // bytes -} -``` - -### Color Types - -The project has color types defined: -- `pub.leaflet.theme.color#rgb` / `#rgba` -- `site.standard.theme.color#rgb` / `#rgba` - -```typescript -// In lexicons/src/theme.ts -export const ColorUnion = { - type: "union", - refs: [ - "pub.leaflet.theme.color#rgba", - "pub.leaflet.theme.color#rgb", - ], -}; -``` - -## Normalization Between Formats - -Use `lexicons/src/normalize.ts` to convert between `pub.leaflet` and `site.standard` formats: - -```typescript -import { - normalizeDocument, - normalizePublication, - isLeafletDocument, - isStandardDocument, - getDocumentPages, -} from "lexicons/src/normalize"; - -// Normalize a document from either format -const normalized = normalizeDocument(record); -if (normalized) { - // normalized is always in site.standard.document format - console.log(normalized.title, normalized.site); - - // Get pages if content is pub.leaflet.content - const pages = getDocumentPages(normalized); -} - -// Normalize a publication -const pub = normalizePublication(record); -if (pub) { - console.log(pub.name, pub.url); -} -``` - -## Handling in Appview (Firehose Consumer) - -When processing records from the firehose in `appview/index.ts`: - -```typescript -import { ids } from "lexicons/api/lexicons"; -import { PubLeafletMyLexicon } from "lexicons/api"; - -// In filterCollections: -filterCollections: [ - ids.PubLeafletMyLexicon, - // ... -], - -// In handleEvent: -if (evt.collection === ids.PubLeafletMyLexicon) { - if (evt.event === "create" || evt.event === "update") { - let record = PubLeafletMyLexicon.validateRecord(evt.record); - if (!record.success) return; - - // Store in database - await supabase.from("my_table").upsert({ - uri: evt.uri.toString(), - data: record.value as Json, - }); - } - if (evt.event === "delete") { - await supabase.from("my_table").delete().eq("uri", evt.uri.toString()); - } -} -``` - -## Publishing Lexicons - -To publish lexicons to an AT Protocol PDS: - -```bash -npm run publish-lexicons -``` - -This runs `lexicons/publish.ts` which publishes lexicons to the configured PDS. diff --git a/app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts b/app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts index 131b821f..92ca1bf6 100644 --- a/app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts +++ b/app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts @@ -26,6 +26,7 @@ export async function getProfilePosts( `*, comments_on_documents(count), document_mentions_in_bsky(count), + recommends_on_documents(count), documents_in_publications(publications(*))`, ) .like("uri", `at://${did}/%`) @@ -39,18 +40,19 @@ export async function getProfilePosts( ); } - let [{ data: rawDocs }, { data: rawPubs }, { data: profile }] = await Promise.all([ - query, - supabaseServerClient - .from("publications") - .select("*") - .eq("identity_did", did), - supabaseServerClient - .from("bsky_profiles") - .select("handle") - .eq("did", did) - .single(), - ]); + let [{ data: rawDocs }, { data: rawPubs }, { data: profile }] = + await Promise.all([ + query, + supabaseServerClient + .from("publications") + .select("*") + .eq("identity_did", did), + supabaseServerClient + .from("bsky_profiles") + .select("handle") + .eq("did", did) + .single(), + ]); // Deduplicate records that may exist under both pub.leaflet and site.standard namespaces const docs = deduplicateByUriOrdered(rawDocs || []); @@ -82,6 +84,7 @@ export async function getProfilePosts( sort_date: doc.sort_date, comments_on_documents: doc.comments_on_documents, document_mentions_in_bsky: doc.document_mentions_in_bsky, + recommends_on_documents: doc.recommends_on_documents, }, }; diff --git a/app/(home-pages)/reader/getReaderFeed.ts b/app/(home-pages)/reader/getReaderFeed.ts index d3996eb6..4ff16591 100644 --- a/app/(home-pages)/reader/getReaderFeed.ts +++ b/app/(home-pages)/reader/getReaderFeed.ts @@ -32,6 +32,7 @@ export async function getReaderFeed( `*, comments_on_documents(count), document_mentions_in_bsky(count), + recommends_on_documents(count), documents_in_publications!inner(publications!inner(*, publication_subscriptions!inner(*)))`, ) .eq( @@ -76,6 +77,7 @@ export async function getReaderFeed( documents: { comments_on_documents: post.comments_on_documents, document_mentions_in_bsky: post.document_mentions_in_bsky, + recommends_on_documents: post.recommends_on_documents, data: normalizedData, uri: post.uri, sort_date: post.sort_date, @@ -112,5 +114,6 @@ export type Post = { sort_date: string; comments_on_documents: { count: number }[] | undefined; document_mentions_in_bsky: { count: number }[] | undefined; + recommends_on_documents: { count: number }[] | undefined; }; }; diff --git a/app/(home-pages)/tag/[tag]/getDocumentsByTag.ts b/app/(home-pages)/tag/[tag]/getDocumentsByTag.ts index ebaffbe0..bb6394c9 100644 --- a/app/(home-pages)/tag/[tag]/getDocumentsByTag.ts +++ b/app/(home-pages)/tag/[tag]/getDocumentsByTag.ts @@ -21,6 +21,7 @@ export async function getDocumentsByTag( `*, comments_on_documents(count), document_mentions_in_bsky(count), + recommends_on_documents(count), documents_in_publications(publications(*))`, ) .contains("data->tags", `["${tag}"]`) @@ -67,6 +68,7 @@ export async function getDocumentsByTag( documents: { comments_on_documents: doc.comments_on_documents, document_mentions_in_bsky: doc.document_mentions_in_bsky, + recommends_on_documents: doc.recommends_on_documents, data: normalizedData, uri: doc.uri, sort_date: doc.sort_date, diff --git a/app/api/oauth/[route]/route.ts b/app/api/oauth/[route]/route.ts index 33b37f07..db2ab8f4 100644 --- a/app/api/oauth/[route]/route.ts +++ b/app/api/oauth/[route]/route.ts @@ -105,7 +105,7 @@ export async function GET( }) .select() .single(); - + console.log({ token }); if (token) await setAuthToken(token.id); // Process successful authentication here @@ -114,6 +114,7 @@ export async function GET( console.log("User authenticated as:", session.did); return handleAction(s.action, redirectPath); } catch (e) { + console.log(e); redirect(redirectPath); } } diff --git a/app/api/rpc/[command]/get_publication_data.ts b/app/api/rpc/[command]/get_publication_data.ts index b33b9203..09cc91f4 100644 --- a/app/api/rpc/[command]/get_publication_data.ts +++ b/app/api/rpc/[command]/get_publication_data.ts @@ -40,7 +40,8 @@ export const get_publication_data = makeRoute({ documents_in_publications(documents( *, comments_on_documents(count), - document_mentions_in_bsky(count) + document_mentions_in_bsky(count), + recommends_on_documents(count) )), publication_subscriptions(*, identities(bsky_profiles(*))), publication_domains(*), @@ -87,6 +88,8 @@ export const get_publication_data = makeRoute({ data: dip.documents.data, commentsCount: dip.documents.comments_on_documents[0]?.count || 0, mentionsCount: dip.documents.document_mentions_in_bsky[0]?.count || 0, + recommendsCount: + dip.documents.recommends_on_documents?.[0]?.count || 0, }; }) .filter((d): d is NonNullable => d !== null); diff --git a/app/api/rpc/[command]/get_user_recommendations.ts b/app/api/rpc/[command]/get_user_recommendations.ts new file mode 100644 index 00000000..1c608695 --- /dev/null +++ b/app/api/rpc/[command]/get_user_recommendations.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; +import { makeRoute } from "../lib"; +import type { Env } from "./route"; +import { getIdentityData } from "actions/getIdentityData"; + +export type GetUserRecommendationsReturnType = Awaited< + ReturnType<(typeof get_user_recommendations)["handler"]> +>; + +export const get_user_recommendations = makeRoute({ + route: "get_user_recommendations", + input: z.object({ + documentUris: z.array(z.string()), + }), + handler: async ({ documentUris }, { supabase }: Pick) => { + const identity = await getIdentityData(); + const currentUserDid = identity?.atp_did; + + if (!currentUserDid || documentUris.length === 0) { + return { + result: {} as Record, + }; + } + + const { data: recommendations } = await supabase + .from("recommends_on_documents") + .select("document") + .eq("recommender_did", currentUserDid) + .in("document", documentUris); + + const recommendedSet = new Set(recommendations?.map((r) => r.document)); + + const result: Record = {}; + for (const uri of documentUris) { + result[uri] = recommendedSet.has(uri); + } + + return { result }; + }, +}); diff --git a/app/api/rpc/[command]/route.ts b/app/api/rpc/[command]/route.ts index df7a865b..c6ba128e 100644 --- a/app/api/rpc/[command]/route.ts +++ b/app/api/rpc/[command]/route.ts @@ -14,6 +14,7 @@ import { get_publication_data } from "./get_publication_data"; import { search_publication_names } from "./search_publication_names"; import { search_publication_documents } from "./search_publication_documents"; import { get_profile_data } from "./get_profile_data"; +import { get_user_recommendations } from "./get_user_recommendations"; let supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, @@ -41,6 +42,7 @@ let Routes = [ search_publication_names, search_publication_documents, get_profile_data, + get_user_recommendations, ]; export async function POST( req: Request, diff --git a/app/lish/Subscribe.tsx b/app/lish/Subscribe.tsx index 4cfd5326..eb2e1411 100644 --- a/app/lish/Subscribe.tsx +++ b/app/lish/Subscribe.tsx @@ -87,7 +87,9 @@ export const ManageSubscription = (props: { return ( Manage Subscription +
+ Manage Subscription +
} >
diff --git a/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx b/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx index 9b3a1058..74e3763b 100644 --- a/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx @@ -71,6 +71,7 @@ export function CanvasPage({ preferences={preferences} commentsCount={getCommentCount(document.comments_on_documents, pageId)} quotesCount={getQuoteCount(document.quotesAndMentions, pageId)} + recommendsCount={document.recommendsCount} /> { let isMobile = useIsMobile(); return ( @@ -216,8 +219,10 @@ const CanvasMetadata = (props: { {!props.isSubpage && ( @@ -233,6 +238,7 @@ const CanvasMetadata = (props: { data={props.data} profile={props.profile} preferences={props.preferences} + isCanvas /> diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx index 9dcc4421..36713943 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx @@ -18,6 +18,9 @@ import { useIdentityData } from "components/IdentityProvider"; import { ManageSubscription, SubscribeWithBluesky } from "app/lish/Subscribe"; import { EditTiny } from "components/Icons/EditTiny"; import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; +import { RecommendButton } from "components/RecommendButton"; +import { ButtonSecondary } from "components/Buttons"; +import { Separator } from "components/Layout"; export type InteractionState = { drawerOpen: undefined | boolean; @@ -105,12 +108,18 @@ export function openInteractionDrawer( export const Interactions = (props: { quotesCount: number; commentsCount: number; + recommendsCount: number; className?: string; showComments: boolean; showMentions: boolean; + showRecommends: boolean; pageId?: string; }) => { - const { uri: document_uri, quotesAndMentions, normalizedDocument } = useDocument(); + const { + uri: document_uri, + quotesAndMentions, + normalizedDocument, + } = useDocument(); let { identity } = useIdentityData(); let { drawerOpen, drawer, pageId } = useInteractionState(document_uri); @@ -124,13 +133,24 @@ export const Interactions = (props: { const tags = normalizedDocument.tags; const tagCount = tags?.length || 0; + let interactionsAvailable = + props.showComments || props.showMentions || props.showRecommends; + return ( -
- {tagCount > 0 && } +
+ {props.showRecommends === false ? null : ( + + )} + {/*MENTIONS BUTTON*/} {props.quotesCount === 0 || props.showMentions === false ? null : ( )} + {/*COMMENT BUTTON*/} {props.showComments === false ? null : ( )} + + {tagCount > 0 && ( + <> + {interactionsAvailable && } + + + )}
); }; @@ -163,12 +191,20 @@ export const Interactions = (props: { export const ExpandedInteractions = (props: { quotesCount: number; commentsCount: number; + recommendsCount: number; className?: string; showComments: boolean; showMentions: boolean; + showRecommends: boolean; pageId?: string; }) => { - const { uri: document_uri, quotesAndMentions, normalizedDocument, publication, leafletId } = useDocument(); + const { + uri: document_uri, + quotesAndMentions, + normalizedDocument, + publication, + leafletId, + } = useDocument(); let { identity } = useIdentityData(); let { drawerOpen, drawer, pageId } = useInteractionState(document_uri); @@ -182,7 +218,8 @@ export const ExpandedInteractions = (props: { const tags = normalizedDocument.tags; const tagCount = tags?.length || 0; - let noInteractions = !props.showComments && !props.showMentions; + let noInteractions = + !props.showComments && !props.showMentions && !props.showRecommends; let subscribed = identity?.atp_did && @@ -191,11 +228,6 @@ export const ExpandedInteractions = (props: { (s) => s.identity === identity.atp_did, ); - let isAuthor = - identity && - identity.atp_did === publication?.identity_did && - leafletId; - return (
) : ( - <> -
+
+
+ {props.showRecommends === false ? null : ( + + )} {props.quotesCount === 0 || !props.showMentions ? null : ( - + {props.quotesCount} + + Mention{props.quotesCount > 1 ? "s" : ""} + )} {!props.showComments ? null : ( - + Comment{props.commentsCount > 1 ? "s" : ""} + )}
- + {subscribed && publication && ( + + )} +
)} - {subscribed && publication && ( - - )}
); @@ -313,7 +349,10 @@ const TagList = (props: { className?: string; tags: string[] | undefined }) => {
); }; -export function getQuoteCount(quotesAndMentions: { uri: string; link?: string }[], pageId?: string) { +export function getQuoteCount( + quotesAndMentions: { uri: string; link?: string }[], + pageId?: string, +) { return getQuoteCountFromArray(quotesAndMentions, pageId); } @@ -338,7 +377,10 @@ export function getQuoteCountFromArray( } } -export function getCommentCount(comments: CommentOnDocument[], pageId?: string) { +export function getCommentCount( + comments: CommentOnDocument[], + pageId?: string, +) { if (pageId) return comments.filter( (c) => (c.record as PubLeafletComment.Record)?.onPage === pageId, @@ -362,7 +404,7 @@ const EditButton = (props: { return ( Edit Post diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts b/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts new file mode 100644 index 00000000..d0295f26 --- /dev/null +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts @@ -0,0 +1,135 @@ +"use server"; + +import { AtpBaseClient, PubLeafletInteractionsRecommend } from "lexicons/api"; +import { getIdentityData } from "actions/getIdentityData"; +import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; +import { TID } from "@atproto/common"; +import { AtUri, Un$Typed } from "@atproto/api"; +import { supabaseServerClient } from "supabase/serverClient"; +import { Json } from "supabase/database.types"; + +type RecommendResult = + | { success: true; uri: string } + | { + success: false; + error: OAuthSessionError | { type: string; message: string }; + }; + +export async function recommendAction(args: { + document: string; +}): Promise { + console.log("recommend action..."); + let identity = await getIdentityData(); + if (!identity || !identity.atp_did) { + return { + success: false, + error: { + type: "oauth_session_expired", + message: "Not authenticated", + did: "", + }, + }; + } + + const sessionResult = await restoreOAuthSession(identity.atp_did); + if (!sessionResult.ok) { + return { success: false, error: sessionResult.error }; + } + let credentialSession = sessionResult.value; + let agent = new AtpBaseClient( + credentialSession.fetchHandler.bind(credentialSession), + ); + + let record: Un$Typed = { + subject: args.document, + createdAt: new Date().toISOString(), + }; + + let rkey = TID.nextStr(); + let uri = AtUri.make( + credentialSession.did!, + "pub.leaflet.interactions.recommend", + rkey, + ); + + await agent.pub.leaflet.interactions.recommend.create( + { rkey, repo: credentialSession.did! }, + record, + ); + + let res = await supabaseServerClient.from("recommends_on_documents").upsert({ + uri: uri.toString(), + document: args.document, + recommender_did: credentialSession.did!, + record: { + $type: "pub.leaflet.interactions.recommend", + ...record, + } as unknown as Json, + }); + console.log(res); + + return { + success: true, + uri: uri.toString(), + }; +} + +export async function unrecommendAction(args: { + document: string; +}): Promise { + let identity = await getIdentityData(); + if (!identity || !identity.atp_did) { + return { + success: false, + error: { + type: "oauth_session_expired", + message: "Not authenticated", + did: "", + }, + }; + } + + const sessionResult = await restoreOAuthSession(identity.atp_did); + if (!sessionResult.ok) { + return { success: false, error: sessionResult.error }; + } + let credentialSession = sessionResult.value; + let agent = new AtpBaseClient( + credentialSession.fetchHandler.bind(credentialSession), + ); + + // Find the existing recommend record + const { data: existingRecommend } = await supabaseServerClient + .from("recommends_on_documents") + .select("uri") + .eq("document", args.document) + .eq("recommender_did", credentialSession.did!) + .single(); + + if (!existingRecommend) { + return { + success: false, + error: { + type: "not_found", + message: "Recommend not found", + }, + }; + } + + let uri = new AtUri(existingRecommend.uri); + + await agent.pub.leaflet.interactions.recommend.delete({ + rkey: uri.rkey, + repo: credentialSession.did!, + }); + + await supabaseServerClient + .from("recommends_on_documents") + .delete() + .eq("uri", existingRecommend.uri); + + return { + success: true, + uri: existingRecommend.uri, + }; +} diff --git a/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx b/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx index 1046025a..04b9d7d9 100644 --- a/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx @@ -87,8 +87,12 @@ export function LinearDocumentPage({ pageId={pageId} showComments={preferences.showComments !== false} showMentions={preferences.showMentions !== false} - commentsCount={getCommentCount(document.comments_on_documents, pageId) || 0} + showRecommends={preferences.showRecommends !== false} + commentsCount={ + getCommentCount(document.comments_on_documents, pageId) || 0 + } quotesCount={getQuoteCount(document.quotesAndMentions, pageId) || 0} + recommendsCount={document.recommendsCount} /> {!hasPageBackground &&
} diff --git a/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx b/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx index 86216159..1159683a 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx @@ -18,7 +18,12 @@ import { ProfilePopover } from "components/ProfilePopover"; export function PostHeader(props: { data: PostPageData; profile: ProfileViewDetailed; - preferences: { showComments?: boolean; showMentions?: boolean }; + preferences: { + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + }; + isCanvas?: boolean; }) { let { identity } = useIdentityData(); let document = props.data; @@ -84,12 +89,20 @@ export function PostHeader(props: { ) : null}
- + {!props.isCanvas && ( + + )} } /> diff --git a/app/lish/[did]/[publication]/[rkey]/PostPages.tsx b/app/lish/[did]/[publication]/[rkey]/PostPages.tsx index 234f2041..9b2a8a67 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostPages.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostPages.tsx @@ -170,6 +170,7 @@ export type SharedPageProps = { preferences: { showComments?: boolean; showMentions?: boolean; + showRecommends?: boolean; showPrevNext?: boolean; }; pubRecord?: NormalizedPublication | null; @@ -233,6 +234,7 @@ export function PostPages({ preferences: { showComments?: boolean; showMentions?: boolean; + showRecommends?: boolean; showPrevNext?: boolean; }; pollData: PollData[]; diff --git a/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts b/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts index 9f7f44a2..7f16418a 100644 --- a/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts +++ b/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts @@ -22,7 +22,8 @@ export async function getPostPageData(did: string, rkey: string) { publication_subscriptions(*)) ), document_mentions_in_bsky(*), - leaflets_in_publications(*) + leaflets_in_publications(*), + recommends_on_documents(count) `, ) .or(documentUriFilter(did, rkey)) @@ -33,12 +34,15 @@ export async function getPostPageData(did: string, rkey: string) { if (!document) return null; // Normalize the document record - this is the primary way consumers should access document data - const normalizedDocument = normalizeDocumentRecord(document.data, document.uri); + const normalizedDocument = normalizeDocumentRecord( + document.data, + document.uri, + ); if (!normalizedDocument) return null; // Normalize the publication record - this is the primary way consumers should access publication data const normalizedPublication = normalizePublicationRecord( - document.documents_in_publications[0]?.publications?.record + document.documents_in_publications[0]?.publications?.record, ); // Fetch constellation backlinks for mentions @@ -83,7 +87,10 @@ export async function getPostPageData(did: string, rkey: string) { // Filter and sort documents by publishedAt const sortedDocs = allDocs .map((dip) => { - const normalizedData = normalizeDocumentRecord(dip?.documents?.data, dip?.documents?.uri); + const normalizedData = normalizeDocumentRecord( + dip?.documents?.data, + dip?.documents?.uri, + ); return { uri: dip?.documents?.uri, title: normalizedData?.title, @@ -98,7 +105,9 @@ export async function getPostPageData(did: string, rkey: string) { ); // Find current document index - const currentIndex = sortedDocs.findIndex((doc) => doc.uri === document.uri); + const currentIndex = sortedDocs.findIndex( + (doc) => doc.uri === document.uri, + ); if (currentIndex !== -1) { prevNext = { @@ -122,13 +131,21 @@ export async function getPostPageData(did: string, rkey: string) { // Build explicit publication context for consumers const rawPub = document.documents_in_publications[0]?.publications; - const publication = rawPub ? { - uri: rawPub.uri, - name: rawPub.name, - identity_did: rawPub.identity_did, - record: rawPub.record as PubLeafletPublication.Record | SiteStandardPublication.Record | null, - publication_subscriptions: rawPub.publication_subscriptions || [], - } : null; + const publication = rawPub + ? { + uri: rawPub.uri, + name: rawPub.name, + identity_did: rawPub.identity_did, + record: rawPub.record as + | PubLeafletPublication.Record + | SiteStandardPublication.Record + | null, + publication_subscriptions: rawPub.publication_subscriptions || [], + } + : null; + + // Get recommends count from the aggregated query result + const recommendsCount = document.recommends_on_documents?.[0]?.count ?? 0; return { ...document, @@ -143,6 +160,8 @@ export async function getPostPageData(did: string, rkey: string) { comments: document.comments_on_documents, mentions: document.document_mentions_in_bsky, leafletId: document.leaflets_in_publications[0]?.leaflet || null, + // Recommends data + recommendsCount, }; } diff --git a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx index b8a24406..a7e4c630 100644 --- a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx @@ -60,7 +60,9 @@ export function PublishedPostsList(props: { function PublishedPostItem(props: { doc: PublishedDocument; - publication: NonNullable["data"]>["publication"]>; + publication: NonNullable< + NonNullable["data"]>["publication"] + >; pubRecord: ReturnType; showPageBackground: boolean; }) { @@ -94,10 +96,7 @@ function PublishedPostItem(props: {
{leaflet && leaflet.permission_tokens && ( <> - + @@ -129,9 +128,7 @@ function PublishedPostItem(props: {
{doc.record.description ? ( -

- {doc.record.description} -

+

{doc.record.description}

) : null}
{doc.record.publishedAt ? ( @@ -140,9 +137,12 @@ function PublishedPostItem(props: {
diff --git a/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx b/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx index 076568ef..70ae3d75 100644 --- a/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx +++ b/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx @@ -29,6 +29,11 @@ export const PostOptions = (props: { ? true : record.preferences.showMentions, ); + let [showRecommends, setShowRecommends] = useState( + record?.preferences?.showRecommends === undefined + ? true + : record.preferences.showRecommends, + ); let [showPrevNext, setShowPrevNext] = useState( record?.preferences?.showPrevNext === undefined ? true @@ -53,6 +58,7 @@ export const PostOptions = (props: { showComments: showComments, showMentions: showMentions, showPrevNext: showPrevNext, + showRecommends: showRecommends, }, }); toast({ type: "success", content: Posts Updated! }); @@ -99,7 +105,21 @@ export const PostOptions = (props: {
Show Mentions
- Display a list of posts on Bluesky that mention your post + Display a list Bluesky mentions about your post +
+
+ + + { + setShowRecommends(!showRecommends); + }} + > +
+
Show Recommends
+
+ Allow readers to recommend/like your post
diff --git a/app/lish/[did]/[publication]/page.tsx b/app/lish/[did]/[publication]/page.tsx index 0a51e1b4..457f38b2 100644 --- a/app/lish/[did]/[publication]/page.tsx +++ b/app/lish/[did]/[publication]/page.tsx @@ -38,7 +38,8 @@ export default async function Publication(props: { documents_in_publications(documents( *, comments_on_documents(count), - document_mentions_in_bsky(count) + document_mentions_in_bsky(count), + recommends_on_documents(count) )) `, ) @@ -119,7 +120,9 @@ export default async function Publication(props: { }) .map((doc) => { if (!doc.documents) return null; - const doc_record = normalizeDocumentRecord(doc.documents.data); + const doc_record = normalizeDocumentRecord( + doc.documents.data, + ); if (!doc_record) return null; let uri = new AtUri(doc.documents.uri); let quotes = @@ -128,6 +131,8 @@ export default async function Publication(props: { record?.preferences?.showComments === false ? 0 : doc.documents.comments_on_documents[0].count || 0; + let recommends = + doc.documents.recommends_on_documents?.[0]?.count || 0; let tags = doc_record.tags || []; return ( @@ -143,7 +148,7 @@ export default async function Publication(props: {

-
+

{doc_record.publishedAt && ( )}{" "}

- {comments > 0 || quotes > 0 || tags.length > 0 ? ( - - ) : ( - "" - )} +
diff --git a/app/lish/createPub/CreatePubForm.tsx b/app/lish/createPub/CreatePubForm.tsx index 2dbe924e..749b4397 100644 --- a/app/lish/createPub/CreatePubForm.tsx +++ b/app/lish/createPub/CreatePubForm.tsx @@ -58,6 +58,7 @@ export const CreatePubForm = () => { showComments: true, showMentions: true, showPrevNext: true, + showRecommends: true, }, }); diff --git a/app/lish/createPub/UpdatePubForm.tsx b/app/lish/createPub/UpdatePubForm.tsx index 3672c5f8..f9844796 100644 --- a/app/lish/createPub/UpdatePubForm.tsx +++ b/app/lish/createPub/UpdatePubForm.tsx @@ -88,6 +88,7 @@ export const EditPubForm = (props: { showComments: showComments, showMentions: showMentions, showPrevNext: showPrevNext, + showRecommends: record?.preferences?.showRecommends ?? true, }, }); toast({ type: "success", content: "Updated!" }); @@ -194,8 +195,6 @@ export const EditPubForm = (props: {

- - ); diff --git a/app/lish/createPub/createPublication.ts b/app/lish/createPub/createPublication.ts index a705a0e6..5adcc7ee 100644 --- a/app/lish/createPub/createPublication.ts +++ b/app/lish/createPub/createPublication.ts @@ -5,10 +5,7 @@ import { PubLeafletPublication, SiteStandardPublication, } from "lexicons/api"; -import { - restoreOAuthSession, - OAuthSessionError, -} from "src/atproto-oauth"; +import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; import { getIdentityData } from "actions/getIdentityData"; import { supabaseServerClient } from "supabase/serverClient"; import { Json } from "supabase/database.types"; @@ -76,7 +73,11 @@ export async function createPublication({ // Build record based on publication type let record: SiteStandardPublication.Record | PubLeafletPublication.Record; - let iconBlob: Awaited>["data"]["blob"] | undefined; + let iconBlob: + | Awaited< + ReturnType + >["data"]["blob"] + | undefined; // Upload the icon if provided if (iconFile && iconFile.size > 0) { @@ -97,16 +98,29 @@ export async function createPublication({ ...(iconBlob && { icon: iconBlob }), basicTheme: { $type: "site.standard.theme.basic", - background: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.background }, - foreground: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.foreground }, - accent: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.accent }, - accentForeground: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.accentForeground }, + background: { + $type: "site.standard.theme.color#rgb", + ...PubThemeDefaultsRGB.background, + }, + foreground: { + $type: "site.standard.theme.color#rgb", + ...PubThemeDefaultsRGB.foreground, + }, + accent: { + $type: "site.standard.theme.color#rgb", + ...PubThemeDefaultsRGB.accent, + }, + accentForeground: { + $type: "site.standard.theme.color#rgb", + ...PubThemeDefaultsRGB.accentForeground, + }, }, preferences: { showInDiscover: preferences.showInDiscover, showComments: preferences.showComments, showMentions: preferences.showMentions, showPrevNext: preferences.showPrevNext, + showRecommends: preferences.showRecommends, }, } satisfies SiteStandardPublication.Record; } else { diff --git a/app/lish/createPub/updatePublication.ts b/app/lish/createPub/updatePublication.ts index a9bd3e56..350b3f88 100644 --- a/app/lish/createPub/updatePublication.ts +++ b/app/lish/createPub/updatePublication.ts @@ -77,7 +77,9 @@ async function withPublicationUpdate( } const aturi = new AtUri(existingPub.uri); - const publicationType = getPublicationType(aturi.collection) as PublicationType; + const publicationType = getPublicationType( + aturi.collection, + ) as PublicationType; // Normalize existing record const normalizedPub = normalizePublicationRecord(existingPub.record); @@ -128,7 +130,11 @@ interface RecordOverrides { } /** Merges override with existing value, respecting explicit undefined */ -function resolveField(override: T | undefined, existing: T | undefined, hasOverride: boolean): T | undefined { +function resolveField( + override: T | undefined, + existing: T | undefined, + hasOverride: boolean, +): T | undefined { return hasOverride ? override : existing; } @@ -146,17 +152,32 @@ function buildLeafletRecord( return { $type: "pub.leaflet.publication", name: overrides.name ?? normalizedPub?.name ?? "", - description: resolveField(overrides.description, normalizedPub?.description, "description" in overrides), - icon: resolveField(overrides.icon, normalizedPub?.icon, "icon" in overrides), - theme: resolveField(overrides.theme, normalizedPub?.theme, "theme" in overrides), + description: resolveField( + overrides.description, + normalizedPub?.description, + "description" in overrides, + ), + icon: resolveField( + overrides.icon, + normalizedPub?.icon, + "icon" in overrides, + ), + theme: resolveField( + overrides.theme, + normalizedPub?.theme, + "theme" in overrides, + ), base_path: overrides.basePath ?? existingBasePath, - preferences: preferences ? { - $type: "pub.leaflet.publication#preferences", - showInDiscover: preferences.showInDiscover, - showComments: preferences.showComments, - showMentions: preferences.showMentions, - showPrevNext: preferences.showPrevNext, - } : undefined, + preferences: preferences + ? { + $type: "pub.leaflet.publication#preferences", + showInDiscover: preferences.showInDiscover, + showComments: preferences.showComments, + showMentions: preferences.showMentions, + showPrevNext: preferences.showPrevNext, + showRecommends: preferences.showRecommends, + } + : undefined, }; } @@ -175,17 +196,36 @@ function buildStandardRecord( return { $type: "site.standard.publication", name: overrides.name ?? normalizedPub?.name ?? "", - description: resolveField(overrides.description, normalizedPub?.description, "description" in overrides), - icon: resolveField(overrides.icon, normalizedPub?.icon, "icon" in overrides), - theme: resolveField(overrides.theme, normalizedPub?.theme, "theme" in overrides), - basicTheme: resolveField(overrides.basicTheme, normalizedPub?.basicTheme, "basicTheme" in overrides), + description: resolveField( + overrides.description, + normalizedPub?.description, + "description" in overrides, + ), + icon: resolveField( + overrides.icon, + normalizedPub?.icon, + "icon" in overrides, + ), + theme: resolveField( + overrides.theme, + normalizedPub?.theme, + "theme" in overrides, + ), + basicTheme: resolveField( + overrides.basicTheme, + normalizedPub?.basicTheme, + "basicTheme" in overrides, + ), url: basePath ? `https://${basePath}` : normalizedPub?.url || "", - preferences: preferences ? { - showInDiscover: preferences.showInDiscover, - showComments: preferences.showComments, - showMentions: preferences.showMentions, - showPrevNext: preferences.showPrevNext, - } : undefined, + preferences: preferences + ? { + showInDiscover: preferences.showInDiscover, + showComments: preferences.showComments, + showMentions: preferences.showMentions, + showPrevNext: preferences.showPrevNext, + showRecommends: preferences.showRecommends, + } + : undefined, }; } @@ -217,27 +257,30 @@ export async function updatePublication({ iconFile?: File | null; preferences?: Omit; }): Promise { - return withPublicationUpdate(uri, async ({ normalizedPub, existingBasePath, publicationType, agent }) => { - // Upload icon if provided - let iconBlob = normalizedPub?.icon; - if (iconFile && iconFile.size > 0) { - const buffer = await iconFile.arrayBuffer(); - const uploadResult = await agent.com.atproto.repo.uploadBlob( - new Uint8Array(buffer), - { encoding: iconFile.type }, - ); - if (uploadResult.data.blob) { - iconBlob = uploadResult.data.blob; + return withPublicationUpdate( + uri, + async ({ normalizedPub, existingBasePath, publicationType, agent }) => { + // Upload icon if provided + let iconBlob = normalizedPub?.icon; + if (iconFile && iconFile.size > 0) { + const buffer = await iconFile.arrayBuffer(); + const uploadResult = await agent.com.atproto.repo.uploadBlob( + new Uint8Array(buffer), + { encoding: iconFile.type }, + ); + if (uploadResult.data.blob) { + iconBlob = uploadResult.data.blob; + } } - } - return buildRecord(normalizedPub, existingBasePath, publicationType, { - name, - description, - icon: iconBlob, - preferences, - }); - }); + return buildRecord(normalizedPub, existingBasePath, publicationType, { + name, + description, + icon: iconBlob, + preferences, + }); + }, + ); } export async function updatePublicationBasePath({ @@ -247,11 +290,14 @@ export async function updatePublicationBasePath({ uri: string; base_path: string; }): Promise { - return withPublicationUpdate(uri, async ({ normalizedPub, existingBasePath, publicationType }) => { - return buildRecord(normalizedPub, existingBasePath, publicationType, { - basePath: base_path, - }); - }); + return withPublicationUpdate( + uri, + async ({ normalizedPub, existingBasePath, publicationType }) => { + return buildRecord(normalizedPub, existingBasePath, publicationType, { + basePath: base_path, + }); + }, + ); } type Color = @@ -275,58 +321,81 @@ export async function updatePublicationTheme({ accentText: Color; }; }): Promise { - return withPublicationUpdate(uri, async ({ normalizedPub, existingBasePath, publicationType, agent }) => { - // Build theme object - const themeData = { - $type: "pub.leaflet.publication#theme" as const, - backgroundImage: theme.backgroundImage - ? { - $type: "pub.leaflet.theme.backgroundImage", - image: ( - await agent.com.atproto.repo.uploadBlob( - new Uint8Array(await theme.backgroundImage.arrayBuffer()), - { encoding: theme.backgroundImage.type }, - ) - )?.data.blob, - width: theme.backgroundRepeat || undefined, - repeat: !!theme.backgroundRepeat, - } - : theme.backgroundImage === null - ? undefined - : normalizedPub?.theme?.backgroundImage, - backgroundColor: theme.backgroundColor - ? { - ...theme.backgroundColor, - } - : undefined, - pageWidth: theme.pageWidth, - primary: { - ...theme.primary, - }, - pageBackground: { - ...theme.pageBackground, - }, - showPageBackground: theme.showPageBackground, - accentBackground: { - ...theme.accentBackground, - }, - accentText: { - ...theme.accentText, - }, - }; + return withPublicationUpdate( + uri, + async ({ normalizedPub, existingBasePath, publicationType, agent }) => { + // Build theme object + const themeData = { + $type: "pub.leaflet.publication#theme" as const, + backgroundImage: theme.backgroundImage + ? { + $type: "pub.leaflet.theme.backgroundImage", + image: ( + await agent.com.atproto.repo.uploadBlob( + new Uint8Array(await theme.backgroundImage.arrayBuffer()), + { encoding: theme.backgroundImage.type }, + ) + )?.data.blob, + width: theme.backgroundRepeat || undefined, + repeat: !!theme.backgroundRepeat, + } + : theme.backgroundImage === null + ? undefined + : normalizedPub?.theme?.backgroundImage, + backgroundColor: theme.backgroundColor + ? { + ...theme.backgroundColor, + } + : undefined, + pageWidth: theme.pageWidth, + primary: { + ...theme.primary, + }, + pageBackground: { + ...theme.pageBackground, + }, + showPageBackground: theme.showPageBackground, + accentBackground: { + ...theme.accentBackground, + }, + accentText: { + ...theme.accentText, + }, + }; - // Derive basicTheme from the theme colors for site.standard.publication - const basicTheme: NormalizedPublication["basicTheme"] = { - $type: "site.standard.theme.basic", - background: { $type: "site.standard.theme.color#rgb", r: theme.backgroundColor.r, g: theme.backgroundColor.g, b: theme.backgroundColor.b }, - foreground: { $type: "site.standard.theme.color#rgb", r: theme.primary.r, g: theme.primary.g, b: theme.primary.b }, - accent: { $type: "site.standard.theme.color#rgb", r: theme.accentBackground.r, g: theme.accentBackground.g, b: theme.accentBackground.b }, - accentForeground: { $type: "site.standard.theme.color#rgb", r: theme.accentText.r, g: theme.accentText.g, b: theme.accentText.b }, - }; + // Derive basicTheme from the theme colors for site.standard.publication + const basicTheme: NormalizedPublication["basicTheme"] = { + $type: "site.standard.theme.basic", + background: { + $type: "site.standard.theme.color#rgb", + r: theme.backgroundColor.r, + g: theme.backgroundColor.g, + b: theme.backgroundColor.b, + }, + foreground: { + $type: "site.standard.theme.color#rgb", + r: theme.primary.r, + g: theme.primary.g, + b: theme.primary.b, + }, + accent: { + $type: "site.standard.theme.color#rgb", + r: theme.accentBackground.r, + g: theme.accentBackground.g, + b: theme.accentBackground.b, + }, + accentForeground: { + $type: "site.standard.theme.color#rgb", + r: theme.accentText.r, + g: theme.accentText.g, + b: theme.accentText.b, + }, + }; - return buildRecord(normalizedPub, existingBasePath, publicationType, { - theme: themeData, - basicTheme, - }); - }); + return buildRecord(normalizedPub, existingBasePath, publicationType, { + theme: themeData, + basicTheme, + }); + }, + ); } diff --git a/components/Canvas.tsx b/components/Canvas.tsx index 0faa3302..118c163e 100644 --- a/components/Canvas.tsx +++ b/components/Canvas.tsx @@ -19,10 +19,11 @@ import { Popover } from "./Popover"; import { Separator } from "./Layout"; import { CommentTiny } from "./Icons/CommentTiny"; import { QuoteTiny } from "./Icons/QuoteTiny"; -import { PublicationMetadata } from "./Pages/PublicationMetadata"; +import { AddTags, PublicationMetadata } from "./Pages/PublicationMetadata"; import { useLeafletPublicationData } from "./PageSWRDataProvider"; import { useHandleCanvasDrop } from "./Blocks/useHandleCanvasDrop"; import { useBlockMouseHandlers } from "./Blocks/useBlockMouseHandlers"; +import { RecommendTinyEmpty } from "./Icons/RecommendTiny"; export function Canvas(props: { entityID: string; @@ -168,20 +169,34 @@ const CanvasMetadata = (props: { isSubpage: boolean | undefined }) => { if (!normalizedPublication) return null; let showComments = normalizedPublication.preferences?.showComments !== false; let showMentions = normalizedPublication.preferences?.showMentions !== false; + let showRecommends = + normalizedPublication.preferences?.showRecommends !== false; return (
+ {showRecommends && ( +
+ — +
+ )} {showComments && (
—
)} - {showComments && ( + {showMentions && (
—
)} + {showMentions !== false || + showComments !== false || + showRecommends === false ? ( + + ) : null} + + {!props.isSubpage && ( <> @@ -191,7 +206,7 @@ const CanvasMetadata = (props: { isSubpage: boolean | undefined }) => { className="flex flex-col gap-2 p-0! max-w-sm w-[1000px]" trigger={} > - + )} diff --git a/components/Icons/RecommendTiny.tsx b/components/Icons/RecommendTiny.tsx new file mode 100644 index 00000000..7ec07c14 --- /dev/null +++ b/components/Icons/RecommendTiny.tsx @@ -0,0 +1,37 @@ +import { Props } from "./Props"; + +export const RecommendTinyFilled = (props: Props) => { + return ( + + + + ); +}; + +export const RecommendTinyEmpty = (props: Props) => { + return ( + + + + ); +}; diff --git a/components/InteractionsPreview.tsx b/components/InteractionsPreview.tsx index 30ddefbb..9f393f86 100644 --- a/components/InteractionsPreview.tsx +++ b/components/InteractionsPreview.tsx @@ -7,35 +7,36 @@ import { Tag } from "./Tags"; import { Popover } from "./Popover"; import { TagTiny } from "./Icons/TagTiny"; import { SpeedyLink } from "./SpeedyLink"; +import { RecommendButton } from "./RecommendButton"; export const InteractionPreview = (props: { quotesCount: number; commentsCount: number; + recommendsCount: number; + documentUri: string; tags?: string[]; postUrl: string; showComments: boolean; showMentions: boolean; + showRecommends: boolean; share?: boolean; }) => { let smoker = useSmoker(); let interactionsAvailable = (props.quotesCount > 0 && props.showMentions) || - (props.showComments !== false && props.commentsCount > 0); + (props.showComments !== false && props.commentsCount > 0) || + (props.showRecommends !== false && props.recommendsCount > 0); const tagsCount = props.tags?.length || 0; return ( -
- {tagsCount === 0 ? null : ( - <> - - {interactionsAvailable || props.share ? ( - - ) : null} - +
+ {props.showRecommends === false ? null : ( + )} {!props.showMentions || props.quotesCount === 0 ? null : ( @@ -56,11 +57,16 @@ export const InteractionPreview = (props: { {props.commentsCount} )} - {interactionsAvailable && props.share ? ( - - ) : null} + {tagsCount === 0 ? null : ( + <> + {interactionsAvailable ? : null} + + + )} {props.share && ( <> + + + ); +} diff --git a/contexts/DocumentContext.tsx b/contexts/DocumentContext.tsx index 467d9ad9..d3157d69 100644 --- a/contexts/DocumentContext.tsx +++ b/contexts/DocumentContext.tsx @@ -21,6 +21,7 @@ export type DocumentContextValue = Pick< | "comments" | "mentions" | "leafletId" + | "recommendsCount" >; const DocumentContext = createContext(null); diff --git a/lexicons/api/types/pub/leaflet/publication.ts b/lexicons/api/types/pub/leaflet/publication.ts index 88314a28..51143ee0 100644 --- a/lexicons/api/types/pub/leaflet/publication.ts +++ b/lexicons/api/types/pub/leaflet/publication.ts @@ -1,89 +1,94 @@ /** * 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' -import type * as PubLeafletThemeColor from './theme/color' -import type * as PubLeafletThemeBackgroundImage from './theme/backgroundImage' +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"; +import type * as PubLeafletThemeColor from "./theme/color"; +import type * as PubLeafletThemeBackgroundImage from "./theme/backgroundImage"; const is$typed = _is$typed, - validate = _validate -const id = 'pub.leaflet.publication' + validate = _validate; +const id = "pub.leaflet.publication"; export interface Record { - $type: 'pub.leaflet.publication' - name: string - base_path?: string - description?: string - icon?: BlobRef - theme?: Theme - preferences?: Preferences - [k: string]: unknown + $type: "pub.leaflet.publication"; + name: string; + base_path?: string; + description?: string; + icon?: BlobRef; + theme?: Theme; + preferences?: Preferences; + [k: string]: unknown; } -const hashRecord = 'main' +const hashRecord = "main"; export function isRecord(v: V) { - return is$typed(v, id, hashRecord) + return is$typed(v, id, hashRecord); } export function validateRecord(v: V) { - return validate(v, id, hashRecord, true) + return validate(v, id, hashRecord, true); } export interface Preferences { - $type?: 'pub.leaflet.publication#preferences' - showInDiscover: boolean - showComments: boolean - showMentions: boolean - showPrevNext: boolean + $type?: "pub.leaflet.publication#preferences"; + showInDiscover: boolean; + showComments: boolean; + showMentions: boolean; + showPrevNext: boolean; + showRecommends: boolean; } -const hashPreferences = 'preferences' +const hashPreferences = "preferences"; export function isPreferences(v: V) { - return is$typed(v, id, hashPreferences) + return is$typed(v, id, hashPreferences); } export function validatePreferences(v: V) { - return validate(v, id, hashPreferences) + return validate(v, id, hashPreferences); } export interface Theme { - $type?: 'pub.leaflet.publication#theme' + $type?: "pub.leaflet.publication#theme"; backgroundColor?: | $Typed | $Typed - | { $type: string } - backgroundImage?: PubLeafletThemeBackgroundImage.Main - pageWidth?: number + | { $type: string }; + backgroundImage?: PubLeafletThemeBackgroundImage.Main; + pageWidth?: number; primary?: | $Typed | $Typed - | { $type: string } + | { $type: string }; pageBackground?: | $Typed | $Typed - | { $type: string } - showPageBackground: boolean + | { $type: string }; + showPageBackground: boolean; accentBackground?: | $Typed | $Typed - | { $type: string } + | { $type: string }; accentText?: | $Typed | $Typed - | { $type: string } + | { $type: string }; } -const hashTheme = 'theme' +const hashTheme = "theme"; export function isTheme(v: V) { - return is$typed(v, id, hashTheme) + return is$typed(v, id, hashTheme); } export function validateTheme(v: V) { - return validate(v, id, hashTheme) + return validate(v, id, hashTheme); } diff --git a/lexicons/api/types/site/standard/publication.ts b/lexicons/api/types/site/standard/publication.ts index 2daf12b6..08f80d9c 100644 --- a/lexicons/api/types/site/standard/publication.ts +++ b/lexicons/api/types/site/standard/publication.ts @@ -1,53 +1,58 @@ /** * 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' -import type * as SiteStandardThemeBasic from './theme/basic' -import type * as PubLeafletPublication from '../../pub/leaflet/publication' +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"; +import type * as SiteStandardThemeBasic from "./theme/basic"; +import type * as PubLeafletPublication from "../../pub/leaflet/publication"; const is$typed = _is$typed, - validate = _validate -const id = 'site.standard.publication' + validate = _validate; +const id = "site.standard.publication"; export interface Record { - $type: 'site.standard.publication' - basicTheme?: SiteStandardThemeBasic.Main - theme?: $Typed | { $type: string } - description?: string - icon?: BlobRef - name: string - preferences?: Preferences - url: string - [k: string]: unknown + $type: "site.standard.publication"; + basicTheme?: SiteStandardThemeBasic.Main; + theme?: $Typed | { $type: string }; + description?: string; + icon?: BlobRef; + name: string; + preferences?: Preferences; + url: string; + [k: string]: unknown; } -const hashRecord = 'main' +const hashRecord = "main"; export function isRecord(v: V) { - return is$typed(v, id, hashRecord) + return is$typed(v, id, hashRecord); } export function validateRecord(v: V) { - return validate(v, id, hashRecord, true) + return validate(v, id, hashRecord, true); } export interface Preferences { - $type?: 'site.standard.publication#preferences' - showInDiscover: boolean - showComments: boolean - showMentions: boolean - showPrevNext: boolean + $type?: "site.standard.publication#preferences"; + showInDiscover: boolean; + showComments: boolean; + showMentions: boolean; + showPrevNext: boolean; + showRecommends: boolean; } -const hashPreferences = 'preferences' +const hashPreferences = "preferences"; export function isPreferences(v: V) { - return is$typed(v, id, hashPreferences) + return is$typed(v, id, hashPreferences); } export function validatePreferences(v: V) { - return validate(v, id, hashPreferences) + return validate(v, id, hashPreferences); } diff --git a/lexicons/pub/leaflet/publication.json b/lexicons/pub/leaflet/publication.json index 0627addf..c98b1eb6 100644 --- a/lexicons/pub/leaflet/publication.json +++ b/lexicons/pub/leaflet/publication.json @@ -59,6 +59,10 @@ "showPrevNext": { "type": "boolean", "default": true + }, + "showRecommends": { + "type": "boolean", + "default": true } } }, diff --git a/lexicons/site/standard/publication.json b/lexicons/site/standard/publication.json index b1449052..7fdbcc22 100644 --- a/lexicons/site/standard/publication.json +++ b/lexicons/site/standard/publication.json @@ -58,6 +58,10 @@ "showPrevNext": { "default": false, "type": "boolean" + }, + "showRecommends": { + "default": true, + "type": "boolean" } }, "type": "object" diff --git a/lexicons/src/normalize.ts b/lexicons/src/normalize.ts index d4890e07..4f7294c7 100644 --- a/lexicons/src/normalize.ts +++ b/lexicons/src/normalize.ts @@ -50,7 +50,7 @@ export type NormalizedPublication = { * Checks if the record is a pub.leaflet.document */ export function isLeafletDocument( - record: unknown + record: unknown, ): record is PubLeafletDocument.Record { if (!record || typeof record !== "object") return false; const r = record as Record; @@ -65,7 +65,7 @@ export function isLeafletDocument( * Checks if the record is a site.standard.document */ export function isStandardDocument( - record: unknown + record: unknown, ): record is SiteStandardDocument.Record { if (!record || typeof record !== "object") return false; const r = record as Record; @@ -76,7 +76,7 @@ export function isStandardDocument( * Checks if the record is a pub.leaflet.publication */ export function isLeafletPublication( - record: unknown + record: unknown, ): record is PubLeafletPublication.Record { if (!record || typeof record !== "object") return false; const r = record as Record; @@ -91,7 +91,7 @@ export function isLeafletPublication( * Checks if the record is a site.standard.publication */ export function isStandardPublication( - record: unknown + record: unknown, ): record is SiteStandardPublication.Record { if (!record || typeof record !== "object") return false; const r = record as Record; @@ -106,7 +106,7 @@ function extractRgb( | $Typed | $Typed | { $type: string } - | undefined + | undefined, ): { r: number; g: number; b: number } | undefined { if (!color || typeof color !== "object") return undefined; const c = color as Record; @@ -124,12 +124,13 @@ function extractRgb( * Converts a pub.leaflet theme to a site.standard.theme.basic format */ export function leafletThemeToBasicTheme( - theme: PubLeafletPublication.Theme | undefined + theme: PubLeafletPublication.Theme | undefined, ): SiteStandardThemeBasic.Main | undefined { if (!theme) return undefined; const background = extractRgb(theme.backgroundColor); - const accent = extractRgb(theme.accentBackground) || extractRgb(theme.primary); + const accent = + extractRgb(theme.accentBackground) || extractRgb(theme.primary); const accentForeground = extractRgb(theme.accentText); // If we don't have the required colors, return undefined @@ -160,7 +161,10 @@ export function leafletThemeToBasicTheme( * @param uri - Optional document URI, used to extract the rkey for the path field when normalizing pub.leaflet records * @returns A normalized document in site.standard format, or null if invalid/unrecognized */ -export function normalizeDocument(record: unknown, uri?: string): NormalizedDocument | null { +export function normalizeDocument( + record: unknown, + uri?: string, +): NormalizedDocument | null { if (!record || typeof record !== "object") return null; // Pass through site.standard records directly (theme is already in correct format if present) @@ -219,7 +223,7 @@ export function normalizeDocument(record: unknown, uri?: string): NormalizedDocu * @returns A normalized publication in site.standard format, or null if invalid/unrecognized */ export function normalizePublication( - record: unknown + record: unknown, ): NormalizedPublication | null { if (!record || typeof record !== "object") return null; @@ -268,6 +272,7 @@ export function normalizePublication( showComments: record.preferences.showComments, showMentions: record.preferences.showMentions, showPrevNext: record.preferences.showPrevNext, + showRecommends: record.preferences.showRecommends, } : undefined; @@ -290,7 +295,7 @@ export function normalizePublication( * Type guard to check if a normalized document has leaflet content */ export function hasLeafletContent( - doc: NormalizedDocument + doc: NormalizedDocument, ): doc is NormalizedDocument & { content: $Typed; } { @@ -304,7 +309,7 @@ export function hasLeafletContent( * Gets the pages array from a normalized document, handling both formats */ export function getDocumentPages( - doc: NormalizedDocument + doc: NormalizedDocument, ): PubLeafletContent.Main["pages"] | undefined { if (!doc.content) return undefined; diff --git a/lexicons/src/publication.ts b/lexicons/src/publication.ts index 57bbbef9..cf5904d2 100644 --- a/lexicons/src/publication.ts +++ b/lexicons/src/publication.ts @@ -29,6 +29,7 @@ export const PubLeafletPublication: LexiconDoc = { showComments: { type: "boolean", default: true }, showMentions: { type: "boolean", default: true }, showPrevNext: { type: "boolean", default: true }, + showRecommends: { type: "boolean", default: true }, }, }, theme: { diff --git a/package-lock.json b/package-lock.json index 08cf16a1..bffdd153 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "@vercel/analytics": "^1.5.0", "@vercel/functions": "^2.2.12", "@vercel/sdk": "^1.11.4", + "@yornaath/batshit": "^0.14.0", "babel-plugin-react-compiler": "^19.1.0-rc.1", "base64-js": "^1.5.1", "colorjs.io": "^0.5.2", @@ -8661,6 +8662,21 @@ } } }, + "node_modules/@yornaath/batshit": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@yornaath/batshit/-/batshit-0.14.0.tgz", + "integrity": "sha512-0I+xMi5JoRs3+qVXXhk2AmsEl43MwrG+L+VW+nqw/qQqMFtgRPszLaxhJCfsBKnjfJ0gJzTI1Q9Q9+y903HyHQ==", + "license": "MIT", + "dependencies": { + "@yornaath/batshit-devtools": "^1.7.1" + } + }, + "node_modules/@yornaath/batshit-devtools": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@yornaath/batshit-devtools/-/batshit-devtools-1.7.1.tgz", + "integrity": "sha512-AyttV1Njj5ug+XqEWY1smV45dTWMlWKtj1B8jcFYgBKUFyUlF/qEhD+iP1E5UaRYW6hQRYD9T2WNDwFTrOMWzQ==", + "license": "MIT" + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", diff --git a/package.json b/package.json index 8c589ee7..5e9e2ee4 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@vercel/analytics": "^1.5.0", "@vercel/functions": "^2.2.12", "@vercel/sdk": "^1.11.4", + "@yornaath/batshit": "^0.14.0", "babel-plugin-react-compiler": "^19.1.0-rc.1", "base64-js": "^1.5.1", "colorjs.io": "^0.5.2", -- 2.51.2 From 8ae93e160e0a14b929337d19620e98f59992bde4 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 8 Feb 2026 05:09:42 +0400 Subject: [PATCH 2/7] add oauth scopes --- app/api/oauth/[route]/oauth-metadata.ts | 3 ++- app/api/oauth/[route]/route.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/api/oauth/[route]/oauth-metadata.ts b/app/api/oauth/[route]/oauth-metadata.ts index 8375f993..855fc0bc 100644 --- a/app/api/oauth/[route]/oauth-metadata.ts +++ b/app/api/oauth/[route]/oauth-metadata.ts @@ -7,7 +7,8 @@ const hostname = ? "http://localhost:3000" : "https://leaflet.pub"; -const scope = "atproto transition:generic transition:email"; +const scope = + "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts"; 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 db2ab8f4..3d2c7641 100644 --- a/app/api/oauth/[route]/route.ts +++ b/app/api/oauth/[route]/route.ts @@ -42,7 +42,8 @@ export async function GET( const ac = new AbortController(); const url = await client.authorize(handle || "https://bsky.social", { - scope: "atproto transition:generic transition:email", + scope: + "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts", signal: ac.signal, state: JSON.stringify(state), }); -- 2.51.2 From dbf7cf24e7b43fd2541d024c7c92848929e15f02 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 8 Feb 2026 10:07:24 +0400 Subject: [PATCH 3/7] add per post interactions --- actions/publishToPublication.ts | 21 + app/[leaflet_id]/Sidebar.tsx | 2 + app/[leaflet_id]/actions/PublishButton.tsx | 10 + app/[leaflet_id]/publish/PublishPost.tsx | 10 + app/api/rpc/[command]/pull.ts | 7 + .../[rkey]/DocumentPageRenderer.tsx | 3 +- .../[did]/[publication]/[rkey]/PostPages.tsx | 8 +- components/Canvas.tsx | 21 +- components/Pages/PublicationMetadata.tsx | 25 +- components/PostListing.tsx | 13 +- components/PostSettings.tsx | 96 +++++ lexicons/api/lexicons.ts | 17 + lexicons/api/types/pub/leaflet/document.ts | 1 + lexicons/api/types/pub/leaflet/publication.ts | 84 ++-- lexicons/api/types/site/standard/document.ts | 1 + .../api/types/site/standard/publication.ts | 62 ++- lexicons/pub/leaflet/document.json | 4 + lexicons/site/standard/document.json | 5 + lexicons/src/document.ts | 4 + lexicons/src/normalize.ts | 10 + package-lock.json | 397 +----------------- package.json | 1 - src/replicache/mutations.ts | 13 + src/utils/mergePreferences.ts | 24 ++ supabase/database.types.ts | 6 + ...260208000000_add_preferences_to_drafts.sql | 2 + 26 files changed, 356 insertions(+), 491 deletions(-) create mode 100644 components/PostSettings.tsx create mode 100644 src/utils/mergePreferences.ts create mode 100644 supabase/migrations/20260208000000_add_preferences_to_drafts.sql diff --git a/actions/publishToPublication.ts b/actions/publishToPublication.ts index db3c789e..a1a93592 100644 --- a/actions/publishToPublication.ts +++ b/actions/publishToPublication.ts @@ -78,6 +78,7 @@ export async function publishToPublication({ cover_image, entitiesToDelete, publishedAt, + postPreferences, }: { root_entity: string; publication_uri?: string; @@ -88,6 +89,11 @@ export async function publishToPublication({ cover_image?: string | null; entitiesToDelete?: string[]; publishedAt?: string; + postPreferences?: { + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + } | null; }): Promise { let identity = await getIdentityData(); if (!identity || !identity.atp_did) { @@ -175,6 +181,9 @@ export async function publishToPublication({ }; } + // Resolve preferences: explicit param > draft DB value + const preferences = postPreferences ?? draft?.preferences; + // Extract theme for standalone documents (not for publications) let theme: PubLeafletPublication.Theme | undefined; if (!publication_uri) { @@ -245,6 +254,12 @@ export async function publishToPublication({ ...(coverImageBlob && { coverImage: coverImageBlob }), // Include theme for standalone documents (not for publication documents) ...(!publication_uri && theme && { theme }), + ...(preferences && { + preferences: { + $type: "pub.leaflet.publication#preferences" as const, + ...preferences, + }, + }), content: { $type: "pub.leaflet.content" as const, pages: pagesArray, @@ -257,6 +272,12 @@ export async function publishToPublication({ author: credentialSession.did!, ...(publication_uri && { publication: publication_uri }), ...(theme && { theme }), + ...(preferences && { + preferences: { + $type: "pub.leaflet.publication#preferences" as const, + ...preferences, + }, + }), title: title || "Untitled", description: description || "", ...(tags !== undefined && { tags }), diff --git a/app/[leaflet_id]/Sidebar.tsx b/app/[leaflet_id]/Sidebar.tsx index 568ed74d..14ac86cc 100644 --- a/app/[leaflet_id]/Sidebar.tsx +++ b/app/[leaflet_id]/Sidebar.tsx @@ -8,6 +8,7 @@ import { useLeafletPublicationData } from "components/PageSWRDataProvider"; import { ShareOptions } from "app/[leaflet_id]/actions/ShareOptions"; import { ThemePopover } from "components/ThemeManager/ThemeSetter"; import { PublishButton } from "./actions/PublishButton"; +import { PostSettings } from "components/PostSettings"; import { Watermark } from "components/Watermark"; import { BackToPubButton } from "./actions/BackToPubButton"; import { useIdentityData } from "components/IdentityProvider"; @@ -30,6 +31,7 @@ export function LeafletSidebar() { +
diff --git a/app/[leaflet_id]/actions/PublishButton.tsx b/app/[leaflet_id]/actions/PublishButton.tsx index 0e6c4bc2..7ba596ef 100644 --- a/app/[leaflet_id]/actions/PublishButton.tsx +++ b/app/[leaflet_id]/actions/PublishButton.tsx @@ -96,6 +96,15 @@ const UpdateButton = () => { tx.get("publication_cover_image"), ); + // Get post preferences from Replicache state + let postPreferences = useSubscribe(rep, (tx) => + tx.get<{ + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + } | null>("post_preferences"), + ); + // Get local published at from Replicache (session-only state, not persisted to DB) let publishedAt = useLocalPublishedAt((s) => pub?.doc ? s[pub?.doc] : undefined, @@ -118,6 +127,7 @@ const UpdateButton = () => { tags: currentTags, cover_image: coverImage, publishedAt: publishedAt?.toISOString(), + postPreferences, }); setIsLoading(false); mutate(); diff --git a/app/[leaflet_id]/publish/PublishPost.tsx b/app/[leaflet_id]/publish/PublishPost.tsx index adf48c04..20c3e202 100644 --- a/app/[leaflet_id]/publish/PublishPost.tsx +++ b/app/[leaflet_id]/publish/PublishPost.tsx @@ -91,6 +91,15 @@ const PublishPostForm = ( tx.get("publication_cover_image"), ); + // Get post preferences from Replicache state + let postPreferences = useSubscribe(rep, (tx) => + tx.get<{ + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + } | null>("post_preferences"), + ); + // Use Replicache tags only when we have a draft const currentTags = props.hasDraft ? Array.isArray(replicacheTags) @@ -124,6 +133,7 @@ const PublishPostForm = ( cover_image: replicacheCoverImage, entitiesToDelete: props.entitiesToDelete, publishedAt: localPublishedAt?.toISOString() || new Date().toISOString(), + postPreferences, }); if (!result.success) { diff --git a/app/api/rpc/[command]/pull.ts b/app/api/rpc/[command]/pull.ts index ea3b3845..31ae3ac6 100644 --- a/app/api/rpc/[command]/pull.ts +++ b/app/api/rpc/[command]/pull.ts @@ -9,6 +9,7 @@ import { FactWithIndexes } from "src/replicache/utils"; import type { Attribute } from "src/replicache/attributes"; import { makeRoute } from "../lib"; import type { Env } from "./route"; +import type { Json } from "supabase/database.types"; // First define the sub-types for V0 and V1 requests const pullRequestV0 = z.object({ @@ -75,6 +76,7 @@ export const pull = makeRoute({ title: string; tags: string[]; cover_image: string | null; + preferences: Json | null; }[]; let pub_patch = publication_data?.[0] ? [ @@ -98,6 +100,11 @@ export const pull = makeRoute({ key: "publication_cover_image", value: publication_data[0].cover_image || null, }, + { + op: "put", + key: "post_preferences", + value: publication_data[0].preferences || null, + }, ] : []; diff --git a/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx b/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx index bf57101e..7c9a5d55 100644 --- a/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx +++ b/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx @@ -21,6 +21,7 @@ import { } from "src/utils/normalizeRecords"; import { DocumentProvider } from "contexts/DocumentContext"; import { LeafletContentProvider } from "contexts/LeafletContentContext"; +import { mergePreferences } from "src/utils/mergePreferences"; export async function DocumentPageRenderer({ did, @@ -133,7 +134,7 @@ export async function DocumentPageRenderer({ { let { data: pub, normalizedPublication } = useLeafletPublicationData(); + let { rep } = useReplicache(); + let postPreferences = useSubscribe(rep, (tx) => + tx.get<{ + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + } | null>("post_preferences"), + ); if (!pub || !pub.publications) return null; if (!normalizedPublication) return null; - let showComments = normalizedPublication.preferences?.showComments !== false; - let showMentions = normalizedPublication.preferences?.showMentions !== false; - let showRecommends = - normalizedPublication.preferences?.showRecommends !== false; + let merged = mergePreferences( + postPreferences || undefined, + normalizedPublication.preferences, + ); + let showComments = merged.showComments !== false; + let showMentions = merged.showMentions !== false; + let showRecommends = merged.showRecommends !== false; return (
diff --git a/components/Pages/PublicationMetadata.tsx b/components/Pages/PublicationMetadata.tsx index 2caa7d15..27803b9b 100644 --- a/components/Pages/PublicationMetadata.tsx +++ b/components/Pages/PublicationMetadata.tsx @@ -21,6 +21,7 @@ import { useIdentityData } from "components/IdentityProvider"; import { PostHeaderLayout } from "app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader"; import { Backdater } from "./Backdater"; import { RecommendTinyEmpty } from "components/Icons/RecommendTiny"; +import { mergePreferences } from "src/utils/mergePreferences"; export const PublicationMetadata = (props: { noInteractions?: boolean }) => { let { rep } = useReplicache(); @@ -34,6 +35,17 @@ export const PublicationMetadata = (props: { noInteractions?: boolean }) => { let description = useSubscribe(rep, (tx) => tx.get("publication_description"), ); + let postPreferences = useSubscribe(rep, (tx) => + tx.get<{ + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + } | null>("post_preferences"), + ); + let merged = mergePreferences( + postPreferences || undefined, + normalizedPublication?.preferences, + ); let publishedAt = normalizedDocument?.publishedAt; if (!pub) return null; @@ -121,28 +133,27 @@ export const PublicationMetadata = (props: { noInteractions?: boolean }) => { )} {!props.noInteractions && (
- {normalizedPublication?.preferences?.showRecommends !== false && ( + {merged.showRecommends !== false && (
—
)} - {normalizedPublication?.preferences?.showMentions !== false && ( + {merged.showMentions !== false && (
—
)} - {normalizedPublication?.preferences?.showComments !== false && ( + {merged.showComments !== false && (
—
)} {tags && ( <> - {normalizedPublication?.preferences?.showRecommends !== - false || - normalizedPublication?.preferences?.showMentions !== false || - normalizedPublication?.preferences?.showComments !== false ? ( + {merged.showRecommends !== false || + merged.showMentions !== false || + merged.showComments !== false ? ( ) : null} diff --git a/components/PostListing.tsx b/components/PostListing.tsx index ef6105a4..32bd9bd4 100644 --- a/components/PostListing.tsx +++ b/components/PostListing.tsx @@ -17,6 +17,7 @@ import type { Post } from "app/(home-pages)/reader/getReaderFeed"; import Link from "next/link"; import { InteractionPreview } from "./InteractionsPreview"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; +import { mergePreferences } from "src/utils/mergePreferences"; export const PostListing = (props: Post) => { let pubRecord = props.publication?.pubRecord as @@ -48,9 +49,11 @@ export const PostListing = (props: Post) => { ? pubRecord?.theme?.showPageBackground : postRecord.theme?.showPageBackground ?? true; + let mergedPrefs = mergePreferences(postRecord?.preferences, pubRecord?.preferences); + let quotes = props.documents.document_mentions_in_bsky?.[0]?.count || 0; let comments = - pubRecord?.preferences?.showComments === false + mergedPrefs.showComments === false ? 0 : props.documents.comments_on_documents?.[0]?.count || 0; let recommends = props.documents.recommends_on_documents?.[0]?.count || 0; @@ -109,11 +112,9 @@ export const PostListing = (props: Post) => { recommendsCount={recommends} documentUri={props.documents.uri} tags={tags} - showComments={pubRecord?.preferences?.showComments !== false} - showMentions={pubRecord?.preferences?.showMentions !== false} - showRecommends={ - pubRecord?.preferences?.showRecommends !== false - } + showComments={mergedPrefs.showComments !== false} + showMentions={mergedPrefs.showMentions !== false} + showRecommends={mergedPrefs.showRecommends !== false} share />
diff --git a/components/PostSettings.tsx b/components/PostSettings.tsx new file mode 100644 index 00000000..d8a92a0b --- /dev/null +++ b/components/PostSettings.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { ActionButton } from "components/ActionBar/ActionButton"; +import { SettingsSmall } from "components/Icons/SettingsSmall"; +import { Toggle } from "components/Toggle"; +import { Popover } from "components/Popover"; +import { useLeafletPublicationData } from "components/PageSWRDataProvider"; +import { useReplicache } from "src/replicache"; +import { useSubscribe } from "src/replicache/useSubscribe"; + +type PostPreferences = { + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; +}; + +export function PostSettings() { + let { data: pub, normalizedPublication } = useLeafletPublicationData(); + let { rep } = useReplicache(); + + let postPreferences = useSubscribe(rep, (tx) => + tx.get("post_preferences"), + ); + + if (!pub || !pub.publications) return null; + + let pubPrefs = normalizedPublication?.preferences; + + let showComments = + postPreferences?.showComments ?? pubPrefs?.showComments ?? true; + let showMentions = + postPreferences?.showMentions ?? pubPrefs?.showMentions ?? true; + let showRecommends = + postPreferences?.showRecommends ?? pubPrefs?.showRecommends ?? true; + + const updatePreference = ( + field: keyof PostPreferences, + value: boolean, + ) => { + let current: PostPreferences = postPreferences || {}; + rep?.mutate.updatePublicationDraft({ + preferences: { ...current, [field]: value }, + }); + }; + + return ( + } + label="Settings" + /> + } + > +
+
+ This Post Settings +
+
+ updatePreference("showComments", !showComments)} + > +
Show Comments
+
+ updatePreference("showMentions", !showMentions)} + > +
+
Show Mentions
+
+ Display a list Bluesky mentions about your post +
+
+
+ updatePreference("showRecommends", !showRecommends)} + > +
+
Show Recommends
+
+ Allow readers to recommend/like your post +
+
+
+
+
+
+ ); +} diff --git a/lexicons/api/lexicons.ts b/lexicons/api/lexicons.ts index a197ea95..a4b2a81f 100644 --- a/lexicons/api/lexicons.ts +++ b/lexicons/api/lexicons.ts @@ -1469,6 +1469,10 @@ export const schemaDict = { type: 'ref', ref: 'lex:pub.leaflet.publication#theme', }, + preferences: { + type: 'ref', + ref: 'lex:pub.leaflet.publication#preferences', + }, tags: { type: 'array', items: { @@ -1868,6 +1872,10 @@ export const schemaDict = { type: 'boolean', default: true, }, + showRecommends: { + type: 'boolean', + default: true, + }, }, }, theme: { @@ -2194,6 +2202,11 @@ export const schemaDict = { maxLength: 5000, type: 'string', }, + preferences: { + type: 'union', + refs: ['lex:pub.leaflet.publication#preferences'], + closed: false, + }, updatedAt: { format: 'datetime', type: 'string', @@ -2290,6 +2303,10 @@ export const schemaDict = { default: false, type: 'boolean', }, + showRecommends: { + default: true, + type: 'boolean', + }, }, type: 'object', }, diff --git a/lexicons/api/types/pub/leaflet/document.ts b/lexicons/api/types/pub/leaflet/document.ts index 6f6805da..ff587ec7 100644 --- a/lexicons/api/types/pub/leaflet/document.ts +++ b/lexicons/api/types/pub/leaflet/document.ts @@ -23,6 +23,7 @@ export interface Record { publication?: string author: string theme?: PubLeafletPublication.Theme + preferences?: PubLeafletPublication.Preferences tags?: string[] coverImage?: BlobRef pages: ( diff --git a/lexicons/api/types/pub/leaflet/publication.ts b/lexicons/api/types/pub/leaflet/publication.ts index 51143ee0..a2aaee34 100644 --- a/lexicons/api/types/pub/leaflet/publication.ts +++ b/lexicons/api/types/pub/leaflet/publication.ts @@ -1,94 +1,90 @@ /** * 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"; -import type * as PubLeafletThemeColor from "./theme/color"; -import type * as PubLeafletThemeBackgroundImage from "./theme/backgroundImage"; +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' +import type * as PubLeafletThemeColor from './theme/color' +import type * as PubLeafletThemeBackgroundImage from './theme/backgroundImage' const is$typed = _is$typed, - validate = _validate; -const id = "pub.leaflet.publication"; + validate = _validate +const id = 'pub.leaflet.publication' export interface Record { - $type: "pub.leaflet.publication"; - name: string; - base_path?: string; - description?: string; - icon?: BlobRef; - theme?: Theme; - preferences?: Preferences; - [k: string]: unknown; + $type: 'pub.leaflet.publication' + name: string + base_path?: string + description?: string + icon?: BlobRef + theme?: Theme + preferences?: Preferences + [k: string]: unknown } -const hashRecord = "main"; +const hashRecord = 'main' export function isRecord(v: V) { - return is$typed(v, id, hashRecord); + return is$typed(v, id, hashRecord) } export function validateRecord(v: V) { - return validate(v, id, hashRecord, true); + return validate(v, id, hashRecord, true) } export interface Preferences { - $type?: "pub.leaflet.publication#preferences"; - showInDiscover: boolean; - showComments: boolean; - showMentions: boolean; - showPrevNext: boolean; - showRecommends: boolean; + $type?: 'pub.leaflet.publication#preferences' + showInDiscover: boolean + showComments: boolean + showMentions: boolean + showPrevNext: boolean + showRecommends: boolean } -const hashPreferences = "preferences"; +const hashPreferences = 'preferences' export function isPreferences(v: V) { - return is$typed(v, id, hashPreferences); + return is$typed(v, id, hashPreferences) } export function validatePreferences(v: V) { - return validate(v, id, hashPreferences); + return validate(v, id, hashPreferences) } export interface Theme { - $type?: "pub.leaflet.publication#theme"; + $type?: 'pub.leaflet.publication#theme' backgroundColor?: | $Typed | $Typed - | { $type: string }; - backgroundImage?: PubLeafletThemeBackgroundImage.Main; - pageWidth?: number; + | { $type: string } + backgroundImage?: PubLeafletThemeBackgroundImage.Main + pageWidth?: number primary?: | $Typed | $Typed - | { $type: string }; + | { $type: string } pageBackground?: | $Typed | $Typed - | { $type: string }; - showPageBackground: boolean; + | { $type: string } + showPageBackground: boolean accentBackground?: | $Typed | $Typed - | { $type: string }; + | { $type: string } accentText?: | $Typed | $Typed - | { $type: string }; + | { $type: string } } -const hashTheme = "theme"; +const hashTheme = 'theme' export function isTheme(v: V) { - return is$typed(v, id, hashTheme); + return is$typed(v, id, hashTheme) } export function validateTheme(v: V) { - return validate(v, id, hashTheme); + return validate(v, id, hashTheme) } diff --git a/lexicons/api/types/site/standard/document.ts b/lexicons/api/types/site/standard/document.ts index dccb1dfe..b6ff4fc0 100644 --- a/lexicons/api/types/site/standard/document.ts +++ b/lexicons/api/types/site/standard/document.ts @@ -28,6 +28,7 @@ export interface Record { textContent?: string theme?: PubLeafletPublication.Theme title: string + preferences?: $Typed | { $type: string } updatedAt?: string [k: string]: unknown } diff --git a/lexicons/api/types/site/standard/publication.ts b/lexicons/api/types/site/standard/publication.ts index 08f80d9c..42211459 100644 --- a/lexicons/api/types/site/standard/publication.ts +++ b/lexicons/api/types/site/standard/publication.ts @@ -1,58 +1,54 @@ /** * 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"; -import type * as SiteStandardThemeBasic from "./theme/basic"; -import type * as PubLeafletPublication from "../../pub/leaflet/publication"; +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' +import type * as SiteStandardThemeBasic from './theme/basic' +import type * as PubLeafletPublication from '../../pub/leaflet/publication' const is$typed = _is$typed, - validate = _validate; -const id = "site.standard.publication"; + validate = _validate +const id = 'site.standard.publication' export interface Record { - $type: "site.standard.publication"; - basicTheme?: SiteStandardThemeBasic.Main; - theme?: $Typed | { $type: string }; - description?: string; - icon?: BlobRef; - name: string; - preferences?: Preferences; - url: string; - [k: string]: unknown; + $type: 'site.standard.publication' + basicTheme?: SiteStandardThemeBasic.Main + theme?: $Typed | { $type: string } + description?: string + icon?: BlobRef + name: string + preferences?: Preferences + url: string + [k: string]: unknown } -const hashRecord = "main"; +const hashRecord = 'main' export function isRecord(v: V) { - return is$typed(v, id, hashRecord); + return is$typed(v, id, hashRecord) } export function validateRecord(v: V) { - return validate(v, id, hashRecord, true); + return validate(v, id, hashRecord, true) } export interface Preferences { - $type?: "site.standard.publication#preferences"; - showInDiscover: boolean; - showComments: boolean; - showMentions: boolean; - showPrevNext: boolean; - showRecommends: boolean; + $type?: 'site.standard.publication#preferences' + showInDiscover: boolean + showComments: boolean + showMentions: boolean + showPrevNext: boolean + showRecommends: boolean } -const hashPreferences = "preferences"; +const hashPreferences = 'preferences' export function isPreferences(v: V) { - return is$typed(v, id, hashPreferences); + return is$typed(v, id, hashPreferences) } export function validatePreferences(v: V) { - return validate(v, id, hashPreferences); + return validate(v, id, hashPreferences) } diff --git a/lexicons/pub/leaflet/document.json b/lexicons/pub/leaflet/document.json index 2e714226..7ab00088 100644 --- a/lexicons/pub/leaflet/document.json +++ b/lexicons/pub/leaflet/document.json @@ -46,6 +46,10 @@ "type": "ref", "ref": "pub.leaflet.publication#theme" }, + "preferences": { + "type": "ref", + "ref": "pub.leaflet.publication#preferences" + }, "tags": { "type": "array", "items": { diff --git a/lexicons/site/standard/document.json b/lexicons/site/standard/document.json index 433f19dc..8305dd47 100644 --- a/lexicons/site/standard/document.json +++ b/lexicons/site/standard/document.json @@ -57,6 +57,11 @@ "maxLength": 5000, "type": "string" }, + "preferences": { + "type": "union", + "refs": ["pub.leaflet.publication#preferences"], + "closed": false + }, "updatedAt": { "format": "datetime", "type": "string" diff --git a/lexicons/src/document.ts b/lexicons/src/document.ts index f1beabff..15e3f00b 100644 --- a/lexicons/src/document.ts +++ b/lexicons/src/document.ts @@ -23,6 +23,10 @@ export const PubLeafletDocument: LexiconDoc = { publication: { type: "string", format: "at-uri" }, author: { type: "string", format: "at-identifier" }, theme: { type: "ref", ref: "pub.leaflet.publication#theme" }, + preferences: { + type: "ref", + ref: "pub.leaflet.publication#preferences", + }, tags: { type: "array", items: { type: "string", maxLength: 50 } }, coverImage: { type: "blob", diff --git a/lexicons/src/normalize.ts b/lexicons/src/normalize.ts index 4f7294c7..9e8755ec 100644 --- a/lexicons/src/normalize.ts +++ b/lexicons/src/normalize.ts @@ -28,6 +28,7 @@ import { AtUri } from "@atproto/syntax"; export type NormalizedDocument = SiteStandardDocument.Record & { // Keep the original theme for components that need leaflet-specific styling theme?: PubLeafletPublication.Theme; + preferences?: SiteStandardPublication.Preferences; }; // Normalized publication type - uses the generated site.standard.publication type @@ -169,9 +170,13 @@ export function normalizeDocument( // Pass through site.standard records directly (theme is already in correct format if present) if (isStandardDocument(record)) { + const preferences = record.preferences as + | SiteStandardPublication.Preferences + | undefined; return { ...record, theme: record.theme, + preferences, } as NormalizedDocument; } @@ -198,6 +203,10 @@ export function normalizeDocument( } : undefined; + // Extract preferences if present (available after lexicon rebuild) + const leafletPrefs = (record as Record) + .preferences as SiteStandardPublication.Preferences | undefined; + return { $type: "site.standard.document", title: record.title, @@ -210,6 +219,7 @@ export function normalizeDocument( bskyPostRef: record.postRef, content, theme: record.theme, + preferences: leafletPrefs, }; } diff --git a/package-lock.json b/package-lock.json index bffdd153..9fb9d0cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "@atproto/oauth-client-node": "^0.3.8", "@atproto/sync": "^0.1.34", "@atproto/syntax": "^0.3.3", - "@atproto/tap": "^0.1.1", "@atproto/xrpc": "^0.7.5", "@atproto/xrpc-server": "^0.9.5", "@hono/node-server": "^1.14.3", @@ -396,92 +395,6 @@ "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", "license": "(Apache-2.0 AND MIT)" }, - "node_modules/@atproto/lex": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@atproto/lex/-/lex-0.0.9.tgz", - "integrity": "sha512-o6gauf1lz0iyzJR0rqSj4VHOrO+Nt8+/iPb0KPojw1ieXk13zOSTSxotAoDzO/dP6y8Ey5jxwuCQGuzab/4XnQ==", - "license": "MIT", - "dependencies": { - "@atproto/lex-builder": "0.0.9", - "@atproto/lex-client": "0.0.7", - "@atproto/lex-data": "0.0.6", - "@atproto/lex-installer": "0.0.9", - "@atproto/lex-json": "0.0.6", - "@atproto/lex-schema": "0.0.7", - "tslib": "^2.8.1", - "yargs": "^17.0.0" - }, - "bin": { - "lex": "bin/lex", - "ts-lex": "bin/lex" - } - }, - "node_modules/@atproto/lex-builder": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@atproto/lex-builder/-/lex-builder-0.0.9.tgz", - "integrity": "sha512-buOFk1JpuW3twI7To7f/67zQQ1NulLHf/oasH/kTOPUAd0dNyeAa13t9eRSVGbwi0BcZYxRxBm0QzPmdLKyuyw==", - "license": "MIT", - "dependencies": { - "@atproto/lex-document": "0.0.8", - "@atproto/lex-schema": "0.0.7", - "prettier": "^3.2.5", - "ts-morph": "^27.0.0", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-builder/node_modules/@ts-morph/common": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.28.1.tgz", - "integrity": "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==", - "license": "MIT", - "dependencies": { - "minimatch": "^10.0.1", - "path-browserify": "^1.0.1", - "tinyglobby": "^0.2.14" - } - }, - "node_modules/@atproto/lex-builder/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@atproto/lex-builder/node_modules/ts-morph": { - "version": "27.0.2", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-27.0.2.tgz", - "integrity": "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==", - "license": "MIT", - "dependencies": { - "@ts-morph/common": "~0.28.1", - "code-block-writer": "^13.0.3" - } - }, - "node_modules/@atproto/lex-cbor": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.6.tgz", - "integrity": "sha512-lee2T00owDy3I1plRHuURT6f98NIpYZZr2wXa5pJZz5JzefZ+nv8gJ2V70C2f+jmSG+5S9NTIy4uJw94vaHf4A==", - "license": "MIT", - "dependencies": { - "@atproto/lex-data": "0.0.6", - "multiformats": "^9.9.0", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-cbor/node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", - "license": "(Apache-2.0 AND MIT)" - }, "node_modules/@atproto/lex-cli": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@atproto/lex-cli/-/lex-cli-0.9.5.tgz", @@ -515,149 +428,6 @@ "tslib": "^2.8.1" } }, - "node_modules/@atproto/lex-client": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@atproto/lex-client/-/lex-client-0.0.7.tgz", - "integrity": "sha512-ofUz3yXJ0nN/M9aqqF2ZUL/4D1wWT1P4popCfV3OEDsDrtWofMflYPFz1IWuyPa2e83paaEHRhaw3bZEhgXH1w==", - "license": "MIT", - "dependencies": { - "@atproto/lex-data": "0.0.6", - "@atproto/lex-json": "0.0.6", - "@atproto/lex-schema": "0.0.7", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-data": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.6.tgz", - "integrity": "sha512-MBNB4ghRJQzuXK1zlUPljpPbQcF1LZ5dzxy274KqPt4p3uPuRw0mHjgcCoWzRUNBQC685WMQR4IN9DHtsnG57A==", - "license": "MIT", - "dependencies": { - "@atproto/syntax": "0.4.2", - "multiformats": "^9.9.0", - "tslib": "^2.8.1", - "uint8arrays": "3.0.0", - "unicode-segmenter": "^0.14.0" - } - }, - "node_modules/@atproto/lex-data/node_modules/@atproto/syntax": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", - "license": "MIT" - }, - "node_modules/@atproto/lex-data/node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", - "license": "(Apache-2.0 AND MIT)" - }, - "node_modules/@atproto/lex-document": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@atproto/lex-document/-/lex-document-0.0.8.tgz", - "integrity": "sha512-p3l5h96Hx0vxUwbO/eas6x5h2vU0JVN1a/ktX4k3PlK9YLXfWMFsv+RdVwVZom8o0irHwlcyh1D/cY0PyUojDA==", - "license": "MIT", - "dependencies": { - "@atproto/lex-schema": "0.0.7", - "core-js": "^3", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-installer": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@atproto/lex-installer/-/lex-installer-0.0.9.tgz", - "integrity": "sha512-zEeIeSaSCb3j+zNsqqMY7+X5FO6fxy/MafaCEj42KsXQHNcobuygZsnG/0fxMj/kMvhjrNUCp/w9PyOMwx4hQg==", - "license": "MIT", - "dependencies": { - "@atproto/lex-builder": "0.0.9", - "@atproto/lex-cbor": "0.0.6", - "@atproto/lex-data": "0.0.6", - "@atproto/lex-document": "0.0.8", - "@atproto/lex-resolver": "0.0.8", - "@atproto/lex-schema": "0.0.7", - "@atproto/syntax": "0.4.2", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-installer/node_modules/@atproto/syntax": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", - "license": "MIT" - }, - "node_modules/@atproto/lex-json": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.6.tgz", - "integrity": "sha512-EILnN5cditPvf+PCNjXt7reMuzjugxAL1fpSzmzJbEMGMUwxOf5pPWxRsaA/M3Boip4NQZ+6DVrPOGUMlnqceg==", - "license": "MIT", - "dependencies": { - "@atproto/lex-data": "0.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-resolver": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@atproto/lex-resolver/-/lex-resolver-0.0.8.tgz", - "integrity": "sha512-4hXT560+k5BIttouuhXOr+UkhAuFvvkJaVdqYb8vx2Ez7eHPiZ+yWkUK6FKpyGsx2whHkJzgleEA6DNWtdDlWA==", - "license": "MIT", - "dependencies": { - "@atproto-labs/did-resolver": "0.2.5", - "@atproto/crypto": "0.4.5", - "@atproto/lex-client": "0.0.7", - "@atproto/lex-data": "0.0.6", - "@atproto/lex-document": "0.0.8", - "@atproto/lex-schema": "0.0.7", - "@atproto/repo": "0.8.12", - "@atproto/syntax": "0.4.2", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-resolver/node_modules/@atproto-labs/did-resolver": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.2.5.tgz", - "integrity": "sha512-he7EC6OMSifNs01a4RT9mta/yYitoKDzlK9ty2TFV5Uj/+HpB4vYMRdIDFrRW0Hcsehy90E2t/dw0t7361MEKQ==", - "license": "MIT", - "dependencies": { - "@atproto-labs/fetch": "0.2.3", - "@atproto-labs/pipe": "0.1.1", - "@atproto-labs/simple-store": "0.3.0", - "@atproto-labs/simple-store-memory": "0.1.4", - "@atproto/did": "0.2.4", - "zod": "^3.23.8" - } - }, - "node_modules/@atproto/lex-resolver/node_modules/@atproto/did": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.2.4.tgz", - "integrity": "sha512-nxNiCgXeo7pfjojq9fpfZxCO0X0xUipNVKW+AHNZwQKiUDt6zYL0VXEfm8HBUwQOCmKvj2pRRSM1Cur+tUWk3g==", - "license": "MIT", - "dependencies": { - "zod": "^3.23.8" - } - }, - "node_modules/@atproto/lex-resolver/node_modules/@atproto/syntax": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", - "license": "MIT" - }, - "node_modules/@atproto/lex-schema": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@atproto/lex-schema/-/lex-schema-0.0.7.tgz", - "integrity": "sha512-/7HkTUsnP1rlzmVE6nnY0kl/hydL/W8V29V8BhFwdAvdDKpYcdRgzzsMe38LAt+ZOjHknRCZDIKGsbQMSbJErw==", - "license": "MIT", - "dependencies": { - "@atproto/lex-data": "0.0.6", - "@atproto/syntax": "0.4.2", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-schema/node_modules/@atproto/syntax": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", - "license": "MIT" - }, "node_modules/@atproto/lexicon": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.1.tgz", @@ -848,135 +618,6 @@ "integrity": "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==", "license": "MIT" }, - "node_modules/@atproto/tap": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@atproto/tap/-/tap-0.1.1.tgz", - "integrity": "sha512-gW4NzLOxj74TzaDOVzzzt5kl2PdC0r75XkIpYpI5xobwCfsc/DmVtwpuSw1fW9gr4Vzk2Q90S9UE4ifAFl2gyA==", - "license": "MIT", - "dependencies": { - "@atproto/common": "^0.5.6", - "@atproto/lex": "^0.0.9", - "@atproto/syntax": "^0.4.2", - "@atproto/ws-client": "^0.0.4", - "ws": "^8.12.0", - "zod": "^3.23.8" - }, - "engines": { - "node": ">=18.7.0" - } - }, - "node_modules/@atproto/tap/node_modules/@atproto/common": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.10.tgz", - "integrity": "sha512-A1+4W3JmjZIgmtJFLJBAaoVruZhRL0ANtyjZ91aJR4rjHcZuaQ+v4IFR1UcE6yyTATacLdBk6ADy8OtxXzq14g==", - "license": "MIT", - "dependencies": { - "@atproto/common-web": "^0.4.15", - "@atproto/lex-cbor": "^0.0.10", - "@atproto/lex-data": "^0.0.10", - "iso-datestring-validator": "^2.2.2", - "multiformats": "^9.9.0", - "pino": "^8.21.0" - }, - "engines": { - "node": ">=18.7.0" - } - }, - "node_modules/@atproto/tap/node_modules/@atproto/lex-cbor": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.10.tgz", - "integrity": "sha512-5RtV90iIhRNCXXvvETd3KlraV8XGAAAgOmiszUb+l8GySDU/sGk7AlVvArFfXnj/S/GXJq8DP6IaUxCw/sPASA==", - "license": "MIT", - "dependencies": { - "@atproto/lex-data": "^0.0.10", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/tap/node_modules/@atproto/lex-data": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.10.tgz", - "integrity": "sha512-FDbcy8VIUVzS9Mi1F8SMxbkL/jOUmRRpqbeM1xB4A0fMxeZJTxf6naAbFt4gYF3quu/+TPJGmio6/7cav05FqQ==", - "license": "MIT", - "dependencies": { - "multiformats": "^9.9.0", - "tslib": "^2.8.1", - "uint8arrays": "3.0.0", - "unicode-segmenter": "^0.14.0" - } - }, - "node_modules/@atproto/tap/node_modules/@atproto/syntax": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", - "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/tap/node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", - "license": "(Apache-2.0 AND MIT)" - }, - "node_modules/@atproto/ws-client": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@atproto/ws-client/-/ws-client-0.0.4.tgz", - "integrity": "sha512-dox1XIymuC7/ZRhUqKezIGgooZS45C6vHCfu0PnWjfvsLCK2kAlnvX4IBkA/WpcoijDhQ9ejChnFbo/sLmgvAg==", - "license": "MIT", - "dependencies": { - "@atproto/common": "^0.5.3", - "ws": "^8.12.0" - }, - "engines": { - "node": ">=18.7.0" - } - }, - "node_modules/@atproto/ws-client/node_modules/@atproto/common": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.10.tgz", - "integrity": "sha512-A1+4W3JmjZIgmtJFLJBAaoVruZhRL0ANtyjZ91aJR4rjHcZuaQ+v4IFR1UcE6yyTATacLdBk6ADy8OtxXzq14g==", - "license": "MIT", - "dependencies": { - "@atproto/common-web": "^0.4.15", - "@atproto/lex-cbor": "^0.0.10", - "@atproto/lex-data": "^0.0.10", - "iso-datestring-validator": "^2.2.2", - "multiformats": "^9.9.0", - "pino": "^8.21.0" - }, - "engines": { - "node": ">=18.7.0" - } - }, - "node_modules/@atproto/ws-client/node_modules/@atproto/lex-cbor": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.10.tgz", - "integrity": "sha512-5RtV90iIhRNCXXvvETd3KlraV8XGAAAgOmiszUb+l8GySDU/sGk7AlVvArFfXnj/S/GXJq8DP6IaUxCw/sPASA==", - "license": "MIT", - "dependencies": { - "@atproto/lex-data": "^0.0.10", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/ws-client/node_modules/@atproto/lex-data": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.10.tgz", - "integrity": "sha512-FDbcy8VIUVzS9Mi1F8SMxbkL/jOUmRRpqbeM1xB4A0fMxeZJTxf6naAbFt4gYF3quu/+TPJGmio6/7cav05FqQ==", - "license": "MIT", - "dependencies": { - "multiformats": "^9.9.0", - "tslib": "^2.8.1", - "uint8arrays": "3.0.0", - "unicode-segmenter": "^0.14.0" - } - }, - "node_modules/@atproto/ws-client/node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", - "license": "(Apache-2.0 AND MIT)" - }, "node_modules/@atproto/xrpc": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.5.tgz", @@ -3052,27 +2693,6 @@ "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", "license": "(Apache-2.0 AND MIT)" }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -9631,6 +9251,7 @@ "version": "13.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, "license": "MIT" }, "node_modules/collapse-white-space": { @@ -9739,17 +9360,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, - "node_modules/core-js": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", - "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/crelt": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", @@ -16142,6 +15752,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, "license": "MIT" }, "node_modules/path-exists": { @@ -16424,6 +16035,7 @@ "version": "3.2.5", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -18520,6 +18132,7 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -18536,6 +18149,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -18553,6 +18167,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 5e9e2ee4..4f9c5397 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "@atproto/oauth-client-node": "^0.3.8", "@atproto/sync": "^0.1.34", "@atproto/syntax": "^0.3.3", - "@atproto/tap": "^0.1.1", "@atproto/xrpc": "^0.7.5", "@atproto/xrpc-server": "^0.9.5", "@hono/node-server": "^1.14.3", diff --git a/src/replicache/mutations.ts b/src/replicache/mutations.ts index d8bf93c6..40db9f73 100644 --- a/src/replicache/mutations.ts +++ b/src/replicache/mutations.ts @@ -659,6 +659,11 @@ const updatePublicationDraft: Mutation<{ tags?: string[]; cover_image?: string | null; localPublishedAt?: string | null; + preferences?: { + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + } | null; }> = async (args, ctx) => { await ctx.runOnServer(async (serverCtx) => { console.log("updating"); @@ -667,11 +672,17 @@ const updatePublicationDraft: Mutation<{ title?: string; tags?: string[]; cover_image?: string | null; + preferences?: { + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + } | null; } = {}; if (args.description !== undefined) updates.description = args.description; if (args.title !== undefined) updates.title = args.title; if (args.tags !== undefined) updates.tags = args.tags; if (args.cover_image !== undefined) updates.cover_image = args.cover_image; + if (args.preferences !== undefined) updates.preferences = args.preferences; if (Object.keys(updates).length > 0) { // First try to update leaflets_in_publications (for publications) @@ -700,6 +711,8 @@ const updatePublicationDraft: Mutation<{ await tx.set("publication_cover_image", args.cover_image); if (args.localPublishedAt !== undefined) await tx.set("publication_local_published_at", args.localPublishedAt); + if (args.preferences !== undefined) + await tx.set("post_preferences", args.preferences); }); }; diff --git a/src/utils/mergePreferences.ts b/src/utils/mergePreferences.ts new file mode 100644 index 00000000..c2e75a87 --- /dev/null +++ b/src/utils/mergePreferences.ts @@ -0,0 +1,24 @@ +type PreferencesInput = { + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + showPrevNext?: boolean; +} | null; + +export function mergePreferences( + documentPrefs?: PreferencesInput, + publicationPrefs?: PreferencesInput, +): { + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + showPrevNext?: boolean; +} { + return { + showComments: documentPrefs?.showComments ?? publicationPrefs?.showComments, + showMentions: documentPrefs?.showMentions ?? publicationPrefs?.showMentions, + showRecommends: + documentPrefs?.showRecommends ?? publicationPrefs?.showRecommends, + showPrevNext: publicationPrefs?.showPrevNext, + }; +} diff --git a/supabase/database.types.ts b/supabase/database.types.ts index edbfe86e..6153540b 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -589,6 +589,7 @@ export type Database = { description: string doc: string | null leaflet: string + preferences: Json | null publication: string tags: string[] | null title: string @@ -599,6 +600,7 @@ export type Database = { description?: string doc?: string | null leaflet: string + preferences?: Json | null publication: string tags?: string[] | null title?: string @@ -609,6 +611,7 @@ export type Database = { description?: string doc?: string | null leaflet?: string + preferences?: Json | null publication?: string tags?: string[] | null title?: string @@ -645,6 +648,7 @@ export type Database = { description: string document: string leaflet: string + preferences: Json | null tags: string[] | null title: string } @@ -655,6 +659,7 @@ export type Database = { description?: string document: string leaflet: string + preferences?: Json | null tags?: string[] | null title?: string } @@ -665,6 +670,7 @@ export type Database = { description?: string document?: string leaflet?: string + preferences?: Json | null tags?: string[] | null title?: string } diff --git a/supabase/migrations/20260208000000_add_preferences_to_drafts.sql b/supabase/migrations/20260208000000_add_preferences_to_drafts.sql new file mode 100644 index 00000000..e54b639a --- /dev/null +++ b/supabase/migrations/20260208000000_add_preferences_to_drafts.sql @@ -0,0 +1,2 @@ +ALTER TABLE leaflets_in_publications ADD COLUMN preferences jsonb; +ALTER TABLE leaflets_to_documents ADD COLUMN preferences jsonb; -- 2.51.2 From 04578602b038f2f1a9a5c885e1242056632676ea Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 8 Feb 2026 10:37:22 +0400 Subject: [PATCH 4/7] fix types --- lexicons/src/normalize.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lexicons/src/normalize.ts b/lexicons/src/normalize.ts index 9e8755ec..70ac45b9 100644 --- a/lexicons/src/normalize.ts +++ b/lexicons/src/normalize.ts @@ -219,7 +219,9 @@ export function normalizeDocument( bskyPostRef: record.postRef, content, theme: record.theme, - preferences: leafletPrefs, + preferences: leafletPrefs + ? { ...leafletPrefs, $type: "site.standard.publication#preferences" as const } + : undefined, }; } -- 2.51.2 From 71e47d3e8fada27ccc60eab1fefb9f904bd1a3ac Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 8 Feb 2026 11:21:25 +0400 Subject: [PATCH 5/7] request blob permissions --- 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 855fc0bc..32ed3b35 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"; + "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts 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 3d2c7641..108e032b 100644 --- a/app/api/oauth/[route]/route.ts +++ b/app/api/oauth/[route]/route.ts @@ -43,7 +43,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", + "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts blob:*/*", signal: ac.signal, state: JSON.stringify(state), }); -- 2.51.2 From 7451ae3aa9c984ed04883220af38376d7d071f08 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 8 Feb 2026 11:27:31 +0400 Subject: [PATCH 6/7] add post settings to footer --- app/[leaflet_id]/Footer.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/[leaflet_id]/Footer.tsx b/app/[leaflet_id]/Footer.tsx index d51cff0d..a202d2ee 100644 --- a/app/[leaflet_id]/Footer.tsx +++ b/app/[leaflet_id]/Footer.tsx @@ -14,6 +14,7 @@ import { useLeafletPublicationData } from "components/PageSWRDataProvider"; import { useIdentityData } from "components/IdentityProvider"; import { useEntity } from "src/replicache"; import { block } from "sharp"; +import { PostSettings } from "components/PostSettings"; export function hasBlockToolbar(blockType: string | null | undefined) { return ( @@ -64,6 +65,7 @@ export function LeafletFooter(props: { entityID: string }) { + ) : ( -- 2.51.2 From 42887e837060062581abca488709212b39c866ec Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 8 Feb 2026 02:45:45 -0500 Subject: [PATCH 7/7] Fix missing 'of' in Bluesky mentions copy (#265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the missing "of" between "list" and "Bluesky" in the "Show Mentions" toggle description across both PostSettings and PostOptions components. Changes: - components/PostSettings.tsx: "Display a list Bluesky mentions..." → "Display a list of Bluesky mentions..." - app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx: Same fix https://claude.ai/code/session_01UmMsAY9kbwJy4p1nD6sCh8 Co-authored-by: Claude --- app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx | 2 +- components/PostSettings.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx b/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx index 70ae3d75..07c02452 100644 --- a/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx +++ b/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx @@ -105,7 +105,7 @@ export const PostOptions = (props: {
Show Mentions
- Display a list Bluesky mentions about your post + Display a list of Bluesky mentions about your post
diff --git a/components/PostSettings.tsx b/components/PostSettings.tsx index d8a92a0b..a6ec7db8 100644 --- a/components/PostSettings.tsx +++ b/components/PostSettings.tsx @@ -74,7 +74,7 @@ export function PostSettings() {
Show Mentions
- Display a list Bluesky mentions about your post + Display a list of Bluesky mentions about your post
-- 2.51.2