diff --git a/app/(home-pages)/home/HomeLayout.tsx b/app/(home-pages)/home/HomeLayout.tsx index 4ab12a44..7c31a67f 100644 --- a/app/(home-pages)/home/HomeLayout.tsx +++ b/app/(home-pages)/home/HomeLayout.tsx @@ -235,25 +235,6 @@ export function LeafletList(props: { l.doc) || - !!leaflet.leaflets_to_documents?.find((l) => !!l.documents) - } - publishedAt={ - leaflet.leaflets_in_publications?.find((l) => l.doc)?.documents - ?.indexed_at || - leaflet.leaflets_to_documents?.find((l) => !!l.documents) - ?.documents?.indexed_at - } - document_uri={ - leaflet.leaflets_in_publications?.find((l) => l.doc)?.documents - ?.uri - } - leaflet_id={leaflet.root_entity} loggedIn={!!identity} display={display} added_at={added_at} diff --git a/app/(home-pages)/home/LeafletList/LeafletInfo.tsx b/app/(home-pages)/home/LeafletList/LeafletInfo.tsx index 20004a7c..b18be9bf 100644 --- a/app/(home-pages)/home/LeafletList/LeafletInfo.tsx +++ b/app/(home-pages)/home/LeafletList/LeafletInfo.tsx @@ -1,31 +1,27 @@ "use client"; -import { PermissionToken, useEntity } from "src/replicache"; +import { useEntity } from "src/replicache"; import { LeafletOptions } from "./LeafletOptions"; -import { useState } from "react"; import { timeAgo } from "src/utils/timeAgo"; import { usePageTitle } from "components/utils/UpdateLeafletTitle"; +import { useLeafletPublicationStatus } from "components/PageSWRDataProvider"; export const LeafletInfo = (props: { title?: string; - draftInPublication?: string; - published?: boolean; - token: PermissionToken; - leaflet_id: string; - loggedIn: boolean; className?: string; display: "grid" | "list"; added_at: string; - publishedAt?: string; - document_uri?: string; archived?: boolean | null; + loggedIn: boolean; }) => { - let [prefetch, setPrefetch] = useState(false); + const pubStatus = useLeafletPublicationStatus(); let prettyCreatedAt = props.added_at ? timeAgo(props.added_at) : ""; - let prettyPublishedAt = props.publishedAt ? timeAgo(props.publishedAt) : ""; + let prettyPublishedAt = pubStatus?.publishedAt + ? timeAgo(pubStatus.publishedAt) + : ""; // Look up root page first, like UpdateLeafletTitle does - let firstPage = useEntity(props.leaflet_id, "root/page")[0]; - let entityID = firstPage?.data.value || props.leaflet_id; + let firstPage = useEntity(pubStatus?.leafletId ?? "", "root/page")[0]; + let entityID = firstPage?.data.value || pubStatus?.leafletId || ""; let titleFromDb = usePageTitle(entityID); let title = props.title ?? titleFromDb ?? "Untitled"; @@ -39,24 +35,17 @@ export const LeafletInfo = (props: { {title}
- +
{props.archived ? (
Archived
- ) : props.draftInPublication || props.published ? ( + ) : pubStatus?.draftInPublication || pubStatus?.isPublished ? (
- {props.published + {pubStatus?.isPublished ? `Published ${prettyPublishedAt}` : `Draft ${prettyCreatedAt}`}
diff --git a/app/(home-pages)/home/LeafletList/LeafletListItem.tsx b/app/(home-pages)/home/LeafletList/LeafletListItem.tsx index 9537f81b..aaaa146c 100644 --- a/app/(home-pages)/home/LeafletList/LeafletListItem.tsx +++ b/app/(home-pages)/home/LeafletList/LeafletListItem.tsx @@ -1,27 +1,22 @@ "use client"; -import { PermissionToken } from "src/replicache"; import { LeafletListPreview, LeafletGridPreview } from "./LeafletPreview"; import { LeafletInfo } from "./LeafletInfo"; import { useState, useRef, useEffect } from "react"; import { SpeedyLink } from "components/SpeedyLink"; +import { useLeafletPublicationStatus } from "components/PageSWRDataProvider"; export const LeafletListItem = (props: { - token: PermissionToken; archived?: boolean | null; - leaflet_id: string; loggedIn: boolean; display: "list" | "grid"; cardBorderHidden: boolean; added_at: string; title?: string; - draftInPublication?: string; - published?: boolean; - publishedAt?: string; - document_uri?: string; index: number; isHidden: boolean; showPreview?: boolean; }) => { + const pubStatus = useLeafletPublicationStatus(); let [isOnScreen, setIsOnScreen] = useState(props.index < 16 ? true : false); let previewRef = useRef(null); @@ -43,6 +38,8 @@ export const LeafletListItem = (props: { return () => observer.disconnect(); }, [previewRef]); + const tokenId = pubStatus?.shareLink ?? ""; + if (props.display === "list") return ( <> @@ -58,13 +55,17 @@ export const LeafletListItem = (props: { }} > - {props.showPreview && ( - - )} - + {props.showPreview && } +
{props.cardBorderHidden && (
- +
); diff --git a/app/(home-pages)/home/LeafletList/LeafletOptions.tsx b/app/(home-pages)/home/LeafletList/LeafletOptions.tsx index 0e9b3db5..58287b02 100644 --- a/app/(home-pages)/home/LeafletList/LeafletOptions.tsx +++ b/app/(home-pages)/home/LeafletList/LeafletOptions.tsx @@ -21,7 +21,6 @@ import { ShareSmall } from "components/Icons/ShareSmall"; import { HideSmall } from "components/Icons/HideSmall"; import { hideDoc } from "../storage"; -import { PermissionToken } from "src/replicache"; import { useIdentityData, mutateIdentityData, @@ -31,20 +30,18 @@ import { mutatePublicationData, } from "app/lish/[did]/[publication]/dashboard/PublicationSWRProvider"; import { ShareButton } from "app/[leaflet_id]/actions/ShareOptions"; +import { useLeafletPublicationStatus } from "components/PageSWRDataProvider"; export const LeafletOptions = (props: { - leaflet: PermissionToken; - draftInPublication?: string; - document_uri?: string; - shareLink: string; archived?: boolean | null; loggedIn?: boolean; }) => { + const pubStatus = useLeafletPublicationStatus(); let [state, setState] = useState<"normal" | "areYouSure">("normal"); let [open, setOpen] = useState(false); let { identity } = useIdentityData(); let isPublicationOwner = - !!identity?.atp_did && !!props.document_uri?.includes(identity.atp_did); + !!identity?.atp_did && !!pubStatus?.documentUri?.includes(identity.atp_did); return ( <> {state === "normal" ? ( !props.loggedIn ? ( - - ) : props.document_uri && isPublicationOwner ? ( - + + ) : pubStatus?.documentUri && isPublicationOwner ? ( + ) : ( - + ) ) : state === "areYouSure" ? ( - setState("normal")} - leaflet={props.leaflet} - document_uri={props.document_uri} - draft={!!props.draftInPublication} - /> + setState("normal")} /> ) : null} @@ -97,72 +81,36 @@ export const LeafletOptions = (props: { const DefaultOptions = (props: { setState: (s: "areYouSure") => void; - draftInPublication?: string; - leaflet: PermissionToken; - shareLink: string; archived?: boolean | null; }) => { - let toaster = useToaster(); - let { mutate: mutatePub } = usePublicationData(); - let { mutate: mutateIdentity } = useIdentityData(); + const pubStatus = useLeafletPublicationStatus(); + const toaster = useToaster(); + const { setArchived } = useArchiveMutations(); + const tokenId = pubStatus?.token.id; + const itemType = pubStatus?.draftInPublication ? "Draft" : "Leaflet"; + return ( <> - - - Copy Edit Link - - } - subtext="" - smokerText="Link copied!" - id="get-link" - link={props.shareLink} - /> +
{ + if (!tokenId) return; + setArchived(tokenId, !props.archived); + if (!props.archived) { - mutateIdentityData(mutateIdentity, (data) => { - let item = data.permission_token_on_homepage.find( - (p) => p.permission_tokens?.id === props.leaflet.id, - ); - if (item) item.archived = true; - }); - mutatePublicationData(mutatePub, (data) => { - let item = data.publication?.leaflets_in_publications.find( - (l) => l.permission_tokens?.id === props.leaflet.id, - ); - if (item) item.archived = true; - }); - await archivePost(props.leaflet.id); + await archivePost(tokenId); toaster({ content: (
- Archived{props.draftInPublication ? " Draft" : " Leaflet"}! + Archived {itemType}! { - mutateIdentityData(mutateIdentity, (data) => { - let item = data.permission_token_on_homepage.find( - (p) => p.permission_tokens?.id === props.leaflet.id, - ); - if (item) item.archived = false; - }); - mutatePublicationData(mutatePub, (data) => { - let item = - data.publication?.leaflets_in_publications.find( - (l) => l.permission_tokens?.id === props.leaflet.id, - ); - if (item) item.archived = false; - }); - await unarchivePost(props.leaflet.id); + setArchived(tokenId, false); + await unarchivePost(tokenId); toaster({ - content: ( -
- Unarchived! -
- ), + content:
Unarchived!
, type: "success", }); }} @@ -174,19 +122,7 @@ const DefaultOptions = (props: { type: "success", }); } else { - mutateIdentityData(mutateIdentity, (data) => { - let item = data.permission_token_on_homepage.find( - (p) => p.permission_tokens?.id === props.leaflet.id, - ); - if (item) item.archived = false; - }); - mutatePublicationData(mutatePub, (data) => { - let item = data.publication?.leaflets_in_publications.find( - (l) => l.permission_tokens?.id === props.leaflet.id, - ); - if (item) item.archived = false; - }); - await unarchivePost(props.leaflet.id); + await unarchivePost(tokenId); toaster({ content:
Unarchived!
, type: "success", @@ -195,46 +131,29 @@ const DefaultOptions = (props: { }} > - {!props.archived ? " Archive" : "Unarchive"} - {props.draftInPublication ? " Draft" : " Leaflet"} + {!props.archived ? " Archive" : "Unarchive"} {itemType} - { e.preventDefault(); props.setState("areYouSure"); }} - > - - Delete Forever - + /> ); }; -const LoggedOutOptions = (props: { - leaflet: PermissionToken; - setState: (s: "areYouSure") => void; - shareLink: string; -}) => { - let toaster = useToaster(); +const LoggedOutOptions = (props: { setState: (s: "areYouSure") => void }) => { + const pubStatus = useLeafletPublicationStatus(); + const toaster = useToaster(); + return ( <> - - - Copy Edit Link -
- } - subtext="" - smokerText="Link copied!" - id="get-link" - link={`/${props.shareLink}`} - /> +
{ - hideDoc(props.leaflet); + if (pubStatus?.token) hideDoc(pubStatus.token); toaster({ content:
Removed from Home!
, type: "success", @@ -244,26 +163,24 @@ const LoggedOutOptions = (props: { Remove from Home
- { e.preventDefault(); props.setState("areYouSure"); }} - > - - Delete Forever - + /> ); }; const PublishedPostOptions = (props: { setState: (s: "areYouSure") => void; - document_uri: string; - leaflet: PermissionToken; - shareLink: string; }) => { - let toaster = useToaster(); + const pubStatus = useLeafletPublicationStatus(); + const toaster = useToaster(); + const postLink = pubStatus?.postShareLink ?? ""; + const isFullUrl = postLink.includes("http"); + return ( <> -
{ - if (props.document_uri) { - await unpublishPost(props.document_uri); + if (pubStatus?.documentUri) { + await unpublishPost(pubStatus.documentUri); } toaster({ content:
Unpublished Post!
, @@ -299,33 +215,28 @@ const PublishedPostOptions = (props: {
- { e.preventDefault(); props.setState("areYouSure"); }} - > - -
- Delete Post -
- Unpublish AND delete -
-
-
+ subtext="Post" + /> ); }; -const DeleteAreYouSureForm = (props: { - backToMenu: () => void; - document_uri?: string; - leaflet: PermissionToken; - draft?: boolean; -}) => { - let toaster = useToaster(); - let { mutate: mutatePub } = usePublicationData(); - let { mutate: mutateIdentity } = useIdentityData(); +const DeleteAreYouSureForm = (props: { backToMenu: () => void }) => { + const pubStatus = useLeafletPublicationStatus(); + const toaster = useToaster(); + const { removeFromLists } = useArchiveMutations(); + const tokenId = pubStatus?.token.id; + + const itemType = pubStatus?.documentUri + ? "Post" + : pubStatus?.draftInPublication + ? "Draft" + : "Leaflet"; return (
@@ -339,35 +250,14 @@ const DeleteAreYouSureForm = (props: { { - mutateIdentityData(mutateIdentity, (data) => { - data.permission_token_on_homepage = - data.permission_token_on_homepage.filter( - (p) => p.permission_tokens?.id !== props.leaflet.id, - ); - }); - mutatePublicationData(mutatePub, (data) => { - if (!data.publication) return; - data.publication.leaflets_in_publications = - data.publication.leaflets_in_publications.filter( - (l) => l.permission_tokens?.id !== props.leaflet.id, - ); - }); - if (props.document_uri) { - await deletePost(props.document_uri); + if (tokenId) removeFromLists(tokenId); + if (pubStatus?.documentUri) { + await deletePost(pubStatus.documentUri); } - deleteLeaflet(props.leaflet); + if (pubStatus?.token) deleteLeaflet(pubStatus.token); toaster({ - content: ( -
- Deleted{" "} - {props.document_uri - ? "Post!" - : props.draft - ? "Draft" - : "Leaflet!"} -
- ), + content:
Deleted {itemType}!
, type: "success", }); }} @@ -378,3 +268,76 @@ const DeleteAreYouSureForm = (props: {
); }; + +// Shared menu items +const EditLinkShareButton = (props: { link: string }) => ( + + + Copy Edit Link + + } + subtext="" + smokerText="Link copied!" + id="get-link" + link={props.link} + /> +); + +const DeleteForeverMenuItem = (props: { + onSelect: (e: Event) => void; + subtext?: string; +}) => ( + + + {props.subtext ? ( +
+ Delete {props.subtext} +
+ Unpublish AND delete +
+
+ ) : ( + "Delete Forever" + )} +
+); + +// Helper to update archived state in both identity and publication data +function useArchiveMutations() { + const { mutate: mutatePub } = usePublicationData(); + const { mutate: mutateIdentity } = useIdentityData(); + + return { + setArchived: (tokenId: string, archived: boolean) => { + mutateIdentityData(mutateIdentity, (data) => { + const item = data.permission_token_on_homepage.find( + (p) => p.permission_tokens?.id === tokenId, + ); + if (item) item.archived = archived; + }); + mutatePublicationData(mutatePub, (data) => { + const item = data.publication?.leaflets_in_publications.find( + (l) => l.permission_tokens?.id === tokenId, + ); + if (item) item.archived = archived; + }); + }, + removeFromLists: (tokenId: string) => { + mutateIdentityData(mutateIdentity, (data) => { + data.permission_token_on_homepage = + data.permission_token_on_homepage.filter( + (p) => p.permission_tokens?.id !== tokenId, + ); + }); + mutatePublicationData(mutatePub, (data) => { + if (!data.publication) return; + data.publication.leaflets_in_publications = + data.publication.leaflets_in_publications.filter( + (l) => l.permission_tokens?.id !== tokenId, + ); + }); + }, + }; +} diff --git a/app/(home-pages)/home/LeafletList/LeafletPreview.tsx b/app/(home-pages)/home/LeafletList/LeafletPreview.tsx index e3530f8a..dc506acc 100644 --- a/app/(home-pages)/home/LeafletList/LeafletPreview.tsx +++ b/app/(home-pages)/home/LeafletList/LeafletPreview.tsx @@ -3,40 +3,59 @@ import { ThemeBackgroundProvider, ThemeProvider, } from "components/ThemeManager/ThemeProvider"; -import { - PermissionToken, - useEntity, - useReferenceToEntity, -} from "src/replicache"; +import { useEntity, useReferenceToEntity } from "src/replicache"; import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; import { LeafletContent } from "./LeafletContent"; import { Tooltip } from "components/Tooltip"; +import { useLeafletPublicationStatus } from "components/PageSWRDataProvider"; +import { CSSProperties } from "react"; -export const LeafletListPreview = (props: { - draft?: boolean; - published?: boolean; - isVisible: boolean; - token: PermissionToken; - leaflet_id: string; - loggedIn: boolean; -}) => { - let root = - useReferenceToEntity("root/page", props.leaflet_id)[0]?.entity || - props.leaflet_id; - let firstPage = useEntity(root, "root/page")[0]; - let page = firstPage?.data.value || root; +function useLeafletPreviewData() { + const pubStatus = useLeafletPublicationStatus(); + const leafletId = pubStatus?.leafletId ?? ""; + const root = + useReferenceToEntity("root/page", leafletId)[0]?.entity || leafletId; + const firstPage = useEntity(root, "root/page")[0]; + const page = firstPage?.data.value || root; - let cardBorderHidden = useCardBorderHidden(root); - let rootBackgroundImage = useEntity(root, "theme/card-background-image"); - let rootBackgroundRepeat = useEntity( + const cardBorderHidden = useCardBorderHidden(root); + const rootBackgroundImage = useEntity(root, "theme/card-background-image"); + const rootBackgroundRepeat = useEntity( root, "theme/card-background-image-repeat", ); - let rootBackgroundOpacity = useEntity( + const rootBackgroundOpacity = useEntity( root, "theme/card-background-image-opacity", ); + const contentWrapperStyle: CSSProperties = cardBorderHidden + ? {} + : { + backgroundImage: rootBackgroundImage + ? `url(${rootBackgroundImage.data.src}), url(${rootBackgroundImage.data.fallback})` + : undefined, + backgroundRepeat: rootBackgroundRepeat ? "repeat" : "no-repeat", + backgroundPosition: "center", + backgroundSize: !rootBackgroundRepeat + ? "cover" + : rootBackgroundRepeat?.data.value / 3, + opacity: + rootBackgroundImage?.data.src && rootBackgroundOpacity + ? rootBackgroundOpacity.data.value + : 1, + backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))", + }; + + 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"}`; + + return { root, page, cardBorderHidden, contentWrapperStyle, contentWrapperClass }; +} + +export const LeafletListPreview = (props: { isVisible: boolean }) => { + const { root, page, cardBorderHidden, contentWrapperStyle, contentWrapperClass } = + useLeafletPreviewData(); + return (
-
+
@@ -107,30 +102,10 @@ export const LeafletListPreview = (props: { ); }; -export const LeafletGridPreview = (props: { - draft?: boolean; - published?: boolean; - token: PermissionToken; - leaflet_id: string; - loggedIn: boolean; - isVisible: boolean; -}) => { - let root = - useReferenceToEntity("root/page", props.leaflet_id)[0]?.entity || - props.leaflet_id; - let firstPage = useEntity(root, "root/page")[0]; - let page = firstPage?.data.value || root; +export const LeafletGridPreview = (props: { isVisible: boolean }) => { + const { root, page, contentWrapperStyle, contentWrapperClass } = + useLeafletPreviewData(); - let cardBorderHidden = useCardBorderHidden(root); - let rootBackgroundImage = useEntity(root, "theme/card-background-image"); - let rootBackgroundRepeat = useEntity( - root, - "theme/card-background-image-repeat", - ); - let rootBackgroundOpacity = useEntity( - root, - "theme/card-background-image-opacity", - ); return (
@@ -140,31 +115,7 @@ export const LeafletGridPreview = (props: { inert className="leafletPreview grow shrink-0 h-full w-full px-2 pt-2 sm:px-3 sm:pt-3 flex items-end pointer-events-none" > -
+
diff --git a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx index 38cf82d3..cacd36e8 100644 --- a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx @@ -19,6 +19,7 @@ import { QuoteTiny } from "components/Icons/QuoteTiny"; import { CommentTiny } from "components/Icons/CommentTiny"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; import { LeafletOptions } from "app/(home-pages)/home/LeafletList/LeafletOptions"; +import { StaticLeafletDataContext } from "components/PageSWRDataProvider"; export function PublishedPostsList(props: { searchValue: string; @@ -84,7 +85,7 @@ export function PublishedPostsList(props: {
- {leaflet && ( + {leaflet && leaflet.permission_tokens && ( <> - + + + )}
diff --git a/app/lish/createPub/getPublicationURL.ts b/app/lish/createPub/getPublicationURL.ts index 7730a955..357af329 100644 --- a/app/lish/createPub/getPublicationURL.ts +++ b/app/lish/createPub/getPublicationURL.ts @@ -3,23 +3,15 @@ import { PubLeafletPublication } from "lexicons/api"; import { isProductionDomain } from "src/utils/isProductionDeployment"; import { Json } from "supabase/database.types"; -export function getPublicationURL(pub: { - uri: string; - name: string; - record: Json; -}) { +export function getPublicationURL(pub: { uri: string; record: Json }) { let record = pub.record as PubLeafletPublication.Record; if (isProductionDomain() && record?.base_path) return `https://${record.base_path}`; else return getBasePublicationURL(pub); } -export function getBasePublicationURL(pub: { - uri: string; - name: string; - record: Json; -}) { +export function getBasePublicationURL(pub: { uri: string; record: Json }) { let record = pub.record as PubLeafletPublication.Record; let aturi = new AtUri(pub.uri); - return `/lish/${aturi.host}/${encodeURIComponent(aturi.rkey || record?.name || pub.name)}`; + return `/lish/${aturi.host}/${encodeURIComponent(aturi.rkey || record?.name)}`; } diff --git a/components/PageSWRDataProvider.tsx b/components/PageSWRDataProvider.tsx index 54b55817..ab866443 100644 --- a/components/PageSWRDataProvider.tsx +++ b/components/PageSWRDataProvider.tsx @@ -8,6 +8,8 @@ import { getPollData } from "actions/pollActions"; import type { GetLeafletDataReturnType } from "app/api/rpc/[command]/get_leaflet_data"; import { createContext, useContext } from "react"; import { getPublicationMetadataFromLeafletData } from "src/utils/getPublicationMetadataFromLeafletData"; +import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; +import { AtUri } from "@atproto/syntax"; export const StaticLeafletDataContext = createContext< null | GetLeafletDataReturnType["result"]["data"] @@ -80,3 +82,47 @@ export function useLeafletDomains() { let { data, mutate } = useLeafletData(); return { data: data?.custom_domain_routes, mutate: mutate }; } + +export function useLeafletPublicationStatus() { + const data = useContext(StaticLeafletDataContext); + if (!data) return null; + + const publishedInPublication = data.leaflets_in_publications?.find( + (l) => l.doc, + ); + const publishedStandalone = data.leaflets_to_documents?.find( + (l) => !!l.documents, + ); + + const documentUri = + publishedInPublication?.documents?.uri ?? publishedStandalone?.document; + + // Compute the full post URL for sharing + let postShareLink: string | undefined; + if (publishedInPublication?.publications && publishedInPublication.documents) { + // Published in a publication - use publication URL + document rkey + const docUri = new AtUri(publishedInPublication.documents.uri); + postShareLink = `${getPublicationURL(publishedInPublication.publications)}/${docUri.rkey}`; + } else if (publishedStandalone?.document) { + // Standalone published post - use /p/{did}/{rkey} format + const docUri = new AtUri(publishedStandalone.document); + postShareLink = `/p/${docUri.host}/${docUri.rkey}`; + } + + return { + token: data, + leafletId: data.root_entity, + shareLink: data.id, + // Draft state - in a publication but not yet published + draftInPublication: + data.leaflets_in_publications?.[0]?.publication ?? undefined, + // Published state + isPublished: !!(publishedInPublication || publishedStandalone), + publishedAt: + publishedInPublication?.documents?.indexed_at ?? + publishedStandalone?.documents?.indexed_at, + documentUri, + // Full URL for sharing published posts + postShareLink, + }; +}