From 5cc5627df4061340b77697983f5a9cd62d36a2d8 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 27 May 2026 17:03:36 -0400 Subject: [PATCH] use profile cache on subs list --- .../dashboard/PublicationSubscribers.tsx | 108 ++++++---------- .../[publication]/dashboard/SubsPageShell.tsx | 98 +++++++++++++++ .../dashboard/SubscribersList.tsx | 99 +++++++++++++++ .../[publication]/dashboard/subs/page.tsx | 116 +++--------------- 4 files changed, 252 insertions(+), 169 deletions(-) create mode 100644 app/(app)/lish/[did]/[publication]/dashboard/SubsPageShell.tsx create mode 100644 app/(app)/lish/[did]/[publication]/dashboard/SubscribersList.tsx diff --git a/app/(app)/lish/[did]/[publication]/dashboard/PublicationSubscribers.tsx b/app/(app)/lish/[did]/[publication]/dashboard/PublicationSubscribers.tsx index 0bae1cf6..d0ead28d 100644 --- a/app/(app)/lish/[did]/[publication]/dashboard/PublicationSubscribers.tsx +++ b/app/(app)/lish/[did]/[publication]/dashboard/PublicationSubscribers.tsx @@ -1,21 +1,16 @@ "use client"; import { usePublicationData } from "./PublicationSWRProvider"; import { ButtonPrimary } from "components/Buttons"; -import { getPublicationURL } from "app/(app)/lish/createPub/getPublicationURL"; import { useSmoker } from "components/Toast"; -import { Menu, MenuItem } from "components/Menu"; import { Separator } from "components/Layout"; -import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; import { useDashboardState } from "components/PageLayouts/dashboardState"; import { AtmosphereAccount } from "components/Icons/AtmosphereAccount"; import { EmailTiny } from "components/Icons/EmailTiny"; -type subscriber = { email: string | undefined; did: string | undefined }; +export type SubscriberStatus = "subscribed" | "unconfirmed" | "unsubscribed"; -type SubscriberStatus = "subscribed" | "unconfirmed" | "unsubscribed"; - -type MergedSubscriber = { +export type MergedSubscriber = { key: string; did: string | undefined; handle: string | undefined; @@ -86,31 +81,13 @@ export function useMergedSubscribers(): MergedSubscriber[] | null { return [...byDid.values(), ...emailOnly]; } -export function PublicationSubscribers(props: { +export function SubscribersListView(props: { + subscribers: MergedSubscriber[]; + publicationShareUrl: string; showPageBackground?: boolean; }) { let smoker = useSmoker(); - let { data: publication } = usePublicationData(); let { subscriberStatus } = useDashboardState(); - let subscribers = useMergedSubscribers(); - - if (!publication || !subscribers) return
null
; - - // useEffect(() => { - // const allSubscribersSelected = subscribers.every((subscriber) => - // checkedSubscribers.some( - // (checked) => - // checked.email === "dummyemail@email.com" && - // checked.did === subscriber.identities?.bsky_profiles?.did, - // ), - // ); - - // if (allSubscribersSelected && subscribers.length > 0) { - // setCheckAll(true); - // } else { - // setCheckAll(false); - // } - // }, [checkedSubscribers]); let activeStatuses = ( Object.keys(subscriberStatus) as SubscriberStatus[] @@ -118,14 +95,10 @@ export function PublicationSubscribers(props: { let isDefaultStatusFilter = activeStatuses.length === 1 && activeStatuses[0] === "subscribed"; - if (subscribers.length === 0) { + let filtered = props.subscribers.filter((s) => subscriberStatus[s.status]); + + if (filtered.length === 0) { if (!isDefaultStatusFilter) { - let label = - activeStatuses.length === 0 - ? "any status" - : activeStatuses - .map((s) => (s === "unconfirmed" ? "unconfirmed" : s)) - .join(", "); return (
{ e.preventDefault(); let rect = (e.currentTarget as Element)?.getBoundingClientRect(); - navigator.clipboard.writeText( - getPublicationURL(publication.publication!), - ); + navigator.clipboard.writeText(props.publicationShareUrl); smoker({ position: { x: rect ? rect.left + (rect.right - rect.left) / 2 : 0, @@ -191,26 +162,52 @@ export function PublicationSubscribers(props: { } >
- {subscribers + {filtered .sort((a, b) => b.created_at.localeCompare(a.created_at)) .map((subscriber) => ( - <> +
-
- +
+
))}
); } +export function SubscribersListSkeleton(props: { showPageBackground?: boolean }) { + return ( +
+
+ {[0, 1, 2, 3].map((i) => ( +
+
+
+
+ ))} +
+
+ ); +} + const SubscriberListItem = (props: { handle: string | undefined; did: string | undefined; @@ -263,31 +260,6 @@ const SubscriberListItem = (props: { ); }; -const SubscriberOptions = (props: { - checkedSubscribers: subscriber[]; - allSelected: boolean; -}) => { - return ( - - {props.allSelected ? "All" : props.checkedSubscribers.length} Selected{" "} - - - } - > - {}}> - Export {props.allSelected ? "All" : "Selected"} - - {}}> - Remove {props.allSelected ? "All" : "Selected"} - - - ); -}; - function SubscriberDate(props: { createdAt: string }) { const formattedDate = useLocalizedDate(props.createdAt, { year: "2-digit", diff --git a/app/(app)/lish/[did]/[publication]/dashboard/SubsPageShell.tsx b/app/(app)/lish/[did]/[publication]/dashboard/SubsPageShell.tsx new file mode 100644 index 00000000..bac567e3 --- /dev/null +++ b/app/(app)/lish/[did]/[publication]/dashboard/SubsPageShell.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; +import { Popover } from "components/Popover"; +import { Checkbox } from "components/Checkbox"; +import { + useDashboardState, + useSetDashboardState, +} from "components/PageLayouts/dashboardState"; +import { usePublicationData } from "./PublicationSWRProvider"; +import { useMergedSubscribers } from "./PublicationSubscribers"; + +export function SubsPageShell({ children }: { children: React.ReactNode }) { + let { data } = usePublicationData(); + let pubUri = data?.publication?.uri || ""; + let subscribers = useMergedSubscribers(); + let count = subscribers?.length ?? 0; + + return ( + } + publication={pubUri} + showHeader={true} + controls={ +
+
+ {count} Subscriber{count !== 1 && "s"} +
+ +
+ } + > + {children} +
+ ); +} + +const SubscriberStatusFilter = () => { + let { subscriberStatus } = useDashboardState(); + let setState = useSetDashboardState(); + let count = Object.values(subscriberStatus).filter(Boolean).length; + + return ( + + Filters {count > 0 && `(${count})`} +
+ } + > + + setState({ + subscriberStatus: { + ...subscriberStatus, + subscribed: !!e.target.checked, + }, + }) + } + > + Subscribed + + + setState({ + subscriberStatus: { + ...subscriberStatus, + unconfirmed: !!e.target.checked, + }, + }) + } + > + Unconfirmed + + + setState({ + subscriberStatus: { + ...subscriberStatus, + unsubscribed: !!e.target.checked, + }, + }) + } + > + Unsubscribed + + + ); +}; diff --git a/app/(app)/lish/[did]/[publication]/dashboard/SubscribersList.tsx b/app/(app)/lish/[did]/[publication]/dashboard/SubscribersList.tsx new file mode 100644 index 00000000..d362edc9 --- /dev/null +++ b/app/(app)/lish/[did]/[publication]/dashboard/SubscribersList.tsx @@ -0,0 +1,99 @@ +import { supabaseServerClient } from "supabase/serverClient"; +import { getProfiles } from "src/identity"; +import { get_publication_data } from "app/api/rpc/[command]/get_publication_data"; +import { getPublicationURL } from "app/(app)/lish/createPub/getPublicationURL"; +import { normalizePublicationRecord } from "src/utils/normalizeRecords"; +import { + SubscribersListView, + type MergedSubscriber, + type SubscriberStatus, +} from "./PublicationSubscribers"; + +export async function SubscribersList({ + did, + publication, +}: { + did: string; + publication: string; +}) { + const { result } = await get_publication_data.handler( + { did, publication_name: publication }, + { supabase: supabaseServerClient }, + ); + const pub = result.publication; + if (!pub) { + return ( + + ); + } + const record = normalizePublicationRecord(pub.record); + const showPageBackground = !!record?.theme?.showPageBackground; + + const atprotoSubs = pub.publication_subscriptions || []; + const newsletterEnabled = !!pub.publication_newsletter_settings?.enabled; + const emailSubs = newsletterEnabled + ? pub.publication_email_subscribers || [] + : []; + + const dids = new Set(); + for (const s of atprotoSubs) { + const d = s.identities?.bsky_profiles?.did; + if (d) dids.add(d); + } + for (const s of emailSubs) { + if (s.identities?.atp_did) dids.add(s.identities.atp_did); + } + + const profiles = await getProfiles(Array.from(dids)); + + const byDid = new Map(); + const emailOnly: MergedSubscriber[] = []; + + for (const s of atprotoSubs) { + const d = s.identities?.bsky_profiles?.did; + if (!d) continue; + const p = profiles.get(d); + byDid.set(d, { + key: `did:${d}`, + did: d, + handle: p?.handle ?? undefined, + email: undefined, + created_at: s.created_at, + status: "subscribed", + }); + } + + for (const s of emailSubs) { + const status: SubscriberStatus = + s.state === "pending" + ? "unconfirmed" + : s.state === "unsubscribed" + ? "unsubscribed" + : "subscribed"; + const linkedDid = s.identities?.atp_did ?? undefined; + const existing = linkedDid ? byDid.get(linkedDid) : undefined; + if (existing && status === "subscribed") { + existing.email = s.email; + continue; + } + const p = linkedDid ? profiles.get(linkedDid) : null; + emailOnly.push({ + key: `email:${s.id}`, + did: linkedDid, + handle: p?.handle ?? undefined, + email: s.email, + created_at: s.created_at, + status, + }); + } + + const subscribers = [...byDid.values(), ...emailOnly]; + + return ( + + ); +} diff --git a/app/(app)/lish/[did]/[publication]/dashboard/subs/page.tsx b/app/(app)/lish/[did]/[publication]/dashboard/subs/page.tsx index fd88c02c..4979e0f6 100644 --- a/app/(app)/lish/[did]/[publication]/dashboard/subs/page.tsx +++ b/app/(app)/lish/[did]/[publication]/dashboard/subs/page.tsx @@ -1,106 +1,20 @@ -"use client"; +import { Suspense } from "react"; +import { SubsPageShell } from "../SubsPageShell"; +import { SubscribersList } from "../SubscribersList"; +import { SubscribersListSkeleton } from "../PublicationSubscribers"; -import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; -import { - PublicationSubscribers, - useMergedSubscribers, -} from "../PublicationSubscribers"; -import { - usePublicationData, - useNormalizedPublicationRecord, -} from "../PublicationSWRProvider"; -import { Popover } from "components/Popover"; -import { Checkbox } from "components/Checkbox"; -import { - useDashboardState, - useSetDashboardState, -} from "components/PageLayouts/dashboardState"; - -export default function SubsPage() { - let { data } = usePublicationData(); - let record = useNormalizedPublicationRecord(); - let pubUri = data?.publication?.uri || ""; - const showPageBackground = !!record?.theme?.showPageBackground; - let subscribers = useMergedSubscribers(); - let count = subscribers?.length ?? 0; +export default async function SubsPage(props: { + params: Promise<{ did: string; publication: string }>; +}) { + const params = await props.params; + const did = decodeURIComponent(params.did); + const publication = decodeURIComponent(params.publication); return ( - } - publication={pubUri} - showHeader={true} - controls={ -
-
- {count} Subscriber{count !== 1 && "s"} -
- -
- } - > - -
+ + }> + + + ); } - -const SubscriberStatusFilter = () => { - let { subscriberStatus } = useDashboardState(); - let setState = useSetDashboardState(); - let count = Object.values(subscriberStatus).filter(Boolean).length; - - return ( - - Filters {count > 0 && `(${count})`} -
- } - > - - setState({ - subscriberStatus: { - ...subscriberStatus, - subscribed: !!e.target.checked, - }, - }) - } - > - Subscribed - - - setState({ - subscriberStatus: { - ...subscriberStatus, - unconfirmed: !!e.target.checked, - }, - }) - } - > - Unconfirmed - - - setState({ - subscriberStatus: { - ...subscriberStatus, - unsubscribed: !!e.target.checked, - }, - }) - } - > - Unsubscribed - - - ); -}; -- 2.51.2