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 && (
<>
+
+