diff --git a/app/(home-pages)/notifications/NotificationList.tsx b/app/(home-pages)/notifications/NotificationList.tsx
index a0ec12ee..30852227 100644
--- a/app/(home-pages)/notifications/NotificationList.tsx
+++ b/app/(home-pages)/notifications/NotificationList.tsx
@@ -11,6 +11,7 @@ import { QuoteNotification } from "./QuoteNotification";
import { BskyPostEmbedNotification } from "./BskyPostEmbedNotification";
import { MentionNotification } from "./MentionNotification";
import { CommentMentionNotification } from "./CommentMentionNotification";
+import { RecommendNotification } from "./RecommendNotification";
export function NotificationList({
notifications,
@@ -58,6 +59,9 @@ export function NotificationList({
if (n.type === "comment_mention") {
return ;
}
+ if (n.type === "recommend") {
+ return ;
+ }
})}
diff --git a/app/(home-pages)/notifications/RecommendNotification.tsx b/app/(home-pages)/notifications/RecommendNotification.tsx
new file mode 100644
index 00000000..3abb49d3
--- /dev/null
+++ b/app/(home-pages)/notifications/RecommendNotification.tsx
@@ -0,0 +1,48 @@
+import { ContentLayout, Notification } from "./Notification";
+import { HydratedRecommendNotification } from "src/notifications";
+import { blobRefToSrc } from "src/utils/blobRefToSrc";
+import { AppBskyActorProfile } from "lexicons/api";
+import { Avatar } from "components/Avatar";
+import { AtUri } from "@atproto/api";
+import { RecommendTinyFilled } from "components/Icons/RecommendTiny";
+
+export const RecommendNotification = (
+ props: HydratedRecommendNotification,
+) => {
+ const profileRecord = props.recommendData?.identities?.bsky_profiles
+ ?.record as AppBskyActorProfile.Record;
+ const displayName =
+ profileRecord?.displayName ||
+ props.recommendData?.identities?.bsky_profiles?.handle ||
+ "Someone";
+ const docRecord = props.normalizedDocument;
+ const pubRecord = props.normalizedPublication;
+ const avatarSrc =
+ profileRecord?.avatar?.ref &&
+ blobRefToSrc(
+ profileRecord.avatar.ref,
+ props.recommendData?.recommender_did || "",
+ );
+
+ if (!docRecord) return null;
+
+ const docUri = new AtUri(props.document.uri);
+ const rkey = docUri.rkey;
+ const did = docUri.host;
+
+ const href = pubRecord ? `${pubRecord.url}/${rkey}` : `/p/${did}/${rkey}`;
+
+ return (
+ }
+ actionText={<>{displayName} recommended your post>}
+ content={
+
+ {null}
+
+ }
+ />
+ );
+};
diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts b/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts
index d0295f26..c219720c 100644
--- a/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts
+++ b/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts
@@ -7,6 +7,11 @@ import { TID } from "@atproto/common";
import { AtUri, Un$Typed } from "@atproto/api";
import { supabaseServerClient } from "supabase/serverClient";
import { Json } from "supabase/database.types";
+import { v7 } from "uuid";
+import {
+ Notification,
+ pingIdentityToUpdateNotification,
+} from "src/notifications";
type RecommendResult =
| { success: true; uri: string }
@@ -68,6 +73,22 @@ export async function recommendAction(args: {
});
console.log(res);
+ // Notify the document owner
+ let documentOwner = new AtUri(args.document).host;
+ if (documentOwner !== credentialSession.did) {
+ let notification: Notification = {
+ id: v7(),
+ recipient: documentOwner,
+ data: {
+ type: "recommend",
+ document_uri: args.document,
+ recommend_uri: uri.toString(),
+ },
+ };
+ await supabaseServerClient.from("notifications").insert(notification);
+ await pingIdentityToUpdateNotification(documentOwner);
+ }
+
return {
success: true,
uri: uri.toString(),
diff --git a/src/notifications.ts b/src/notifications.ts
index f4353d2b..be5ae4bb 100644
--- a/src/notifications.ts
+++ b/src/notifications.ts
@@ -27,7 +27,8 @@ export type NotificationData =
| { type: "mention"; document_uri: string; mention_type: "document"; mentioned_uri: string }
| { type: "comment_mention"; comment_uri: string; mention_type: "did" }
| { type: "comment_mention"; comment_uri: string; mention_type: "publication"; mentioned_uri: string }
- | { type: "comment_mention"; comment_uri: string; mention_type: "document"; mentioned_uri: string };
+ | { type: "comment_mention"; comment_uri: string; mention_type: "document"; mentioned_uri: string }
+ | { type: "recommend"; document_uri: string; recommend_uri: string };
export type HydratedNotification =
| HydratedCommentNotification
@@ -35,22 +36,24 @@ export type HydratedNotification =
| HydratedQuoteNotification
| HydratedBskyPostEmbedNotification
| HydratedMentionNotification
- | HydratedCommentMentionNotification;
+ | HydratedCommentMentionNotification
+ | HydratedRecommendNotification;
export async function hydrateNotifications(
notifications: NotificationRow[],
): Promise> {
// Call all hydrators in parallel
- const [commentNotifications, subscribeNotifications, quoteNotifications, bskyPostEmbedNotifications, mentionNotifications, commentMentionNotifications] = await Promise.all([
+ const [commentNotifications, subscribeNotifications, quoteNotifications, bskyPostEmbedNotifications, mentionNotifications, commentMentionNotifications, recommendNotifications] = await Promise.all([
hydrateCommentNotifications(notifications),
hydrateSubscribeNotifications(notifications),
hydrateQuoteNotifications(notifications),
hydrateBskyPostEmbedNotifications(notifications),
hydrateMentionNotifications(notifications),
hydrateCommentMentionNotifications(notifications),
+ hydrateRecommendNotifications(notifications),
]);
// Combine all hydrated notifications
- const allHydrated = [...commentNotifications, ...subscribeNotifications, ...quoteNotifications, ...bskyPostEmbedNotifications, ...mentionNotifications, ...commentMentionNotifications];
+ const allHydrated = [...commentNotifications, ...subscribeNotifications, ...quoteNotifications, ...bskyPostEmbedNotifications, ...mentionNotifications, ...commentMentionNotifications, ...recommendNotifications];
// Sort by created_at to maintain order
allHydrated.sort(
@@ -519,6 +522,58 @@ async function hydrateCommentMentionNotifications(notifications: NotificationRow
.filter((n) => n !== null);
}
+export type HydratedRecommendNotification = Awaited<
+ ReturnType
+>[0];
+
+async function hydrateRecommendNotifications(notifications: NotificationRow[]) {
+ const recommendNotifications = notifications.filter(
+ (n): n is NotificationRow & { data: ExtractNotificationType<"recommend"> } =>
+ (n.data as NotificationData)?.type === "recommend",
+ );
+
+ if (recommendNotifications.length === 0) {
+ return [];
+ }
+
+ // Fetch recommend data from the database
+ const recommendUris = recommendNotifications.map((n) => n.data.recommend_uri);
+ const documentUris = recommendNotifications.map((n) => n.data.document_uri);
+
+ const [{ data: recommends }, { data: documents }] = await Promise.all([
+ supabaseServerClient
+ .from("recommends_on_documents")
+ .select("*, identities(bsky_profiles(*))")
+ .in("uri", recommendUris),
+ supabaseServerClient
+ .from("documents")
+ .select("*, documents_in_publications(publications(*))")
+ .in("uri", documentUris),
+ ]);
+
+ return recommendNotifications
+ .map((notification) => {
+ const recommendData = recommends?.find((r) => r.uri === notification.data.recommend_uri);
+ const document = documents?.find((d) => d.uri === notification.data.document_uri);
+ if (!recommendData || !document) return null;
+ return {
+ id: notification.id,
+ recipient: notification.recipient,
+ created_at: notification.created_at,
+ type: "recommend" as const,
+ recommend_uri: notification.data.recommend_uri,
+ document_uri: notification.data.document_uri,
+ recommendData,
+ document,
+ normalizedDocument: normalizeDocumentRecord(document.data, document.uri),
+ normalizedPublication: normalizePublicationRecord(
+ document.documents_in_publications[0]?.publications?.record,
+ ),
+ };
+ })
+ .filter((n) => n !== null);
+}
+
export async function pingIdentityToUpdateNotification(did: string) {
let channel = supabaseServerClient.channel(`identity.atp_did:${did}`);
await channel.send({