a tool for shared writing and social publishing

Compare changes

Choose any two refs to compare.

+6 -6
actions/publishToPublication.ts
··· 305 if (!b) return []; 306 let block: PubLeafletPagesLinearDocument.Block = { 307 $type: "pub.leaflet.pages.linearDocument#block", 308 - alignment, 309 block: b, 310 }; 311 return [block]; 312 } else { 313 let block: PubLeafletPagesLinearDocument.Block = { ··· 405 let [stringValue, facets] = getBlockContent(b.value); 406 let block: $Typed<PubLeafletBlocksHeader.Main> = { 407 $type: "pub.leaflet.blocks.header", 408 - level: headingLevel?.data.value || 1, 409 plaintext: stringValue, 410 facets, 411 }; ··· 438 let block: $Typed<PubLeafletBlocksIframe.Main> = { 439 $type: "pub.leaflet.blocks.iframe", 440 url: url.data.value, 441 - height: height?.data.value || 600, 442 }; 443 return block; 444 } ··· 452 $type: "pub.leaflet.blocks.image", 453 image: blobref, 454 aspectRatio: { 455 - height: image.data.height, 456 - width: image.data.width, 457 }, 458 alt: altText ? altText.data.value : undefined, 459 }; ··· 770 image: blob.data.blob, 771 repeat: backgroundImageRepeat?.data.value ? true : false, 772 ...(backgroundImageRepeat?.data.value && { 773 - width: backgroundImageRepeat.data.value, 774 }), 775 }; 776 }
··· 305 if (!b) return []; 306 let block: PubLeafletPagesLinearDocument.Block = { 307 $type: "pub.leaflet.pages.linearDocument#block", 308 block: b, 309 }; 310 + if (alignment) block.alignment = alignment; 311 return [block]; 312 } else { 313 let block: PubLeafletPagesLinearDocument.Block = { ··· 405 let [stringValue, facets] = getBlockContent(b.value); 406 let block: $Typed<PubLeafletBlocksHeader.Main> = { 407 $type: "pub.leaflet.blocks.header", 408 + level: Math.floor(headingLevel?.data.value || 1), 409 plaintext: stringValue, 410 facets, 411 }; ··· 438 let block: $Typed<PubLeafletBlocksIframe.Main> = { 439 $type: "pub.leaflet.blocks.iframe", 440 url: url.data.value, 441 + height: Math.floor(height?.data.value || 600), 442 }; 443 return block; 444 } ··· 452 $type: "pub.leaflet.blocks.image", 453 image: blobref, 454 aspectRatio: { 455 + height: Math.floor(image.data.height), 456 + width: Math.floor(image.data.width), 457 }, 458 alt: altText ? altText.data.value : undefined, 459 }; ··· 770 image: blob.data.blob, 771 repeat: backgroundImageRepeat?.data.value ? true : false, 772 ...(backgroundImageRepeat?.data.value && { 773 + width: Math.floor(backgroundImageRepeat.data.value), 774 }), 775 }; 776 }
+3 -1
app/(home-pages)/discover/page.tsx
··· 17 return ( 18 <DashboardLayout 19 id="discover" 20 currentPage="discover" 21 defaultTab="default" 22 actions={null} ··· 31 } 32 33 const DiscoverContent = async (props: { order: string }) => { 34 - const orderValue = props.order === "popular" ? "popular" : "recentlyUpdated"; 35 let { publications, nextCursor } = await getPublications(orderValue); 36 37 return (
··· 17 return ( 18 <DashboardLayout 19 id="discover" 20 + cardBorderHidden={false} 21 currentPage="discover" 22 defaultTab="default" 23 actions={null} ··· 32 } 33 34 const DiscoverContent = async (props: { order: string }) => { 35 + const orderValue = 36 + props.order === "popular" ? "popular" : "recentlyUpdated"; 37 let { publications, nextCursor } = await getPublications(orderValue); 38 39 return (
+8
app/(home-pages)/home/HomeLayout.tsx
··· 20 useDashboardState, 21 } from "components/PageLayouts/DashboardLayout"; 22 import { Actions } from "./Actions/Actions"; 23 import { GetLeafletDataReturnType } from "app/api/rpc/[command]/get_leaflet_data"; 24 import { useState } from "react"; 25 import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; ··· 55 props.entityID, 56 "theme/background-image", 57 ); 58 59 let [searchValue, setSearchValue] = useState(""); 60 let [debouncedSearchValue, setDebouncedSearchValue] = useState(""); ··· 79 return ( 80 <DashboardLayout 81 id="home" 82 currentPage="home" 83 defaultTab="home" 84 actions={<Actions />} ··· 98 <HomeLeafletList 99 titles={props.titles} 100 initialFacts={props.initialFacts} 101 searchValue={debouncedSearchValue} 102 /> 103 ), ··· 113 [root_entity: string]: Fact<Attribute>[]; 114 }; 115 searchValue: string; 116 }) { 117 let { identity } = useIdentityData(); 118 let { data: initialFacts } = useSWR( ··· 166 searchValue={props.searchValue} 167 leaflets={leaflets} 168 titles={initialFacts?.titles || {}} 169 initialFacts={initialFacts?.facts || {}} 170 showPreview 171 /> ··· 186 [root_entity: string]: Fact<Attribute>[]; 187 }; 188 searchValue: string; 189 showPreview?: boolean; 190 }) { 191 let { identity } = useIdentityData(); ··· 231 loggedIn={!!identity} 232 display={display} 233 added_at={added_at} 234 index={index} 235 showPreview={props.showPreview} 236 isHidden={
··· 20 useDashboardState, 21 } from "components/PageLayouts/DashboardLayout"; 22 import { Actions } from "./Actions/Actions"; 23 + import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 24 import { GetLeafletDataReturnType } from "app/api/rpc/[command]/get_leaflet_data"; 25 import { useState } from "react"; 26 import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; ··· 56 props.entityID, 57 "theme/background-image", 58 ); 59 + let cardBorderHidden = !!useCardBorderHidden(props.entityID); 60 61 let [searchValue, setSearchValue] = useState(""); 62 let [debouncedSearchValue, setDebouncedSearchValue] = useState(""); ··· 81 return ( 82 <DashboardLayout 83 id="home" 84 + cardBorderHidden={cardBorderHidden} 85 currentPage="home" 86 defaultTab="home" 87 actions={<Actions />} ··· 101 <HomeLeafletList 102 titles={props.titles} 103 initialFacts={props.initialFacts} 104 + cardBorderHidden={cardBorderHidden} 105 searchValue={debouncedSearchValue} 106 /> 107 ), ··· 117 [root_entity: string]: Fact<Attribute>[]; 118 }; 119 searchValue: string; 120 + cardBorderHidden: boolean; 121 }) { 122 let { identity } = useIdentityData(); 123 let { data: initialFacts } = useSWR( ··· 171 searchValue={props.searchValue} 172 leaflets={leaflets} 173 titles={initialFacts?.titles || {}} 174 + cardBorderHidden={props.cardBorderHidden} 175 initialFacts={initialFacts?.facts || {}} 176 showPreview 177 /> ··· 192 [root_entity: string]: Fact<Attribute>[]; 193 }; 194 searchValue: string; 195 + cardBorderHidden: boolean; 196 showPreview?: boolean; 197 }) { 198 let { identity } = useIdentityData(); ··· 238 loggedIn={!!identity} 239 display={display} 240 added_at={added_at} 241 + cardBorderHidden={props.cardBorderHidden} 242 index={index} 243 showPreview={props.showPreview} 244 isHidden={
+5 -6
app/(home-pages)/home/LeafletList/LeafletListItem.tsx
··· 4 import { useState, useRef, useEffect } from "react"; 5 import { SpeedyLink } from "components/SpeedyLink"; 6 import { useLeafletPublicationStatus } from "components/PageSWRDataProvider"; 7 - import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 8 9 export const LeafletListItem = (props: { 10 archived?: boolean | null; 11 loggedIn: boolean; 12 display: "list" | "grid"; 13 added_at: string; 14 title?: string; 15 index: number; 16 isHidden: boolean; 17 showPreview?: boolean; 18 }) => { 19 - const cardBorderHidden = useCardBorderHidden(); 20 const pubStatus = useLeafletPublicationStatus(); 21 let [isOnScreen, setIsOnScreen] = useState(props.index < 16 ? true : false); 22 let previewRef = useRef<HTMLDivElement | null>(null); ··· 48 ref={previewRef} 49 className={`relative flex gap-3 w-full 50 ${props.isHidden ? "hidden" : "flex"} 51 - ${cardBorderHidden ? "" : "px-2 py-1 block-border hover:outline-border relative"}`} 52 style={{ 53 - backgroundColor: cardBorderHidden 54 ? "transparent" 55 : "rgba(var(--bg-page), var(--bg-page-alpha))", 56 }} ··· 68 loggedIn={props.loggedIn} 69 /> 70 </div> 71 - {cardBorderHidden && ( 72 <hr 73 className="last:hidden border-border-light" 74 style={{ ··· 88 ${props.isHidden ? "hidden" : "flex"} 89 `} 90 style={{ 91 - backgroundColor: cardBorderHidden 92 ? "transparent" 93 : "rgba(var(--bg-page), var(--bg-page-alpha))", 94 }}
··· 4 import { useState, useRef, useEffect } from "react"; 5 import { SpeedyLink } from "components/SpeedyLink"; 6 import { useLeafletPublicationStatus } from "components/PageSWRDataProvider"; 7 8 export const LeafletListItem = (props: { 9 archived?: boolean | null; 10 loggedIn: boolean; 11 display: "list" | "grid"; 12 + cardBorderHidden: boolean; 13 added_at: string; 14 title?: string; 15 index: number; 16 isHidden: boolean; 17 showPreview?: boolean; 18 }) => { 19 const pubStatus = useLeafletPublicationStatus(); 20 let [isOnScreen, setIsOnScreen] = useState(props.index < 16 ? true : false); 21 let previewRef = useRef<HTMLDivElement | null>(null); ··· 47 ref={previewRef} 48 className={`relative flex gap-3 w-full 49 ${props.isHidden ? "hidden" : "flex"} 50 + ${props.cardBorderHidden ? "" : "px-2 py-1 block-border hover:outline-border relative"}`} 51 style={{ 52 + backgroundColor: props.cardBorderHidden 53 ? "transparent" 54 : "rgba(var(--bg-page), var(--bg-page-alpha))", 55 }} ··· 67 loggedIn={props.loggedIn} 68 /> 69 </div> 70 + {props.cardBorderHidden && ( 71 <hr 72 className="last:hidden border-border-light" 73 style={{ ··· 87 ${props.isHidden ? "hidden" : "flex"} 88 `} 89 style={{ 90 + backgroundColor: props.cardBorderHidden 91 ? "transparent" 92 : "rgba(var(--bg-page), var(--bg-page-alpha))", 93 }}
+4 -16
app/(home-pages)/home/LeafletList/LeafletPreview.tsx
··· 18 const firstPage = useEntity(root, "root/page")[0]; 19 const page = firstPage?.data.value || root; 20 21 - const cardBorderHidden = useEntity(root, "theme/card-border-hidden")?.data 22 - .value; 23 const rootBackgroundImage = useEntity(root, "theme/card-background-image"); 24 const rootBackgroundRepeat = useEntity( 25 root, ··· 50 51 const contentWrapperClass = `leafletContentWrapper h-full sm:w-48 w-40 mx-auto overflow-clip ${!cardBorderHidden && "border border-border-light border-b-0 rounded-t-md"}`; 52 53 - return { 54 - root, 55 - page, 56 - cardBorderHidden, 57 - contentWrapperStyle, 58 - contentWrapperClass, 59 - }; 60 } 61 62 export const LeafletListPreview = (props: { isVisible: boolean }) => { 63 - const { 64 - root, 65 - page, 66 - cardBorderHidden, 67 - contentWrapperStyle, 68 - contentWrapperClass, 69 - } = useLeafletPreviewData(); 70 71 return ( 72 <Tooltip
··· 18 const firstPage = useEntity(root, "root/page")[0]; 19 const page = firstPage?.data.value || root; 20 21 + const cardBorderHidden = useCardBorderHidden(root); 22 const rootBackgroundImage = useEntity(root, "theme/card-background-image"); 23 const rootBackgroundRepeat = useEntity( 24 root, ··· 49 50 const contentWrapperClass = `leafletContentWrapper h-full sm:w-48 w-40 mx-auto overflow-clip ${!cardBorderHidden && "border border-border-light border-b-0 rounded-t-md"}`; 51 52 + return { root, page, cardBorderHidden, contentWrapperStyle, contentWrapperClass }; 53 } 54 55 export const LeafletListPreview = (props: { isVisible: boolean }) => { 56 + const { root, page, cardBorderHidden, contentWrapperStyle, contentWrapperClass } = 57 + useLeafletPreviewData(); 58 59 return ( 60 <Tooltip
+6
app/(home-pages)/looseleafs/LooseleafsLayout.tsx
··· 1 "use client"; 2 import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; 3 import { useState } from "react"; 4 import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; 5 import { Fact, PermissionToken } from "src/replicache"; ··· 29 [searchValue], 30 ); 31 32 return ( 33 <DashboardLayout 34 id="looseleafs" 35 currentPage="looseleafs" 36 defaultTab="home" 37 actions={<Actions />} ··· 42 <LooseleafList 43 titles={props.titles} 44 initialFacts={props.initialFacts} 45 searchValue={debouncedSearchValue} 46 /> 47 ), ··· 57 [root_entity: string]: Fact<Attribute>[]; 58 }; 59 searchValue: string; 60 }) => { 61 let { identity } = useIdentityData(); 62 let { data: initialFacts } = useSWR( ··· 103 searchValue={props.searchValue} 104 leaflets={leaflets} 105 titles={initialFacts?.titles || {}} 106 initialFacts={initialFacts?.facts || {}} 107 showPreview 108 />
··· 1 "use client"; 2 import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; 3 + import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 4 import { useState } from "react"; 5 import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; 6 import { Fact, PermissionToken } from "src/replicache"; ··· 30 [searchValue], 31 ); 32 33 + let cardBorderHidden = !!useCardBorderHidden(props.entityID); 34 return ( 35 <DashboardLayout 36 id="looseleafs" 37 + cardBorderHidden={cardBorderHidden} 38 currentPage="looseleafs" 39 defaultTab="home" 40 actions={<Actions />} ··· 45 <LooseleafList 46 titles={props.titles} 47 initialFacts={props.initialFacts} 48 + cardBorderHidden={cardBorderHidden} 49 searchValue={debouncedSearchValue} 50 /> 51 ), ··· 61 [root_entity: string]: Fact<Attribute>[]; 62 }; 63 searchValue: string; 64 + cardBorderHidden: boolean; 65 }) => { 66 let { identity } = useIdentityData(); 67 let { data: initialFacts } = useSWR( ··· 108 searchValue={props.searchValue} 109 leaflets={leaflets} 110 titles={initialFacts?.titles || {}} 111 + cardBorderHidden={props.cardBorderHidden} 112 initialFacts={initialFacts?.facts || {}} 113 showPreview 114 />
+1
app/(home-pages)/notifications/page.tsx
··· 10 return ( 11 <DashboardLayout 12 id="discover" 13 currentPage="notifications" 14 defaultTab="default" 15 actions={null}
··· 10 return ( 11 <DashboardLayout 12 id="discover" 13 + cardBorderHidden={true} 14 currentPage="notifications" 15 defaultTab="default" 16 actions={null}
-88
app/(home-pages)/p/[didOrHandle]/PostsContent.tsx
··· 1 - "use client"; 2 - 3 - import { PostListing } from "components/PostListing"; 4 - import type { Post } from "app/(home-pages)/reader/getReaderFeed"; 5 - import type { Cursor } from "./getProfilePosts"; 6 - import { getProfilePosts } from "./getProfilePosts"; 7 - import useSWRInfinite from "swr/infinite"; 8 - import { useEffect, useRef } from "react"; 9 - 10 - export const ProfilePostsContent = (props: { 11 - did: string; 12 - posts: Post[]; 13 - nextCursor: Cursor | null; 14 - }) => { 15 - const getKey = ( 16 - pageIndex: number, 17 - previousPageData: { 18 - posts: Post[]; 19 - nextCursor: Cursor | null; 20 - } | null, 21 - ) => { 22 - // Reached the end 23 - if (previousPageData && !previousPageData.nextCursor) return null; 24 - 25 - // First page, we don't have previousPageData 26 - if (pageIndex === 0) return ["profile-posts", props.did, null] as const; 27 - 28 - // Add the cursor to the key 29 - return ["profile-posts", props.did, previousPageData?.nextCursor] as const; 30 - }; 31 - 32 - const { data, size, setSize, isValidating } = useSWRInfinite( 33 - getKey, 34 - ([_, did, cursor]) => getProfilePosts(did, cursor), 35 - { 36 - fallbackData: [{ posts: props.posts, nextCursor: props.nextCursor }], 37 - revalidateFirstPage: false, 38 - }, 39 - ); 40 - 41 - const loadMoreRef = useRef<HTMLDivElement>(null); 42 - 43 - // Set up intersection observer to load more when trigger element is visible 44 - useEffect(() => { 45 - const observer = new IntersectionObserver( 46 - (entries) => { 47 - if (entries[0].isIntersecting && !isValidating) { 48 - const hasMore = data && data[data.length - 1]?.nextCursor; 49 - if (hasMore) { 50 - setSize(size + 1); 51 - } 52 - } 53 - }, 54 - { threshold: 0.1 }, 55 - ); 56 - 57 - if (loadMoreRef.current) { 58 - observer.observe(loadMoreRef.current); 59 - } 60 - 61 - return () => observer.disconnect(); 62 - }, [data, size, setSize, isValidating]); 63 - 64 - const allPosts = data ? data.flatMap((page) => page.posts) : []; 65 - 66 - if (allPosts.length === 0 && !isValidating) { 67 - return <div className="text-tertiary text-center py-4">No posts yet</div>; 68 - } 69 - 70 - return ( 71 - <div className="flex flex-col gap-3 text-left relative"> 72 - {allPosts.map((post) => ( 73 - <PostListing key={post.documents.uri} {...post} /> 74 - ))} 75 - {/* Trigger element for loading more posts */} 76 - <div 77 - ref={loadMoreRef} 78 - className="absolute bottom-96 left-0 w-full h-px pointer-events-none" 79 - aria-hidden="true" 80 - /> 81 - {isValidating && ( 82 - <div className="text-center text-tertiary py-4"> 83 - Loading more posts... 84 - </div> 85 - )} 86 - </div> 87 - ); 88 - };
···
-200
app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx
··· 1 - "use client"; 2 - import { Avatar } from "components/Avatar"; 3 - import { AppBskyActorProfile, PubLeafletPublication } from "lexicons/api"; 4 - import { blobRefToSrc } from "src/utils/blobRefToSrc"; 5 - import type { ProfileData } from "./layout"; 6 - import { usePubTheme } from "components/ThemeManager/PublicationThemeProvider"; 7 - import { colorToString } from "components/ThemeManager/useColorAttribute"; 8 - import { PubIcon } from "components/ActionBar/Publications"; 9 - import { Json } from "supabase/database.types"; 10 - import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 11 - import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 12 - import { SpeedyLink } from "components/SpeedyLink"; 13 - import { ReactNode } from "react"; 14 - 15 - export const ProfileHeader = (props: { 16 - profile: ProfileViewDetailed; 17 - publications: { record: Json; uri: string }[]; 18 - popover?: boolean; 19 - }) => { 20 - let profileRecord = props.profile; 21 - const profileUrl = `/p/${props.profile.handle}`; 22 - 23 - const avatarElement = ( 24 - <Avatar 25 - src={profileRecord.avatar} 26 - displayName={profileRecord.displayName} 27 - className="mx-auto mt-3 sm:mt-4" 28 - giant 29 - /> 30 - ); 31 - 32 - const displayNameElement = ( 33 - <h3 className=" px-3 sm:px-4 pt-2 leading-tight"> 34 - {profileRecord.displayName 35 - ? profileRecord.displayName 36 - : `@${props.profile.handle}`} 37 - </h3> 38 - ); 39 - 40 - const handleElement = profileRecord.displayName && ( 41 - <div 42 - className={`text-tertiary ${props.popover ? "text-xs" : "text-sm"} pb-1 italic px-3 sm:px-4 truncate`} 43 - > 44 - @{props.profile.handle} 45 - </div> 46 - ); 47 - 48 - return ( 49 - <div 50 - className={`flex flex-col relative ${props.popover && "text-sm"}`} 51 - id="profile-header" 52 - > 53 - <ProfileLinks handle={props.profile.handle || ""} /> 54 - <div className="flex flex-col"> 55 - <div className="flex flex-col group"> 56 - {props.popover ? ( 57 - <SpeedyLink className={"hover:no-underline!"} href={profileUrl}> 58 - {avatarElement} 59 - </SpeedyLink> 60 - ) : ( 61 - avatarElement 62 - )} 63 - {props.popover ? ( 64 - <SpeedyLink 65 - className={" text-primary group-hover:underline"} 66 - href={profileUrl} 67 - > 68 - {displayNameElement} 69 - </SpeedyLink> 70 - ) : ( 71 - displayNameElement 72 - )} 73 - {props.popover && handleElement ? ( 74 - <SpeedyLink className={"group-hover:underline"} href={profileUrl}> 75 - {handleElement} 76 - </SpeedyLink> 77 - ) : ( 78 - handleElement 79 - )} 80 - </div> 81 - <pre className="text-secondary px-3 sm:px-4 whitespace-pre-wrap"> 82 - {profileRecord.description 83 - ? parseDescription(profileRecord.description) 84 - : null} 85 - </pre> 86 - <div className=" w-full overflow-x-scroll py-3 mb-3 "> 87 - <div 88 - className={`grid grid-flow-col gap-2 mx-auto w-fit px-3 sm:px-4 ${props.popover ? "auto-cols-[164px]" : "auto-cols-[164px] sm:auto-cols-[240px]"}`} 89 - > 90 - {props.publications.map((p) => ( 91 - <PublicationCard 92 - key={p.uri} 93 - record={p.record as PubLeafletPublication.Record} 94 - uri={p.uri} 95 - /> 96 - ))} 97 - </div> 98 - </div> 99 - </div> 100 - </div> 101 - ); 102 - }; 103 - 104 - const ProfileLinks = (props: { handle: string }) => { 105 - return ( 106 - <div className="absolute sm:top-4 top-3 sm:right-4 right-3 flex flex-row gap-2"> 107 - <a 108 - className="text-tertiary hover:text-accent-contrast hover:no-underline!" 109 - href={`https://bsky.app/profile/${props.handle}`} 110 - > 111 - <BlueskyTiny /> 112 - </a> 113 - </div> 114 - ); 115 - }; 116 - const PublicationCard = (props: { 117 - record: PubLeafletPublication.Record; 118 - uri: string; 119 - }) => { 120 - const { record, uri } = props; 121 - const { bgLeaflet, bgPage, primary } = usePubTheme(record.theme); 122 - 123 - return ( 124 - <a 125 - href={`https://${record.base_path}`} 126 - className="border border-border p-2 rounded-lg hover:no-underline! text-primary basis-1/2" 127 - style={{ backgroundColor: `rgb(${colorToString(bgLeaflet, "rgb")})` }} 128 - > 129 - <div 130 - className="rounded-md p-2 flex flex-row gap-2" 131 - style={{ 132 - backgroundColor: record.theme?.showPageBackground 133 - ? `rgb(${colorToString(bgPage, "rgb")})` 134 - : undefined, 135 - }} 136 - > 137 - <PubIcon record={record} uri={uri} /> 138 - <h4 139 - className="truncate min-w-0" 140 - style={{ 141 - color: `rgb(${colorToString(primary, "rgb")})`, 142 - }} 143 - > 144 - {record.name} 145 - </h4> 146 - </div> 147 - </a> 148 - ); 149 - }; 150 - 151 - function parseDescription(description: string): ReactNode[] { 152 - const combinedRegex = /(@\S+|https?:\/\/\S+)/g; 153 - 154 - const parts: ReactNode[] = []; 155 - let lastIndex = 0; 156 - let match; 157 - let key = 0; 158 - 159 - while ((match = combinedRegex.exec(description)) !== null) { 160 - // Add text before this match 161 - if (match.index > lastIndex) { 162 - parts.push(description.slice(lastIndex, match.index)); 163 - } 164 - 165 - const matched = match[0]; 166 - 167 - if (matched.startsWith("@")) { 168 - // It's a mention 169 - const handle = matched.slice(1); 170 - parts.push( 171 - <SpeedyLink key={key++} href={`/p/${handle}`}> 172 - {matched} 173 - </SpeedyLink>, 174 - ); 175 - } else { 176 - // It's a URL 177 - const urlWithoutProtocol = matched 178 - .replace(/^https?:\/\//, "") 179 - .replace(/\/+$/, ""); 180 - const displayText = 181 - urlWithoutProtocol.length > 50 182 - ? urlWithoutProtocol.slice(0, 50) + "โ€ฆ" 183 - : urlWithoutProtocol; 184 - parts.push( 185 - <a key={key++} href={matched} target="_blank" rel="noopener noreferrer"> 186 - {displayText} 187 - </a>, 188 - ); 189 - } 190 - 191 - lastIndex = match.index + matched.length; 192 - } 193 - 194 - // Add remaining text after last match 195 - if (lastIndex < description.length) { 196 - parts.push(description.slice(lastIndex)); 197 - } 198 - 199 - return parts; 200 - }
···
-24
app/(home-pages)/p/[didOrHandle]/ProfileLayout.tsx
··· 1 - "use client"; 2 - 3 - import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 4 - 5 - export function ProfileLayout(props: { children: React.ReactNode }) { 6 - let cardBorderHidden = useCardBorderHidden(); 7 - return ( 8 - <div 9 - id="profile-content" 10 - className={` 11 - ${ 12 - cardBorderHidden 13 - ? "" 14 - : "overflow-y-scroll h-full border border-border-light rounded-lg bg-bg-page" 15 - } 16 - max-w-prose mx-auto w-full 17 - flex flex-col 18 - text-center 19 - `} 20 - > 21 - {props.children} 22 - </div> 23 - ); 24 - }
···
-119
app/(home-pages)/p/[didOrHandle]/ProfileTabs.tsx
··· 1 - "use client"; 2 - 3 - import { SpeedyLink } from "components/SpeedyLink"; 4 - import { useSelectedLayoutSegment } from "next/navigation"; 5 - import { useState, useEffect } from "react"; 6 - import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 7 - 8 - export type ProfileTabType = "posts" | "comments" | "subscriptions"; 9 - 10 - export const ProfileTabs = (props: { didOrHandle: string }) => { 11 - const cardBorderHidden = useCardBorderHidden(); 12 - const segment = useSelectedLayoutSegment(); 13 - const currentTab = (segment || "posts") as ProfileTabType; 14 - const [scrollPosWithinTabContent, setScrollPosWithinTabContent] = useState(0); 15 - const [headerHeight, setHeaderHeight] = useState(0); 16 - useEffect(() => { 17 - let headerHeight = 18 - document.getElementById("profile-header")?.clientHeight || 0; 19 - setHeaderHeight(headerHeight); 20 - 21 - const profileContent = cardBorderHidden 22 - ? document.getElementById("home-content") 23 - : document.getElementById("profile-content"); 24 - const handleScroll = () => { 25 - if (profileContent) { 26 - setScrollPosWithinTabContent( 27 - profileContent.scrollTop - headerHeight > 0 28 - ? profileContent.scrollTop - headerHeight 29 - : 0, 30 - ); 31 - } 32 - }; 33 - 34 - if (profileContent) { 35 - profileContent.addEventListener("scroll", handleScroll); 36 - return () => profileContent.removeEventListener("scroll", handleScroll); 37 - } 38 - }, []); 39 - 40 - const baseUrl = `/p/${props.didOrHandle}`; 41 - const bgColor = cardBorderHidden ? "var(--bg-leaflet)" : "var(--bg-page)"; 42 - 43 - return ( 44 - <div className="flex flex-col w-full sticky top-3 sm:top-4 z-20 sm:px-4 px-3"> 45 - <div 46 - style={ 47 - scrollPosWithinTabContent < 20 48 - ? { 49 - paddingLeft: `calc(${scrollPosWithinTabContent / 20} * 12px )`, 50 - paddingRight: `calc(${scrollPosWithinTabContent / 20} * 12px )`, 51 - } 52 - : { paddingLeft: "12px", paddingRight: "12px" } 53 - } 54 - > 55 - <div 56 - className={` 57 - border rounded-lg 58 - ${scrollPosWithinTabContent > 20 ? "border-border-light" : "border-transparent"} 59 - py-1 60 - w-full `} 61 - style={ 62 - scrollPosWithinTabContent < 20 63 - ? { 64 - backgroundColor: !cardBorderHidden 65 - ? `rgba(${bgColor}, ${scrollPosWithinTabContent / 60 + 0.75})` 66 - : `rgba(${bgColor}, ${scrollPosWithinTabContent / 20})`, 67 - paddingLeft: !cardBorderHidden 68 - ? "4px" 69 - : `calc(${scrollPosWithinTabContent / 20} * 4px)`, 70 - paddingRight: !cardBorderHidden 71 - ? "4px" 72 - : `calc(${scrollPosWithinTabContent / 20} * 4px)`, 73 - } 74 - : { 75 - backgroundColor: `rgb(${bgColor})`, 76 - paddingLeft: "4px", 77 - paddingRight: "4px", 78 - } 79 - } 80 - > 81 - <div className="flex gap-2 justify-between"> 82 - <div className="flex gap-2"> 83 - <TabLink 84 - href={baseUrl} 85 - name="Posts" 86 - selected={currentTab === "posts"} 87 - /> 88 - <TabLink 89 - href={`${baseUrl}/comments`} 90 - name="Comments" 91 - selected={currentTab === "comments"} 92 - /> 93 - </div> 94 - <TabLink 95 - href={`${baseUrl}/subscriptions`} 96 - name="Subscriptions" 97 - selected={currentTab === "subscriptions"} 98 - /> 99 - </div> 100 - </div> 101 - </div> 102 - </div> 103 - ); 104 - }; 105 - 106 - const TabLink = (props: { href: string; name: string; selected: boolean }) => { 107 - return ( 108 - <SpeedyLink 109 - href={props.href} 110 - className={`pubTabs px-1 py-0 flex gap-1 items-center rounded-md hover:cursor-pointer hover:no-underline! ${ 111 - props.selected 112 - ? "text-accent-2 bg-accent-1 font-bold -mb-px" 113 - : "text-tertiary" 114 - }`} 115 - > 116 - {props.name} 117 - </SpeedyLink> 118 - ); 119 - };
···
-219
app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx
··· 1 - "use client"; 2 - 3 - import { useEffect, useRef, useMemo } from "react"; 4 - import useSWRInfinite from "swr/infinite"; 5 - import { AppBskyActorProfile, AtUri } from "@atproto/api"; 6 - import { PubLeafletComment, PubLeafletDocument } from "lexicons/api"; 7 - import { ReplyTiny } from "components/Icons/ReplyTiny"; 8 - import { Avatar } from "components/Avatar"; 9 - import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock"; 10 - import { blobRefToSrc } from "src/utils/blobRefToSrc"; 11 - import { 12 - getProfileComments, 13 - type ProfileComment, 14 - type Cursor, 15 - } from "./getProfileComments"; 16 - import { timeAgo } from "src/utils/timeAgo"; 17 - import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 18 - 19 - export const ProfileCommentsContent = (props: { 20 - did: string; 21 - comments: ProfileComment[]; 22 - nextCursor: Cursor | null; 23 - }) => { 24 - const getKey = ( 25 - pageIndex: number, 26 - previousPageData: { 27 - comments: ProfileComment[]; 28 - nextCursor: Cursor | null; 29 - } | null, 30 - ) => { 31 - // Reached the end 32 - if (previousPageData && !previousPageData.nextCursor) return null; 33 - 34 - // First page, we don't have previousPageData 35 - if (pageIndex === 0) return ["profile-comments", props.did, null] as const; 36 - 37 - // Add the cursor to the key 38 - return [ 39 - "profile-comments", 40 - props.did, 41 - previousPageData?.nextCursor, 42 - ] as const; 43 - }; 44 - 45 - const { data, size, setSize, isValidating } = useSWRInfinite( 46 - getKey, 47 - ([_, did, cursor]) => getProfileComments(did, cursor), 48 - { 49 - fallbackData: [ 50 - { comments: props.comments, nextCursor: props.nextCursor }, 51 - ], 52 - revalidateFirstPage: false, 53 - }, 54 - ); 55 - 56 - const loadMoreRef = useRef<HTMLDivElement>(null); 57 - 58 - // Set up intersection observer to load more when trigger element is visible 59 - useEffect(() => { 60 - const observer = new IntersectionObserver( 61 - (entries) => { 62 - if (entries[0].isIntersecting && !isValidating) { 63 - const hasMore = data && data[data.length - 1]?.nextCursor; 64 - if (hasMore) { 65 - setSize(size + 1); 66 - } 67 - } 68 - }, 69 - { threshold: 0.1 }, 70 - ); 71 - 72 - if (loadMoreRef.current) { 73 - observer.observe(loadMoreRef.current); 74 - } 75 - 76 - return () => observer.disconnect(); 77 - }, [data, size, setSize, isValidating]); 78 - 79 - const allComments = data ? data.flatMap((page) => page.comments) : []; 80 - 81 - if (allComments.length === 0 && !isValidating) { 82 - return ( 83 - <div className="text-tertiary text-center py-4">No comments yet</div> 84 - ); 85 - } 86 - 87 - return ( 88 - <div className="flex flex-col gap-2 text-left relative"> 89 - {allComments.map((comment) => ( 90 - <CommentItem key={comment.uri} comment={comment} /> 91 - ))} 92 - {/* Trigger element for loading more comments */} 93 - <div 94 - ref={loadMoreRef} 95 - className="absolute bottom-96 left-0 w-full h-px pointer-events-none" 96 - aria-hidden="true" 97 - /> 98 - {isValidating && ( 99 - <div className="text-center text-tertiary py-4"> 100 - Loading more comments... 101 - </div> 102 - )} 103 - </div> 104 - ); 105 - }; 106 - 107 - const CommentItem = ({ comment }: { comment: ProfileComment }) => { 108 - const record = comment.record as PubLeafletComment.Record; 109 - const profile = comment.bsky_profiles?.record as 110 - | AppBskyActorProfile.Record 111 - | undefined; 112 - const displayName = 113 - profile?.displayName || comment.bsky_profiles?.handle || "Unknown"; 114 - 115 - // Get commenter DID from comment URI 116 - const commenterDid = new AtUri(comment.uri).host; 117 - 118 - const isReply = !!record.reply; 119 - 120 - // Get document title 121 - const docData = comment.document?.data as 122 - | PubLeafletDocument.Record 123 - | undefined; 124 - const postTitle = docData?.title || "Untitled"; 125 - 126 - // Get parent comment info for replies 127 - const parentRecord = comment.parentComment?.record as 128 - | PubLeafletComment.Record 129 - | undefined; 130 - const parentProfile = comment.parentComment?.bsky_profiles?.record as 131 - | AppBskyActorProfile.Record 132 - | undefined; 133 - const parentDisplayName = 134 - parentProfile?.displayName || comment.parentComment?.bsky_profiles?.handle; 135 - 136 - // Build direct link to the comment 137 - const commentLink = useMemo(() => { 138 - if (!comment.document) return null; 139 - const docUri = new AtUri(comment.document.uri); 140 - 141 - // Get base URL using getPublicationURL if publication exists, otherwise build path 142 - let baseUrl: string; 143 - if (comment.publication) { 144 - baseUrl = getPublicationURL(comment.publication); 145 - const pubUri = new AtUri(comment.publication.uri); 146 - // If getPublicationURL returns a relative path, append the document rkey 147 - if (baseUrl.startsWith("/")) { 148 - baseUrl = `${baseUrl}/${docUri.rkey}`; 149 - } else { 150 - // For custom domains, append the document rkey 151 - baseUrl = `${baseUrl}/${docUri.rkey}`; 152 - } 153 - } else { 154 - baseUrl = `/lish/${docUri.host}/-/${docUri.rkey}`; 155 - } 156 - 157 - // Build query parameters 158 - const params = new URLSearchParams(); 159 - params.set("interactionDrawer", "comments"); 160 - if (record.onPage) { 161 - params.set("page", record.onPage); 162 - } 163 - 164 - // Use comment URI as hash for direct reference 165 - const commentId = encodeURIComponent(comment.uri); 166 - 167 - return `${baseUrl}?${params.toString()}#${commentId}`; 168 - }, [comment.document, comment.publication, comment.uri, record.onPage]); 169 - 170 - // Get avatar source 171 - const avatarSrc = profile?.avatar?.ref 172 - ? blobRefToSrc(profile.avatar.ref, commenterDid) 173 - : undefined; 174 - 175 - return ( 176 - <div id={comment.uri} className="w-full flex flex-col text-left mb-8"> 177 - <div className="flex gap-2 w-full"> 178 - <Avatar src={avatarSrc} displayName={displayName} /> 179 - <div className="flex flex-col w-full min-w-0 grow"> 180 - <div className="flex flex-row gap-2"> 181 - <div className="text-tertiary text-sm truncate"> 182 - <span className="font-bold text-secondary">{displayName}</span>{" "} 183 - {isReply ? "replied" : "commented"} on{" "} 184 - {commentLink ? ( 185 - <a 186 - href={commentLink} 187 - className="italic text-accent-contrast hover:underline" 188 - > 189 - {postTitle} 190 - </a> 191 - ) : ( 192 - <span className="italic text-accent-contrast">{postTitle}</span> 193 - )} 194 - </div> 195 - </div> 196 - {isReply && parentRecord && ( 197 - <div className="text-xs text-tertiary flex flex-row gap-2 w-full my-0.5 items-center"> 198 - <ReplyTiny className="shrink-0 scale-75" /> 199 - {parentDisplayName && ( 200 - <div className="font-bold shrink-0">{parentDisplayName}</div> 201 - )} 202 - <div className="grow truncate">{parentRecord.plaintext}</div> 203 - </div> 204 - )} 205 - <pre 206 - style={{ wordBreak: "break-word" }} 207 - className="whitespace-pre-wrap text-secondary" 208 - > 209 - <BaseTextBlock 210 - index={[]} 211 - plaintext={record.plaintext} 212 - facets={record.facets} 213 - /> 214 - </pre> 215 - </div> 216 - </div> 217 - </div> 218 - ); 219 - };
···
-133
app/(home-pages)/p/[didOrHandle]/comments/getProfileComments.ts
··· 1 - "use server"; 2 - 3 - import { supabaseServerClient } from "supabase/serverClient"; 4 - import { Json } from "supabase/database.types"; 5 - import { PubLeafletComment } from "lexicons/api"; 6 - 7 - export type Cursor = { 8 - indexed_at: string; 9 - uri: string; 10 - }; 11 - 12 - export type ProfileComment = { 13 - uri: string; 14 - record: Json; 15 - indexed_at: string; 16 - bsky_profiles: { record: Json; handle: string | null } | null; 17 - document: { 18 - uri: string; 19 - data: Json; 20 - } | null; 21 - publication: { 22 - uri: string; 23 - record: Json; 24 - } | null; 25 - // For replies, include the parent comment info 26 - parentComment: { 27 - uri: string; 28 - record: Json; 29 - bsky_profiles: { record: Json; handle: string | null } | null; 30 - } | null; 31 - }; 32 - 33 - export async function getProfileComments( 34 - did: string, 35 - cursor?: Cursor | null, 36 - ): Promise<{ comments: ProfileComment[]; nextCursor: Cursor | null }> { 37 - const limit = 20; 38 - 39 - let query = supabaseServerClient 40 - .from("comments_on_documents") 41 - .select( 42 - `*, 43 - bsky_profiles(record, handle), 44 - documents(uri, data, documents_in_publications(publications(*)))`, 45 - ) 46 - .eq("profile", did) 47 - .order("indexed_at", { ascending: false }) 48 - .order("uri", { ascending: false }) 49 - .limit(limit); 50 - 51 - if (cursor) { 52 - query = query.or( 53 - `indexed_at.lt.${cursor.indexed_at},and(indexed_at.eq.${cursor.indexed_at},uri.lt.${cursor.uri})`, 54 - ); 55 - } 56 - 57 - const { data: rawComments } = await query; 58 - 59 - if (!rawComments || rawComments.length === 0) { 60 - return { comments: [], nextCursor: null }; 61 - } 62 - 63 - // Collect parent comment URIs for replies 64 - const parentUris = rawComments 65 - .map((c) => (c.record as PubLeafletComment.Record).reply?.parent) 66 - .filter((uri): uri is string => !!uri); 67 - 68 - // Fetch parent comments if there are any replies 69 - let parentCommentsMap = new Map< 70 - string, 71 - { 72 - uri: string; 73 - record: Json; 74 - bsky_profiles: { record: Json; handle: string | null } | null; 75 - } 76 - >(); 77 - 78 - if (parentUris.length > 0) { 79 - const { data: parentComments } = await supabaseServerClient 80 - .from("comments_on_documents") 81 - .select(`uri, record, bsky_profiles(record, handle)`) 82 - .in("uri", parentUris); 83 - 84 - if (parentComments) { 85 - for (const pc of parentComments) { 86 - parentCommentsMap.set(pc.uri, { 87 - uri: pc.uri, 88 - record: pc.record, 89 - bsky_profiles: pc.bsky_profiles, 90 - }); 91 - } 92 - } 93 - } 94 - 95 - // Transform to ProfileComment format 96 - const comments: ProfileComment[] = rawComments.map((comment) => { 97 - const record = comment.record as PubLeafletComment.Record; 98 - const doc = comment.documents; 99 - const pub = doc?.documents_in_publications?.[0]?.publications; 100 - 101 - return { 102 - uri: comment.uri, 103 - record: comment.record, 104 - indexed_at: comment.indexed_at, 105 - bsky_profiles: comment.bsky_profiles, 106 - document: doc 107 - ? { 108 - uri: doc.uri, 109 - data: doc.data, 110 - } 111 - : null, 112 - publication: pub 113 - ? { 114 - uri: pub.uri, 115 - record: pub.record, 116 - } 117 - : null, 118 - parentComment: record.reply?.parent 119 - ? parentCommentsMap.get(record.reply.parent) || null 120 - : null, 121 - }; 122 - }); 123 - 124 - const nextCursor = 125 - comments.length === limit 126 - ? { 127 - indexed_at: comments[comments.length - 1].indexed_at, 128 - uri: comments[comments.length - 1].uri, 129 - } 130 - : null; 131 - 132 - return { comments, nextCursor }; 133 - }
···
-28
app/(home-pages)/p/[didOrHandle]/comments/page.tsx
··· 1 - import { idResolver } from "app/(home-pages)/reader/idResolver"; 2 - import { getProfileComments } from "./getProfileComments"; 3 - import { ProfileCommentsContent } from "./CommentsContent"; 4 - 5 - export default async function ProfileCommentsPage(props: { 6 - params: Promise<{ didOrHandle: string }>; 7 - }) { 8 - let params = await props.params; 9 - let didOrHandle = decodeURIComponent(params.didOrHandle); 10 - 11 - // Resolve handle to DID if necessary 12 - let did = didOrHandle; 13 - if (!didOrHandle.startsWith("did:")) { 14 - let resolved = await idResolver.handle.resolve(didOrHandle); 15 - if (!resolved) return null; 16 - did = resolved; 17 - } 18 - 19 - const { comments, nextCursor } = await getProfileComments(did); 20 - 21 - return ( 22 - <ProfileCommentsContent 23 - did={did} 24 - comments={comments} 25 - nextCursor={nextCursor} 26 - /> 27 - ); 28 - }
···
-95
app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts
··· 1 - "use server"; 2 - 3 - import { supabaseServerClient } from "supabase/serverClient"; 4 - import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 5 - import type { Post } from "app/(home-pages)/reader/getReaderFeed"; 6 - 7 - export type Cursor = { 8 - indexed_at: string; 9 - uri: string; 10 - }; 11 - 12 - export async function getProfilePosts( 13 - did: string, 14 - cursor?: Cursor | null, 15 - ): Promise<{ posts: Post[]; nextCursor: Cursor | null }> { 16 - const limit = 20; 17 - 18 - let query = supabaseServerClient 19 - .from("documents") 20 - .select( 21 - `*, 22 - comments_on_documents(count), 23 - document_mentions_in_bsky(count), 24 - documents_in_publications(publications(*))`, 25 - ) 26 - .like("uri", `at://${did}/%`) 27 - .order("indexed_at", { ascending: false }) 28 - .order("uri", { ascending: false }) 29 - .limit(limit); 30 - 31 - if (cursor) { 32 - query = query.or( 33 - `indexed_at.lt.${cursor.indexed_at},and(indexed_at.eq.${cursor.indexed_at},uri.lt.${cursor.uri})`, 34 - ); 35 - } 36 - 37 - let [{ data: docs }, { data: pubs }, { data: profile }] = await Promise.all([ 38 - query, 39 - supabaseServerClient 40 - .from("publications") 41 - .select("*") 42 - .eq("identity_did", did), 43 - supabaseServerClient 44 - .from("bsky_profiles") 45 - .select("handle") 46 - .eq("did", did) 47 - .single(), 48 - ]); 49 - 50 - // Build a map of publications for quick lookup 51 - let pubMap = new Map<string, NonNullable<typeof pubs>[number]>(); 52 - for (let pub of pubs || []) { 53 - pubMap.set(pub.uri, pub); 54 - } 55 - 56 - // Transform data to Post[] format 57 - let handle = profile?.handle ? `@${profile.handle}` : null; 58 - let posts: Post[] = []; 59 - 60 - for (let doc of docs || []) { 61 - let pubFromDoc = doc.documents_in_publications?.[0]?.publications; 62 - let pub = pubFromDoc ? pubMap.get(pubFromDoc.uri) || pubFromDoc : null; 63 - 64 - let post: Post = { 65 - author: handle, 66 - documents: { 67 - data: doc.data, 68 - uri: doc.uri, 69 - indexed_at: doc.indexed_at, 70 - comments_on_documents: doc.comments_on_documents, 71 - document_mentions_in_bsky: doc.document_mentions_in_bsky, 72 - }, 73 - }; 74 - 75 - if (pub) { 76 - post.publication = { 77 - href: getPublicationURL(pub), 78 - pubRecord: pub.record, 79 - uri: pub.uri, 80 - }; 81 - } 82 - 83 - posts.push(post); 84 - } 85 - 86 - const nextCursor = 87 - posts.length === limit 88 - ? { 89 - indexed_at: posts[posts.length - 1].documents.indexed_at, 90 - uri: posts[posts.length - 1].documents.uri, 91 - } 92 - : null; 93 - 94 - return { posts, nextCursor }; 95 - }
···
-77
app/(home-pages)/p/[didOrHandle]/layout.tsx
··· 1 - import { idResolver } from "app/(home-pages)/reader/idResolver"; 2 - import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; 3 - import { supabaseServerClient } from "supabase/serverClient"; 4 - import { Json } from "supabase/database.types"; 5 - import { ProfileHeader } from "./ProfileHeader"; 6 - import { ProfileTabs } from "./ProfileTabs"; 7 - import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; 8 - import { ProfileLayout } from "./ProfileLayout"; 9 - import { Agent } from "@atproto/api"; 10 - import { get_profile_data } from "app/api/rpc/[command]/get_profile_data"; 11 - 12 - export default async function ProfilePageLayout(props: { 13 - params: Promise<{ didOrHandle: string }>; 14 - children: React.ReactNode; 15 - }) { 16 - let params = await props.params; 17 - let didOrHandle = decodeURIComponent(params.didOrHandle); 18 - 19 - // Resolve handle to DID if necessary 20 - let did = didOrHandle; 21 - 22 - if (!didOrHandle.startsWith("did:")) { 23 - let resolved = await idResolver.handle.resolve(didOrHandle); 24 - if (!resolved) { 25 - return ( 26 - <NotFoundLayout> 27 - <p className="font-bold">Sorry, can&apos;t resolve handle!</p> 28 - <p> 29 - This may be a glitch on our end. If the issue persists please{" "} 30 - <a href="mailto:contact@leaflet.pub">send us a note</a>. 31 - </p> 32 - </NotFoundLayout> 33 - ); 34 - } 35 - did = resolved; 36 - } 37 - let profileData = await get_profile_data.handler( 38 - { didOrHandle: did }, 39 - { supabase: supabaseServerClient }, 40 - ); 41 - let { publications, profile } = profileData.result; 42 - 43 - if (!profile) return null; 44 - 45 - return ( 46 - <DashboardLayout 47 - id="profile" 48 - defaultTab="default" 49 - currentPage="profile" 50 - actions={null} 51 - tabs={{ 52 - default: { 53 - controls: null, 54 - content: ( 55 - <ProfileLayout> 56 - <ProfileHeader 57 - profile={profile} 58 - publications={publications || []} 59 - /> 60 - <ProfileTabs didOrHandle={params.didOrHandle} /> 61 - <div className="h-full pt-3 pb-4 px-3 sm:px-4 flex flex-col"> 62 - {props.children} 63 - </div> 64 - </ProfileLayout> 65 - ), 66 - }, 67 - }} 68 - /> 69 - ); 70 - } 71 - 72 - export type ProfileData = { 73 - did: string; 74 - handle: string | null; 75 - indexed_at: string; 76 - record: Json; 77 - };
···
-24
app/(home-pages)/p/[didOrHandle]/page.tsx
··· 1 - import { idResolver } from "app/(home-pages)/reader/idResolver"; 2 - import { getProfilePosts } from "./getProfilePosts"; 3 - import { ProfilePostsContent } from "./PostsContent"; 4 - 5 - export default async function ProfilePostsPage(props: { 6 - params: Promise<{ didOrHandle: string }>; 7 - }) { 8 - let params = await props.params; 9 - let didOrHandle = decodeURIComponent(params.didOrHandle); 10 - 11 - // Resolve handle to DID if necessary 12 - let did = didOrHandle; 13 - if (!didOrHandle.startsWith("did:")) { 14 - let resolved = await idResolver.handle.resolve(didOrHandle); 15 - if (!resolved) return null; 16 - did = resolved; 17 - } 18 - 19 - const { posts, nextCursor } = await getProfilePosts(did); 20 - 21 - return ( 22 - <ProfilePostsContent did={did} posts={posts} nextCursor={nextCursor} /> 23 - ); 24 - }
···
-103
app/(home-pages)/p/[didOrHandle]/subscriptions/SubscriptionsContent.tsx
··· 1 - "use client"; 2 - 3 - import { useEffect, useRef } from "react"; 4 - import useSWRInfinite from "swr/infinite"; 5 - import { PubListing } from "app/(home-pages)/discover/PubListing"; 6 - import { 7 - getSubscriptions, 8 - type PublicationSubscription, 9 - } from "app/(home-pages)/reader/getSubscriptions"; 10 - import { Cursor } from "app/(home-pages)/reader/getReaderFeed"; 11 - 12 - export const ProfileSubscriptionsContent = (props: { 13 - did: string; 14 - subscriptions: PublicationSubscription[]; 15 - nextCursor: Cursor | null; 16 - }) => { 17 - const getKey = ( 18 - pageIndex: number, 19 - previousPageData: { 20 - subscriptions: PublicationSubscription[]; 21 - nextCursor: Cursor | null; 22 - } | null, 23 - ) => { 24 - // Reached the end 25 - if (previousPageData && !previousPageData.nextCursor) return null; 26 - 27 - // First page, we don't have previousPageData 28 - if (pageIndex === 0) 29 - return ["profile-subscriptions", props.did, null] as const; 30 - 31 - // Add the cursor to the key 32 - return [ 33 - "profile-subscriptions", 34 - props.did, 35 - previousPageData?.nextCursor, 36 - ] as const; 37 - }; 38 - 39 - const { data, size, setSize, isValidating } = useSWRInfinite( 40 - getKey, 41 - ([_, did, cursor]) => getSubscriptions(did, cursor), 42 - { 43 - fallbackData: [ 44 - { subscriptions: props.subscriptions, nextCursor: props.nextCursor }, 45 - ], 46 - revalidateFirstPage: false, 47 - }, 48 - ); 49 - 50 - const loadMoreRef = useRef<HTMLDivElement>(null); 51 - 52 - // Set up intersection observer to load more when trigger element is visible 53 - useEffect(() => { 54 - const observer = new IntersectionObserver( 55 - (entries) => { 56 - if (entries[0].isIntersecting && !isValidating) { 57 - const hasMore = data && data[data.length - 1]?.nextCursor; 58 - if (hasMore) { 59 - setSize(size + 1); 60 - } 61 - } 62 - }, 63 - { threshold: 0.1 }, 64 - ); 65 - 66 - if (loadMoreRef.current) { 67 - observer.observe(loadMoreRef.current); 68 - } 69 - 70 - return () => observer.disconnect(); 71 - }, [data, size, setSize, isValidating]); 72 - 73 - const allSubscriptions = data 74 - ? data.flatMap((page) => page.subscriptions) 75 - : []; 76 - 77 - if (allSubscriptions.length === 0 && !isValidating) { 78 - return ( 79 - <div className="text-tertiary text-center py-4">No subscriptions yet</div> 80 - ); 81 - } 82 - 83 - return ( 84 - <div className="relative"> 85 - <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3"> 86 - {allSubscriptions.map((sub) => ( 87 - <PubListing key={sub.uri} {...sub} /> 88 - ))} 89 - </div> 90 - {/* Trigger element for loading more subscriptions */} 91 - <div 92 - ref={loadMoreRef} 93 - className="absolute bottom-96 left-0 w-full h-px pointer-events-none" 94 - aria-hidden="true" 95 - /> 96 - {isValidating && ( 97 - <div className="text-center text-tertiary py-4"> 98 - Loading more subscriptions... 99 - </div> 100 - )} 101 - </div> 102 - ); 103 - };
···
-28
app/(home-pages)/p/[didOrHandle]/subscriptions/page.tsx
··· 1 - import { idResolver } from "app/(home-pages)/reader/idResolver"; 2 - import { getSubscriptions } from "app/(home-pages)/reader/getSubscriptions"; 3 - import { ProfileSubscriptionsContent } from "./SubscriptionsContent"; 4 - 5 - export default async function ProfileSubscriptionsPage(props: { 6 - params: Promise<{ didOrHandle: string }>; 7 - }) { 8 - const params = await props.params; 9 - const didOrHandle = decodeURIComponent(params.didOrHandle); 10 - 11 - // Resolve handle to DID if necessary 12 - let did = didOrHandle; 13 - if (!didOrHandle.startsWith("did:")) { 14 - const resolved = await idResolver.handle.resolve(didOrHandle); 15 - if (!resolved) return null; 16 - did = resolved; 17 - } 18 - 19 - const { subscriptions, nextCursor } = await getSubscriptions(did); 20 - 21 - return ( 22 - <ProfileSubscriptionsContent 23 - did={did} 24 - subscriptions={subscriptions} 25 - nextCursor={nextCursor} 26 - /> 27 - ); 28 - }
···
+1 -1
app/(home-pages)/reader/SubscriptionsContent.tsx
··· 32 33 const { data, error, size, setSize, isValidating } = useSWRInfinite( 34 getKey, 35 - ([_, cursor]) => getSubscriptions(null, cursor), 36 { 37 fallbackData: [ 38 { subscriptions: props.publications, nextCursor: props.nextCursor },
··· 32 33 const { data, error, size, setSize, isValidating } = useSWRInfinite( 34 getKey, 35 + ([_, cursor]) => getSubscriptions(cursor), 36 { 37 fallbackData: [ 38 { subscriptions: props.publications, nextCursor: props.nextCursor },
+1 -1
app/(home-pages)/reader/getReaderFeed.ts
··· 83 84 export type Post = { 85 author: string | null; 86 - publication?: { 87 href: string; 88 pubRecord: Json; 89 uri: string;
··· 83 84 export type Post = { 85 author: string | null; 86 + publication: { 87 href: string; 88 pubRecord: Json; 89 uri: string;
+4 -13
app/(home-pages)/reader/getSubscriptions.ts
··· 8 import { idResolver } from "./idResolver"; 9 import { Cursor } from "./getReaderFeed"; 10 11 - export async function getSubscriptions( 12 - did?: string | null, 13 - cursor?: Cursor | null, 14 - ): Promise<{ 15 nextCursor: null | Cursor; 16 subscriptions: PublicationSubscription[]; 17 }> { 18 - // If no DID provided, use logged-in user's DID 19 - let identity = did; 20 - if (!identity) { 21 - const auth_res = await getIdentityData(); 22 - if (!auth_res?.atp_did) return { subscriptions: [], nextCursor: null }; 23 - identity = auth_res.atp_did; 24 - } 25 - 26 let query = supabaseServerClient 27 .from("publication_subscriptions") 28 .select(`*, publications(*, documents_in_publications(*, documents(*)))`) ··· 34 }) 35 .limit(1, { referencedTable: "publications.documents_in_publications" }) 36 .limit(25) 37 - .eq("identity", identity); 38 39 if (cursor) { 40 query = query.or(
··· 8 import { idResolver } from "./idResolver"; 9 import { Cursor } from "./getReaderFeed"; 10 11 + export async function getSubscriptions(cursor?: Cursor | null): Promise<{ 12 nextCursor: null | Cursor; 13 subscriptions: PublicationSubscription[]; 14 }> { 15 + let auth_res = await getIdentityData(); 16 + if (!auth_res?.atp_did) return { subscriptions: [], nextCursor: null }; 17 let query = supabaseServerClient 18 .from("publication_subscriptions") 19 .select(`*, publications(*, documents_in_publications(*, documents(*)))`) ··· 25 }) 26 .limit(1, { referencedTable: "publications.documents_in_publications" }) 27 .limit(25) 28 + .eq("identity", auth_res.atp_did); 29 30 if (cursor) { 31 query = query.or(
+1
app/(home-pages)/reader/page.tsx
··· 12 return ( 13 <DashboardLayout 14 id="reader" 15 currentPage="reader" 16 defaultTab="Read" 17 actions={null}
··· 12 return ( 13 <DashboardLayout 14 id="reader" 15 + cardBorderHidden={false} 16 currentPage="reader" 17 defaultTab="Read" 18 actions={null}
+1 -4
app/(home-pages)/tag/[tag]/getDocumentsByTag.ts
··· 10 export async function getDocumentsByTag( 11 tag: string, 12 ): Promise<{ posts: Post[] }> { 13 - // Normalize tag to lowercase for case-insensitive search 14 - const normalizedTag = tag.toLowerCase(); 15 - 16 // Query documents that have this tag 17 const { data: documents, error } = await supabaseServerClient 18 .from("documents") ··· 22 document_mentions_in_bsky(count), 23 documents_in_publications(publications(*))`, 24 ) 25 - .contains("data->tags", `["${normalizedTag}"]`) 26 .order("indexed_at", { ascending: false }) 27 .limit(50); 28
··· 10 export async function getDocumentsByTag( 11 tag: string, 12 ): Promise<{ posts: Post[] }> { 13 // Query documents that have this tag 14 const { data: documents, error } = await supabaseServerClient 15 .from("documents") ··· 19 document_mentions_in_bsky(count), 20 documents_in_publications(publications(*))`, 21 ) 22 + .contains("data->tags", `["${tag}"]`) 23 .order("indexed_at", { ascending: false }) 24 .limit(50); 25
+1
app/(home-pages)/tag/[tag]/page.tsx
··· 14 return ( 15 <DashboardLayout 16 id="tag" 17 currentPage="tag" 18 defaultTab="default" 19 actions={null}
··· 14 return ( 15 <DashboardLayout 16 id="tag" 17 + cardBorderHidden={false} 18 currentPage="tag" 19 defaultTab="default" 20 actions={null}
-69
app/api/rpc/[command]/get_profile_data.ts
··· 1 - import { z } from "zod"; 2 - import { makeRoute } from "../lib"; 3 - import type { Env } from "./route"; 4 - import { idResolver } from "app/(home-pages)/reader/idResolver"; 5 - import { supabaseServerClient } from "supabase/serverClient"; 6 - import { Agent } from "@atproto/api"; 7 - import { getIdentityData } from "actions/getIdentityData"; 8 - import { createOauthClient } from "src/atproto-oauth"; 9 - 10 - export type GetProfileDataReturnType = Awaited< 11 - ReturnType<(typeof get_profile_data)["handler"]> 12 - >; 13 - 14 - export const get_profile_data = makeRoute({ 15 - route: "get_profile_data", 16 - input: z.object({ 17 - didOrHandle: z.string(), 18 - }), 19 - handler: async ({ didOrHandle }, { supabase }: Pick<Env, "supabase">) => { 20 - // Resolve handle to DID if necessary 21 - let did = didOrHandle; 22 - 23 - if (!didOrHandle.startsWith("did:")) { 24 - const resolved = await idResolver.handle.resolve(didOrHandle); 25 - if (!resolved) { 26 - throw new Error("Could not resolve handle to DID"); 27 - } 28 - did = resolved; 29 - } 30 - let agent; 31 - let authed_identity = await getIdentityData(); 32 - if (authed_identity?.atp_did) { 33 - try { 34 - const oauthClient = await createOauthClient(); 35 - let credentialSession = await oauthClient.restore( 36 - authed_identity.atp_did, 37 - ); 38 - agent = new Agent(credentialSession); 39 - } catch (e) { 40 - agent = new Agent({ 41 - service: "https://public.api.bsky.app", 42 - }); 43 - } 44 - } else { 45 - agent = new Agent({ 46 - service: "https://public.api.bsky.app", 47 - }); 48 - } 49 - 50 - let profileReq = agent.app.bsky.actor.getProfile({ actor: did }); 51 - 52 - let publicationsReq = supabase 53 - .from("publications") 54 - .select("*") 55 - .eq("identity_did", did); 56 - 57 - let [{ data: profile }, { data: publications }] = await Promise.all([ 58 - profileReq, 59 - publicationsReq, 60 - ]); 61 - 62 - return { 63 - result: { 64 - profile, 65 - publications: publications || [], 66 - }, 67 - }; 68 - }, 69 - });
···
-2
app/api/rpc/[command]/route.ts
··· 13 import { get_publication_data } from "./get_publication_data"; 14 import { search_publication_names } from "./search_publication_names"; 15 import { search_publication_documents } from "./search_publication_documents"; 16 - import { get_profile_data } from "./get_profile_data"; 17 18 let supabase = createClient<Database>( 19 process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, ··· 40 get_publication_data, 41 search_publication_names, 42 search_publication_documents, 43 - get_profile_data, 44 ]; 45 export async function POST( 46 req: Request,
··· 13 import { get_publication_data } from "./get_publication_data"; 14 import { search_publication_names } from "./search_publication_names"; 15 import { search_publication_documents } from "./search_publication_documents"; 16 17 let supabase = createClient<Database>( 18 process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, ··· 39 get_publication_data, 40 search_publication_names, 41 search_publication_documents, 42 ]; 43 export async function POST( 44 req: Request,
+4 -22
app/globals.css
··· 296 @apply py-[1.5px]; 297 } 298 299 - .ProseMirror:focus-within .selection-highlight { 300 - background-color: transparent; 301 - } 302 - 303 .ProseMirror .atMention.ProseMirror-selectednode, 304 .ProseMirror .didMention.ProseMirror-selectednode { 305 - @apply text-accent-contrast; 306 - @apply px-0.5; 307 - @apply -mx-[3px]; /* extra px to account for the border*/ 308 - @apply -my-px; /*to account for the border*/ 309 - @apply rounded-[4px]; 310 - @apply box-decoration-clone; 311 - background-color: rgba(var(--accent-contrast), 0.2); 312 - border: 1px solid rgba(var(--accent-contrast), 1); 313 } 314 315 - .mention { 316 - @apply cursor-pointer; 317 - @apply text-accent-contrast; 318 - @apply px-0.5; 319 - @apply -mx-[3px]; 320 - @apply -my-px; /*to account for the border*/ 321 - @apply rounded-[4px]; 322 - @apply box-decoration-clone; 323 - background-color: rgba(var(--accent-contrast), 0.2); 324 - border: 1px solid transparent; 325 } 326 327 .multiselected:focus-within .selection-highlight {
··· 296 @apply py-[1.5px]; 297 } 298 299 + /* Underline mention nodes when selected in ProseMirror */ 300 .ProseMirror .atMention.ProseMirror-selectednode, 301 .ProseMirror .didMention.ProseMirror-selectednode { 302 + text-decoration: underline; 303 } 304 305 + .ProseMirror:focus-within .selection-highlight { 306 + background-color: transparent; 307 } 308 309 .multiselected:focus-within .selection-highlight {
+13 -10
app/lish/[did]/[publication]/[rkey]/BaseTextBlock.tsx
··· 2 import { PubLeafletRichtextFacet } from "lexicons/api"; 3 import { didToBlueskyUrl } from "src/utils/mentionUtils"; 4 import { AtMentionLink } from "components/AtMentionLink"; 5 - import { ProfilePopover } from "components/ProfilePopover"; 6 7 type Facet = PubLeafletRichtextFacet.Main; 8 export function BaseTextBlock(props: { ··· 28 let isDidMention = segment.facet?.find( 29 PubLeafletRichtextFacet.isDidMention, 30 ); 31 - let isAtMention = segment.facet?.find(PubLeafletRichtextFacet.isAtMention); 32 let isUnderline = segment.facet?.find(PubLeafletRichtextFacet.isUnderline); 33 let isItalic = segment.facet?.find(PubLeafletRichtextFacet.isItalic); 34 let isHighlighted = segment.facet?.find( ··· 44 ${isHighlighted ? "highlight bg-highlight-1" : ""}`.replaceAll("\n", " "); 45 46 // Split text by newlines and insert <br> tags 47 - const textParts = segment.text.split("\n"); 48 const renderedText = textParts.flatMap((part, i) => 49 - i < textParts.length - 1 50 - ? [part, <br key={`br-${counter}-${i}`} />] 51 - : [part], 52 ); 53 54 if (isCode) { ··· 59 ); 60 } else if (isDidMention) { 61 children.push( 62 - <ProfilePopover 63 key={counter} 64 - didOrHandle={isDidMention.did} 65 - trigger={<span className="mention">{renderedText}</span>} 66 - />, 67 ); 68 } else if (isAtMention) { 69 children.push(
··· 2 import { PubLeafletRichtextFacet } from "lexicons/api"; 3 import { didToBlueskyUrl } from "src/utils/mentionUtils"; 4 import { AtMentionLink } from "components/AtMentionLink"; 5 6 type Facet = PubLeafletRichtextFacet.Main; 7 export function BaseTextBlock(props: { ··· 27 let isDidMention = segment.facet?.find( 28 PubLeafletRichtextFacet.isDidMention, 29 ); 30 + let isAtMention = segment.facet?.find( 31 + PubLeafletRichtextFacet.isAtMention, 32 + ); 33 let isUnderline = segment.facet?.find(PubLeafletRichtextFacet.isUnderline); 34 let isItalic = segment.facet?.find(PubLeafletRichtextFacet.isItalic); 35 let isHighlighted = segment.facet?.find( ··· 45 ${isHighlighted ? "highlight bg-highlight-1" : ""}`.replaceAll("\n", " "); 46 47 // Split text by newlines and insert <br> tags 48 + const textParts = segment.text.split('\n'); 49 const renderedText = textParts.flatMap((part, i) => 50 + i < textParts.length - 1 ? [part, <br key={`br-${counter}-${i}`} />] : [part] 51 ); 52 53 if (isCode) { ··· 58 ); 59 } else if (isDidMention) { 60 children.push( 61 + <a 62 key={counter} 63 + href={didToBlueskyUrl(isDidMention.did)} 64 + className={`text-accent-contrast hover:underline cursor-pointer ${className}`} 65 + target="_blank" 66 + rel="noopener noreferrer" 67 + > 68 + {renderedText} 69 + </a>, 70 ); 71 } else if (isAtMention) { 72 children.push(
+1
app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx
··· 57 <PageWrapper 58 pageType="canvas" 59 fullPageScroll={fullPageScroll} 60 id={pageId ? `post-page-${pageId}` : "post-page"} 61 drawerOpen={ 62 !!drawer && (pageId ? drawer.pageId === pageId : !drawer.pageId)
··· 57 <PageWrapper 58 pageType="canvas" 59 fullPageScroll={fullPageScroll} 60 + cardBorderHidden={!hasPageBackground} 61 id={pageId ? `post-page-${pageId}` : "post-page"} 62 drawerOpen={ 63 !!drawer && (pageId ? drawer.pageId === pageId : !drawer.pageId)
+2 -5
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/CommentBox.tsx
··· 1 - import { AtUri, UnicodeString } from "@atproto/api"; 2 import { autolink } from "components/Blocks/TextBlock/autolink-plugin"; 3 import { multiBlockSchema } from "components/Blocks/TextBlock/schema"; 4 import { PubLeafletRichtextFacet } from "lexicons/api"; ··· 196 { 197 record: comment.record, 198 uri: comment.uri, 199 - bsky_profiles: { 200 - record: comment.profile as Json, 201 - did: new AtUri(comment.uri).host, 202 - }, 203 }, 204 ], 205 }));
··· 1 + import { UnicodeString } from "@atproto/api"; 2 import { autolink } from "components/Blocks/TextBlock/autolink-plugin"; 3 import { multiBlockSchema } from "components/Blocks/TextBlock/schema"; 4 import { PubLeafletRichtextFacet } from "lexicons/api"; ··· 196 { 197 record: comment.record, 198 uri: comment.uri, 199 + bsky_profiles: { record: comment.profile as Json }, 200 }, 201 ], 202 }));
+99 -15
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
··· 18 import { QuoteContent } from "../Quotes"; 19 import { timeAgo } from "src/utils/timeAgo"; 20 import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 21 - import { ProfilePopover } from "components/ProfilePopover"; 22 23 export type Comment = { 24 record: Json; 25 uri: string; 26 - bsky_profiles: { record: Json; did: string } | null; 27 }; 28 export function Comments(props: { 29 document_uri: string; ··· 110 document: string; 111 comment: Comment; 112 comments: Comment[]; 113 - profile: AppBskyActorProfile.Record; 114 record: PubLeafletComment.Record; 115 pageId?: string; 116 }) => { 117 - const did = props.comment.bsky_profiles?.did; 118 - 119 return ( 120 - <div id={props.comment.uri} className="comment"> 121 <div className="flex gap-2"> 122 - {did && ( 123 - <ProfilePopover 124 - didOrHandle={did} 125 - trigger={ 126 - <div className="text-sm text-tertiary font-bold hover:underline"> 127 - {props.profile.displayName} 128 - </div> 129 - } 130 - /> 131 )} 132 </div> 133 {props.record.attachment && 134 PubLeafletComment.isLinearDocumentQuote(props.record.attachment) && ( ··· 300 </Popover> 301 ); 302 };
··· 18 import { QuoteContent } from "../Quotes"; 19 import { timeAgo } from "src/utils/timeAgo"; 20 import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 21 22 export type Comment = { 23 record: Json; 24 uri: string; 25 + bsky_profiles: { record: Json } | null; 26 }; 27 export function Comments(props: { 28 document_uri: string; ··· 109 document: string; 110 comment: Comment; 111 comments: Comment[]; 112 + profile?: AppBskyActorProfile.Record; 113 record: PubLeafletComment.Record; 114 pageId?: string; 115 }) => { 116 return ( 117 + <div className="comment"> 118 <div className="flex gap-2"> 119 + {props.profile && ( 120 + <ProfilePopover profile={props.profile} comment={props.comment.uri} /> 121 )} 122 + <DatePopover date={props.record.createdAt} /> 123 </div> 124 {props.record.attachment && 125 PubLeafletComment.isLinearDocumentQuote(props.record.attachment) && ( ··· 291 </Popover> 292 ); 293 }; 294 + 295 + const ProfilePopover = (props: { 296 + profile: AppBskyActorProfile.Record; 297 + comment: string; 298 + }) => { 299 + let commenterId = new AtUri(props.comment).host; 300 + 301 + return ( 302 + <> 303 + <a 304 + className="font-bold text-tertiary text-sm hover:underline" 305 + href={`https://bsky.app/profile/${commenterId}`} 306 + > 307 + {props.profile.displayName} 308 + </a> 309 + {/*<Media mobile={false}> 310 + <Popover 311 + align="start" 312 + trigger={ 313 + <div 314 + onMouseOver={() => { 315 + setHovering(true); 316 + hoverTimeout.current = window.setTimeout(() => { 317 + setLoadProfile(true); 318 + }, 500); 319 + }} 320 + onMouseOut={() => { 321 + setHovering(false); 322 + clearTimeout(hoverTimeout.current); 323 + }} 324 + className="font-bold text-tertiary text-sm hover:underline" 325 + > 326 + {props.profile.displayName} 327 + </div> 328 + } 329 + className="max-w-sm" 330 + > 331 + {profile && ( 332 + <> 333 + <div className="profilePopover text-sm flex gap-2"> 334 + <div className="w-5 h-5 bg-test rounded-full shrink-0 mt-[2px]" /> 335 + <div className="flex flex-col"> 336 + <div className="flex justify-between"> 337 + <div className="profileHeader flex gap-2 items-center"> 338 + <div className="font-bold">celine</div> 339 + <a className="text-tertiary" href="/"> 340 + @{profile.handle} 341 + </a> 342 + </div> 343 + </div> 344 + 345 + <div className="profileBio text-secondary "> 346 + {profile.description} 347 + </div> 348 + <div className="flex flex-row gap-2 items-center pt-2 font-bold"> 349 + {!profile.viewer?.following ? ( 350 + <div className="text-tertiary bg-border-light rounded-md px-1 py-0"> 351 + Following 352 + </div> 353 + ) : ( 354 + <ButtonPrimary compact className="text-sm"> 355 + Follow <BlueskyTiny /> 356 + </ButtonPrimary> 357 + )} 358 + {profile.viewer?.followedBy && ( 359 + <div className="text-tertiary">Follows You</div> 360 + )} 361 + </div> 362 + </div> 363 + </div> 364 + 365 + <hr className="my-2 border-border-light" /> 366 + <div className="flex gap-2 leading-tight items-center text-tertiary text-sm"> 367 + <div className="flex flex-col w-6 justify-center"> 368 + {profile.viewer?.knownFollowers?.followers.map((follower) => { 369 + return ( 370 + <div 371 + className="w-[18px] h-[18px] bg-test rounded-full border-2 border-bg-page" 372 + key={follower.did} 373 + /> 374 + ); 375 + })} 376 + <div className="w-[18px] h-[18px] bg-test rounded-full -mt-2 border-2 border-bg-page" /> 377 + <div className="w-[18px] h-[18px] bg-test rounded-full -mt-2 border-2 border-bg-page" /> 378 + </div> 379 + </div> 380 + </> 381 + )} 382 + </Popover> 383 + </Media>*/} 384 + </> 385 + ); 386 + };
+1 -4
app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx
··· 58 export const useDrawerOpen = (uri: string) => { 59 let params = useSearchParams(); 60 let interactionDrawerSearchParam = params.get("interactionDrawer"); 61 - let pageParam = params.get("page"); 62 let { drawerOpen: open, drawer, pageId } = useInteractionState(uri); 63 if (open === false || (open === undefined && !interactionDrawerSearchParam)) 64 return null; 65 drawer = 66 drawer || (interactionDrawerSearchParam as InteractionState["drawer"]); 67 - // Use pageId from state, or fall back to page search param 68 - const resolvedPageId = pageId ?? pageParam ?? undefined; 69 - return { drawer, pageId: resolvedPageId }; 70 };
··· 58 export const useDrawerOpen = (uri: string) => { 59 let params = useSearchParams(); 60 let interactionDrawerSearchParam = params.get("interactionDrawer"); 61 let { drawerOpen: open, drawer, pageId } = useInteractionState(uri); 62 if (open === false || (open === undefined && !interactionDrawerSearchParam)) 63 return null; 64 drawer = 65 drawer || (interactionDrawerSearchParam as InteractionState["drawer"]); 66 + return { drawer, pageId }; 67 };
+1
app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx
··· 61 <PageWrapper 62 pageType="doc" 63 fullPageScroll={fullPageScroll} 64 id={pageId ? `post-page-${pageId}` : "post-page"} 65 drawerOpen={ 66 !!drawer && (pageId ? drawer.pageId === pageId : !drawer.pageId)
··· 61 <PageWrapper 62 pageType="doc" 63 fullPageScroll={fullPageScroll} 64 + cardBorderHidden={!hasPageBackground} 65 id={pageId ? `post-page-${pageId}` : "post-page"} 66 drawerOpen={ 67 !!drawer && (pageId ? drawer.pageId === pageId : !drawer.pageId)
+1 -1
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 324 return ( 325 // all this margin stuff is a highly unfortunate hack so that the border-l on blockquote is the height of just the text rather than the height of the block, which includes padding. 326 <blockquote 327 - className={`blockquoteBlock py-0! mb-2! ${className} ${PubLeafletBlocksBlockquote.isMain(previousBlock?.block) ? "-mt-2! pt-3!" : "mt-1!"}`} 328 {...blockProps} 329 > 330 <TextBlock
··· 324 return ( 325 // all this margin stuff is a highly unfortunate hack so that the border-l on blockquote is the height of just the text rather than the height of the block, which includes padding. 326 <blockquote 327 + className={`blockquote py-0! mb-2! ${className} ${PubLeafletBlocksBlockquote.isMain(previousBlock?.block) ? "-mt-2! pt-3!" : "mt-1!"}`} 328 {...blockProps} 329 > 330 <TextBlock
+2 -2
app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx
··· 119 {props.postTitle ? props.postTitle : "Untitled"} 120 </h2> 121 {props.postDescription ? ( 122 - <div className="postDescription italic text-secondary outline-hidden bg-transparent pt-1"> 123 {props.postDescription} 124 - </div> 125 ) : null} 126 <div className="postInfo text-sm text-tertiary pt-3 flex gap-1 flex-wrap justify-between"> 127 {props.postInfo}
··· 119 {props.postTitle ? props.postTitle : "Untitled"} 120 </h2> 121 {props.postDescription ? ( 122 + <p className="postDescription italic text-secondary outline-hidden bg-transparent pt-1"> 123 {props.postDescription} 124 + </p> 125 ) : null} 126 <div className="postInfo text-sm text-tertiary pt-3 flex gap-1 flex-wrap justify-between"> 127 {props.postInfo}
+10 -27
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 19 import { Fragment, useEffect } from "react"; 20 import { flushSync } from "react-dom"; 21 import { scrollIntoView } from "src/utils/scrollIntoView"; 22 - import { useParams, useSearchParams } from "next/navigation"; 23 import { decodeQuotePosition } from "./quotePosition"; 24 import { PollData } from "./fetchPollData"; 25 import { LinearDocumentPage } from "./LinearDocumentPage"; ··· 32 33 export const useOpenPages = () => { 34 const { quote } = useParams(); 35 - const searchParams = useSearchParams(); 36 - const pageParam = searchParams.get("page"); 37 const state = usePostPageUIState((s) => s); 38 39 - if (!state.initialized) { 40 - // Check for page search param first (for comment links) 41 - if (pageParam) { 42 - return [pageParam]; 43 - } 44 - // Then check for quote param 45 - if (quote) { 46 - const decodedQuote = decodeQuotePosition(quote as string); 47 - if (decodedQuote?.pageId) { 48 - return [decodedQuote.pageId]; 49 - } 50 } 51 } 52 ··· 55 56 export const useInitializeOpenPages = () => { 57 const { quote } = useParams(); 58 - const searchParams = useSearchParams(); 59 - const pageParam = searchParams.get("page"); 60 61 useEffect(() => { 62 const state = usePostPageUIState.getState(); 63 if (!state.initialized) { 64 - // Check for page search param first (for comment links) 65 - if (pageParam) { 66 - usePostPageUIState.setState({ 67 - pages: [pageParam], 68 - initialized: true, 69 - }); 70 - return; 71 - } 72 - // Then check for quote param 73 if (quote) { 74 const decodedQuote = decodeQuotePosition(quote as string); 75 if (decodedQuote?.pageId) { ··· 83 // Mark as initialized even if no pageId found 84 usePostPageUIState.setState({ initialized: true }); 85 } 86 - }, [quote, pageParam]); 87 }; 88 89 export const openPage = ( ··· 310 absolute sm:-right-[20px] right-3 sm:top-3 top-0 311 flex sm:flex-col flex-row-reverse gap-1 items-start`} 312 > 313 - <PageOptionButton onClick={props.onClick}> 314 <CloseTiny /> 315 </PageOptionButton> 316 </div>
··· 19 import { Fragment, useEffect } from "react"; 20 import { flushSync } from "react-dom"; 21 import { scrollIntoView } from "src/utils/scrollIntoView"; 22 + import { useParams } from "next/navigation"; 23 import { decodeQuotePosition } from "./quotePosition"; 24 import { PollData } from "./fetchPollData"; 25 import { LinearDocumentPage } from "./LinearDocumentPage"; ··· 32 33 export const useOpenPages = () => { 34 const { quote } = useParams(); 35 const state = usePostPageUIState((s) => s); 36 37 + if (!state.initialized && quote) { 38 + const decodedQuote = decodeQuotePosition(quote as string); 39 + if (decodedQuote?.pageId) { 40 + return [decodedQuote.pageId]; 41 } 42 } 43 ··· 46 47 export const useInitializeOpenPages = () => { 48 const { quote } = useParams(); 49 50 useEffect(() => { 51 const state = usePostPageUIState.getState(); 52 if (!state.initialized) { 53 if (quote) { 54 const decodedQuote = decodeQuotePosition(quote as string); 55 if (decodedQuote?.pageId) { ··· 63 // Mark as initialized even if no pageId found 64 usePostPageUIState.setState({ initialized: true }); 65 } 66 + }, [quote]); 67 }; 68 69 export const openPage = ( ··· 290 absolute sm:-right-[20px] right-3 sm:top-3 top-0 291 flex sm:flex-col flex-row-reverse gap-1 items-start`} 292 > 293 + <PageOptionButton 294 + cardBorderHidden={!props.hasPageBackground} 295 + onClick={props.onClick} 296 + > 297 <CloseTiny /> 298 </PageOptionButton> 299 </div>
+1
app/lish/[did]/[publication]/dashboard/DraftList.tsx
··· 23 searchValue={props.searchValue} 24 showPreview={false} 25 defaultDisplay="list" 26 leaflets={leaflets_in_publications 27 .filter((l) => !l.documents) 28 .filter((l) => !l.archived)
··· 23 searchValue={props.searchValue} 24 showPreview={false} 25 defaultDisplay="list" 26 + cardBorderHidden={!props.showPageBackground} 27 leaflets={leaflets_in_publications 28 .filter((l) => !l.documents) 29 .filter((l) => !l.archived)
+1
app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx
··· 39 return ( 40 <DashboardLayout 41 id={publication.uri} 42 defaultTab="Drafts" 43 tabs={{ 44 Drafts: {
··· 39 return ( 40 <DashboardLayout 41 id={publication.uri} 42 + cardBorderHidden={!!record.theme?.showPageBackground} 43 defaultTab="Drafts" 44 tabs={{ 45 Drafts: {
+7 -9
app/p/[didOrHandle]/[rkey]/page.tsx
··· 5 import { Metadata } from "next"; 6 import { idResolver } from "app/(home-pages)/reader/idResolver"; 7 import { DocumentPageRenderer } from "app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer"; 8 - import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; 9 10 export async function generateMetadata(props: { 11 params: Promise<{ didOrHandle: string; rkey: string }>; ··· 35 let docRecord = document.data as PubLeafletDocument.Record; 36 37 // For documents in publications, include publication name 38 - let publicationName = 39 - document.documents_in_publications[0]?.publications?.name; 40 41 return { 42 icons: { ··· 65 let resolved = await idResolver.handle.resolve(didOrHandle); 66 if (!resolved) { 67 return ( 68 - <NotFoundLayout> 69 - <p className="font-bold">Sorry, we can't find this handle!</p> 70 <p> 71 This may be a glitch on our end. If the issue persists please{" "} 72 <a href="mailto:contact@leaflet.pub">send us a note</a>. 73 </p> 74 - </NotFoundLayout> 75 ); 76 } 77 did = resolved; 78 } catch (e) { 79 return ( 80 - <NotFoundLayout> 81 - <p className="font-bold">Sorry, we can't find this leaflet!</p> 82 <p> 83 This may be a glitch on our end. If the issue persists please{" "} 84 <a href="mailto:contact@leaflet.pub">send us a note</a>. 85 </p> 86 - </NotFoundLayout> 87 ); 88 } 89 }
··· 5 import { Metadata } from "next"; 6 import { idResolver } from "app/(home-pages)/reader/idResolver"; 7 import { DocumentPageRenderer } from "app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer"; 8 9 export async function generateMetadata(props: { 10 params: Promise<{ didOrHandle: string; rkey: string }>; ··· 34 let docRecord = document.data as PubLeafletDocument.Record; 35 36 // For documents in publications, include publication name 37 + let publicationName = document.documents_in_publications[0]?.publications?.name; 38 39 return { 40 icons: { ··· 63 let resolved = await idResolver.handle.resolve(didOrHandle); 64 if (!resolved) { 65 return ( 66 + <div className="p-4 text-lg text-center flex flex-col gap-4"> 67 + <p>Sorry, can&apos;t resolve handle.</p> 68 <p> 69 This may be a glitch on our end. If the issue persists please{" "} 70 <a href="mailto:contact@leaflet.pub">send us a note</a>. 71 </p> 72 + </div> 73 ); 74 } 75 did = resolved; 76 } catch (e) { 77 return ( 78 + <div className="p-4 text-lg text-center flex flex-col gap-4"> 79 + <p>Sorry, can&apos;t resolve handle.</p> 80 <p> 81 This may be a glitch on our end. If the issue persists please{" "} 82 <a href="mailto:contact@leaflet.pub">send us a note</a>. 83 </p> 84 + </div> 85 ); 86 } 87 }
+1 -2
components/ActionBar/Navigation.tsx
··· 25 | "discover" 26 | "notifications" 27 | "looseleafs" 28 - | "tag" 29 - | "profile"; 30 31 export const DesktopNavigation = (props: { 32 currentPage: navPages;
··· 25 | "discover" 26 | "notifications" 27 | "looseleafs" 28 + | "tag"; 29 30 export const DesktopNavigation = (props: { 31 currentPage: navPages;
+1 -1
components/ActionBar/Publications.tsx
··· 193 194 return props.record.icon ? ( 195 <div 196 - className={`${iconSizeClassName} ${props.className} relative overflow-hidden shrink-0`} 197 > 198 <img 199 src={`/api/atproto_images?did=${new AtUri(props.uri).host}&cid=${(props.record.icon?.ref as unknown as { $link: string })["$link"]}`}
··· 193 194 return props.record.icon ? ( 195 <div 196 + className={`${iconSizeClassName} ${props.className} relative overflow-hidden`} 197 > 198 <img 199 src={`/api/atproto_images?did=${new AtUri(props.uri).host}&cid=${(props.record.icon?.ref as unknown as { $link: string })["$link"]}`}
+2 -2
components/AtMentionLink.tsx
··· 24 isPublication || isDocument ? ( 25 <img 26 src={`/api/pub_icon?at_uri=${encodeURIComponent(atURI)}`} 27 - className="inline-block w-4 h-4 rounded-full mr-1 mt-[3px] align-text-top" 28 alt="" 29 width="20" 30 height="20" ··· 37 href={atUriToUrl(atURI)} 38 target="_blank" 39 rel="noopener noreferrer" 40 - className={`mention ${isPublication ? "font-bold" : ""} ${isDocument ? "italic" : ""} ${className}`} 41 > 42 {icon} 43 {children}
··· 24 isPublication || isDocument ? ( 25 <img 26 src={`/api/pub_icon?at_uri=${encodeURIComponent(atURI)}`} 27 + className="inline-block w-5 h-5 rounded-full mr-1 align-text-top" 28 alt="" 29 width="20" 30 height="20" ··· 37 href={atUriToUrl(atURI)} 38 target="_blank" 39 rel="noopener noreferrer" 40 + className={`text-accent-contrast hover:underline cursor-pointer ${isPublication ? "font-bold" : ""} ${isDocument ? "italic" : ""} ${className}`} 41 > 42 {icon} 43 {children}
+1 -4
components/Avatar.tsx
··· 3 export const Avatar = (props: { 4 src: string | undefined; 5 displayName: string | undefined; 6 - className?: string; 7 tiny?: boolean; 8 - large?: boolean; 9 - giant?: boolean; 10 }) => { 11 if (props.src) 12 return ( 13 <img 14 - className={`${props.tiny ? "w-4 h-4" : props.large ? "h-8 w-8" : props.giant ? "h-16 w-16" : "w-5 h-5"} rounded-full shrink-0 border border-border-light ${props.className}`} 15 src={props.src} 16 alt={ 17 props.displayName
··· 3 export const Avatar = (props: { 4 src: string | undefined; 5 displayName: string | undefined; 6 tiny?: boolean; 7 }) => { 8 if (props.src) 9 return ( 10 <img 11 + className={`${props.tiny ? "w-4 h-4" : "w-5 h-5"} rounded-full shrink-0 border border-border-light`} 12 src={props.src} 13 alt={ 14 props.displayName
+5 -14
components/Blocks/TextBlock/RenderYJSFragment.tsx
··· 6 import { didToBlueskyUrl } from "src/utils/mentionUtils"; 7 import { AtMentionLink } from "components/AtMentionLink"; 8 import { Delta } from "src/utils/yjsFragmentToString"; 9 - import { ProfilePopover } from "components/ProfilePopover"; 10 11 type BlockElements = "h1" | "h2" | "h3" | null | "blockquote" | "p"; 12 export function RenderYJSFragment({ ··· 64 ); 65 } 66 67 - if ( 68 - node.constructor === XmlElement && 69 - node.nodeName === "hard_break" 70 - ) { 71 return <br key={index} />; 72 } 73 74 // Handle didMention inline nodes 75 - if ( 76 - node.constructor === XmlElement && 77 - node.nodeName === "didMention" 78 - ) { 79 const did = node.getAttribute("did") || ""; 80 const text = node.getAttribute("text") || ""; 81 return ( ··· 84 target="_blank" 85 rel="noopener noreferrer" 86 key={index} 87 - className="mention" 88 > 89 {text} 90 </a> ··· 92 } 93 94 // Handle atMention inline nodes 95 - if ( 96 - node.constructor === XmlElement && 97 - node.nodeName === "atMention" 98 - ) { 99 const atURI = node.getAttribute("atURI") || ""; 100 const text = node.getAttribute("text") || ""; 101 return ( ··· 171 172 return props; 173 }
··· 6 import { didToBlueskyUrl } from "src/utils/mentionUtils"; 7 import { AtMentionLink } from "components/AtMentionLink"; 8 import { Delta } from "src/utils/yjsFragmentToString"; 9 10 type BlockElements = "h1" | "h2" | "h3" | null | "blockquote" | "p"; 11 export function RenderYJSFragment({ ··· 63 ); 64 } 65 66 + if (node.constructor === XmlElement && node.nodeName === "hard_break") { 67 return <br key={index} />; 68 } 69 70 // Handle didMention inline nodes 71 + if (node.constructor === XmlElement && node.nodeName === "didMention") { 72 const did = node.getAttribute("did") || ""; 73 const text = node.getAttribute("text") || ""; 74 return ( ··· 77 target="_blank" 78 rel="noopener noreferrer" 79 key={index} 80 + className="text-accent-contrast hover:underline cursor-pointer" 81 > 82 {text} 83 </a> ··· 85 } 86 87 // Handle atMention inline nodes 88 + if (node.constructor === XmlElement && node.nodeName === "atMention") { 89 const atURI = node.getAttribute("atURI") || ""; 90 const text = node.getAttribute("text") || ""; 91 return ( ··· 161 162 return props; 163 } 164 +
+3 -4
components/Blocks/TextBlock/schema.ts
··· 147 toDOM(node) { 148 // NOTE: This rendering should match the AtMentionLink component in 149 // components/AtMentionLink.tsx. If you update one, update the other. 150 - let className = "atMention mention"; 151 let aturi = new AtUri(node.attrs.atURI); 152 if (aturi.collection === "pub.leaflet.publication") 153 className += " font-bold"; ··· 168 "img", 169 { 170 src: `/api/pub_icon?at_uri=${encodeURIComponent(node.attrs.atURI)}`, 171 - class: 172 - "inline-block w-4 h-4 rounded-full mt-[3px] mr-1 align-text-top", 173 alt: "", 174 width: "16", 175 height: "16", ··· 215 return [ 216 "span", 217 { 218 - class: "didMention mention", 219 "data-did": node.attrs.did, 220 }, 221 node.attrs.text,
··· 147 toDOM(node) { 148 // NOTE: This rendering should match the AtMentionLink component in 149 // components/AtMentionLink.tsx. If you update one, update the other. 150 + let className = "atMention text-accent-contrast"; 151 let aturi = new AtUri(node.attrs.atURI); 152 if (aturi.collection === "pub.leaflet.publication") 153 className += " font-bold"; ··· 168 "img", 169 { 170 src: `/api/pub_icon?at_uri=${encodeURIComponent(node.attrs.atURI)}`, 171 + class: "inline-block w-5 h-5 rounded-full mr-1 align-text-top", 172 alt: "", 173 width: "16", 174 height: "16", ··· 214 return [ 215 "span", 216 { 217 + class: "didMention text-accent-contrast", 218 "data-did": node.attrs.did, 219 }, 220 node.attrs.text,
-1
components/Icons/ReplyTiny.tsx
··· 8 viewBox="0 0 16 16" 9 fill="none" 10 xmlns="http://www.w3.org/2000/svg" 11 - {...props} 12 > 13 <path 14 fillRule="evenodd"
··· 8 viewBox="0 0 16 16" 9 fill="none" 10 xmlns="http://www.w3.org/2000/svg" 11 > 12 <path 13 fillRule="evenodd"
+8 -7
components/PageHeader.tsx
··· 1 "use client"; 2 import { useState, useEffect } from "react"; 3 - import { useCardBorderHidden } from "./Pages/useCardBorderHidden"; 4 5 - export const Header = (props: { children: React.ReactNode }) => { 6 - let cardBorderHidden = useCardBorderHidden(); 7 let [scrollPos, setScrollPos] = useState(0); 8 9 useEffect(() => { ··· 21 } 22 }, []); 23 24 - let headerBGColor = !cardBorderHidden 25 ? "var(--bg-leaflet)" 26 : "var(--bg-page)"; 27 ··· 53 style={ 54 scrollPos < 20 55 ? { 56 - backgroundColor: !cardBorderHidden 57 ? `rgba(${headerBGColor}, ${scrollPos / 60 + 0.75})` 58 : `rgba(${headerBGColor}, ${scrollPos / 20})`, 59 - paddingLeft: !cardBorderHidden 60 ? "4px" 61 : `calc(${scrollPos / 20}*4px)`, 62 - paddingRight: !cardBorderHidden 63 ? "8px" 64 : `calc(${scrollPos / 20}*8px)`, 65 }
··· 1 "use client"; 2 import { useState, useEffect } from "react"; 3 4 + export const Header = (props: { 5 + children: React.ReactNode; 6 + cardBorderHidden: boolean; 7 + }) => { 8 let [scrollPos, setScrollPos] = useState(0); 9 10 useEffect(() => { ··· 22 } 23 }, []); 24 25 + let headerBGColor = props.cardBorderHidden 26 ? "var(--bg-leaflet)" 27 : "var(--bg-page)"; 28 ··· 54 style={ 55 scrollPos < 20 56 ? { 57 + backgroundColor: props.cardBorderHidden 58 ? `rgba(${headerBGColor}, ${scrollPos / 60 + 0.75})` 59 : `rgba(${headerBGColor}, ${scrollPos / 20})`, 60 + paddingLeft: props.cardBorderHidden 61 ? "4px" 62 : `calc(${scrollPos / 20}*4px)`, 63 + paddingRight: props.cardBorderHidden 64 ? "8px" 65 : `calc(${scrollPos / 20}*8px)`, 66 }
+23 -3
components/PageLayouts/DashboardLayout.tsx
··· 25 import Link from "next/link"; 26 import { ExternalLinkTiny } from "components/Icons/ExternalLinkTiny"; 27 import { usePreserveScroll } from "src/hooks/usePreserveScroll"; 28 - import { Tab } from "components/Tab"; 29 30 export type DashboardState = { 31 display?: "grid" | "list"; ··· 134 }, 135 >(props: { 136 id: string; 137 tabs: T; 138 defaultTab: keyof T; 139 currentPage: navPages; ··· 186 > 187 {Object.keys(props.tabs).length <= 1 && !controls ? null : ( 188 <> 189 - <Header> 190 {headerState === "default" ? ( 191 <> 192 {Object.keys(props.tabs).length > 1 && ( ··· 355 ); 356 }; 357 358 - const FilterOptions = (props: { hasPubs: boolean; hasArchived: boolean }) => { 359 let { filter } = useDashboardState(); 360 let setState = useSetDashboardState(); 361 let filterCount = Object.values(filter).filter(Boolean).length;
··· 25 import Link from "next/link"; 26 import { ExternalLinkTiny } from "components/Icons/ExternalLinkTiny"; 27 import { usePreserveScroll } from "src/hooks/usePreserveScroll"; 28 29 export type DashboardState = { 30 display?: "grid" | "list"; ··· 133 }, 134 >(props: { 135 id: string; 136 + cardBorderHidden: boolean; 137 tabs: T; 138 defaultTab: keyof T; 139 currentPage: navPages; ··· 186 > 187 {Object.keys(props.tabs).length <= 1 && !controls ? null : ( 188 <> 189 + <Header cardBorderHidden={props.cardBorderHidden}> 190 {headerState === "default" ? ( 191 <> 192 {Object.keys(props.tabs).length > 1 && ( ··· 355 ); 356 }; 357 358 + function Tab(props: { 359 + name: string; 360 + selected: boolean; 361 + onSelect: () => void; 362 + href?: string; 363 + }) { 364 + return ( 365 + <div 366 + className={`pubTabs px-1 py-0 flex gap-1 items-center rounded-md hover:cursor-pointer ${props.selected ? "text-accent-2 bg-accent-1 font-bold -mb-px" : "text-tertiary"}`} 367 + onClick={() => props.onSelect()} 368 + > 369 + {props.name} 370 + {props.href && <ExternalLinkTiny />} 371 + </div> 372 + ); 373 + } 374 + 375 + const FilterOptions = (props: { 376 + hasPubs: boolean; 377 + hasArchived: boolean; 378 + }) => { 379 let { filter } = useDashboardState(); 380 let setState = useSetDashboardState(); 381 let filterCount = Object.values(filter).filter(Boolean).length;
+5 -3
components/Pages/Page.tsx
··· 34 return focusedPageID === props.entityID; 35 }); 36 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 37 38 let drawerOpen = useDrawerOpen(props.entityID); 39 return ( ··· 48 }} 49 id={elementId.page(props.entityID).container} 50 drawerOpen={!!drawerOpen} 51 isFocused={isFocused} 52 fullPageScroll={props.fullPageScroll} 53 pageType={pageType} ··· 75 id: string; 76 children: React.ReactNode; 77 pageOptions?: React.ReactNode; 78 fullPageScroll: boolean; 79 isFocused?: boolean; 80 onClickAction?: (e: React.MouseEvent) => void; 81 pageType: "canvas" | "doc"; 82 drawerOpen: boolean | undefined; 83 }) => { 84 - const cardBorderHidden = useCardBorderHidden(); 85 let { ref } = usePreserveScroll<HTMLDivElement>(props.id); 86 return ( 87 // this div wraps the contents AND the page options. ··· 104 shrink-0 snap-center 105 overflow-y-scroll 106 ${ 107 - !cardBorderHidden && 108 `h-full border 109 bg-[rgba(var(--bg-page),var(--bg-page-alpha))] 110 ${props.drawerOpen ? "rounded-l-lg " : "rounded-lg"} 111 ${props.isFocused ? "shadow-md border-border" : "border-border-light"}` 112 } 113 - ${cardBorderHidden && "sm:h-[calc(100%+48px)] h-[calc(100%+20px)] sm:-my-6 -my-3 sm:pt-6 pt-3"} 114 ${props.fullPageScroll && "max-w-full "} 115 ${props.pageType === "doc" && !props.fullPageScroll && "w-[10000px] sm:mx-0 max-w-[var(--page-width-units)]"} 116 ${
··· 34 return focusedPageID === props.entityID; 35 }); 36 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 37 + let cardBorderHidden = useCardBorderHidden(props.entityID); 38 39 let drawerOpen = useDrawerOpen(props.entityID); 40 return ( ··· 49 }} 50 id={elementId.page(props.entityID).container} 51 drawerOpen={!!drawerOpen} 52 + cardBorderHidden={!!cardBorderHidden} 53 isFocused={isFocused} 54 fullPageScroll={props.fullPageScroll} 55 pageType={pageType} ··· 77 id: string; 78 children: React.ReactNode; 79 pageOptions?: React.ReactNode; 80 + cardBorderHidden: boolean; 81 fullPageScroll: boolean; 82 isFocused?: boolean; 83 onClickAction?: (e: React.MouseEvent) => void; 84 pageType: "canvas" | "doc"; 85 drawerOpen: boolean | undefined; 86 }) => { 87 let { ref } = usePreserveScroll<HTMLDivElement>(props.id); 88 return ( 89 // this div wraps the contents AND the page options. ··· 106 shrink-0 snap-center 107 overflow-y-scroll 108 ${ 109 + !props.cardBorderHidden && 110 `h-full border 111 bg-[rgba(var(--bg-page),var(--bg-page-alpha))] 112 ${props.drawerOpen ? "rounded-l-lg " : "rounded-lg"} 113 ${props.isFocused ? "shadow-md border-border" : "border-border-light"}` 114 } 115 + ${props.cardBorderHidden && "sm:h-[calc(100%+48px)] h-[calc(100%+20px)] sm:-my-6 -my-3 sm:pt-6 pt-3"} 116 ${props.fullPageScroll && "max-w-full "} 117 ${props.pageType === "doc" && !props.fullPageScroll && "w-[10000px] sm:mx-0 max-w-[var(--page-width-units)]"} 118 ${
+29 -7
components/Pages/PageOptions.tsx
··· 21 export const PageOptionButton = ({ 22 children, 23 secondary, 24 className, 25 disabled, 26 ...props 27 }: { 28 children: React.ReactNode; 29 secondary?: boolean; 30 className?: string; 31 disabled?: boolean; 32 } & Omit<JSX.IntrinsicElements["button"], "content">) => { 33 - const cardBorderHidden = useCardBorderHidden(); 34 return ( 35 <button 36 className={` ··· 57 first: boolean | undefined; 58 isFocused: boolean; 59 }) => { 60 return ( 61 <div 62 className={`pageOptions w-fit z-10 ··· 66 > 67 {!props.first && ( 68 <PageOptionButton 69 secondary 70 onClick={() => { 71 useUIState.getState().closePage(props.entityID); ··· 74 <CloseTiny /> 75 </PageOptionButton> 76 )} 77 - <OptionsMenu entityID={props.entityID} first={!!props.first} /> 78 - <UndoButtons /> 79 </div> 80 ); 81 }; 82 83 - export const UndoButtons = () => { 84 let undoState = useUndoState(); 85 let { undoManager } = useReplicache(); 86 return ( 87 <Media mobile> 88 {undoState.canUndo && ( 89 <div className="gap-1 flex sm:flex-col"> 90 - <PageOptionButton secondary onClick={() => undoManager.undo()}> 91 <UndoTiny /> 92 </PageOptionButton> 93 94 <PageOptionButton 95 secondary 96 onClick={() => undoManager.undo()} 97 disabled={!undoState.canRedo} 98 > ··· 104 ); 105 }; 106 107 - export const OptionsMenu = (props: { entityID: string; first: boolean }) => { 108 let [state, setState] = useState<"normal" | "theme" | "share">("normal"); 109 let { permissions } = useEntitySetContext(); 110 if (!permissions.write) return null; ··· 119 if (!open) setState("normal"); 120 }} 121 trigger={ 122 - <PageOptionButton className="!w-8 !h-5 sm:!w-5 sm:!h-8"> 123 <MoreOptionsTiny className="sm:rotate-90" /> 124 </PageOptionButton> 125 }
··· 21 export const PageOptionButton = ({ 22 children, 23 secondary, 24 + cardBorderHidden, 25 className, 26 disabled, 27 ...props 28 }: { 29 children: React.ReactNode; 30 secondary?: boolean; 31 + cardBorderHidden: boolean | undefined; 32 className?: string; 33 disabled?: boolean; 34 } & Omit<JSX.IntrinsicElements["button"], "content">) => { 35 return ( 36 <button 37 className={` ··· 58 first: boolean | undefined; 59 isFocused: boolean; 60 }) => { 61 + let cardBorderHidden = useCardBorderHidden(props.entityID); 62 + 63 return ( 64 <div 65 className={`pageOptions w-fit z-10 ··· 69 > 70 {!props.first && ( 71 <PageOptionButton 72 + cardBorderHidden={cardBorderHidden} 73 secondary 74 onClick={() => { 75 useUIState.getState().closePage(props.entityID); ··· 78 <CloseTiny /> 79 </PageOptionButton> 80 )} 81 + <OptionsMenu 82 + entityID={props.entityID} 83 + first={!!props.first} 84 + cardBorderHidden={cardBorderHidden} 85 + /> 86 + <UndoButtons cardBorderHidden={cardBorderHidden} /> 87 </div> 88 ); 89 }; 90 91 + export const UndoButtons = (props: { 92 + cardBorderHidden: boolean | undefined; 93 + }) => { 94 let undoState = useUndoState(); 95 let { undoManager } = useReplicache(); 96 return ( 97 <Media mobile> 98 {undoState.canUndo && ( 99 <div className="gap-1 flex sm:flex-col"> 100 + <PageOptionButton 101 + secondary 102 + cardBorderHidden={props.cardBorderHidden} 103 + onClick={() => undoManager.undo()} 104 + > 105 <UndoTiny /> 106 </PageOptionButton> 107 108 <PageOptionButton 109 secondary 110 + cardBorderHidden={props.cardBorderHidden} 111 onClick={() => undoManager.undo()} 112 disabled={!undoState.canRedo} 113 > ··· 119 ); 120 }; 121 122 + export const OptionsMenu = (props: { 123 + entityID: string; 124 + first: boolean; 125 + cardBorderHidden: boolean | undefined; 126 + }) => { 127 let [state, setState] = useState<"normal" | "theme" | "share">("normal"); 128 let { permissions } = useEntitySetContext(); 129 if (!permissions.write) return null; ··· 138 if (!open) setState("normal"); 139 }} 140 trigger={ 141 + <PageOptionButton 142 + cardBorderHidden={props.cardBorderHidden} 143 + className="!w-8 !h-5 sm:!w-5 sm:!h-8" 144 + > 145 <MoreOptionsTiny className="sm:rotate-90" /> 146 </PageOptionButton> 147 }
+18 -3
components/Pages/useCardBorderHidden.ts
··· 1 - import { useCardBorderHiddenContext } from "components/ThemeManager/ThemeProvider"; 2 3 - export function useCardBorderHidden(entityID?: string | null) { 4 - return useCardBorderHiddenContext(); 5 }
··· 1 + import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 2 + import { PubLeafletPublication } from "lexicons/api"; 3 + import { useEntity, useReplicache } from "src/replicache"; 4 + 5 + export function useCardBorderHidden(entityID: string | null) { 6 + let { rootEntity } = useReplicache(); 7 + let { data: pub } = useLeafletPublicationData(); 8 + let rootCardBorderHidden = useEntity(rootEntity, "theme/card-border-hidden"); 9 10 + let cardBorderHidden = 11 + useEntity(entityID, "theme/card-border-hidden") || rootCardBorderHidden; 12 + if (!cardBorderHidden && !rootCardBorderHidden) { 13 + if (pub?.publications?.record) { 14 + let record = pub.publications.record as PubLeafletPublication.Record; 15 + return !record.theme?.showPageBackground; 16 + } 17 + return false; 18 + } 19 + return (cardBorderHidden || rootCardBorderHidden)?.data.value; 20 }
+22 -31
components/PostListing.tsx
··· 15 import { InteractionPreview } from "./InteractionsPreview"; 16 17 export const PostListing = (props: Post) => { 18 - let pubRecord = props.publication?.pubRecord as 19 - | PubLeafletPublication.Record 20 - | undefined; 21 22 let postRecord = props.documents.data as PubLeafletDocument.Record; 23 let postUri = new AtUri(props.documents.uri); 24 25 - let theme = usePubTheme(pubRecord?.theme); 26 - let backgroundImage = 27 - pubRecord?.theme?.backgroundImage?.image?.ref && props.publication 28 - ? blobRefToSrc( 29 - pubRecord.theme.backgroundImage.image.ref, 30 - new AtUri(props.publication.uri).host, 31 - ) 32 - : null; 33 34 let backgroundImageRepeat = pubRecord?.theme?.backgroundImage?.repeat; 35 let backgroundImageSize = pubRecord?.theme?.backgroundImage?.width || 500; 36 37 - let showPageBackground = pubRecord?.theme?.showPageBackground; 38 39 let quotes = props.documents.document_mentions_in_bsky?.[0]?.count || 0; 40 let comments = 41 - pubRecord?.preferences?.showComments === false 42 ? 0 43 : props.documents.comments_on_documents?.[0]?.count || 0; 44 let tags = (postRecord?.tags as string[] | undefined) || []; 45 46 - // For standalone posts, link directly to the document 47 - let postHref = props.publication 48 - ? `${props.publication.href}/${postUri.rkey}` 49 - : `/doc/${postUri.host}/${postUri.rkey}`; 50 - 51 return ( 52 <BaseThemeProvider {...theme} local> 53 <div 54 style={{ 55 - backgroundImage: backgroundImage 56 - ? `url(${backgroundImage})` 57 - : undefined, 58 backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 59 backgroundSize: `${backgroundImageRepeat ? `${backgroundImageSize}px` : "cover"}`, 60 }} ··· 65 hover:outline-accent-contrast hover:border-accent-contrast 66 `} 67 > 68 - <Link className="h-full w-full absolute top-0 left-0" href={postHref} /> 69 <div 70 className={`${showPageBackground ? "bg-bg-page " : "bg-transparent"} rounded-md w-full px-[10px] pt-2 pb-2`} 71 style={{ ··· 78 79 <p className="text-secondary italic">{postRecord.description}</p> 80 <div className="flex flex-col-reverse md:flex-row md gap-2 text-sm text-tertiary items-center justify-start pt-1.5 md:pt-3 w-full"> 81 - {props.publication && pubRecord && ( 82 - <PubInfo 83 - href={props.publication.href} 84 - pubRecord={pubRecord} 85 - uri={props.publication.uri} 86 - /> 87 - )} 88 <div className="flex flex-row justify-between gap-2 items-center w-full"> 89 <PostInfo publishedAt={postRecord.publishedAt} /> 90 <InteractionPreview 91 - postUrl={postHref} 92 quotesCount={quotes} 93 commentsCount={comments} 94 tags={tags} 95 - showComments={pubRecord?.preferences?.showComments} 96 share 97 /> 98 </div>
··· 15 import { InteractionPreview } from "./InteractionsPreview"; 16 17 export const PostListing = (props: Post) => { 18 + let pubRecord = props.publication.pubRecord as PubLeafletPublication.Record; 19 20 let postRecord = props.documents.data as PubLeafletDocument.Record; 21 let postUri = new AtUri(props.documents.uri); 22 23 + let theme = usePubTheme(pubRecord.theme); 24 + let backgroundImage = pubRecord?.theme?.backgroundImage?.image?.ref 25 + ? blobRefToSrc( 26 + pubRecord?.theme?.backgroundImage?.image?.ref, 27 + new AtUri(props.publication.uri).host, 28 + ) 29 + : null; 30 31 let backgroundImageRepeat = pubRecord?.theme?.backgroundImage?.repeat; 32 let backgroundImageSize = pubRecord?.theme?.backgroundImage?.width || 500; 33 34 + let showPageBackground = pubRecord.theme?.showPageBackground; 35 36 let quotes = props.documents.document_mentions_in_bsky?.[0]?.count || 0; 37 let comments = 38 + pubRecord.preferences?.showComments === false 39 ? 0 40 : props.documents.comments_on_documents?.[0]?.count || 0; 41 let tags = (postRecord?.tags as string[] | undefined) || []; 42 43 return ( 44 <BaseThemeProvider {...theme} local> 45 <div 46 style={{ 47 + backgroundImage: `url(${backgroundImage})`, 48 backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 49 backgroundSize: `${backgroundImageRepeat ? `${backgroundImageSize}px` : "cover"}`, 50 }} ··· 55 hover:outline-accent-contrast hover:border-accent-contrast 56 `} 57 > 58 + <Link 59 + className="h-full w-full absolute top-0 left-0" 60 + href={`${props.publication.href}/${postUri.rkey}`} 61 + /> 62 <div 63 className={`${showPageBackground ? "bg-bg-page " : "bg-transparent"} rounded-md w-full px-[10px] pt-2 pb-2`} 64 style={{ ··· 71 72 <p className="text-secondary italic">{postRecord.description}</p> 73 <div className="flex flex-col-reverse md:flex-row md gap-2 text-sm text-tertiary items-center justify-start pt-1.5 md:pt-3 w-full"> 74 + <PubInfo 75 + href={props.publication.href} 76 + pubRecord={pubRecord} 77 + uri={props.publication.uri} 78 + /> 79 <div className="flex flex-row justify-between gap-2 items-center w-full"> 80 <PostInfo publishedAt={postRecord.publishedAt} /> 81 <InteractionPreview 82 + postUrl={`${props.publication.href}/${postUri.rkey}`} 83 quotesCount={quotes} 84 commentsCount={comments} 85 tags={tags} 86 + showComments={pubRecord.preferences?.showComments} 87 share 88 /> 89 </div>
-98
components/ProfilePopover.tsx
··· 1 - "use client"; 2 - import { Popover } from "./Popover"; 3 - import useSWR from "swr"; 4 - import { callRPC } from "app/api/rpc/client"; 5 - import { useRef, useState } from "react"; 6 - import { ProfileHeader } from "app/(home-pages)/p/[didOrHandle]/ProfileHeader"; 7 - import { SpeedyLink } from "./SpeedyLink"; 8 - import { Tooltip } from "./Tooltip"; 9 - import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 10 - 11 - export const ProfilePopover = (props: { 12 - trigger: React.ReactNode; 13 - didOrHandle: string; 14 - }) => { 15 - const [isOpen, setIsOpen] = useState(false); 16 - let [isHovered, setIsHovered] = useState(false); 17 - const hoverTimeout = useRef<null | number>(null); 18 - 19 - const { data, isLoading } = useSWR( 20 - isHovered ? ["profile-data", props.didOrHandle] : null, 21 - async () => { 22 - const response = await callRPC("get_profile_data", { 23 - didOrHandle: props.didOrHandle, 24 - }); 25 - return response.result; 26 - }, 27 - ); 28 - 29 - return ( 30 - <Tooltip 31 - className="max-w-sm p-0! text-center" 32 - asChild 33 - trigger={ 34 - <a 35 - className="no-underline" 36 - href={`https://leaflet.pub/p/${props.didOrHandle}`} 37 - target="_blank" 38 - onPointerEnter={(e) => { 39 - if (hoverTimeout.current) { 40 - window.clearTimeout(hoverTimeout.current); 41 - } 42 - hoverTimeout.current = window.setTimeout(async () => { 43 - setIsHovered(true); 44 - }, 150); 45 - }} 46 - onPointerLeave={() => { 47 - if (isHovered) return; 48 - if (hoverTimeout.current) { 49 - window.clearTimeout(hoverTimeout.current); 50 - hoverTimeout.current = null; 51 - } 52 - setIsHovered(false); 53 - }} 54 - > 55 - {props.trigger} 56 - </a> 57 - } 58 - onOpenChange={setIsOpen} 59 - > 60 - {isLoading ? ( 61 - <div className="text-secondary p-4">Loading...</div> 62 - ) : data ? ( 63 - <div> 64 - <ProfileHeader 65 - profile={data.profile} 66 - publications={data.publications} 67 - popover 68 - /> 69 - <KnownFollowers viewer={data.profile.viewer} did={data.profile.did} /> 70 - </div> 71 - ) : ( 72 - <div className="text-secondary py-2 px-4">Profile not found</div> 73 - )} 74 - </Tooltip> 75 - ); 76 - }; 77 - 78 - let KnownFollowers = (props: { 79 - viewer: ProfileViewDetailed["viewer"]; 80 - did: string; 81 - }) => { 82 - if (!props.viewer?.knownFollowers) return null; 83 - let count = props.viewer.knownFollowers.count; 84 - return ( 85 - <> 86 - <hr className="border-border" /> 87 - Followed by{" "} 88 - <a 89 - className="hover:underline" 90 - href={`https://bsky.social/profile/${props.did}/known-followers`} 91 - target="_blank" 92 - > 93 - {props.viewer?.knownFollowers?.followers[0]?.displayName}{" "} 94 - {count > 1 ? `and ${count - 1} other${count > 2 ? "s" : ""}` : ""} 95 - </a> 96 - </> 97 - ); 98 - };
···
-18
components/Tab.tsx
··· 1 - import { ExternalLinkTiny } from "./Icons/ExternalLinkTiny"; 2 - 3 - export const Tab = (props: { 4 - name: string; 5 - selected: boolean; 6 - onSelect: () => void; 7 - href?: string; 8 - }) => { 9 - return ( 10 - <div 11 - className={`pubTabs px-1 py-0 flex gap-1 items-center rounded-md hover:cursor-pointer ${props.selected ? "text-accent-2 bg-accent-1 font-bold -mb-px" : "text-tertiary"}`} 12 - onClick={() => props.onSelect()} 13 - > 14 - {props.name} 15 - {props.href && <ExternalLinkTiny />} 16 - </div> 17 - ); 18 - };
···
+4 -7
components/ThemeManager/PublicationThemeProvider.tsx
··· 4 import { useEntity } from "src/replicache"; 5 import { getColorContrast } from "./themeUtils"; 6 import { useColorAttribute, colorToString } from "./useColorAttribute"; 7 - import { BaseThemeProvider, CardBorderHiddenContext } from "./ThemeProvider"; 8 import { PubLeafletPublication, PubLeafletThemeColor } from "lexicons/api"; 9 import { usePublicationData } from "app/lish/[did]/[publication]/dashboard/PublicationSWRProvider"; 10 import { blobRefToSrc } from "src/utils/blobRefToSrc"; ··· 103 isStandalone?: boolean; 104 }) { 105 let colors = usePubTheme(props.theme, props.isStandalone); 106 - let cardBorderHidden = !colors.showPageBackground; 107 return ( 108 - <CardBorderHiddenContext.Provider value={cardBorderHidden}> 109 - <BaseThemeProvider local={props.local} {...colors}> 110 - {props.children} 111 - </BaseThemeProvider> 112 - </CardBorderHiddenContext.Provider> 113 ); 114 } 115
··· 4 import { useEntity } from "src/replicache"; 5 import { getColorContrast } from "./themeUtils"; 6 import { useColorAttribute, colorToString } from "./useColorAttribute"; 7 + import { BaseThemeProvider } from "./ThemeProvider"; 8 import { PubLeafletPublication, PubLeafletThemeColor } from "lexicons/api"; 9 import { usePublicationData } from "app/lish/[did]/[publication]/dashboard/PublicationSWRProvider"; 10 import { blobRefToSrc } from "src/utils/blobRefToSrc"; ··· 103 isStandalone?: boolean; 104 }) { 105 let colors = usePubTheme(props.theme, props.isStandalone); 106 return ( 107 + <BaseThemeProvider local={props.local} {...colors}> 108 + {props.children} 109 + </BaseThemeProvider> 110 ); 111 } 112
+22 -26
components/ThemeManager/ThemeProvider.tsx
··· 1 "use client"; 2 3 - import { createContext, CSSProperties, useContext, useEffect } from "react"; 4 - 5 - // Context for cardBorderHidden 6 - export const CardBorderHiddenContext = createContext<boolean>(false); 7 - 8 - export function useCardBorderHiddenContext() { 9 - return useContext(CardBorderHiddenContext); 10 - } 11 import { 12 colorToString, 13 useColorAttribute, ··· 60 }) { 61 let bgLeaflet = useColorAttribute(props.entityID, "theme/page-background"); 62 let bgPage = useColorAttribute(props.entityID, "theme/card-background"); 63 - let cardBorderHiddenValue = useEntity( 64 props.entityID, 65 "theme/card-border-hidden", 66 )?.data.value; 67 - let showPageBackground = !cardBorderHiddenValue; 68 let primary = useColorAttribute(props.entityID, "theme/primary"); 69 70 let highlight1 = useEntity(props.entityID, "theme/highlight-1"); ··· 75 let accent2 = useColorAttribute(props.entityID, "theme/accent-text"); 76 77 return ( 78 - <CardBorderHiddenContext.Provider value={!!cardBorderHiddenValue}> 79 - <BaseThemeProvider 80 - local={props.local} 81 - bgLeaflet={bgLeaflet} 82 - bgPage={bgPage} 83 - primary={primary} 84 - highlight2={highlight2} 85 - highlight3={highlight3} 86 - highlight1={highlight1?.data.value} 87 - accent1={accent1} 88 - accent2={accent2} 89 - showPageBackground={showPageBackground} 90 - > 91 - {props.children} 92 - </BaseThemeProvider> 93 - </CardBorderHiddenContext.Provider> 94 ); 95 } 96 ··· 342 </div> 343 ); 344 };
··· 1 "use client"; 2 3 + import { 4 + createContext, 5 + CSSProperties, 6 + useContext, 7 + useEffect, 8 + } from "react"; 9 import { 10 colorToString, 11 useColorAttribute, ··· 58 }) { 59 let bgLeaflet = useColorAttribute(props.entityID, "theme/page-background"); 60 let bgPage = useColorAttribute(props.entityID, "theme/card-background"); 61 + let showPageBackground = !useEntity( 62 props.entityID, 63 "theme/card-border-hidden", 64 )?.data.value; 65 let primary = useColorAttribute(props.entityID, "theme/primary"); 66 67 let highlight1 = useEntity(props.entityID, "theme/highlight-1"); ··· 72 let accent2 = useColorAttribute(props.entityID, "theme/accent-text"); 73 74 return ( 75 + <BaseThemeProvider 76 + local={props.local} 77 + bgLeaflet={bgLeaflet} 78 + bgPage={bgPage} 79 + primary={primary} 80 + highlight2={highlight2} 81 + highlight3={highlight3} 82 + highlight1={highlight1?.data.value} 83 + accent1={accent1} 84 + accent2={accent2} 85 + showPageBackground={showPageBackground} 86 + > 87 + {props.children} 88 + </BaseThemeProvider> 89 ); 90 } 91 ··· 337 </div> 338 ); 339 }; 340 +
+1 -1
components/Tooltip.tsx
··· 26 props.skipDelayDuration ? props.skipDelayDuration : 300 27 } 28 > 29 - <RadixTooltip.Root onOpenChange={props.onOpenChange} open={props.open}> 30 <RadixTooltip.Trigger disabled={props.disabled} asChild={props.asChild}> 31 {props.trigger} 32 </RadixTooltip.Trigger>
··· 26 props.skipDelayDuration ? props.skipDelayDuration : 300 27 } 28 > 29 + <RadixTooltip.Root> 30 <RadixTooltip.Trigger disabled={props.disabled} asChild={props.asChild}> 31 {props.trigger} 32 </RadixTooltip.Trigger>
+2
src/hooks/useLocalizedDate.ts
··· 31 ? timezone || "UTC" 32 : Intl.DateTimeFormat().resolvedOptions().timeZone; 33 34 // Apply timezone if available 35 if (effectiveTimezone) { 36 dateTime = dateTime.setZone(effectiveTimezone);
··· 31 ? timezone || "UTC" 32 : Intl.DateTimeFormat().resolvedOptions().timeZone; 33 34 + console.log("tz", effectiveTimezone); 35 + 36 // Apply timezone if available 37 if (effectiveTimezone) { 38 dateTime = dateTime.setZone(effectiveTimezone);