tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
28
pulls
pipelines
add recommend notification
awarm.space
1 month ago
aab71b59
66ec0e5a
+132
-4
4 changed files
expand all
collapse all
unified
split
app
(home-pages)
notifications
NotificationList.tsx
RecommendNotification.tsx
lish
[did]
[publication]
[rkey]
Interactions
recommendAction.ts
src
notifications.ts
+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
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
61
+
}
62
62
+
if (n.type === "recommend") {
63
63
+
return <RecommendNotification key={n.id} {...n} />;
60
64
}
61
65
})}
62
66
</div>
+48
app/(home-pages)/notifications/RecommendNotification.tsx
···
1
1
+
import { ContentLayout, Notification } from "./Notification";
2
2
+
import { HydratedRecommendNotification } from "src/notifications";
3
3
+
import { blobRefToSrc } from "src/utils/blobRefToSrc";
4
4
+
import { AppBskyActorProfile } from "lexicons/api";
5
5
+
import { Avatar } from "components/Avatar";
6
6
+
import { AtUri } from "@atproto/api";
7
7
+
import { RecommendTinyFilled } from "components/Icons/RecommendTiny";
8
8
+
9
9
+
export const RecommendNotification = (
10
10
+
props: HydratedRecommendNotification,
11
11
+
) => {
12
12
+
const profileRecord = props.recommendData?.identities?.bsky_profiles
13
13
+
?.record as AppBskyActorProfile.Record;
14
14
+
const displayName =
15
15
+
profileRecord?.displayName ||
16
16
+
props.recommendData?.identities?.bsky_profiles?.handle ||
17
17
+
"Someone";
18
18
+
const docRecord = props.normalizedDocument;
19
19
+
const pubRecord = props.normalizedPublication;
20
20
+
const avatarSrc =
21
21
+
profileRecord?.avatar?.ref &&
22
22
+
blobRefToSrc(
23
23
+
profileRecord.avatar.ref,
24
24
+
props.recommendData?.recommender_did || "",
25
25
+
);
26
26
+
27
27
+
if (!docRecord) return null;
28
28
+
29
29
+
const docUri = new AtUri(props.document.uri);
30
30
+
const rkey = docUri.rkey;
31
31
+
const did = docUri.host;
32
32
+
33
33
+
const href = pubRecord ? `${pubRecord.url}/${rkey}` : `/p/${did}/${rkey}`;
34
34
+
35
35
+
return (
36
36
+
<Notification
37
37
+
timestamp={props.created_at}
38
38
+
href={href}
39
39
+
icon={<RecommendTinyFilled />}
40
40
+
actionText={<>{displayName} recommended your post</>}
41
41
+
content={
42
42
+
<ContentLayout postTitle={docRecord.title} pubRecord={pubRecord}>
43
43
+
{null}
44
44
+
</ContentLayout>
45
45
+
}
46
46
+
/>
47
47
+
);
48
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
10
+
import { v7 } from "uuid";
11
11
+
import {
12
12
+
Notification,
13
13
+
pingIdentityToUpdateNotification,
14
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
75
+
76
76
+
// Notify the document owner
77
77
+
let documentOwner = new AtUri(args.document).host;
78
78
+
if (documentOwner !== credentialSession.did) {
79
79
+
let notification: Notification = {
80
80
+
id: v7(),
81
81
+
recipient: documentOwner,
82
82
+
data: {
83
83
+
type: "recommend",
84
84
+
document_uri: args.document,
85
85
+
recommend_uri: uri.toString(),
86
86
+
},
87
87
+
};
88
88
+
await supabaseServerClient.from("notifications").insert(notification);
89
89
+
await pingIdentityToUpdateNotification(documentOwner);
90
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
30
-
| { type: "comment_mention"; comment_uri: string; mention_type: "document"; mentioned_uri: string };
30
30
+
| { type: "comment_mention"; comment_uri: string; mention_type: "document"; mentioned_uri: string }
31
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
38
-
| HydratedCommentMentionNotification;
39
39
+
| HydratedCommentMentionNotification
40
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
43
-
const [commentNotifications, subscribeNotifications, quoteNotifications, bskyPostEmbedNotifications, mentionNotifications, commentMentionNotifications] = await Promise.all([
45
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
52
+
hydrateRecommendNotifications(notifications),
50
53
]);
51
54
52
55
// Combine all hydrated notifications
53
53
-
const allHydrated = [...commentNotifications, ...subscribeNotifications, ...quoteNotifications, ...bskyPostEmbedNotifications, ...mentionNotifications, ...commentMentionNotifications];
56
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
520
+
};
521
521
+
})
522
522
+
.filter((n) => n !== null);
523
523
+
}
524
524
+
525
525
+
export type HydratedRecommendNotification = Awaited<
526
526
+
ReturnType<typeof hydrateRecommendNotifications>
527
527
+
>[0];
528
528
+
529
529
+
async function hydrateRecommendNotifications(notifications: NotificationRow[]) {
530
530
+
const recommendNotifications = notifications.filter(
531
531
+
(n): n is NotificationRow & { data: ExtractNotificationType<"recommend"> } =>
532
532
+
(n.data as NotificationData)?.type === "recommend",
533
533
+
);
534
534
+
535
535
+
if (recommendNotifications.length === 0) {
536
536
+
return [];
537
537
+
}
538
538
+
539
539
+
// Fetch recommend data from the database
540
540
+
const recommendUris = recommendNotifications.map((n) => n.data.recommend_uri);
541
541
+
const documentUris = recommendNotifications.map((n) => n.data.document_uri);
542
542
+
543
543
+
const [{ data: recommends }, { data: documents }] = await Promise.all([
544
544
+
supabaseServerClient
545
545
+
.from("recommends_on_documents")
546
546
+
.select("*, identities(bsky_profiles(*))")
547
547
+
.in("uri", recommendUris),
548
548
+
supabaseServerClient
549
549
+
.from("documents")
550
550
+
.select("*, documents_in_publications(publications(*))")
551
551
+
.in("uri", documentUris),
552
552
+
]);
553
553
+
554
554
+
return recommendNotifications
555
555
+
.map((notification) => {
556
556
+
const recommendData = recommends?.find((r) => r.uri === notification.data.recommend_uri);
557
557
+
const document = documents?.find((d) => d.uri === notification.data.document_uri);
558
558
+
if (!recommendData || !document) return null;
559
559
+
return {
560
560
+
id: notification.id,
561
561
+
recipient: notification.recipient,
562
562
+
created_at: notification.created_at,
563
563
+
type: "recommend" as const,
564
564
+
recommend_uri: notification.data.recommend_uri,
565
565
+
document_uri: notification.data.document_uri,
566
566
+
recommendData,
567
567
+
document,
568
568
+
normalizedDocument: normalizeDocumentRecord(document.data, document.uri),
569
569
+
normalizedPublication: normalizePublicationRecord(
570
570
+
document.documents_in_publications[0]?.publications?.record,
571
571
+
),
517
572
};
518
573
})
519
574
.filter((n) => n !== null);