From 75b0ea623ed10d5abd7e83b3e308815db22188e0 Mon Sep 17 00:00:00 2001 From: celine Date: Fri, 23 Jan 2026 15:54:58 -0500 Subject: [PATCH 01/20] just like a bunch of stuff sorry --- .../notifications/FollowNotification.tsx | 2 +- .../p/[didOrHandle]/ProfileHeader.tsx | 7 +- .../[rkey]/BlueskyQuotesPage.tsx | 24 +- .../[publication]/[rkey]/BskyPostContent.tsx | 160 +++++++------ .../[rkey]/Interactions/Comments/index.tsx | 2 +- .../[rkey]/Interactions/InteractionDrawer.tsx | 11 +- .../[rkey]/Interactions/Quotes.tsx | 226 ++++++++---------- .../[did]/[publication]/[rkey]/PostLinks.tsx | 1 - .../[did]/[publication]/[rkey]/ThreadPage.tsx | 202 +++++++++------- components/Avatar.tsx | 31 ++- .../Blocks/BlueskyPostBlock/BlueskyEmbed.tsx | 7 +- src/utils/timeAgo.ts | 24 +- 12 files changed, 374 insertions(+), 323 deletions(-) diff --git a/app/(home-pages)/notifications/FollowNotification.tsx b/app/(home-pages)/notifications/FollowNotification.tsx index dbdd5efd..a0d82043 100644 --- a/app/(home-pages)/notifications/FollowNotification.tsx +++ b/app/(home-pages)/notifications/FollowNotification.tsx @@ -23,7 +23,7 @@ export const FollowNotification = (props: HydratedSubscribeNotification) => { } + icon={} actionText={ <> {displayName} subscribed to {pubRecord?.name}! diff --git a/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx b/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx index be7d44da..3fb2b0fd 100644 --- a/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx +++ b/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx @@ -23,7 +23,7 @@ export const ProfileHeader = (props: { src={profileRecord.avatar} displayName={profileRecord.displayName} className="profileAvatar mx-auto mt-3 sm:mt-4" - giant + size="giant" /> ); @@ -100,7 +100,10 @@ const ProfileLinks = (props: { handle: string }) => { ); }; -const PublicationCard = (props: { record: NormalizedPublication; uri: string }) => { +const PublicationCard = (props: { + record: NormalizedPublication; + uri: string; +}) => { const { record, uri } = props; const { bgLeaflet, bgPage, primary } = usePubTheme(record.theme); diff --git a/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx b/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx index 299969a0..014664e5 100644 --- a/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx @@ -7,7 +7,12 @@ import { DotLoader } from "components/utils/DotLoader"; import { QuoteTiny } from "components/Icons/QuoteTiny"; import { openPage } from "./PostPages"; import { BskyPostContent } from "./BskyPostContent"; -import { QuotesLink, getQuotesKey, fetchQuotes, prefetchQuotes } from "./PostLinks"; +import { + QuotesLink, + getQuotesKey, + fetchQuotes, + prefetchQuotes, +} from "./PostLinks"; // Re-export for backwards compatibility export { QuotesLink, getQuotesKey, fetchQuotes, prefetchQuotes }; @@ -27,7 +32,9 @@ export function BlueskyQuotesPage(props: { data: quotesData, isLoading, error, - } = useSWR(postUri ? getQuotesKey(postUri) : null, () => fetchQuotes(postUri)); + } = useSWR(postUri ? getQuotesKey(postUri) : null, () => + fetchQuotes(postUri), + ); return ( {posts.map((post) => ( - + ))} ); } -function QuotePost(props: { - post: PostView; - quotesUri: string; -}) { +function QuotePost(props: { post: PostView; quotesUri: string }) { const { post, quotesUri } = props; const parent = { type: "quotes" as const, uri: quotesUri }; @@ -94,10 +94,8 @@ function QuotePost(props: { e.stopPropagation()} onEmbedClick={(e) => e.stopPropagation()} /> diff --git a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx index 579a9255..cb1e5d0b 100644 --- a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx @@ -1,8 +1,6 @@ "use client"; import { AppBskyFeedDefs, AppBskyFeedPost } from "@atproto/api"; -import { - BlueskyEmbed, -} from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; +import { BlueskyEmbed } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; import { BlueskyRichText } from "components/Blocks/BlueskyPostBlock/BlueskyRichText"; import { BlueskyTiny } from "components/Icons/BlueskyTiny"; import { CommentTiny } from "components/Icons/CommentTiny"; @@ -12,86 +10,104 @@ import { useLocalizedDate } from "src/hooks/useLocalizedDate"; import { useHasPageLoaded } from "components/InitialPageLoadProvider"; import { OpenPage } from "./PostPages"; import { ThreadLink, QuotesLink } from "./PostLinks"; +import { BlueskyLinkTiny } from "components/Icons/BlueskyLinkTiny"; +import { Avatar } from "components/Avatar"; +import { timeAgo } from "src/utils/timeAgo"; +import { ProfilePopover } from "components/ProfilePopover"; type PostView = AppBskyFeedDefs.PostView; export function BskyPostContent(props: { post: PostView; parent?: OpenPage; - linksEnabled?: boolean; - avatarSize?: "sm" | "md"; + avatarSize?: "tiny" | "medium" | "large" | "giant"; + className?: string; showEmbed?: boolean; showBlueskyLink?: boolean; onEmbedClick?: (e: React.MouseEvent) => void; - onLinkClick?: (e: React.MouseEvent) => void; + quoteCountOnClick?: (e: React.MouseEvent) => void; + replyCountOnClick?: (e: React.MouseEvent) => void; }) { const { post, parent, - linksEnabled = true, avatarSize = "md", showEmbed = true, showBlueskyLink = true, onEmbedClick, - onLinkClick, + quoteCountOnClick, + replyCountOnClick, } = props; const record = post.record as AppBskyFeedPost.Record; const postId = post.uri.split("/")[4]; const url = `https://bsky.app/profile/${post.author.handle}/post/${postId}`; - const avatarClass = avatarSize === "sm" ? "w-8 h-8" : "w-10 h-10"; - return ( <> -
- {post.author.avatar ? ( - {`${post.author.displayName}'s - ) : ( -
- )} -
+ -
-
-
- {post.author.displayName} +
+
+
+
+ {post.author.displayName} +
+ + @{post.author.handle} +
+ } + didOrHandle={post.author.handle} + /> +
+
+ {timeAgo(record.createdAt, { compact: true })}
- - @{post.author.handle} -
-
+
{showEmbed && post.embed && (
- +
)}
-
- +
+
+ {showBlueskyLink && ( + <> + + + + + )} +
@@ -101,61 +117,51 @@ export function BskyPostContent(props: { function PostCounts(props: { post: PostView; parent?: OpenPage; - linksEnabled: boolean; + quoteCountOnClick?: (e: React.MouseEvent) => void; + replyCountOnClick?: (e: React.MouseEvent) => void; showBlueskyLink: boolean; url: string; - onLinkClick?: (e: React.MouseEvent) => void; }) { - const { post, parent, linksEnabled, showBlueskyLink, url, onLinkClick } = props; - return ( -
- {post.replyCount != null && post.replyCount > 0 && ( +
+ {props.post.replyCount != null && props.post.replyCount > 0 && ( <> - - {linksEnabled ? ( + {props.replyCountOnClick ? ( - {post.replyCount} + {props.post.replyCount} ) : ( -
- {post.replyCount} +
+ {props.post.replyCount}
)} )} - {post.quoteCount != null && post.quoteCount > 0 && ( - <> - - - {post.quoteCount} - - - - )} - {showBlueskyLink && ( + {props.post.quoteCount != null && props.post.quoteCount > 0 && ( <> - - - - + {props.quoteCountOnClick ? ( + + + {props.post.quoteCount} + + ) : ( +
+ + {props.post.quoteCount} +
+ )} )}
diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx index 33e771f0..fde12e67 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx @@ -25,7 +25,7 @@ export type Comment = { uri: string; bsky_profiles: { record: Json; did: string } | null; }; -export function Comments(props: { +export function CommentsDrawerContent(props: { document_uri: string; comments: Comment[]; pageId?: string; diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx index 9283aa1c..b690925b 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx @@ -1,9 +1,9 @@ "use client"; import { Media } from "components/Media"; -import { Quotes } from "./Quotes"; +import { MentionsDrawerContent } from "./Quotes"; import { InteractionState, useInteractionState } from "./Interactions"; import { Json } from "supabase/database.types"; -import { Comment, Comments } from "./Comments"; +import { Comment, CommentsDrawerContent } from "./Comments"; import { useSearchParams } from "next/navigation"; import { SandwichSpacer } from "components/LeafletLayout"; import { decodeQuotePosition } from "../quotePosition"; @@ -42,9 +42,12 @@ export const InteractionDrawer = (props: { className={`opaque-container h-full w-full px-3 sm:px-4 pt-2 sm:pt-3 pb-6 overflow-scroll ${props.showPageBackground ? "rounded-l-none! rounded-r-lg! -ml-[1px]" : "rounded-lg! sm:ml-4"}`} > {drawer.drawer === "quotes" ? ( - + ) : ( - { @@ -85,18 +86,13 @@ export const Quotes = (props: { }); return ( -
-
- Quotes - -
+
+ {props.quotesAndMentions.length === 0 ? (
no quotes yet!
@@ -108,59 +104,58 @@ export const Quotes = (props: {
) : ( -
- {/* Quotes with links (quoted content) */} - {quotesWithLinks.map((q, index) => { - const pv = postViewMap.get(q.uri); - if (!pv || !q.link) return null; - const url = new URL(q.link); - const quoteParam = url.pathname.split("/l-quote/")[1]; - if (!quoteParam) return null; - const quotePosition = decodeQuotePosition(quoteParam); - if (!quotePosition) return null; - return ( -
- - -
- -
- ); - })} - +
+ {quotesWithLinks.length > 0 && ( +
+ Quotes + {/* Quotes with links (quoted content) */} + {quotesWithLinks.map((q, index) => { + return ( +
+ +
+ ); + })} +
+ )} {/* Direct post mentions (without quoted content) */} {directMentions.length > 0 && (
-
Post Mentions
+
+ Mentions on Bluesky +
{directMentions.map((q, index) => { - const pv = postViewMap.get(q.uri); - if (!pv) return null; + const post = postViewMap.get(q.uri); + if (!post) return null; + + const parent = { type: "thread" as const, uri: q.uri }; return ( - + ); })}
@@ -172,6 +167,44 @@ export const Quotes = (props: { ); }; +const Quote = (props: { + q: { + uri: string; + link?: string; + }; + index: number; + did: string; + postViewMap: Map; +}) => { + const post = props.postViewMap.get(props.q.uri); + if (!post || !props.q.link) return null; + const parent = { type: "thread" as const, uri: props.q.uri }; + const url = new URL(props.q.link); + const quoteParam = url.pathname.split("/l-quote/")[1]; + if (!quoteParam) return null; + const quotePosition = decodeQuotePosition(quoteParam); + if (!quotePosition) return null; + + return ( +
+ + +
+ +
+ ); +}; + export const QuoteContent = (props: { position: QuotePosition; index: number; @@ -206,12 +239,15 @@ export const QuoteContent = (props: { className="quoteSectionQuote text-secondary text-sm text-left hover:cursor-pointer" onClick={(e) => { if (props.position.pageId) - flushSync(() => openPage(undefined, { type: "doc", id: props.position.pageId! })); + flushSync(() => + openPage(undefined, { type: "doc", id: props.position.pageId! }), + ); let scrollMargin = isMobile ? 16 : e.currentTarget.getBoundingClientRect().top; let scrollContainerId = `post-page-${props.position.pageId ?? document_uri}`; - let scrollContainer = window.document.getElementById(scrollContainerId); + let scrollContainer = + window.document.getElementById(scrollContainerId); let el = window.document.getElementById( props.position.start.block.join("."), ); @@ -244,72 +280,6 @@ export const QuoteContent = (props: { ); }; -export const BskyPost = (props: { - uri: string; - rkey: string; - content: string; - user: string; - handle: string; - profile: ProfileViewBasic; - replyCount?: number; - quoteCount?: number; -}) => { - const handleOpenThread = () => { - openPage(undefined, { type: "thread", uri: props.uri }); - }; - - return ( -
- {props.profile.avatar && ( - {props.profile.displayName} - )} -
- -
{props.content}
-
- {props.replyCount != null && props.replyCount > 0 && ( - e.stopPropagation()} - className="flex items-center gap-1 text-tertiary text-xs hover:text-accent-contrast" - > - - {props.replyCount} {props.replyCount === 1 ? "reply" : "replies"} - - )} - {props.quoteCount != null && props.quoteCount > 0 && ( - e.stopPropagation()} - className="flex items-center gap-1 text-tertiary text-xs hover:text-accent-contrast" - > - - {props.quoteCount} {props.quoteCount === 1 ? "quote" : "quotes"} - - )} -
-
-
- ); -}; - function extractQuotedBlocks( blocks: PubLeafletPagesLinearDocument.Block[], quotePosition: QuotePosition, diff --git a/app/lish/[did]/[publication]/[rkey]/PostLinks.tsx b/app/lish/[did]/[publication]/[rkey]/PostLinks.tsx index 80da034b..0800cbda 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostLinks.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostLinks.tsx @@ -104,7 +104,6 @@ export function QuotesLink(props: { const handlePrefetch = () => { prefetchQuotes(postUri); }; - return ( - {isCollapsed ? ( - - ) : ( -
- -
- )} -
- )} - {hasReplies && depth >= 3 && ( - - View more replies - - )} -
+ { + e.stopPropagation(); + e.preventDefault(); + if (parentUri) toggleCollapsed(parentUri); + console.log("collapse?"); + }} + isCollapsed={isCollapsed} + depth={props.depth} + /> ); })} + {parentUri && depth > 0 && replies.length > 3 && ( + +
+ View {replies.length - 3} more{" "} + {replies.length === 4 ? "reply" : "replies"} + + )}
); } -function ReplyPost(props: { +const ReplyPost = (props: { post: ThreadViewPost; - showReplyLine: boolean; isLast: boolean; threadUri: string; -}) { + toggleCollapsed: (e: React.MouseEvent) => void; + isCollapsed: boolean; + depth: number; +}) => { const { post, threadUri } = props; const postView = post.post; const parent = { type: "thread" as const, uri: threadUri }; + const hasReplies = props.post.replies && props.post.replies.length > 0; + + // was in the middle of trying to get the right set of comments to close when this line is clicked + // then i really need to style the parent and grandparent threads, hide some of the content unless its the main post + // the thread line on them is also weird return ( -
openPage(parent, { type: "thread", uri: postView.uri })} - > - e.stopPropagation()} - onEmbedClick={(e) => e.stopPropagation()} - /> +
+ {props.depth > 0 && ( + + )} + + + {hasReplies && props.depth < 3 && ( +
+ {!props.isCollapsed && ( +
+ +
+ )} +
+ )} + + {hasReplies && props.depth >= 3 && ( + + View more replies + + )}
); -} +}; diff --git a/components/Avatar.tsx b/components/Avatar.tsx index 8efc20a3..0e193e34 100644 --- a/components/Avatar.tsx +++ b/components/Avatar.tsx @@ -4,14 +4,25 @@ export const Avatar = (props: { src: string | undefined; displayName: string | undefined; className?: string; - tiny?: boolean; - large?: boolean; - giant?: boolean; + size?: "tiny" | "small" | "medium" | "large" | "giant"; }) => { + let sizeClassName = + props.size === "tiny" + ? "w-4 h-4" + : props.size === "small" + ? "w-5 h-5" + : props.size === "medium" + ? "h-6 w-6" + : props.size === "large" + ? "w-8 h-8" + : props.size === "giant" + ? "h-16 w-16" + : "w-6 h-6"; + if (props.src) return ( { - +
); }; diff --git a/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx b/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx index 96b70eb0..5d8eaefb 100644 --- a/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx +++ b/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx @@ -14,6 +14,7 @@ import { export const BlueskyEmbed = (props: { embed: Exclude; postUrl?: string; + className?: string; }) => { // check this file from bluesky for ref // https://github.com/bluesky-social/social-app/blob/main/bskyembed/src/components/embed.tsx @@ -81,7 +82,7 @@ export const BlueskyEmbed = (props: { {externalEmbed.external.thumb === undefined ? null : ( <> @@ -116,7 +117,7 @@ export const BlueskyEmbed = (props: { : 16 / 9; return (
+
0) { + return `${diffYears}y`; + } else if (diffMonths > 0) { + return `${diffMonths}mo`; + } else if (diffWeeks > 0) { + return `${diffWeeks}w`; + } else if (diffDays > 0) { + return `${diffDays}d`; + } else if (diffHours > 0) { + return `${diffHours}h`; + } else if (diffMinutes > 0) { + return `${diffMinutes}m`; + } else { + return "now"; + } + } + if (diffYears > 0) { return `${diffYears} year${diffYears === 1 ? "" : "s"} ago`; } else if (diffMonths > 0) { From 4b18fdf3c997c1b5cdfad46669f827928e918541 Mon Sep 17 00:00:00 2001 From: celine Date: Fri, 23 Jan 2026 18:23:30 -0500 Subject: [PATCH 02/20] cleaned up naming --- .../[rkey]/Blocks/PublishBskyPostBlock.tsx | 2 +- .../[rkey]/BlueskyQuotesPage.tsx | 20 +- .../[publication]/[rkey]/BskyPostContent.tsx | 245 ++++++++++-------- .../[rkey]/Interactions/Quotes.tsx | 29 +-- .../[did]/[publication]/[rkey]/PostLinks.tsx | 10 +- .../[did]/[publication]/[rkey]/PostPages.tsx | 2 +- .../[did]/[publication]/[rkey]/ThreadPage.tsx | 131 ++++------ 7 files changed, 214 insertions(+), 225 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx b/app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx index 44c41a60..3343af46 100644 --- a/app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx @@ -113,7 +113,7 @@ export const PubBlueskyPostBlock = (props: { {post.replyCount != null && post.replyCount > 0 && ( <> e.stopPropagation()} diff --git a/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx b/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx index 014664e5..1cfe3139 100644 --- a/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx @@ -87,17 +87,13 @@ function QuotePost(props: { post: PostView; quotesUri: string }) { const parent = { type: "quotes" as const, uri: quotesUri }; return ( -
openPage(parent, { type: "thread", uri: post.uri })} - > - e.stopPropagation()} - /> -
+ e.stopPropagation()} + className="relative py-2 px-2 hover:bg-bg-page rounded cursor-pointer" + /> ); } diff --git a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx index cb1e5d0b..e9512897 100644 --- a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx @@ -8,7 +8,7 @@ import { QuoteTiny } from "components/Icons/QuoteTiny"; import { Separator } from "components/Layout"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; import { useHasPageLoaded } from "components/InitialPageLoadProvider"; -import { OpenPage } from "./PostPages"; +import { OpenPage, openPage } from "./PostPages"; import { ThreadLink, QuotesLink } from "./PostLinks"; import { BlueskyLinkTiny } from "components/Icons/BlueskyLinkTiny"; import { Avatar } from "components/Avatar"; @@ -25,8 +25,12 @@ export function BskyPostContent(props: { showEmbed?: boolean; showBlueskyLink?: boolean; onEmbedClick?: (e: React.MouseEvent) => void; - quoteCountOnClick?: (e: React.MouseEvent) => void; - replyCountOnClick?: (e: React.MouseEvent) => void; + quoteEnabled?: boolean; + replyEnabled?: boolean; + replyOnClick?: (e: React.MouseEvent) => void; + replyLine?: { + onToggle: (e: React.MouseEvent) => void; + }; }) { const { post, @@ -35,8 +39,10 @@ export function BskyPostContent(props: { showEmbed = true, showBlueskyLink = true, onEmbedClick, - quoteCountOnClick, - replyCountOnClick, + quoteEnabled, + replyEnabled, + replyOnClick, + replyLine, } = props; const record = post.record as AppBskyFeedPost.Record; @@ -44,126 +50,153 @@ export function BskyPostContent(props: { const url = `https://bsky.app/profile/${post.author.handle}/post/${postId}`; return ( - <> - - -
-
-
-
- {post.author.displayName} -
- - @{post.author.handle} -
- } - didOrHandle={post.author.handle} - /> -
-
- {timeAgo(record.createdAt, { compact: true })} -
+
); } function PostCounts(props: { post: PostView; parent?: OpenPage; - quoteCountOnClick?: (e: React.MouseEvent) => void; - replyCountOnClick?: (e: React.MouseEvent) => void; + quoteEnabled?: boolean; + replyEnabled?: boolean; + replyOnClick?: (e: React.MouseEvent) => void; showBlueskyLink: boolean; url: string; }) { + const replyContent = props.post.replyCount != null && + props.post.replyCount > 0 && ( +
+ + {props.post.replyCount} +
+ ); + + const quoteContent = props.post.quoteCount != null && + props.post.quoteCount > 0 && ( +
+ + {props.post.quoteCount} +
+ ); + return (
- {props.post.replyCount != null && props.post.replyCount > 0 && ( - <> - {props.replyCountOnClick ? ( - - - {props.post.replyCount} - - ) : ( -
- - {props.post.replyCount} -
- )} - - )} - {props.post.quoteCount != null && props.post.quoteCount > 0 && ( - <> - {props.quoteCountOnClick ? ( - - - {props.post.quoteCount} - - ) : ( -
- - {props.post.quoteCount} -
- )} - - )} + {replyContent && + (props.replyEnabled ? ( + + {replyContent} + + ) : ( + replyContent + ))} + {quoteContent && + (props.quoteEnabled ? ( + + {quoteContent} + + ) : ( + quoteContent + ))}
); } diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx index 05c1d550..980b6149 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx @@ -136,26 +136,15 @@ export const MentionsDrawerContent = (props: { const parent = { type: "thread" as const, uri: q.uri }; return ( - + ); })}
diff --git a/app/lish/[did]/[publication]/[rkey]/PostLinks.tsx b/app/lish/[did]/[publication]/[rkey]/PostLinks.tsx index 0800cbda..5762dea8 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostLinks.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostLinks.tsx @@ -55,22 +55,23 @@ export const prefetchQuotes = (uri: string) => { // Link component for opening thread pages with prefetching export function ThreadLink(props: { - threadUri: string; + postUri: string; parent?: OpenPage; children: React.ReactNode; className?: string; onClick?: (e: React.MouseEvent) => void; }) { - const { threadUri, parent, children, className, onClick } = props; + const { postUri, parent, children, className, onClick } = props; const handleClick = (e: React.MouseEvent) => { + e.stopPropagation(); onClick?.(e); if (e.defaultPrevented) return; - openPage(parent, { type: "thread", uri: threadUri }); + openPage(parent, { type: "thread", uri: postUri }); }; const handlePrefetch = () => { - prefetchThread(threadUri); + prefetchThread(postUri); }; return ( @@ -96,6 +97,7 @@ export function QuotesLink(props: { const { postUri, parent, children, className, onClick } = props; const handleClick = (e: React.MouseEvent) => { + e.stopPropagation(); onClick?.(e); if (e.defaultPrevented) return; openPage(parent, { type: "quotes", uri: postUri }); diff --git a/app/lish/[did]/[publication]/[rkey]/PostPages.tsx b/app/lish/[did]/[publication]/[rkey]/PostPages.tsx index c2305d27..63b3637c 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostPages.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostPages.tsx @@ -297,7 +297,7 @@ export function PostPages({ - fetchThread(threadUri), + } = useSWR(parentUri ? getThreadKey(parentUri) : null, () => + fetchThread(parentUri), ); return ( @@ -60,15 +60,15 @@ export function ThreadPage(props: { Failed to load thread
) : thread ? ( - + ) : null}
); } -function ThreadContent(props: { thread: ThreadType; threadUri: string }) { - const { thread, threadUri } = props; +function ThreadContent(props: { thread: ThreadType; parentUri: string }) { + const { thread, parentUri: parentUri } = props; const mainPostRef = useRef(null); // Scroll the main post into view when the thread loads @@ -114,7 +114,7 @@ function ThreadContent(props: { thread: ThreadType; threadUri: string }) { post={parent} isMainPost={false} showReplyLine={index < parents.length - 1 || true} - threadUri={threadUri} + parentUri={parentUri} />
))} @@ -125,7 +125,7 @@ function ThreadContent(props: { thread: ThreadType; threadUri: string }) { post={thread} isMainPost={true} showReplyLine={false} - threadUri={threadUri} + parentUri={parentUri} />
@@ -134,7 +134,7 @@ function ThreadContent(props: { thread: ThreadType; threadUri: string }) {
@@ -148,11 +148,11 @@ function ThreadPost(props: { post: ThreadViewPost; isMainPost: boolean; showReplyLine: boolean; - threadUri: string; + parentUri: string; }) { - const { post, isMainPost, showReplyLine, threadUri } = props; + const { post, isMainPost, showReplyLine, parentUri } = props; const postView = post.post; - const parent = { type: "thread" as const, uri: threadUri }; + const parent = { type: "thread" as const, uri: parentUri }; return (
@@ -165,15 +165,8 @@ function ThreadPost(props: { parent={parent} showBlueskyLink={true} showEmbed={true} - quoteCountOnClick={(e) => { - e.stopPropagation(); - e.preventDefault(); - openPage(parent, { type: "quotes", uri: postView.uri }); - }} - replyCountOnClick={(e) => { - e.stopPropagation(); - e.preventDefault(); - }} + quoteEnabled + replyEnabled />
); @@ -181,12 +174,11 @@ function ThreadPost(props: { function Replies(props: { replies: (ThreadViewPost | NotFoundPost | BlockedPost)[]; - threadUri: string; depth: number; parentAuthorDid?: string; - parentUri?: string; + parentUri: string; }) { - const { replies, threadUri, depth, parentAuthorDid, parentUri } = props; + const { replies, depth, parentAuthorDid, parentUri } = props; const collapsedThreads = useThreadState((s) => s.collapsedThreads); const toggleCollapsed = useThreadState((s) => s.toggleCollapsed); @@ -241,13 +233,8 @@ function Replies(props: { { - e.stopPropagation(); - e.preventDefault(); - if (parentUri) toggleCollapsed(parentUri); - console.log("collapse?"); - }} + parentUri={parentUri} + toggleCollapsed={(uri) => toggleCollapsed(uri)} isCollapsed={isCollapsed} depth={props.depth} /> @@ -255,8 +242,8 @@ function Replies(props: { })} {parentUri && depth > 0 && replies.length > 3 && (
@@ -271,59 +258,42 @@ function Replies(props: { const ReplyPost = (props: { post: ThreadViewPost; isLast: boolean; - threadUri: string; - toggleCollapsed: (e: React.MouseEvent) => void; + parentUri: string; + toggleCollapsed: (uri: string) => void; isCollapsed: boolean; depth: number; }) => { - const { post, threadUri } = props; + const { post, parentUri } = props; const postView = post.post; - const parent = { type: "thread" as const, uri: threadUri }; const hasReplies = props.post.replies && props.post.replies.length > 0; - // was in the middle of trying to get the right set of comments to close when this line is clicked - // then i really need to style the parent and grandparent threads, hide some of the content unless its the main post - // the thread line on them is also weird return (
- {props.depth > 0 && ( - - )} - - + onEmbedClick={(e) => e.stopPropagation()} + className="text-sm z-10" + /> {hasReplies && props.depth < 3 && (
{!props.isCollapsed && ( @@ -331,7 +301,6 @@ const ReplyPost = (props: { @@ -342,8 +311,8 @@ const ReplyPost = (props: { {hasReplies && props.depth >= 3 && ( View more replies From 37729c715d75e3b6b9eb34495b5ec63380922cee Mon Sep 17 00:00:00 2001 From: celine Date: Fri, 23 Jan 2026 23:41:13 -0500 Subject: [PATCH 03/20] 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 --- .../[rkey]/BlueskyQuotesPage.tsx | 2 + .../[publication]/[rkey]/BskyPostContent.tsx | 180 ++++++++++-------- .../[rkey]/Interactions/Quotes.tsx | 3 +- .../[did]/[publication]/[rkey]/ThreadPage.tsx | 33 ++-- 4 files changed, 118 insertions(+), 100 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx b/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx index 1cfe3139..f335211d 100644 --- a/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx @@ -92,6 +92,8 @@ function QuotePost(props: { post: PostView; quotesUri: string }) { parent={parent} showEmbed={true} showBlueskyLink={true} + quoteEnabled + replyEnabled onEmbedClick={(e) => e.stopPropagation()} className="relative py-2 px-2 hover:bg-bg-page rounded cursor-pointer" /> diff --git a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx index e9512897..a0178390 100644 --- a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx @@ -19,7 +19,7 @@ type PostView = AppBskyFeedDefs.PostView; export function BskyPostContent(props: { post: PostView; - parent?: OpenPage; + parent: OpenPage; avatarSize?: "tiny" | "medium" | "large" | "giant"; className?: string; showEmbed?: boolean; @@ -50,97 +50,111 @@ export function BskyPostContent(props: { const url = `https://bsky.app/profile/${post.author.handle}/post/${postId}`; return ( -
- {replyLine && ( -
+
+
+
+ {replyLine && ( + + )} + + {replyLine && ( + + )} +
+
-
- )} - + {props.showBlueskyLink || + (props.post.quoteCount && props.post.quoteCount > 0) || + (props.post.replyCount && props.post.replyCount > 0) ? ( +
+ + +
+ {showBlueskyLink && ( + <> + + + + + )} +
+
+ ) : null}
- +
); } @@ -171,7 +185,7 @@ function PostCounts(props: { ); return ( -
+
{replyContent && (props.replyEnabled ? ( ); })} diff --git a/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx b/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx index 8efc880e..55774899 100644 --- a/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx @@ -60,15 +60,15 @@ export function ThreadPage(props: { Failed to load thread
) : thread ? ( - + ) : null}
); } -function ThreadContent(props: { thread: ThreadType; parentUri: string }) { - const { thread, parentUri: parentUri } = props; +function ThreadContent(props: { post: ThreadType; parentUri: string }) { + const { post, parentUri } = props; const mainPostRef = useRef(null); // Scroll the main post into view when the thread loads @@ -81,11 +81,11 @@ function ThreadContent(props: { thread: ThreadType; parentUri: string }) { } }, []); - if (AppBskyFeedDefs.isNotFoundPost(thread)) { + if (AppBskyFeedDefs.isNotFoundPost(post)) { return ; } - if (AppBskyFeedDefs.isBlockedPost(thread)) { + if (AppBskyFeedDefs.isBlockedPost(post)) { return (
This post is blocked @@ -93,13 +93,13 @@ function ThreadContent(props: { thread: ThreadType; parentUri: string }) { ); } - if (!AppBskyFeedDefs.isThreadViewPost(thread)) { + if (!AppBskyFeedDefs.isThreadViewPost(post)) { return ; } // Collect all parent posts in order (oldest first) const parents: ThreadViewPost[] = []; - let currentParent = thread.parent; + let currentParent = post.parent; while (currentParent && AppBskyFeedDefs.isThreadViewPost(currentParent)) { parents.unshift(currentParent); currentParent = currentParent.parent; @@ -122,7 +122,7 @@ function ThreadContent(props: { thread: ThreadType; parentUri: string }) { {/* Main post */}
{/* Replies */} - {thread.replies && thread.replies.length > 0 && ( + {post.replies && post.replies.length > 0 && (
)} @@ -166,7 +166,7 @@ function ThreadPost(props: { showBlueskyLink={true} showEmbed={true} quoteEnabled - replyEnabled + replyEnabled={!isMainPost} />
); @@ -269,7 +269,9 @@ const ReplyPost = (props: { const hasReplies = props.post.replies && props.post.replies.length > 0; return ( -
+
{ props.toggleCollapsed(props.parentUri); - console.log("click click"); }, } : undefined @@ -299,7 +300,7 @@ const ReplyPost = (props: { {!props.isCollapsed && (
Date: Sat, 24 Jan 2026 01:39:38 -0500 Subject: [PATCH 04/20] styling the grandparents in the threadviewer --- .../[publication]/[rkey]/BskyPostContent.tsx | 110 +++++++++++++++-- .../[did]/[publication]/[rkey]/ThreadPage.tsx | 115 ++++++++++-------- .../Blocks/BlueskyPostBlock/BlueskyEmbed.tsx | 32 +++-- 3 files changed, 188 insertions(+), 69 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx index a0178390..8b7d54f6 100644 --- a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx @@ -20,9 +20,10 @@ type PostView = AppBskyFeedDefs.PostView; export function BskyPostContent(props: { post: PostView; parent: OpenPage; - avatarSize?: "tiny" | "medium" | "large" | "giant"; + avatarSize?: "tiny" | "small" | "medium" | "large" | "giant"; className?: string; showEmbed?: boolean; + compactEmbed?: boolean; showBlueskyLink?: boolean; onEmbedClick?: (e: React.MouseEvent) => void; quoteEnabled?: boolean; @@ -35,8 +36,9 @@ export function BskyPostContent(props: { const { post, parent, - avatarSize = "md", + avatarSize = "medium", showEmbed = true, + compactEmbed = false, showBlueskyLink = true, onEmbedClick, quoteEnabled, @@ -67,7 +69,7 @@ export function BskyPostContent(props: { {replyLine && ( + )} +
+
+
+ +
+
+ +
+
+ + {(post.quoteCount && post.quoteCount > 0) || + (post.replyCount && post.replyCount > 0) ? ( +
+ +
+ ) : null} +
+
+
+ ); +} + function PostCounts(props: { post: PostView; parent?: OpenPage; diff --git a/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx b/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx index 55774899..8805cce4 100644 --- a/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx @@ -6,9 +6,12 @@ import { PageWrapper } from "components/Pages/Page"; import { useDrawerOpen } from "./Interactions/InteractionDrawer"; import { DotLoader } from "components/utils/DotLoader"; import { PostNotAvailable } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; -import { openPage } from "./PostPages"; import { useThreadState } from "src/useThreadState"; -import { BskyPostContent, ClientDate } from "./BskyPostContent"; +import { + BskyPostContent, + CompactBskyPostContent, + ClientDate, +} from "./BskyPostContent"; import { ThreadLink, getThreadKey, @@ -30,7 +33,7 @@ export function ThreadPage(props: { pageOptions?: React.ReactNode; hasPageBackground: boolean; }) { - const { parentUri: parentUri, pageId, pageOptions } = props; + const { parentUri, pageId, pageOptions } = props; const drawer = useDrawerOpen(parentUri); const { @@ -49,7 +52,7 @@ export function ThreadPage(props: { drawerOpen={!!drawer} pageOptions={pageOptions} > -
+
{isLoading ? (
loading thread @@ -106,35 +109,31 @@ function ThreadContent(props: { post: ThreadType; parentUri: string }) { } return ( -
+
{/* grandparent posts, if any */} - {parents.map((parent, index) => ( -
- -
+ {parents.map((parentPost, index) => ( + ))} {/* Main post */}
- +
{/* Replies */} {post.replies && post.replies.length > 0 && ( -
+
@@ -147,26 +146,38 @@ function ThreadContent(props: { post: ThreadType; parentUri: string }) { function ThreadPost(props: { post: ThreadViewPost; isMainPost: boolean; - showReplyLine: boolean; - parentUri: string; + pageUri: string; }) { - const { post, isMainPost, showReplyLine, parentUri } = props; + const { post, isMainPost, pageUri } = props; const postView = post.post; - const parent = { type: "thread" as const, uri: parentUri }; + const page = { type: "thread" as const, uri: pageUri }; + + if (isMainPost) { + return ( +
+ +
+ ); + } return ( -
- {/* Reply line connector */} - {showReplyLine && ( -
- )} - + {}, + }} />
); @@ -176,9 +187,10 @@ function Replies(props: { replies: (ThreadViewPost | NotFoundPost | BlockedPost)[]; depth: number; parentAuthorDid?: string; - parentUri: string; + pageUri: string; + parentPostUri: string; }) { - const { replies, depth, parentAuthorDid, parentUri } = props; + const { replies, depth, parentAuthorDid, pageUri, parentPostUri } = props; const collapsedThreads = useThreadState((s) => s.collapsedThreads); const toggleCollapsed = useThreadState((s) => s.toggleCollapsed); @@ -198,7 +210,7 @@ function Replies(props: { : replies; return ( -
+
{sortedReplies.map((reply, index) => { if (AppBskyFeedDefs.isNotFoundPost(reply)) { return ( @@ -231,19 +243,21 @@ function Replies(props: { return ( toggleCollapsed(uri)} isCollapsed={isCollapsed} depth={props.depth} /> ); })} - {parentUri && depth > 0 && replies.length > 3 && ( + {pageUri && depth > 0 && replies.length > 3 && (
@@ -258,12 +272,13 @@ function Replies(props: { const ReplyPost = (props: { post: ThreadViewPost; isLast: boolean; - parentUri: string; + pageUri: string; + parentPostUri: string; toggleCollapsed: (uri: string) => void; isCollapsed: boolean; depth: number; }) => { - const { post, parentUri } = props; + const { post, pageUri, parentPostUri } = props; const postView = post.post; const hasReplies = props.post.replies && props.post.replies.length > 0; @@ -274,14 +289,14 @@ const ReplyPost = (props: { > 0 ? { onToggle: () => { - props.toggleCollapsed(props.parentUri); + props.toggleCollapsed(parentPostUri); }, } : undefined @@ -291,6 +306,7 @@ const ReplyPost = (props: { replyOnClick={(e) => { e.preventDefault(); props.toggleCollapsed(post.post.uri); + console.log(post.post.uri); }} onEmbedClick={(e) => e.stopPropagation()} className="text-sm z-10" @@ -300,7 +316,8 @@ const ReplyPost = (props: { {!props.isCollapsed && (
= 3 && ( View more replies diff --git a/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx b/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx index 5d8eaefb..192306cc 100644 --- a/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx +++ b/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx @@ -15,6 +15,7 @@ export const BlueskyEmbed = (props: { embed: Exclude; postUrl?: string; className?: string; + compact?: boolean; }) => { // check this file from bluesky for ref // https://github.com/bluesky-social/social-app/blob/main/bskyembed/src/components/embed.tsx @@ -22,7 +23,7 @@ export const BlueskyEmbed = (props: { case AppBskyEmbedImages.isView(props.embed): let imageEmbed = props.embed; return ( -
+
{imageEmbed.images.map( ( image: { @@ -69,7 +70,7 @@ export const BlueskyEmbed = (props: { let isGif = externalEmbed.external.uri.includes(".gif"); if (isGif) { return ( -
+
{externalEmbed.external.title} {externalEmbed.external.thumb === undefined ? null : ( <> -
+ -
+ {!props.compact &&
} )} -
-
-

{externalEmbed.external.title}

-

+

+
+

{externalEmbed.external.title}

+

{externalEmbed.external.description}


-
+
{externalEmbed.external.uri}
@@ -117,7 +123,7 @@ export const BlueskyEmbed = (props: { : 16 / 9; return (
{record.author.avatar && ( From c7c1b52e662086a94d9fd7af5480d62bd1471103 Mon Sep 17 00:00:00 2001 From: celine Date: Sat, 24 Jan 2026 02:14:54 -0500 Subject: [PATCH 05/20] styling the quotepage --- .../[rkey]/BlueskyQuotesPage.tsx | 17 ++-- .../[publication]/[rkey]/BskyPostContent.tsx | 85 ++++++++++--------- .../[did]/[publication]/[rkey]/ThreadPage.tsx | 2 +- .../Blocks/BlueskyPostBlock/BlueskyEmbed.tsx | 64 +++++++------- 4 files changed, 92 insertions(+), 76 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx b/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx index f335211d..8dba71fb 100644 --- a/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx @@ -45,10 +45,9 @@ export function BlueskyQuotesPage(props: { pageOptions={pageOptions} >
-
- +

Bluesky Quotes -

+ {isLoading ? (
loading quotes @@ -75,8 +74,13 @@ function QuotesContent(props: { posts: PostView[]; postUri: string }) { return (
- {posts.map((post) => ( - + {posts.map((post, index) => ( + <> + + {posts.length !== index + 1 && ( +
+ )} + ))}
); @@ -91,11 +95,12 @@ function QuotePost(props: { post: PostView; quotesUri: string }) { post={post} parent={parent} showEmbed={true} + compactEmbed showBlueskyLink={true} quoteEnabled replyEnabled onEmbedClick={(e) => e.stopPropagation()} - className="relative py-2 px-2 hover:bg-bg-page rounded cursor-pointer" + className="relative py-2 px-2 hover:bg-bg-page rounded cursor-pointer text-sm" /> ); } diff --git a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx index 8b7d54f6..6e0079d9 100644 --- a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx @@ -92,26 +92,11 @@ export function BskyPostContent(props: { openPage(parent, { type: "thread", uri: post.uri }); }} > -
-
-
- {post.author.displayName} -
- - @{post.author.handle} -
- } - didOrHandle={post.author.handle} - /> -
-
- {timeAgo(record.createdAt, { compact: true })} -
-
+
@@ -210,24 +195,12 @@ export function CompactBskyPostContent(props: { openPage(parent, { type: "thread", uri: post.uri }); }} > -
-
-
- {post.author.displayName} -
- - @{post.author.handle} -
- } - didOrHandle={post.author.handle} - /> -
-
- {timeAgo(record.createdAt, { compact: true })} -
-
+
@@ -255,6 +228,42 @@ export function CompactBskyPostContent(props: { ); } +function PostInfo(props: { + displayName?: string; + handle: string; + createdAt: string; + compact?: boolean; +}) { + const { displayName, handle, createdAt, compact = false } = props; + + return ( +
+
+
+ {displayName} +
+
+ + @{handle} +
+ } + didOrHandle={handle} + /> +
+
+
+ {timeAgo(createdAt, { compact: true })} +
+
+ ); +} + function PostCounts(props: { post: PostView; parent?: OpenPage; diff --git a/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx b/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx index 8805cce4..ce2bcff3 100644 --- a/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx @@ -49,7 +49,7 @@ export function ThreadPage(props: { pageType="doc" fullPageScroll={false} id={`post-page-${pageId}`} - drawerOpen={!!drawer} + drawerOpen={false} pageOptions={pageOptions} >
diff --git a/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx b/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx index 192306cc..5c6e409e 100644 --- a/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx +++ b/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx @@ -10,6 +10,7 @@ import { AppBskyGraphDefs, AppBskyLabelerDefs, } from "@atproto/api"; +import { Avatar } from "components/Avatar"; export const BlueskyEmbed = (props: { embed: Exclude; @@ -154,38 +155,39 @@ export const BlueskyEmbed = (props: { text = (record.value as AppBskyFeedPost.Record).text; } return ( -
-
- {record.author.avatar && ( - {`${record.author?.displayName}'s - )} -
- {record.author?.displayName} +
+ +
+
+
+ {record.author?.displayName} +
+ + @{record.author?.handle} + +
+
+ {text && ( +
+                    {text}
+                  
+ )} + {/*{record.embeds !== undefined + ? record.embeds.map((embed, index) => ( + + )) + : null}*/}
- - @{record.author?.handle} - -
- -
- {text && ( -
{text}
- )} - {record.embeds !== undefined - ? record.embeds.map((embed, index) => ( - - )) - : null}
); From b93168feec9ea553d02baf6abc9bc16b8e07399c Mon Sep 17 00:00:00 2001 From: celine Date: Sat, 24 Jan 2026 02:53:16 -0500 Subject: [PATCH 06/20] adjustments to make everything more consistent --- .../[rkey]/BlueskyQuotesPage.tsx | 10 ++-- .../[publication]/[rkey]/BskyPostContent.tsx | 5 +- .../[rkey]/Interactions/Comments/index.tsx | 22 ++++--- .../[rkey]/Interactions/Quotes.tsx | 59 +++++++++++-------- 4 files changed, 51 insertions(+), 45 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx b/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx index 8dba71fb..bf1eede2 100644 --- a/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx @@ -41,13 +41,11 @@ export function BlueskyQuotesPage(props: { pageType="doc" fullPageScroll={false} id={`post-page-${pageId}`} - drawerOpen={!!drawer} + drawerOpen={false} pageOptions={pageOptions} >
-

- Bluesky Quotes -

+

Bluesky Quotes

{isLoading ? (
loading quotes @@ -78,7 +76,7 @@ function QuotesContent(props: { posts: PostView[]; postUri: string }) { <> {posts.length !== index + 1 && ( -
+
)} ))} @@ -100,7 +98,7 @@ function QuotePost(props: { post: PostView; quotesUri: string }) { quoteEnabled replyEnabled onEmbedClick={(e) => e.stopPropagation()} - className="relative py-2 px-2 hover:bg-bg-page rounded cursor-pointer text-sm" + className="relative rounded cursor-pointer text-sm" /> ); } diff --git a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx index 6e0079d9..6305dfeb 100644 --- a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx @@ -237,8 +237,8 @@ function PostInfo(props: { const { displayName, handle, createdAt, compact = false } = props; return ( -
-
+
+
{displayName}
@@ -255,6 +255,7 @@ function PostInfo(props: { />
+
diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx index fde12e67..34608196 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx @@ -55,8 +55,8 @@ export function CommentsDrawerContent(props: { id={"commentsDrawer"} className="flex flex-col gap-2 relative text-sm text-secondary" > -
- Comments +
+

Comments

)}
-
+
{comments .sort((a, b) => { let aRecord = a.record as PubLeafletComment.Record; @@ -119,26 +119,23 @@ const Comment = (props: { }) => { const did = props.comment.bsky_profiles?.did; - let timeAgoDate = timeAgo(props.record.createdAt); - const formattedDate = useLocalizedDate(props.record.createdAt, { - year: "numeric", - month: "long", - day: "2-digit", - }); + let timeAgoDate = timeAgo(props.record.createdAt, { compact: true }); return (
-
+
{did ? ( +
{props.profile.displayName}
} /> ) : null} + +
{timeAgoDate}
{props.record.attachment && @@ -210,7 +207,8 @@ const Replies = (props: { setReplyBoxOpen(false); }} > - {replies.length} + {" "} + {replies.length !== 0 && replies.length} {identity?.atp_did && ( <> diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx index e54179f3..d71e16e2 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx @@ -104,51 +104,57 @@ export const MentionsDrawerContent = (props: {
) : ( -
+
{quotesWithLinks.length > 0 && ( -
- Quotes +
+

Quotes on Bluesky

{/* Quotes with links (quoted content) */} {quotesWithLinks.map((q, index) => { return ( -
+ <> -
+ {quotesWithLinks.length !== index + 1 && ( +
+ )} + ); })}
)} {/* Direct post mentions (without quoted content) */} {directMentions.length > 0 && ( -
-
- Mentions on Bluesky -
-
- {directMentions.map((q, index) => { - const post = postViewMap.get(q.uri); - if (!post) return null; - - const parent = { type: "thread" as const, uri: q.uri }; - return ( +
+

Mentions on Bluesky

+ {directMentions.map((q, index) => { + const post = postViewMap.get(q.uri); + if (!post) return null; + + const parent = { type: "thread" as const, uri: q.uri }; + return ( + <> - ); - })} -
+ {directMentions.length !== index + 1 && ( +
+ )} + + ); + })}
)}
@@ -176,20 +182,23 @@ const Quote = (props: { if (!quotePosition) return null; return ( -
+
-
+
); @@ -262,7 +271,7 @@ export const QuoteContent = (props: { blocks={content} did={props.did} preview - className="py-0!" + className="py-0! px-0! text-tertiary" />
From dad2b37a613871de1ab292e592f293eeb5c4f0de Mon Sep 17 00:00:00 2001 From: celine Date: Sat, 24 Jan 2026 03:23:08 -0500 Subject: [PATCH 07/20] use BskyPostContent in the leaflet as well --- .../[rkey]/Blocks/PublishBskyPostBlock.tsx | 126 ++---------------- .../[publication]/[rkey]/BskyPostContent.tsx | 18 ++- .../[rkey]/Interactions/Quotes.tsx | 1 + .../[did]/[publication]/[rkey]/ThreadPage.tsx | 10 -- components/Blocks/BlueskyPostBlock/index.tsx | 76 ++--------- 5 files changed, 38 insertions(+), 193 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx b/app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx index 3343af46..703dc5a6 100644 --- a/app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx @@ -13,6 +13,7 @@ import { } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; import { BlueskyRichText } from "components/Blocks/BlueskyPostBlock/BlueskyRichText"; import { openPage } from "../PostPages"; +import { BskyPostContent } from "../BskyPostContent"; export const PubBlueskyPostBlock = (props: { post: PostView; @@ -49,6 +50,8 @@ export const PubBlueskyPostBlock = (props: { //getting the url to the post let postId = post.uri.split("/")[4]; + let postView = post as PostView; + let url = `https://bsky.app/profile/${post.author.handle}/post/${postId}`; const parent = props.pageId @@ -56,119 +59,16 @@ export const PubBlueskyPostBlock = (props: { : undefined; return ( -
- {post.author && record && ( - <> -
- {post.author.avatar && ( - {`${post.author?.displayName}'s - )} -
-
- {post.author?.displayName} -
- e.stopPropagation()} - > - @{post.author?.handle} - -
-
- -
-
-
-                    {BlueskyRichText({
-                      record: record as AppBskyFeedPost.Record | null,
-                    })}
-                  
-
- {post.embed && ( -
e.stopPropagation()}> - -
- )} -
- - )} -
- -
- {post.replyCount != null && post.replyCount > 0 && ( - <> - e.stopPropagation()} - > - {post.replyCount} - - - - - )} - {post.quoteCount != null && post.quoteCount > 0 && ( - <> - e.stopPropagation()} - > - {post.quoteCount} - - - - - )} - - e.stopPropagation()} - > - - -
-
-
+ ); } }; - -const ClientDate = (props: { date?: string }) => { - let pageLoaded = useHasPageLoaded(); - const formattedDate = useLocalizedDate( - props.date || new Date().toISOString(), - { - month: "short", - day: "numeric", - year: "numeric", - hour: "numeric", - minute: "numeric", - hour12: true, - }, - ); - - if (!pageLoaded) return null; - - return
{formattedDate}
; -}; diff --git a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx index 6305dfeb..571cddef 100644 --- a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx @@ -19,7 +19,7 @@ type PostView = AppBskyFeedDefs.PostView; export function BskyPostContent(props: { post: PostView; - parent: OpenPage; + parent: OpenPage | undefined; avatarSize?: "tiny" | "small" | "medium" | "large" | "giant"; className?: string; showEmbed?: boolean; @@ -87,7 +87,7 @@ export function BskyPostContent(props: { className={`flex flex-col min-w-0 w-full z-0 ${props.replyLine ? "mt-2" : ""}`} > - )} +
- {replyLine && ( - - )}
)} +
0 && "pointer-events-none"}`} + > + { + e.preventDefault(); + props.toggleCollapsed(post.post.uri); + console.log(post.post.uri); + }} + onEmbedClick={(e) => e.stopPropagation()} + className="text-sm" + /> + {hasReplies && props.depth < 3 && ( +
+ {!props.isCollapsed && ( +
+ +
+ )} +
+ )} +
); }; From c98b21d3d67f8c9d0df51750c3b010f0da8e0e0e Mon Sep 17 00:00:00 2001 From: celine Date: Sat, 24 Jan 2026 04:31:44 -0500 Subject: [PATCH 10/20] little tweaks to the embed block --- .../Blocks/BlueskyPostBlock/BlueskyEmbed.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx b/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx index 5c6e409e..441a1b42 100644 --- a/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx +++ b/components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx @@ -84,7 +84,7 @@ export const BlueskyEmbed = (props: { {externalEmbed.external.thumb === undefined ? null : ( @@ -102,15 +102,18 @@ export const BlueskyEmbed = (props: { )}
-
-

{externalEmbed.external.title}

-

+

+ {externalEmbed.external.title}{" "} +

+
+

{externalEmbed.external.description}

-
+ +
{externalEmbed.external.uri}
From f5a91db5792365b6a9f94496c025523c37b86dc6 Mon Sep 17 00:00:00 2001 From: celine Date: Mon, 26 Jan 2026 15:20:55 -0500 Subject: [PATCH 11/20] fixed a bunch of small things --- .../[rkey]/Blocks/PublishBskyPostBlock.tsx | 21 +---- .../[rkey]/BlueskyQuotesPage.tsx | 3 +- .../[publication]/[rkey]/BskyPostContent.tsx | 82 ++++++++----------- .../[rkey]/DocumentPageRenderer.tsx | 18 +++- .../[publication]/[rkey]/PostContent.tsx | 2 - .../[did]/[publication]/[rkey]/PostPages.tsx | 20 ++++- .../[did]/[publication]/[rkey]/ThreadPage.tsx | 55 +++++++------ .../Blocks/BlueskyPostBlock/BlueskyEmbed.tsx | 17 +++- components/Blocks/BlueskyPostBlock/index.tsx | 4 +- components/Pages/Page.tsx | 2 +- src/utils/scrollIntoView.ts | 9 +- 11 files changed, 125 insertions(+), 108 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx b/app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx index 703dc5a6..99cbc481 100644 --- a/app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx @@ -1,18 +1,6 @@ import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; import { AppBskyFeedDefs, AppBskyFeedPost } from "@atproto/api"; -import { Separator } from "components/Layout"; -import { useHasPageLoaded } from "components/InitialPageLoadProvider"; -import { BlueskyTiny } from "components/Icons/BlueskyTiny"; -import { CommentTiny } from "components/Icons/CommentTiny"; -import { QuoteTiny } from "components/Icons/QuoteTiny"; -import { ThreadLink, QuotesLink } from "../PostLinks"; -import { useLocalizedDate } from "src/hooks/useLocalizedDate"; -import { - BlueskyEmbed, - PostNotAvailable, -} from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; -import { BlueskyRichText } from "components/Blocks/BlueskyPostBlock/BlueskyRichText"; -import { openPage } from "../PostPages"; +import { PostNotAvailable } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; import { BskyPostContent } from "../BskyPostContent"; export const PubBlueskyPostBlock = (props: { @@ -22,13 +10,6 @@ export const PubBlueskyPostBlock = (props: { }) => { let post = props.post; - const handleOpenThread = () => { - openPage(props.pageId ? { type: "doc", id: props.pageId } : undefined, { - type: "thread", - uri: post.uri, - }); - }; - switch (true) { case AppBskyFeedDefs.isBlockedPost(post) || AppBskyFeedDefs.isBlockedAuthor(post) || diff --git a/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx b/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx index 0d982e50..f2812cb8 100644 --- a/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx @@ -98,8 +98,7 @@ function QuotePost(props: { post: PostView; quotesUri: string }) { showBlueskyLink={true} quoteEnabled replyEnabled - onEmbedClick={(e) => e.stopPropagation()} - className="relative rounded cursor-pointer text-sm" + className="relative rounded text-sm" /> ); } diff --git a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx index da33db08..d8145d12 100644 --- a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx @@ -25,13 +25,9 @@ export function BskyPostContent(props: { showEmbed?: boolean; compactEmbed?: boolean; showBlueskyLink?: boolean; - onEmbedClick?: (e: React.MouseEvent) => void; quoteEnabled?: boolean; replyEnabled?: boolean; replyOnClick?: (e: React.MouseEvent) => void; - replyLine?: { - onToggle: (e: React.MouseEvent) => void; - }; }) { const { post, @@ -40,11 +36,9 @@ export function BskyPostContent(props: { showEmbed = true, compactEmbed = false, showBlueskyLink = true, - onEmbedClick, quoteEnabled, replyEnabled, replyOnClick, - replyLine, } = props; const record = post.record as AppBskyFeedPost.Record; @@ -52,24 +46,27 @@ export function BskyPostContent(props: { const url = `https://bsky.app/profile/${post.author.handle}/post/${postId}`; return ( - // pointer events non so that is there is a replyLine, it can be clicked even though its underneath the postContent (buttons here have pointer-events-auto applied to make them clickable) -
-
-
+
+ +
{props.showBlueskyLink || (props.post.quoteCount && props.post.quoteCount > 0) || (props.post.replyCount && props.post.replyCount > 0) ? ( @@ -137,12 +138,8 @@ export function CompactBskyPostContent(props: { quoteEnabled?: boolean; replyEnabled?: boolean; replyOnClick?: (e: React.MouseEvent) => void; - replyLine?: { - onToggle: (e: React.MouseEvent) => void; - }; }) { - const { post, parent, quoteEnabled, replyEnabled, replyOnClick, replyLine } = - props; + const { post, parent, quoteEnabled, replyEnabled, replyOnClick } = props; const record = post.record as AppBskyFeedPost.Record; const postId = post.uri.split("/")[4]; @@ -150,28 +147,19 @@ export function CompactBskyPostContent(props: { return (
+ - )} -
-
+ +
+ <> +
+
+
+
-
+ ); } diff --git a/components/Blocks/BlueskyPostBlock/index.tsx b/components/Blocks/BlueskyPostBlock/index.tsx index 4c3639bf..10353ad2 100644 --- a/components/Blocks/BlueskyPostBlock/index.tsx +++ b/components/Blocks/BlueskyPostBlock/index.tsx @@ -6,9 +6,9 @@ import { BlockProps, BlockLayout } from "../Block"; import { elementId } from "src/utils/elementId"; import { focusBlock } from "src/utils/focusBlock"; import { AppBskyFeedDefs, AppBskyFeedPost, RichText } from "@atproto/api"; -import { BlueskyEmbed, PostNotAvailable } from "./BlueskyEmbed"; +import { PostNotAvailable } from "./BlueskyEmbed"; import { BlueskyPostEmpty } from "./BlueskyEmpty"; -import { BlueskyRichText } from "./BlueskyRichText"; + import { Separator } from "components/Layout"; import { BlueskyTiny } from "components/Icons/BlueskyTiny"; import { CommentTiny } from "components/Icons/CommentTiny"; diff --git a/components/Pages/Page.tsx b/components/Pages/Page.tsx index 623a3adf..134f6ede 100644 --- a/components/Pages/Page.tsx +++ b/components/Pages/Page.tsx @@ -113,7 +113,7 @@ export const PageWrapper = (props: { } ${cardBorderHidden && "sm:h-[calc(100%+48px)] h-[calc(100%+20px)] sm:-my-6 -my-3 sm:pt-6 pt-3"} ${props.fullPageScroll && "max-w-full "} - ${props.pageType === "doc" && !props.fullPageScroll ? (props.fixedWidth ? "sm:max-w-prose max-w-[var(--page-width-units)]" : "w-[10000px] sm:mx-0 max-w-[var(--page-width-units)]") : ""} + ${props.pageType === "doc" && !props.fullPageScroll ? (props.fixedWidth ? "w-[10000px] sm:max-w-prose max-w-[var(--page-width-units)]" : "w-[10000px] sm:mx-0 max-w-[var(--page-width-units)]") : ""} ${ props.pageType === "canvas" && !props.fullPageScroll && diff --git a/src/utils/scrollIntoView.ts b/src/utils/scrollIntoView.ts index 1b8ff6a1..ce2dd4de 100644 --- a/src/utils/scrollIntoView.ts +++ b/src/utils/scrollIntoView.ts @@ -6,5 +6,12 @@ export function scrollIntoView( threshold: number = 0.9, ) { const element = document.getElementById(elementId); - scrollIntoViewIfNeeded(element, false, "smooth"); + // Use double requestAnimationFrame to ensure the element is fully painted + // before attempting to scroll. This fixes smooth scrolling when opening + // pages from within other pages. + requestAnimationFrame(() => { + requestAnimationFrame(() => { + scrollIntoViewIfNeeded(element, false, "smooth"); + }); + }); } From 2639efee86f137c601f2c38a5f7e0b1503559ee1 Mon Sep 17 00:00:00 2001 From: celine Date: Tue, 27 Jan 2026 22:11:30 -0500 Subject: [PATCH 12/20] init the recommend button, some stuff is broken tho --- .claude/settings.local.json | 7 ++- actions/getIdentityData.ts | 1 + .../p/[didOrHandle]/getProfilePosts.ts | 27 +++++---- app/(home-pages)/reader/getReaderFeed.ts | 3 + .../tag/[tag]/getDocumentsByTag.ts | 2 + app/api/oauth/[route]/route.ts | 12 ++-- app/api/rpc/[command]/get_publication_data.ts | 5 +- .../[did]/[publication]/[rkey]/CanvasPage.tsx | 6 ++ .../[rkey]/Interactions/Interactions.tsx | 44 +++++++++++--- .../[rkey]/LinearDocumentPage.tsx | 6 +- .../[rkey]/PostHeader/PostHeader.tsx | 6 +- .../[publication]/[rkey]/getPostPageData.ts | 60 +++++++++++++++---- .../dashboard/PublishedPostsLists.tsx | 16 ++--- app/lish/[did]/[publication]/page.tsx | 12 +++- components/InteractionsPreview.tsx | 14 ++++- components/PostListing.tsx | 4 ++ contexts/DocumentContext.tsx | 2 + 17 files changed, 174 insertions(+), 53 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8f66d8d3..457d562b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,12 @@ "allow": [ "mcp__acp__Edit", "mcp__acp__Write", - "mcp__acp__Bash" + "mcp__acp__Bash", + "mcp__primitive__say_hello", + "mcp__primitive__pending_delegations", + "mcp__primitive__claim_delegation", + "mcp__primitive__tasks_update", + "mcp__primitive__contexts_update" ] } } diff --git a/actions/getIdentityData.ts b/actions/getIdentityData.ts index 6ed8e690..c83a287c 100644 --- a/actions/getIdentityData.ts +++ b/actions/getIdentityData.ts @@ -42,6 +42,7 @@ export async function uncachedGetIdentityData() { .eq("confirmed", true) .single() : null; + console.log(auth_res); if (!auth_res?.data?.identities) return null; if (auth_res.data.identities.atp_did) { //I should create a relationship table so I can do this in the above query 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 903ce627..db2ab8f4 100644 --- a/app/api/oauth/[route]/route.ts +++ b/app/api/oauth/[route]/route.ts @@ -89,10 +89,11 @@ export async function GET( // Trigger migration if identity needs it const metadata = identity?.metadata as Record | null; if (metadata?.needsStandardSiteMigration) { - await inngest.send({ - name: "user/migrate-to-standard", - data: { did: session.did }, - }); + if (process.env.NODE_ENV === "production") + await inngest.send({ + name: "user/migrate-to-standard", + data: { did: session.did }, + }); } let { data: token } = await supabaseServerClient @@ -104,7 +105,7 @@ export async function GET( }) .select() .single(); - + console.log({ token }); if (token) await setAuthToken(token.id); // Process successful authentication here @@ -113,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/lish/[did]/[publication]/[rkey]/CanvasPage.tsx b/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx index 9b3a1058..03fb8cab 100644 --- a/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx @@ -71,6 +71,8 @@ export function CanvasPage({ preferences={preferences} commentsCount={getCommentCount(document.comments_on_documents, pageId)} quotesCount={getQuoteCount(document.quotesAndMentions, pageId)} + recommendsCount={document.recommendsCount} + hasRecommended={document.hasRecommended} /> { let isMobile = useIsMobile(); return ( @@ -216,6 +220,8 @@ const CanvasMetadata = (props: { { - const { uri: document_uri, quotesAndMentions, normalizedDocument } = useDocument(); + const { + uri: document_uri, + quotesAndMentions, + normalizedDocument, + } = useDocument(); let { identity } = useIdentityData(); let { drawerOpen, drawer, pageId } = useInteractionState(document_uri); @@ -128,6 +135,12 @@ export const Interactions = (props: {
{tagCount > 0 && } + + {props.quotesCount === 0 || props.showMentions === false ? null : (
); }; -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 +365,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, diff --git a/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx b/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx index 1046025a..c628e044 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} + commentsCount={ + getCommentCount(document.comments_on_documents, pageId) || 0 + } quotesCount={getQuoteCount(document.quotesAndMentions, pageId) || 0} + recommendsCount={document.recommendsCount} + hasRecommended={document.hasRecommended} /> {!hasPageBackground &&
} diff --git a/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx b/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx index 86216159..f8e5de13 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx @@ -88,7 +88,11 @@ export function PostHeader(props: { showComments={props.preferences.showComments !== false} showMentions={props.preferences.showMentions !== false} quotesCount={getQuoteCount(document?.quotesAndMentions || []) || 0} - commentsCount={getCommentCount(document?.comments_on_documents || []) || 0} + commentsCount={ + getCommentCount(document?.comments_on_documents || []) || 0 + } + recommendsCount={document?.recommendsCount || 0} + hasRecommended={document?.hasRecommended || false} /> } diff --git a/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts b/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts index 9f7f44a2..4d18cae5 100644 --- a/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts +++ b/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts @@ -8,8 +8,12 @@ import { } from "src/utils/normalizeRecords"; import { PubLeafletPublication, SiteStandardPublication } from "lexicons/api"; import { documentUriFilter } from "src/utils/uriHelpers"; +import { getIdentityData } from "actions/getIdentityData"; export async function getPostPageData(did: string, rkey: string) { + const identity = await getIdentityData(); + const currentUserDid = identity?.atp_did; + let { data: documents } = await supabaseServerClient .from("documents") .select( @@ -22,7 +26,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)) @@ -32,13 +37,28 @@ export async function getPostPageData(did: string, rkey: string) { if (!document) return null; + // Check if current user has recommended this document + let hasRecommended = false; + if (currentUserDid) { + const { data: userRecommend } = await supabaseServerClient + .from("recommends_on_documents") + .select("uri") + .eq("document", document.uri) + .eq("recommender_did", currentUserDid) + .limit(1); + hasRecommended = (userRecommend?.length ?? 0) > 0; + } + // 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 +103,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 +121,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 +147,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 +176,9 @@ 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, + hasRecommended, }; } diff --git a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx index b8a24406..ae0e12bf 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,6 +137,9 @@ function PublishedPostItem(props: { { 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 ( @@ -164,6 +169,9 @@ export default async function Publication(props: { +
{tagsCount === 0 ? null : ( <> @@ -38,6 +40,12 @@ export const InteractionPreview = (props: { )} + + {!props.showMentions || props.quotesCount === 0 ? null : ( { pubRecord?.preferences?.showComments === false ? 0 : props.documents.comments_on_documents?.[0]?.count || 0; + let recommends = props.documents.recommends_on_documents?.[0]?.count || 0; let tags = (postRecord?.tags as string[] | undefined) || []; // For standalone posts, link directly to the document @@ -103,6 +104,9 @@ export const PostListing = (props: Post) => { postUrl={postHref} quotesCount={quotes} commentsCount={comments} + recommendsCount={recommends} + hasRecommended={false} + documentUri={props.documents.uri} tags={tags} showComments={pubRecord?.preferences?.showComments !== false} showMentions={pubRecord?.preferences?.showMentions !== false} diff --git a/contexts/DocumentContext.tsx b/contexts/DocumentContext.tsx index 467d9ad9..fdd54954 100644 --- a/contexts/DocumentContext.tsx +++ b/contexts/DocumentContext.tsx @@ -21,6 +21,8 @@ export type DocumentContextValue = Pick< | "comments" | "mentions" | "leafletId" + | "recommendsCount" + | "hasRecommended" >; const DocumentContext = createContext(null); From 1bac2981117cfcb590551002fdff02c126b05f2e Mon Sep 17 00:00:00 2001 From: celine Date: Tue, 27 Jan 2026 22:12:54 -0500 Subject: [PATCH 13/20] oops forgot the new files --- .../[rkey]/Interactions/recommendAction.ts | 134 ++++++++++++++++++ components/Icons/RecommendTiny.tsx | 37 +++++ components/RecommendButton.tsx | 69 +++++++++ 3 files changed, 240 insertions(+) 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/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts b/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts new file mode 100644 index 00000000..8f3a2eda --- /dev/null +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts @@ -0,0 +1,134 @@ +"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) { + console.log("recommended"); + + return { + success: false, + error: { + type: "oauth_session_expired", + message: "Not authenticated", + }, + }; + } + + 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, + ); + + 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, + }); + + 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", + }, + }; + } + + 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/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/RecommendButton.tsx b/components/RecommendButton.tsx new file mode 100644 index 00000000..1bab147e --- /dev/null +++ b/components/RecommendButton.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useState } from "react"; +import { RecommendTinyEmpty, RecommendTinyFilled } from "./Icons/RecommendTiny"; +import { + recommendAction, + unrecommendAction, +} from "app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction"; + +export function RecommendButton(props: { + documentUri: string; + recommendsCount: number; + hasRecommended: boolean; + className?: string; + showCount?: boolean; +}) { + const [hasRecommended, setHasRecommended] = useState(props.hasRecommended); + const [count, setCount] = useState(props.recommendsCount); + const [isPending, setIsPending] = useState(false); + + const handleClick = async () => { + if (isPending) return; + + const currentlyRecommended = hasRecommended; + setIsPending(true); + setHasRecommended(!currentlyRecommended); + setCount((c) => (currentlyRecommended ? c - 1 : c + 1)); + + try { + if (currentlyRecommended) { + await unrecommendAction({ document: props.documentUri }); + } else { + await recommendAction({ document: props.documentUri }); + } + } catch (error) { + // Revert on error + setHasRecommended(currentlyRecommended); + setCount((c) => (currentlyRecommended ? c + 1 : c - 1)); + } finally { + setIsPending(false); + } + }; + + const showCount = props.showCount !== false; + + return ( + + ); +} From 412de3500086a933e52345e87663fa0275fe354b Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 28 Jan 2026 17:14:27 -0500 Subject: [PATCH 14/20] add local like data fetching w/ batcher --- actions/getIdentityData.ts | 1 - .../rpc/[command]/get_user_recommendations.ts | 40 ++++++++ app/api/rpc/[command]/route.ts | 2 + .../[did]/[publication]/[rkey]/CanvasPage.tsx | 3 - .../[rkey]/Interactions/Interactions.tsx | 4 - .../[rkey]/Interactions/recommendAction.ts | 7 +- .../[rkey]/LinearDocumentPage.tsx | 1 - .../[rkey]/PostHeader/PostHeader.tsx | 1 - .../[publication]/[rkey]/getPostPageData.ts | 17 ---- .../dashboard/PublishedPostsLists.tsx | 1 - app/lish/[did]/[publication]/page.tsx | 1 - components/InteractionsPreview.tsx | 2 - components/PostListing.tsx | 1 - components/RecommendButton.tsx | 98 +++++++++++++++---- contexts/DocumentContext.tsx | 1 - package-lock.json | 16 +++ package.json | 1 + 17 files changed, 142 insertions(+), 55 deletions(-) create mode 100644 app/api/rpc/[command]/get_user_recommendations.ts diff --git a/actions/getIdentityData.ts b/actions/getIdentityData.ts index c83a287c..6ed8e690 100644 --- a/actions/getIdentityData.ts +++ b/actions/getIdentityData.ts @@ -42,7 +42,6 @@ export async function uncachedGetIdentityData() { .eq("confirmed", true) .single() : null; - console.log(auth_res); if (!auth_res?.data?.identities) return null; if (auth_res.data.identities.atp_did) { //I should create a relationship table so I can do this in the above query 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/[did]/[publication]/[rkey]/CanvasPage.tsx b/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx index 03fb8cab..05c9e1c1 100644 --- a/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx @@ -72,7 +72,6 @@ export function CanvasPage({ commentsCount={getCommentCount(document.comments_on_documents, pageId)} quotesCount={getQuoteCount(document.quotesAndMentions, pageId)} recommendsCount={document.recommendsCount} - hasRecommended={document.hasRecommended} /> { let isMobile = useIsMobile(); return ( @@ -221,7 +219,6 @@ const CanvasMetadata = (props: { quotesCount={props.quotesCount || 0} commentsCount={props.commentsCount || 0} recommendsCount={props.recommendsCount} - hasRecommended={props.hasRecommended} showComments={props.preferences.showComments !== false} showMentions={props.preferences.showMentions !== false} pageId={props.pageId} diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx index 42b22cb9..54d68574 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx @@ -107,7 +107,6 @@ export const Interactions = (props: { quotesCount: number; commentsCount: number; recommendsCount: number; - hasRecommended: boolean; className?: string; showComments: boolean; showMentions: boolean; @@ -138,7 +137,6 @@ export const Interactions = (props: { {props.quotesCount === 0 || props.showMentions === false ? null : ( @@ -177,7 +175,6 @@ export const ExpandedInteractions = (props: { quotesCount: number; commentsCount: number; recommendsCount: number; - hasRecommended: boolean; className?: string; showComments: boolean; showMentions: boolean; @@ -238,7 +235,6 @@ export const ExpandedInteractions = (props: { {props.quotesCount === 0 || !props.showMentions ? null : ( + )} + {/*MENTIONS BUTTON*/} {props.quotesCount === 0 || props.showMentions === false ? null : ( )} - {props.showComments === false ? null : ( - - )} + + + {tagCount > 0 && }
); }; @@ -209,9 +217,6 @@ export const ExpandedInteractions = (props: { (s) => s.identity === identity.atp_did, ); - let isAuthor = - identity && identity.atp_did === publication?.identity_did && leafletId; - return (
) : ( <> -
+
{props.quotesCount === 0 || !props.showMentions ? null : ( - + )} {!props.showComments ? null : ( - + Comment + )}
@@ -388,7 +399,7 @@ const EditButton = (props: { return ( Edit Post diff --git a/app/lish/[did]/[publication]/page.tsx b/app/lish/[did]/[publication]/page.tsx index 1f82469d..4a369f00 100644 --- a/app/lish/[did]/[publication]/page.tsx +++ b/app/lish/[did]/[publication]/page.tsx @@ -148,7 +148,7 @@ export default async function Publication(props: {

-
+

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

- {comments > 0 || quotes > 0 || tags.length > 0 ? ( - - ) : ( - "" - )} + - {tagsCount === 0 ? null : ( - <> - - {interactionsAvailable || props.share ? ( - - ) : null} - - )} - {props.commentsCount} )} - {interactionsAvailable && props.share ? ( - - ) : null} + {tagsCount === 0 ? null : ( + <> + {interactionsAvailable ? : null} + + + )} {props.share && ( <> + + )} - + {props.showRecommends === false ? null : ( + + )} {tagCount > 0 && }
@@ -186,6 +190,7 @@ export const ExpandedInteractions = (props: { className?: string; showComments: boolean; showMentions: boolean; + showRecommends: boolean; pageId?: string; }) => { const { @@ -208,7 +213,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 && @@ -237,11 +243,13 @@ export const ExpandedInteractions = (props: { ) : ( <>
- + {props.showRecommends === false ? null : ( + + )} {props.quotesCount === 0 || !props.showMentions ? null : ( { diff --git a/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx b/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx index 32ada105..04b9d7d9 100644 --- a/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx @@ -87,6 +87,7 @@ export function LinearDocumentPage({ pageId={pageId} showComments={preferences.showComments !== false} showMentions={preferences.showMentions !== false} + showRecommends={preferences.showRecommends !== false} commentsCount={ getCommentCount(document.comments_on_documents, pageId) || 0 } diff --git a/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx b/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx index 8db38856..7a0f8ab0 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx @@ -18,7 +18,11 @@ 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; + }; }) { let { identity } = useIdentityData(); let document = props.data; @@ -87,6 +91,7 @@ export function PostHeader(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 4a369f00..457f38b2 100644 --- a/app/lish/[did]/[publication]/page.tsx +++ b/app/lish/[did]/[publication]/page.tsx @@ -175,6 +175,9 @@ export default async function Publication(props: { showMentions={ record?.preferences?.showMentions !== false } + showRecommends={ + record?.preferences?.showRecommends !== false + } />
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/InteractionsPreview.tsx b/components/InteractionsPreview.tsx index 46f91183..9f393f86 100644 --- a/components/InteractionsPreview.tsx +++ b/components/InteractionsPreview.tsx @@ -18,22 +18,26 @@ export const InteractionPreview = (props: { 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 (
- + {props.showRecommends === false ? null : ( + + )} {!props.showMentions || props.quotesCount === 0 ? null : ( { tags={tags} showComments={pubRecord?.preferences?.showComments !== false} showMentions={pubRecord?.preferences?.showMentions !== false} + showRecommends={ + pubRecord?.preferences?.showRecommends !== false + } share />
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: { From af5a7173e91577e3916568b0bdd62378dd2b654c Mon Sep 17 00:00:00 2001 From: celine Date: Wed, 28 Jan 2026 20:11:10 -0500 Subject: [PATCH 17/20] made some chnages to canvas layout to better match --- .../[did]/[publication]/[rkey]/CanvasPage.tsx | 1 + .../[rkey]/Interactions/Interactions.tsx | 45 ++++++------ .../[rkey]/PostHeader/PostHeader.tsx | 25 ++++--- components/Canvas.tsx | 21 +++++- components/Pages/PublicationMetadata.tsx | 68 ++++++++++++------- 5 files changed, 102 insertions(+), 58 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx b/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx index 12b30135..74e3763b 100644 --- a/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx @@ -238,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 f2645ef9..2c72bc0e 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx @@ -140,19 +140,11 @@ export const Interactions = (props: {
- {/*COMMENT BUTTON*/} - {props.showComments === false ? null : ( - + {props.showRecommends === false ? null : ( + )} {/*MENTIONS BUTTON*/} @@ -171,14 +163,27 @@ export const Interactions = (props: { {props.quotesCount} )} - {props.showRecommends === false ? null : ( - + {/*COMMENT BUTTON*/} + {props.showComments === false ? null : ( + + )} + + {tagCount > 0 && ( + <> + interactionsAvailable && + + )} - - {tagCount > 0 && }
); }; diff --git a/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx b/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx index 7a0f8ab0..1159683a 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx @@ -23,6 +23,7 @@ export function PostHeader(props: { showMentions?: boolean; showRecommends?: boolean; }; + isCanvas?: boolean; }) { let { identity } = useIdentityData(); let document = props.data; @@ -88,16 +89,20 @@ export function PostHeader(props: { ) : null}
- + {!props.isCanvas && ( + + )} } /> 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/Pages/PublicationMetadata.tsx b/components/Pages/PublicationMetadata.tsx index 3d2ef959..2caa7d15 100644 --- a/components/Pages/PublicationMetadata.tsx +++ b/components/Pages/PublicationMetadata.tsx @@ -20,10 +20,15 @@ import { TagSelector } from "components/Tags"; 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"; -export const PublicationMetadata = () => { +export const PublicationMetadata = (props: { noInteractions?: boolean }) => { let { rep } = useReplicache(); - let { data: pub, normalizedDocument, normalizedPublication } = useLeafletPublicationData(); + let { + data: pub, + normalizedDocument, + normalizedPublication, + } = useLeafletPublicationData(); let { identity } = useIdentityData(); let title = useSubscribe(rep, (tx) => tx.get("publication_title")); let description = useSubscribe(rep, (tx) => @@ -114,27 +119,37 @@ export const PublicationMetadata = () => { ) : (

Draft

)} -
- {tags && ( - <> - - {normalizedPublication?.preferences?.showMentions !== false || - normalizedPublication?.preferences?.showComments !== false ? ( - - ) : null} - - )} - {normalizedPublication?.preferences?.showMentions !== false && ( -
- — -
- )} - {normalizedPublication?.preferences?.showComments !== false && ( -
- — -
- )} -
+ {!props.noInteractions && ( +
+ {normalizedPublication?.preferences?.showRecommends !== false && ( +
+ — +
+ )} + + {normalizedPublication?.preferences?.showMentions !== false && ( +
+ — +
+ )} + {normalizedPublication?.preferences?.showComments !== false && ( +
+ — +
+ )} + {tags && ( + <> + {normalizedPublication?.preferences?.showRecommends !== + false || + normalizedPublication?.preferences?.showMentions !== false || + normalizedPublication?.preferences?.showComments !== false ? ( + + ) : null} + + + )} +
+ )} } /> @@ -238,7 +253,7 @@ export const PublicationMetadataPreview = () => { ); }; -const AddTags = () => { +export const AddTags = () => { let { data: pub, normalizedDocument } = useLeafletPublicationData(); let { rep } = useReplicache(); @@ -251,7 +266,10 @@ const AddTags = () => { let tags: string[] = []; if (Array.isArray(replicacheTags)) { tags = replicacheTags; - } else if (normalizedDocument?.tags && Array.isArray(normalizedDocument.tags)) { + } else if ( + normalizedDocument?.tags && + Array.isArray(normalizedDocument.tags) + ) { tags = normalizedDocument.tags as string[]; } From f79adca6dd009bb1d40deb2c9f3906d0aa74a3d8 Mon Sep 17 00:00:00 2001 From: celine Date: Wed, 28 Jan 2026 20:25:16 -0500 Subject: [PATCH 18/20] `add a little smoker to the recommend acion to keep it cute --- components/RecommendButton.tsx | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/components/RecommendButton.tsx b/components/RecommendButton.tsx index f2f94bef..00ed41a4 100644 --- a/components/RecommendButton.tsx +++ b/components/RecommendButton.tsx @@ -9,7 +9,7 @@ import { unrecommendAction, } from "app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction"; import { callRPC } from "app/api/rpc/client"; -import { useToaster } from "./Toast"; +import { useSmoker, useToaster } from "./Toast"; import { OAuthErrorMessage, isOAuthSessionError } from "./OAuthError"; import { ButtonSecondary } from "./Buttons"; import { Separator } from "./Layout"; @@ -67,12 +67,13 @@ export function RecommendButton(props: { boolean | null >(null); const toaster = useToaster(); + const smoker = useSmoker(); // Use optimistic state if set, otherwise use fetched state const displayRecommended = optimisticRecommended !== null ? optimisticRecommended : hasRecommended; - const handleClick = async () => { + const handleClick = async (e: React.MouseEvent) => { if (isPending || isLoading) return; const currentlyRecommended = displayRecommended; @@ -80,10 +81,19 @@ export function RecommendButton(props: { setOptimisticRecommended(!currentlyRecommended); setCount((c) => (currentlyRecommended ? c - 1 : c + 1)); + if (!currentlyRecommended) { + smoker({ + position: { + x: e.clientX, + y: e.clientY - 16, + }, + text:
thanks!
, + }); + } + const result = currentlyRecommended ? await unrecommendAction({ document: props.documentUri }) : await recommendAction({ document: props.documentUri }); - if (!result.success) { // Revert optimistic update setOptimisticRecommended(null); @@ -94,7 +104,7 @@ export function RecommendButton(props: { content: isOAuthSessionError(result.error) ? ( ) : ( - "Failed to update recommendation" + "oh no! error!" ), type: "error", }); @@ -113,7 +123,7 @@ export function RecommendButton(props: { onClick={(e) => { e.preventDefault(); e.stopPropagation(); - handleClick(); + handleClick(e); }} > {displayRecommended ? ( @@ -142,7 +152,7 @@ export function RecommendButton(props: { onClick={(e) => { e.preventDefault(); e.stopPropagation(); - handleClick(); + handleClick(e); }} disabled={isPending || isLoading} className={`recommendButton relative flex gap-1 items-center hover:text-accent-contrast ${props.className || ""}`} From 3a8a45d1af642b3dda74f61c58a843f02459a92a Mon Sep 17 00:00:00 2001 From: celine Date: Wed, 4 Feb 2026 00:31:51 -0500 Subject: [PATCH 19/20] copy fixes --- .../[rkey]/Interactions/Interactions.tsx | 17 +++++------------ components/RecommendButton.tsx | 2 +- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx index 2c72bc0e..245f3d75 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx @@ -180,7 +180,7 @@ export const Interactions = (props: { {tagCount > 0 && ( <> - interactionsAvailable && + {interactionsAvailable && } )} @@ -272,16 +272,9 @@ export const ExpandedInteractions = (props: { aria-label="Post quotes" > {props.quotesCount} - {props.quotesCount > 0 && ( - <> - {props.quotesCount} - - - )} - Mention - {`Mention${props.quotesCount === 1 ? "" : "s"}`} + {props.quotesCount} + + Mention{props.quotesCount > 1 ? "" : "s"} )} {!props.showComments ? null : ( @@ -309,7 +302,7 @@ export const ExpandedInteractions = (props: { )} - Comment + Comment{props.commentsCount > 1 ? "" : "s"} )}
diff --git a/components/RecommendButton.tsx b/components/RecommendButton.tsx index 00ed41a4..2eddd776 100644 --- a/components/RecommendButton.tsx +++ b/components/RecommendButton.tsx @@ -87,7 +87,7 @@ export function RecommendButton(props: { x: e.clientX, y: e.clientY - 16, }, - text:
thanks!
, + text:
recc'd!
, }); } From 24f409c1a14837b1067bad48bd03bfe3f6f69c2b Mon Sep 17 00:00:00 2001 From: celine Date: Wed, 4 Feb 2026 23:52:42 -0500 Subject: [PATCH 20/20] spacing fixes --- app/lish/Subscribe.tsx | 4 ++- .../[rkey]/Interactions/Interactions.tsx | 29 +++++++++---------- components/RecommendButton.tsx | 4 +-- 3 files changed, 19 insertions(+), 18 deletions(-) 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]/Interactions/Interactions.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx index 245f3d75..36713943 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx @@ -138,7 +138,7 @@ export const Interactions = (props: { return (
{props.showRecommends === false ? null : ( { if (!drawerOpen || drawer !== "quotes") openInteractionDrawer("quotes", document_uri, props.pageId); @@ -166,7 +166,7 @@ export const Interactions = (props: { {/*COMMENT BUTTON*/} {props.showComments === false ? null : (
); diff --git a/components/RecommendButton.tsx b/components/RecommendButton.tsx index 2eddd776..12cdd260 100644 --- a/components/RecommendButton.tsx +++ b/components/RecommendButton.tsx @@ -87,7 +87,7 @@ export function RecommendButton(props: { x: e.clientX, y: e.clientY - 16, }, - text:
recc'd!
, + text:
Recc'd!
, }); } @@ -142,7 +142,7 @@ export function RecommendButton(props: { )} - {displayRecommended ? "You recommend!" : "Recommend"} + {displayRecommended ? "Recommended!" : "Recommend"}
);