diff --git a/app/(home-pages)/notifications/page.tsx b/app/(home-pages)/notifications/page.tsx index 7e19c974..796ea99d 100644 --- a/app/(home-pages)/notifications/page.tsx +++ b/app/(home-pages)/notifications/page.tsx @@ -1,3 +1,89 @@ +import { getIdentityData } from "actions/getIdentityData"; +import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock"; +import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; +import { PubLeafletComment, PubLeafletDocument } from "lexicons/api"; +import { redirect } from "next/navigation"; +import { + HydratedCommentNotification, + hydrateNotifications, +} from "src/notifications"; +import { supabaseServerClient } from "supabase/serverClient"; + export default async function Notifications() { - return
Notifications
; + let identity = await getIdentityData(); + if (!identity?.atp_did) return redirect("/home"); + let { data, error } = await supabaseServerClient + .from("notifications") + .select("*") + .eq("recipient", identity.atp_did); + let notifications = await hydrateNotifications(data || []); + return ( + +

Notifications

+ {notifications.map((n) => { + if (n.type === "comment") { + n; + return ; + } + })} + + ), + }, + }} + /> + ); } + +const CommentNotification = (props: HydratedCommentNotification) => { + let docRecord = props.commentData.documents + ?.data as PubLeafletDocument.Record; + let commentRecord = props.commentData.record as PubLeafletComment.Record; + return ( + +

{docRecord.title}

+
+
+              
+            
+
+ + } + /> + ); +}; + +const Notification = (props: { + identity: string; + action: string; + content: React.ReactNode; +}) => { + return ( +
+
+ {props.identity} {props.action} +
+ {props.content} +
+ ); +}; diff --git a/components/ActionBar/Navigation.tsx b/components/ActionBar/Navigation.tsx index 1d996b46..548a9d90 100644 --- a/components/ActionBar/Navigation.tsx +++ b/components/ActionBar/Navigation.tsx @@ -11,8 +11,10 @@ import { ReaderReadSmall, ReaderUnreadSmall, } from "components/Icons/ReaderSmall"; +import { NotificationsUnreadSmall } from "components/Icons/NotificationSmall"; +import { SpeedyLink } from "components/SpeedyLink"; -export type navPages = "home" | "reader" | "pub" | "discover"; +export type navPages = "home" | "reader" | "pub" | "discover" | "notifications"; export const DesktopNavigation = (props: { currentPage: navPages; @@ -27,16 +29,6 @@ export const DesktopNavigation = (props: { /> {/* - - ) : ( - - ) - } - label="Notifications" - /> */} ); @@ -97,6 +89,21 @@ const NavigationOptions = (props: { /> + {identity && ( + + } + label="Notifications" + className={ + props.currentPage === "notifications" + ? "bg-bg-page! border-border-light!" + : "" + } + /> + + )} +
@@ -105,14 +112,14 @@ const NavigationOptions = (props: { const HomeButton = (props: { current?: boolean }) => { return ( - + } label="Home" className={props.current ? "bg-bg-page! border-border-light!" : ""} /> - + ); }; @@ -121,7 +128,7 @@ const ReaderButton = (props: { current?: boolean; subs: boolean }) => { if (!props.subs) return; return ( - + : } @@ -131,7 +138,7 @@ const ReaderButton = (props: { current?: boolean; subs: boolean }) => { ${props.current && "border-accent-contrast!"} `} /> - + ); }; diff --git a/components/Icons/NotificationSmall.tsx b/components/Icons/NotificationSmall.tsx index 2e5ee65a..4577ae3a 100644 --- a/components/Icons/NotificationSmall.tsx +++ b/components/Icons/NotificationSmall.tsx @@ -19,6 +19,23 @@ export const NotificationsUnreadSmall = (props: Props) => { }; export const NotificationsReadSmall = (props: Props) => { + return ( + + + + ); +}; + +export const ReaderUnread = (props: Props) => { return ( ; export type Notification = Omit, "data"> & { data: NotificationData; }; -// Notification data types (for writing to the notifications table) + export type NotificationData = | { type: "comment"; comment_uri: string } | { type: "subscribe"; subscription_uri: string }; -// Hydrated notification types -export type HydratedCommentNotification = { - id: string; - recipient: string; - created_at: string; - type: "comment"; - comment_uri: string; - commentData?: Tables<"comments_on_documents">; -}; +export async function hydrateNotifications(notifications: NotificationRow[]) { + // Call all hydrators in parallel + const [commentNotifications, subscribeNotifications] = await Promise.all([ + hydrateCommentNotifications(notifications), + hydrateSubscribeNotifications(notifications), + ]); -export type HydratedSubscribeNotification = { - id: string; - recipient: string; - created_at: string; - type: "subscribe"; - subscription_uri: string; - subscriptionData?: Tables<"publication_subscriptions">; -}; + // Combine all hydrated notifications + const allHydrated = [...commentNotifications, ...subscribeNotifications]; + + // Sort by created_at to maintain order + allHydrated.sort( + (a, b) => + new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), + ); -export type HydratedNotification = - | HydratedCommentNotification - | HydratedSubscribeNotification; + return allHydrated; +} // Type guard to extract notification type type ExtractNotificationType = Extract< @@ -42,17 +38,11 @@ type ExtractNotificationType = Extract< { type: T } >; -// Hydrator function type -type NotificationHydrator = ( - notifications: NotificationRow[], -) => Promise>; +export type HydratedCommentNotification = Awaited< + ReturnType +>[0]; -/** - * Hydrates comment notifications - */ -async function hydrateCommentNotifications( - notifications: NotificationRow[], -): Promise { +async function hydrateCommentNotifications(notifications: NotificationRow[]) { const commentNotifications = notifications.filter( (n): n is NotificationRow & { data: ExtractNotificationType<"comment"> } => (n.data as NotificationData)?.type === "comment", @@ -66,7 +56,7 @@ async function hydrateCommentNotifications( const commentUris = commentNotifications.map((n) => n.data.comment_uri); const { data: comments } = await supabaseServerClient .from("comments_on_documents") - .select("*") + .select("*,bsky_profiles(*), documents(*)") .in("uri", commentUris); return commentNotifications.map((notification) => ({ @@ -75,13 +65,20 @@ async function hydrateCommentNotifications( created_at: notification.created_at, type: "comment" as const, comment_uri: notification.data.comment_uri, - commentData: comments?.find((c) => c.uri === notification.data.comment_uri), + commentData: comments?.find( + (c) => c.uri === notification.data.comment_uri, + )!, })); } -/** - * Hydrates subscribe notifications - */ +export type HydratedSubscribeNotification = { + id: string; + recipient: string; + created_at: string; + type: "subscribe"; + subscription_uri: string; + subscriptionData?: Tables<"publication_subscriptions">; +}; async function hydrateSubscribeNotifications( notifications: NotificationRow[], ): Promise { @@ -116,27 +113,3 @@ async function hydrateSubscribeNotifications( ), })); } - -/** - * Main hydration function that processes all notifications - */ -export async function hydrateNotifications( - notifications: NotificationRow[], -): Promise { - // Call all hydrators in parallel - const [commentNotifications, subscribeNotifications] = await Promise.all([ - hydrateCommentNotifications(notifications), - hydrateSubscribeNotifications(notifications), - ]); - - // Combine all hydrated notifications - const allHydrated = [...commentNotifications, ...subscribeNotifications]; - - // Sort by created_at to maintain order - allHydrated.sort( - (a, b) => - new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), - ); - - return allHydrated; -}