a tool for shared writing and social publishing

add recommend notification

+132 -4
+4
app/(home-pages)/notifications/NotificationList.tsx
··· 11 11 import { BskyPostEmbedNotification } from "./BskyPostEmbedNotification"; 12 12 import { MentionNotification } from "./MentionNotification"; 13 13 import { CommentMentionNotification } from "./CommentMentionNotification"; 14 + import { RecommendNotification } from "./RecommendNotification"; 14 15 15 16 export function NotificationList({ 16 17 notifications, ··· 57 58 } 58 59 if (n.type === "comment_mention") { 59 60 return <CommentMentionNotification key={n.id} {...n} />; 61 + } 62 + if (n.type === "recommend") { 63 + return <RecommendNotification key={n.id} {...n} />; 60 64 } 61 65 })} 62 66 </div>
+48
app/(home-pages)/notifications/RecommendNotification.tsx
··· 1 + import { ContentLayout, Notification } from "./Notification"; 2 + import { HydratedRecommendNotification } from "src/notifications"; 3 + import { blobRefToSrc } from "src/utils/blobRefToSrc"; 4 + import { AppBskyActorProfile } from "lexicons/api"; 5 + import { Avatar } from "components/Avatar"; 6 + import { AtUri } from "@atproto/api"; 7 + import { RecommendTinyFilled } from "components/Icons/RecommendTiny"; 8 + 9 + export const RecommendNotification = ( 10 + props: HydratedRecommendNotification, 11 + ) => { 12 + const profileRecord = props.recommendData?.identities?.bsky_profiles 13 + ?.record as AppBskyActorProfile.Record; 14 + const displayName = 15 + profileRecord?.displayName || 16 + props.recommendData?.identities?.bsky_profiles?.handle || 17 + "Someone"; 18 + const docRecord = props.normalizedDocument; 19 + const pubRecord = props.normalizedPublication; 20 + const avatarSrc = 21 + profileRecord?.avatar?.ref && 22 + blobRefToSrc( 23 + profileRecord.avatar.ref, 24 + props.recommendData?.recommender_did || "", 25 + ); 26 + 27 + if (!docRecord) return null; 28 + 29 + const docUri = new AtUri(props.document.uri); 30 + const rkey = docUri.rkey; 31 + const did = docUri.host; 32 + 33 + const href = pubRecord ? `${pubRecord.url}/${rkey}` : `/p/${did}/${rkey}`; 34 + 35 + return ( 36 + <Notification 37 + timestamp={props.created_at} 38 + href={href} 39 + icon={<RecommendTinyFilled />} 40 + actionText={<>{displayName} recommended your post</>} 41 + content={ 42 + <ContentLayout postTitle={docRecord.title} pubRecord={pubRecord}> 43 + {null} 44 + </ContentLayout> 45 + } 46 + /> 47 + ); 48 + };
+21
app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts
··· 7 7 import { AtUri, Un$Typed } from "@atproto/api"; 8 8 import { supabaseServerClient } from "supabase/serverClient"; 9 9 import { Json } from "supabase/database.types"; 10 + import { v7 } from "uuid"; 11 + import { 12 + Notification, 13 + pingIdentityToUpdateNotification, 14 + } from "src/notifications"; 10 15 11 16 type RecommendResult = 12 17 | { success: true; uri: string } ··· 67 72 } as unknown as Json, 68 73 }); 69 74 console.log(res); 75 + 76 + // Notify the document owner 77 + let documentOwner = new AtUri(args.document).host; 78 + if (documentOwner !== credentialSession.did) { 79 + let notification: Notification = { 80 + id: v7(), 81 + recipient: documentOwner, 82 + data: { 83 + type: "recommend", 84 + document_uri: args.document, 85 + recommend_uri: uri.toString(), 86 + }, 87 + }; 88 + await supabaseServerClient.from("notifications").insert(notification); 89 + await pingIdentityToUpdateNotification(documentOwner); 90 + } 70 91 71 92 return { 72 93 success: true,
+59 -4
src/notifications.ts
··· 27 27 | { type: "mention"; document_uri: string; mention_type: "document"; mentioned_uri: string } 28 28 | { type: "comment_mention"; comment_uri: string; mention_type: "did" } 29 29 | { type: "comment_mention"; comment_uri: string; mention_type: "publication"; mentioned_uri: string } 30 - | { type: "comment_mention"; comment_uri: string; mention_type: "document"; mentioned_uri: string }; 30 + | { type: "comment_mention"; comment_uri: string; mention_type: "document"; mentioned_uri: string } 31 + | { type: "recommend"; document_uri: string; recommend_uri: string }; 31 32 32 33 export type HydratedNotification = 33 34 | HydratedCommentNotification ··· 35 36 | HydratedQuoteNotification 36 37 | HydratedBskyPostEmbedNotification 37 38 | HydratedMentionNotification 38 - | HydratedCommentMentionNotification; 39 + | HydratedCommentMentionNotification 40 + | HydratedRecommendNotification; 39 41 export async function hydrateNotifications( 40 42 notifications: NotificationRow[], 41 43 ): Promise<Array<HydratedNotification>> { 42 44 // Call all hydrators in parallel 43 - const [commentNotifications, subscribeNotifications, quoteNotifications, bskyPostEmbedNotifications, mentionNotifications, commentMentionNotifications] = await Promise.all([ 45 + const [commentNotifications, subscribeNotifications, quoteNotifications, bskyPostEmbedNotifications, mentionNotifications, commentMentionNotifications, recommendNotifications] = await Promise.all([ 44 46 hydrateCommentNotifications(notifications), 45 47 hydrateSubscribeNotifications(notifications), 46 48 hydrateQuoteNotifications(notifications), 47 49 hydrateBskyPostEmbedNotifications(notifications), 48 50 hydrateMentionNotifications(notifications), 49 51 hydrateCommentMentionNotifications(notifications), 52 + hydrateRecommendNotifications(notifications), 50 53 ]); 51 54 52 55 // Combine all hydrated notifications 53 - const allHydrated = [...commentNotifications, ...subscribeNotifications, ...quoteNotifications, ...bskyPostEmbedNotifications, ...mentionNotifications, ...commentMentionNotifications]; 56 + const allHydrated = [...commentNotifications, ...subscribeNotifications, ...quoteNotifications, ...bskyPostEmbedNotifications, ...mentionNotifications, ...commentMentionNotifications, ...recommendNotifications]; 54 57 55 58 // Sort by created_at to maintain order 56 59 allHydrated.sort( ··· 514 517 ), 515 518 normalizedMentionedPublication: normalizePublicationRecord(mentionedPublication?.record), 516 519 normalizedMentionedDocument: normalizeDocumentRecord(mentionedDoc?.data, mentionedDoc?.uri), 520 + }; 521 + }) 522 + .filter((n) => n !== null); 523 + } 524 + 525 + export type HydratedRecommendNotification = Awaited< 526 + ReturnType<typeof hydrateRecommendNotifications> 527 + >[0]; 528 + 529 + async function hydrateRecommendNotifications(notifications: NotificationRow[]) { 530 + const recommendNotifications = notifications.filter( 531 + (n): n is NotificationRow & { data: ExtractNotificationType<"recommend"> } => 532 + (n.data as NotificationData)?.type === "recommend", 533 + ); 534 + 535 + if (recommendNotifications.length === 0) { 536 + return []; 537 + } 538 + 539 + // Fetch recommend data from the database 540 + const recommendUris = recommendNotifications.map((n) => n.data.recommend_uri); 541 + const documentUris = recommendNotifications.map((n) => n.data.document_uri); 542 + 543 + const [{ data: recommends }, { data: documents }] = await Promise.all([ 544 + supabaseServerClient 545 + .from("recommends_on_documents") 546 + .select("*, identities(bsky_profiles(*))") 547 + .in("uri", recommendUris), 548 + supabaseServerClient 549 + .from("documents") 550 + .select("*, documents_in_publications(publications(*))") 551 + .in("uri", documentUris), 552 + ]); 553 + 554 + return recommendNotifications 555 + .map((notification) => { 556 + const recommendData = recommends?.find((r) => r.uri === notification.data.recommend_uri); 557 + const document = documents?.find((d) => d.uri === notification.data.document_uri); 558 + if (!recommendData || !document) return null; 559 + return { 560 + id: notification.id, 561 + recipient: notification.recipient, 562 + created_at: notification.created_at, 563 + type: "recommend" as const, 564 + recommend_uri: notification.data.recommend_uri, 565 + document_uri: notification.data.document_uri, 566 + recommendData, 567 + document, 568 + normalizedDocument: normalizeDocumentRecord(document.data, document.uri), 569 + normalizedPublication: normalizePublicationRecord( 570 + document.documents_in_publications[0]?.publications?.record, 571 + ), 517 572 }; 518 573 }) 519 574 .filter((n) => n !== null);