+60
-2
__generated__/lexicons.ts
+60
-2
__generated__/lexicons.ts
···
2441
2441
type: 'ref',
2442
2442
ref: 'lex:social.grain.actor.defs#profileView',
2443
2443
},
2444
+
reasonSubject: {
2445
+
type: 'string',
2446
+
format: 'at-uri',
2447
+
},
2444
2448
reason: {
2445
2449
type: 'string',
2446
2450
description:
···
2455
2459
'unknown',
2456
2460
],
2457
2461
},
2458
-
reasonSubject: {
2462
+
record: {
2463
+
type: 'unknown',
2464
+
},
2465
+
isRead: {
2466
+
type: 'boolean',
2467
+
},
2468
+
indexedAt: {
2469
+
type: 'string',
2470
+
format: 'datetime',
2471
+
},
2472
+
},
2473
+
},
2474
+
notificationViewDetailed: {
2475
+
type: 'object',
2476
+
required: [
2477
+
'uri',
2478
+
'cid',
2479
+
'author',
2480
+
'reason',
2481
+
'record',
2482
+
'isRead',
2483
+
'indexedAt',
2484
+
],
2485
+
properties: {
2486
+
uri: {
2459
2487
type: 'string',
2460
2488
format: 'at-uri',
2461
2489
},
2490
+
cid: {
2491
+
type: 'string',
2492
+
format: 'cid',
2493
+
},
2494
+
author: {
2495
+
type: 'ref',
2496
+
ref: 'lex:social.grain.actor.defs#profileView',
2497
+
},
2498
+
reason: {
2499
+
type: 'string',
2500
+
description:
2501
+
'The reason why this notification was delivered - e.g. your gallery was favd, or you received a new follower.',
2502
+
knownValues: [
2503
+
'follow',
2504
+
'gallery-favorite',
2505
+
'gallery-comment',
2506
+
'reply',
2507
+
'gallery-mention',
2508
+
'gallery-comment-mention',
2509
+
'unknown',
2510
+
],
2511
+
},
2512
+
reasonSubject: {
2513
+
type: 'union',
2514
+
refs: [
2515
+
'lex:social.grain.actor.defs#profileView',
2516
+
'lex:social.grain.comment.defs#commentView',
2517
+
'lex:social.grain.gallery.defs#galleryView',
2518
+
],
2519
+
},
2462
2520
record: {
2463
2521
type: 'unknown',
2464
2522
},
···
2508
2566
type: 'array',
2509
2567
items: {
2510
2568
type: 'ref',
2511
-
ref: 'lex:social.grain.notification.defs#notificationView',
2569
+
ref: 'lex:social.grain.notification.defs#notificationViewDetailed',
2512
2570
},
2513
2571
},
2514
2572
seenAt: {
+2
-2
src/api/mod.ts
+2
-2
src/api/mod.ts
···
54
54
getFollowingWithProfiles,
55
55
} from "../lib/follow.ts";
56
56
import { getGalleriesByHashtag, getGallery } from "../lib/gallery.ts";
57
-
import { getNotifications } from "../lib/notifications.ts";
57
+
import { getNotificationsDetailed } from "../lib/notifications.ts";
58
58
import { getTimeline } from "../lib/timeline.ts";
59
59
import { getGalleryComments } from "../modules/comments.tsx";
60
60
···
141
141
(_req, _params, ctx) => {
142
142
// @TODO: this redirects, we should have a json response
143
143
ctx.requireAuth();
144
-
const notifications = getNotifications(
144
+
const notifications = getNotificationsDetailed(
145
145
ctx,
146
146
);
147
147
return ctx.json(
+5
-3
src/lib/actor.ts
+5
-3
src/lib/actor.ts
···
12
12
import { Record as Photo } from "$lexicon/types/social/grain/photo.ts";
13
13
import { isPhotoView } from "$lexicon/types/social/grain/photo/defs.ts";
14
14
import { Record as PhotoExif } from "$lexicon/types/social/grain/photo/exif.ts";
15
-
import { Un$Typed } from "$lexicon/util.ts";
15
+
import { $Typed } from "$lexicon/util.ts";
16
16
import { BffContext, WithBffMeta } from "@bigmoves/bff";
17
17
import { getFollow, getFollowersCount, getFollowsCount } from "./follow.ts";
18
18
import {
···
75
75
export function profileToView(
76
76
record: WithBffMeta<GrainProfile>,
77
77
handle: string,
78
-
): Un$Typed<ProfileView> {
78
+
): $Typed<ProfileView> {
79
79
return {
80
+
$type: "social.grain.actor.defs#profileView",
80
81
cid: record.cid,
81
82
did: record.did,
82
83
handle,
···
96
97
galleryCount: number;
97
98
viewer: ViewerState;
98
99
cameras?: string[];
99
-
}): Un$Typed<ProfileViewDetailed> {
100
+
}): $Typed<ProfileViewDetailed> {
100
101
const {
101
102
record,
102
103
handle,
···
107
108
cameras,
108
109
} = params;
109
110
return {
111
+
$type: "social.grain.actor.defs#profileViewDetailed",
110
112
cid: record.cid,
111
113
did: record.did,
112
114
handle,
+133
-2
src/lib/notifications.ts
+133
-2
src/lib/notifications.ts
···
7
7
isRecord as isComment,
8
8
Record as Comment,
9
9
} from "$lexicon/types/social/grain/comment.ts";
10
+
import { CommentView } from "$lexicon/types/social/grain/comment/defs.ts";
10
11
import {
11
12
isRecord as isFavorite,
12
13
Record as Favorite,
···
15
16
isRecord as isGallery,
16
17
Record as Gallery,
17
18
} from "$lexicon/types/social/grain/gallery.ts";
19
+
import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts";
18
20
import {
19
21
isRecord as isFollow,
20
22
Record as Follow,
21
23
} from "$lexicon/types/social/grain/graph/follow.ts";
22
-
import { NotificationView } from "$lexicon/types/social/grain/notification/defs.ts";
23
-
import { Un$Typed } from "$lexicon/util.ts";
24
+
import {
25
+
NotificationView,
26
+
NotificationViewDetailed,
27
+
} from "$lexicon/types/social/grain/notification/defs.ts";
28
+
import { $Typed, Un$Typed } from "$lexicon/util.ts";
29
+
import { AtUri } from "@atproto/syntax";
24
30
import { ActorTable, BffContext, WithBffMeta } from "@bigmoves/bff";
31
+
import { getComment } from "../modules/comments.tsx";
25
32
import { getActorProfile } from "./actor.ts";
33
+
import { getGallery } from "./gallery.ts";
26
34
27
35
export type NotificationRecords = WithBffMeta<
28
36
Favorite | Follow | Comment | Gallery
···
136
144
}
137
145
return false;
138
146
}
147
+
148
+
export function getNotificationsDetailed(
149
+
ctx: BffContext,
150
+
): Un$Typed<NotificationViewDetailed>[] {
151
+
const notifications = ctx.getNotifications<NotificationRecords>();
152
+
return notifications
153
+
.filter(
154
+
(notification) =>
155
+
notification.$type === "social.grain.favorite" ||
156
+
notification.$type === "social.grain.graph.follow" ||
157
+
notification.$type === "social.grain.comment" ||
158
+
notification.$type === "social.grain.gallery",
159
+
)
160
+
.map((notification) => {
161
+
const actor = ctx.indexService.getActor(notification.did);
162
+
const authorProfile = getActorProfile(notification.did, ctx);
163
+
if (!actor || !authorProfile) return null;
164
+
165
+
let reasonSubject:
166
+
| $Typed<GalleryView | CommentView | ProfileView>
167
+
| undefined = undefined;
168
+
if (notification.$type === "social.grain.favorite") {
169
+
// Favorite: reasonSubject is the gallery view
170
+
const galleryUri = notification.subject;
171
+
try {
172
+
const atUri = new AtUri(galleryUri);
173
+
const did = atUri.hostname;
174
+
const rkey = atUri.rkey;
175
+
reasonSubject = getGallery(did, rkey, ctx) ?? undefined;
176
+
} catch {
177
+
reasonSubject = undefined;
178
+
}
179
+
} else if (notification.$type === "social.grain.graph.follow") {
180
+
reasonSubject = getActorProfile(
181
+
notification.subject,
182
+
ctx,
183
+
) ?? undefined;
184
+
} else if (notification.$type === "social.grain.comment") {
185
+
// Hydrate comment with author, subject, and focus
186
+
reasonSubject = getComment(notification.uri, ctx) ?? undefined;
187
+
} else if (notification.$type === "social.grain.gallery") {
188
+
try {
189
+
const atUri = new AtUri(notification.uri);
190
+
const did = atUri.hostname;
191
+
const rkey = atUri.rkey;
192
+
reasonSubject = getGallery(did, rkey, ctx) ?? undefined;
193
+
} catch {
194
+
reasonSubject = undefined;
195
+
}
196
+
}
197
+
198
+
return notificationDetailedToView(
199
+
notification,
200
+
authorProfile,
201
+
ctx.currentUser,
202
+
reasonSubject,
203
+
);
204
+
})
205
+
.filter((view): view is Un$Typed<NotificationViewDetailed> =>
206
+
Boolean(view)
207
+
);
208
+
}
209
+
210
+
export function notificationDetailedToView(
211
+
record: NotificationRecords,
212
+
author: $Typed<ProfileView>,
213
+
currentUser?: ActorTable,
214
+
reasonSubject?: $Typed<GalleryView | CommentView | ProfileView>,
215
+
): Un$Typed<NotificationViewDetailed> {
216
+
let reason: string;
217
+
if (isFavorite(record)) {
218
+
reason = "gallery-favorite";
219
+
} else if (isFollow(record)) {
220
+
reason = "follow";
221
+
} else if (
222
+
isComment(record) &&
223
+
record.replyTo
224
+
) {
225
+
if (
226
+
recordHasMentionFacet(
227
+
record,
228
+
currentUser?.did,
229
+
)
230
+
) {
231
+
reason = "gallery-comment-mention";
232
+
} else {
233
+
reason = "reply";
234
+
}
235
+
} else if (isComment(record)) {
236
+
if (
237
+
recordHasMentionFacet(
238
+
record,
239
+
currentUser?.did,
240
+
)
241
+
) {
242
+
reason = "gallery-comment-mention";
243
+
} else {
244
+
reason = "gallery-comment";
245
+
}
246
+
} else if (
247
+
isGallery(record) && recordHasMentionFacet(
248
+
record,
249
+
currentUser?.did,
250
+
)
251
+
) {
252
+
reason = "gallery-mention";
253
+
} else {
254
+
reason = "unknown";
255
+
}
256
+
const isRead = currentUser?.lastSeenNotifs
257
+
? record.createdAt <= currentUser.lastSeenNotifs
258
+
: false;
259
+
return {
260
+
uri: record.uri,
261
+
cid: record.cid,
262
+
author,
263
+
reason,
264
+
reasonSubject,
265
+
record,
266
+
isRead,
267
+
indexedAt: record.indexedAt,
268
+
};
269
+
}
+8
-3
src/modules/comments.tsx
+8
-3
src/modules/comments.tsx
···
9
9
isPhotoView,
10
10
PhotoView,
11
11
} from "$lexicon/types/social/grain/photo/defs.ts";
12
+
import { $Typed } from "$lexicon/util.ts";
12
13
import { Facet } from "@atproto/api";
13
14
import { AtUri } from "@atproto/syntax";
14
15
import { BffContext, BffMiddleware, route, WithBffMeta } from "@bigmoves/bff";
···
589
590
);
590
591
}
591
592
592
-
function getComment(uri: string, ctx: BffContext) {
593
+
export function getComment(
594
+
uri: string,
595
+
ctx: BffContext,
596
+
) {
593
597
const { items: comments } = ctx.indexService.getRecords<WithBffMeta<Comment>>(
594
598
"social.grain.comment",
595
599
{
···
611
615
return commentToView(comment, author, subject, focus);
612
616
}
613
617
614
-
function commentToView(
618
+
export function commentToView(
615
619
record: WithBffMeta<Comment>,
616
620
author: ProfileView,
617
621
subject?: GalleryView,
618
622
focus?: PhotoView,
619
-
): CommentView {
623
+
): $Typed<CommentView> {
620
624
return {
625
+
$type: "social.grain.comment.defs#commentView",
621
626
uri: record.uri,
622
627
cid: record.cid,
623
628
text: record.text,