@atproto/bsky 0.1.7 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/dist/api/app/bsky/actor/getProfile.js +1 -1
- package/dist/api/app/bsky/actor/getProfile.js.map +1 -1
- package/dist/api/app/bsky/actor/getProfiles.js +1 -1
- package/dist/api/app/bsky/actor/getProfiles.js.map +1 -1
- package/dist/api/app/bsky/actor/getSuggestions.js +2 -2
- package/dist/api/app/bsky/actor/getSuggestions.js.map +1 -1
- package/dist/api/app/bsky/embed/getEmbedExternalView.d.ts.map +1 -1
- package/dist/api/app/bsky/embed/getEmbedExternalView.js +132 -53
- package/dist/api/app/bsky/embed/getEmbedExternalView.js.map +1 -1
- package/dist/api/app/bsky/feed/getAuthorFeed.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/getAuthorFeed.js +4 -2
- package/dist/api/app/bsky/feed/getAuthorFeed.js.map +1 -1
- package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js +2 -2
- package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js.map +1 -1
- package/dist/api/app/bsky/notification/constants.d.ts +4 -1
- package/dist/api/app/bsky/notification/constants.d.ts.map +1 -1
- package/dist/api/app/bsky/notification/constants.js +3 -0
- package/dist/api/app/bsky/notification/constants.js.map +1 -1
- package/dist/api/app/bsky/notification/getGroupedNotifications.d.ts +1 -1
- package/dist/api/app/bsky/notification/getGroupedNotifications.d.ts.map +1 -1
- package/dist/api/app/bsky/notification/getGroupedNotifications.js +44 -26
- package/dist/api/app/bsky/notification/getGroupedNotifications.js.map +1 -1
- package/dist/api/app/bsky/notification/grouping/spotlight.d.ts +1 -1
- package/dist/api/app/bsky/notification/grouping/spotlight.d.ts.map +1 -1
- package/dist/api/app/bsky/notification/grouping/spotlight.js +114 -80
- package/dist/api/app/bsky/notification/grouping/spotlight.js.map +1 -1
- package/dist/api/app/bsky/notification/listActivitySubscriptions.js +3 -2
- package/dist/api/app/bsky/notification/listActivitySubscriptions.js.map +1 -1
- package/dist/api/app/bsky/unspecced/getAtmosphereExploreTab.d.ts.map +1 -1
- package/dist/api/app/bsky/unspecced/getAtmosphereExploreTab.js +16 -148
- package/dist/api/app/bsky/unspecced/getAtmosphereExploreTab.js.map +1 -1
- package/dist/api/internal/bsky/actor/getProfiles.js +4 -1
- package/dist/api/internal/bsky/actor/getProfiles.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -0
- package/dist/config.js.map +1 -1
- package/dist/data-plane/server/indexing/index.d.ts +3 -1
- package/dist/data-plane/server/indexing/index.d.ts.map +1 -1
- package/dist/data-plane/server/indexing/index.js +2 -0
- package/dist/data-plane/server/indexing/index.js.map +1 -1
- package/dist/data-plane/server/indexing/plugins/profile-link.d.ts +8 -0
- package/dist/data-plane/server/indexing/plugins/profile-link.d.ts.map +1 -0
- package/dist/data-plane/server/indexing/plugins/profile-link.js +30 -0
- package/dist/data-plane/server/indexing/plugins/profile-link.js.map +1 -0
- package/dist/data-plane/server/routes/records.d.ts.map +1 -1
- package/dist/data-plane/server/routes/records.js +1 -0
- package/dist/data-plane/server/routes/records.js.map +1 -1
- package/dist/hydration/actor.d.ts +4 -1
- package/dist/hydration/actor.d.ts.map +1 -1
- package/dist/hydration/actor.js +11 -0
- package/dist/hydration/actor.js.map +1 -1
- package/dist/hydration/external.d.ts +67 -64
- package/dist/hydration/external.d.ts.map +1 -1
- package/dist/hydration/external.js +79 -133
- package/dist/hydration/external.js.map +1 -1
- package/dist/hydration/hydrator.d.ts +53 -14
- package/dist/hydration/hydrator.d.ts.map +1 -1
- package/dist/hydration/hydrator.js +422 -105
- package/dist/hydration/hydrator.js.map +1 -1
- package/dist/lexicons/app/bsky/actor/defs.defs.d.ts +25 -0
- package/dist/lexicons/app/bsky/actor/defs.defs.d.ts.map +1 -1
- package/dist/lexicons/app/bsky/actor/defs.defs.js +16 -0
- package/dist/lexicons/app/bsky/actor/defs.defs.js.map +1 -1
- package/dist/lexicons/app/bsky/actor/link.d.ts +3 -0
- package/dist/lexicons/app/bsky/actor/link.d.ts.map +1 -0
- package/dist/lexicons/app/bsky/actor/link.defs.d.ts +120 -0
- package/dist/lexicons/app/bsky/actor/link.defs.d.ts.map +1 -0
- package/dist/lexicons/app/bsky/actor/link.defs.js +39 -0
- package/dist/lexicons/app/bsky/actor/link.defs.js.map +1 -0
- package/dist/lexicons/app/bsky/actor/link.js +6 -0
- package/dist/lexicons/app/bsky/actor/link.js.map +1 -0
- package/dist/lexicons/app/bsky/actor/profile.defs.d.ts +14 -0
- package/dist/lexicons/app/bsky/actor/profile.defs.d.ts.map +1 -1
- package/dist/lexicons/app/bsky/actor/profile.defs.js +3 -0
- package/dist/lexicons/app/bsky/actor/profile.defs.js.map +1 -1
- package/dist/lexicons/app/bsky/actor.d.ts +1 -0
- package/dist/lexicons/app/bsky/actor.d.ts.map +1 -1
- package/dist/lexicons/app/bsky/actor.js +1 -0
- package/dist/lexicons/app/bsky/actor.js.map +1 -1
- package/dist/lexicons/app/bsky/authFullApp.defs.d.ts.map +1 -1
- package/dist/lexicons/app/bsky/authFullApp.defs.js +1 -0
- package/dist/lexicons/app/bsky/authFullApp.defs.js.map +1 -1
- package/dist/lexicons/app/bsky/authManageProfile.defs.d.ts.map +1 -1
- package/dist/lexicons/app/bsky/authManageProfile.defs.js +1 -0
- package/dist/lexicons/app/bsky/authManageProfile.defs.js.map +1 -1
- package/dist/lexicons/app/bsky/embed/getEmbedExternalView.defs.d.ts +13 -3
- package/dist/lexicons/app/bsky/embed/getEmbedExternalView.defs.d.ts.map +1 -1
- package/dist/lexicons/app/bsky/embed/getEmbedExternalView.defs.js +11 -3
- package/dist/lexicons/app/bsky/embed/getEmbedExternalView.defs.js.map +1 -1
- package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.d.ts +62 -3
- package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.d.ts.map +1 -1
- package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.js +53 -2
- package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.js.map +1 -1
- package/dist/lexicons/tools/ozone/inbox/defs.defs.d.ts +14 -1
- package/dist/lexicons/tools/ozone/inbox/defs.defs.d.ts.map +1 -1
- package/dist/lexicons/tools/ozone/inbox/defs.defs.js +6 -1
- package/dist/lexicons/tools/ozone/inbox/defs.defs.js.map +1 -1
- package/dist/lexicons/tools/ozone/inbox/getNotificationPreferences.defs.d.ts +1 -0
- package/dist/lexicons/tools/ozone/inbox/getNotificationPreferences.defs.d.ts.map +1 -1
- package/dist/lexicons/tools/ozone/inbox/getNotificationPreferences.defs.js +1 -0
- package/dist/lexicons/tools/ozone/inbox/getNotificationPreferences.defs.js.map +1 -1
- package/dist/lexicons/tools/ozone/inbox/putNotificationPreferences.defs.d.ts +7 -2
- package/dist/lexicons/tools/ozone/inbox/putNotificationPreferences.defs.d.ts.map +1 -1
- package/dist/lexicons/tools/ozone/inbox/putNotificationPreferences.defs.js +4 -1
- package/dist/lexicons/tools/ozone/inbox/putNotificationPreferences.defs.js.map +1 -1
- package/dist/proto/bsky_connect.d.ts +10 -1
- package/dist/proto/bsky_connect.d.ts.map +1 -1
- package/dist/proto/bsky_connect.js +10 -1
- package/dist/proto/bsky_connect.js.map +1 -1
- package/dist/proto/bsky_pb.d.ts +34 -0
- package/dist/proto/bsky_pb.d.ts.map +1 -1
- package/dist/proto/bsky_pb.js +60 -0
- package/dist/proto/bsky_pb.js.map +1 -1
- package/dist/telemetry/events.d.ts +35 -0
- package/dist/telemetry/events.d.ts.map +1 -1
- package/dist/telemetry/events.js +68 -0
- package/dist/telemetry/events.js.map +1 -1
- package/dist/util/external/index.d.ts +8 -0
- package/dist/util/external/index.d.ts.map +1 -0
- package/dist/util/external/index.js +50 -0
- package/dist/util/external/index.js.map +1 -0
- package/dist/util/{standard-site.d.ts → external/standard-site.d.ts} +30 -4
- package/dist/util/external/standard-site.d.ts.map +1 -0
- package/dist/util/{standard-site.js → external/standard-site.js} +73 -4
- package/dist/util/external/standard-site.js.map +1 -0
- package/dist/views/external.js +1 -1
- package/dist/views/external.js.map +1 -1
- package/dist/views/index.d.ts +11 -7
- package/dist/views/index.d.ts.map +1 -1
- package/dist/views/index.js +94 -43
- package/dist/views/index.js.map +1 -1
- package/dist/views/profile-link-rules.d.ts +10 -0
- package/dist/views/profile-link-rules.d.ts.map +1 -0
- package/dist/views/profile-link-rules.js +48 -0
- package/dist/views/profile-link-rules.js.map +1 -0
- package/dist/views/types.d.ts +2 -0
- package/dist/views/types.d.ts.map +1 -1
- package/dist/views/types.js.map +1 -1
- package/package.json +7 -7
- package/dist/util/standard-site.d.ts.map +0 -1
- package/dist/util/standard-site.js.map +0 -1
|
@@ -3,112 +3,146 @@ import { AtUri } from '@atproto/syntax';
|
|
|
3
3
|
import { app } from '../../../../../lexicons/index.js';
|
|
4
4
|
import { APPVIEW_NOTIFICATION_REASON, NOTIFICATION_REASON, } from '../constants.js';
|
|
5
5
|
import { MAX_GROUP_SIZE, compareNotificationGroupsNewestFirst, isNotificationRead, } from './shared.js';
|
|
6
|
-
const
|
|
6
|
+
const MAX_SPOTLIGHTS = 3;
|
|
7
|
+
const MULTI_POST_RANGES = [
|
|
7
8
|
{ maxItems: 200, requiredPosts: 4 },
|
|
8
9
|
{ maxItems: 500, requiredPosts: 8 },
|
|
9
10
|
];
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
const SPOTLIGHT_KINDS = {
|
|
12
|
+
[NOTIFICATION_REASON.LIKE]: APPVIEW_NOTIFICATION_REASON.MULTI_POST_LIKE,
|
|
13
|
+
[NOTIFICATION_REASON.REPOST]: APPVIEW_NOTIFICATION_REASON.MULTI_POST_REPOST,
|
|
14
|
+
[NOTIFICATION_REASON.LIKE_VIA_REPOST]: APPVIEW_NOTIFICATION_REASON.MULTI_POST_LIKE_VIA_REPOST,
|
|
15
|
+
[NOTIFICATION_REASON.REPOST_VIA_REPOST]: APPVIEW_NOTIFICATION_REASON.MULTI_POST_REPOST_VIA_REPOST,
|
|
16
|
+
};
|
|
17
|
+
const SPOTLIGHT_REASONS = Object.keys(SPOTLIGHT_KINDS);
|
|
18
|
+
export function buildSpotlight(items, groups, cursor, limit, seenAt) {
|
|
19
|
+
// @NOTE Try one spotlight at a time, keeping the last page where all spotlights pass validation.
|
|
20
|
+
let lastGoodPage = { groups, cursor };
|
|
12
21
|
const itemCount = groups.reduce((count, group) => count + group.items.length, 0);
|
|
13
|
-
// Grouping consumes a continuous raw prefix, even when groups interleave.
|
|
22
|
+
// @NOTE Grouping consumes a continuous raw prefix, even when groups interleave.
|
|
14
23
|
// items contains all fetched notifications, but grouping may stop before consuming them all;
|
|
15
24
|
// itemCount counts notifications included in the ordinary groups, and pageItems contains the notifications on the page.
|
|
16
25
|
const pageItems = items.slice(0, itemCount);
|
|
17
|
-
const spotlight = selectMultiPostLikeSpotlight(pageItems);
|
|
18
|
-
if (!spotlight)
|
|
19
|
-
return noSpotlightPage;
|
|
20
|
-
const selectedItems = new Set(spotlight.items);
|
|
21
26
|
const positions = new Map(pageItems.map((item, index) => [item, index]));
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
let retainedItems = pageItems;
|
|
49
|
-
const extraGroup = proposedGroups[limit];
|
|
50
|
-
if (extraGroup) {
|
|
51
|
-
const cutoff = positions.get(extraGroup.items[0]);
|
|
52
|
-
retainedItems = pageItems.slice(0, cutoff);
|
|
53
|
-
cursor = pageItems[cutoff - 1].raw.indexedAt;
|
|
54
|
-
// Trimming every group at one raw position preserves the page prefix.
|
|
55
|
-
for (const group of proposedGroups) {
|
|
56
|
-
group.items = group.items.filter((item) => positions.get(item) < cutoff);
|
|
57
|
-
group.actorDids = group.items.map((item) => item.actorDid);
|
|
58
|
-
group.itemCount = group.items.length;
|
|
27
|
+
// @NOTE Rank each reason's top actor by full count, then recency, before capping spotlight size.
|
|
28
|
+
const candidates = mapDefined(SPOTLIGHT_REASONS, (reason) => selectSpotlight(pageItems, reason))
|
|
29
|
+
.sort((left, right) => right.items.length - left.items.length ||
|
|
30
|
+
positions.get(left.items[0]) - positions.get(right.items[0]))
|
|
31
|
+
.map((candidate) => ({
|
|
32
|
+
...candidate,
|
|
33
|
+
items: candidate.items.slice(0, MAX_GROUP_SIZE),
|
|
34
|
+
}));
|
|
35
|
+
let lastGoodItems = pageItems;
|
|
36
|
+
const acceptedSpotlights = [];
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
if (acceptedSpotlights.length === MAX_SPOTLIGHTS)
|
|
39
|
+
break;
|
|
40
|
+
// @NOTE 1. Tentatively add this spotlight using only notifications retained on the last accepted page.
|
|
41
|
+
const candidateItems = candidate.items.filter((item) => positions.get(item) < lastGoodItems.length);
|
|
42
|
+
const selectedItems = new Set(candidateItems);
|
|
43
|
+
const proposedGroups = mapDefined([
|
|
44
|
+
...lastGoodPage.groups.map((group) => ({
|
|
45
|
+
// @NOTE Filtering may remove notifications from this group; build a new group with only the remaining items and their updated metadata.
|
|
46
|
+
kind: group.kind,
|
|
47
|
+
groupingKey: group.groupingKey,
|
|
48
|
+
items: group.items.filter((item) => !selectedItems.has(item)),
|
|
49
|
+
})),
|
|
50
|
+
{ ...candidate, items: candidateItems },
|
|
51
|
+
], (group) => {
|
|
52
|
+
const newest = group.items[0];
|
|
59
53
|
const oldest = group.items.at(-1);
|
|
60
|
-
if (oldest)
|
|
61
|
-
|
|
54
|
+
if (!newest || !oldest)
|
|
55
|
+
return;
|
|
56
|
+
// @NOTE Filtering may remove notifications from this group; build a new group with only the remaining items and their updated metadata.
|
|
57
|
+
return {
|
|
58
|
+
kind: group.kind,
|
|
59
|
+
groupingKey: group.groupingKey,
|
|
60
|
+
id: newest.id,
|
|
61
|
+
items: group.items,
|
|
62
|
+
actorDids: group.items.map((item) => item.actorDid),
|
|
63
|
+
itemCount: group.items.length,
|
|
64
|
+
indexedAt: newest.raw.indexedAt,
|
|
65
|
+
firstIndexedAt: oldest.raw.indexedAt,
|
|
66
|
+
isRead: isNotificationRead(newest.raw.indexedAt, seenAt),
|
|
67
|
+
};
|
|
68
|
+
}).sort(compareNotificationGroupsNewestFirst);
|
|
69
|
+
// @NOTE 2. Trim the tentative page to the group limit without changing the saved state.
|
|
70
|
+
let retainedItems = lastGoodItems;
|
|
71
|
+
let nextCursor = lastGoodPage.cursor;
|
|
72
|
+
const extraGroup = proposedGroups[limit];
|
|
73
|
+
if (extraGroup) {
|
|
74
|
+
const cutoff = positions.get(extraGroup.items[0]);
|
|
75
|
+
retainedItems = lastGoodItems.slice(0, cutoff);
|
|
76
|
+
nextCursor = pageItems[cutoff - 1].raw.indexedAt;
|
|
77
|
+
// @NOTE Trimming every group at one raw position preserves the page prefix.
|
|
78
|
+
for (const group of proposedGroups) {
|
|
79
|
+
group.items = group.items.filter((item) => positions.get(item) < cutoff);
|
|
80
|
+
group.actorDids = group.items.map((item) => item.actorDid);
|
|
81
|
+
group.itemCount = group.items.length;
|
|
82
|
+
const oldest = group.items.at(-1);
|
|
83
|
+
if (oldest)
|
|
84
|
+
group.firstIndexedAt = oldest.raw.indexedAt;
|
|
85
|
+
}
|
|
62
86
|
}
|
|
87
|
+
const retained = new Set(retainedItems);
|
|
88
|
+
// @NOTE 3. Reject the attempt if any spotlight fails, preserving the previously accepted page and cursor.
|
|
89
|
+
const allEligible = [...acceptedSpotlights, candidate].every((spotlight) => spotlight.isEligibleAfterTrimming(spotlight.items.filter((item) => retained.has(item)), retainedItems));
|
|
90
|
+
if (!allEligible)
|
|
91
|
+
continue;
|
|
92
|
+
// @NOTE 4. Save the valid page, cursor, and remaining notifications for the next attempt.
|
|
93
|
+
lastGoodPage = {
|
|
94
|
+
cursor: nextCursor,
|
|
95
|
+
groups: proposedGroups.filter((group) => group.items.length > 0),
|
|
96
|
+
};
|
|
97
|
+
lastGoodItems = retainedItems;
|
|
98
|
+
acceptedSpotlights.push(candidate);
|
|
63
99
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
cursor,
|
|
70
|
-
groups: proposedGroups.filter((group) => group.items.length > 0),
|
|
71
|
-
};
|
|
72
|
-
};
|
|
73
|
-
const selectMultiPostLikeSpotlight = (items) => {
|
|
74
|
-
const range = MULTI_POST_LIKE_RANGES.find(({ maxItems }) => items.length <= maxItems);
|
|
100
|
+
return lastGoodPage;
|
|
101
|
+
}
|
|
102
|
+
function selectSpotlight(items, reason) {
|
|
103
|
+
const range = MULTI_POST_RANGES.find(({ maxItems }) => items.length <= maxItems);
|
|
75
104
|
if (!range)
|
|
76
105
|
return;
|
|
77
|
-
// Eligibility uses the ordinary page's volume, including after trimming.
|
|
106
|
+
// @NOTE Eligibility uses the ordinary page's volume, including after trimming.
|
|
78
107
|
const { requiredPosts } = range;
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
108
|
+
const topItems = getNotificationsByTopActor(items, reason);
|
|
109
|
+
if (topItems.length < requiredPosts)
|
|
81
110
|
return;
|
|
82
|
-
const actorDid =
|
|
111
|
+
const actorDid = topItems[0].actorDid;
|
|
83
112
|
return {
|
|
84
|
-
kind:
|
|
113
|
+
kind: SPOTLIGHT_KINDS[reason],
|
|
85
114
|
groupingKey: actorDid,
|
|
86
|
-
items:
|
|
115
|
+
items: topItems,
|
|
87
116
|
isEligibleAfterTrimming: (spotlightItems, retainedItems) =>
|
|
88
|
-
// Check if after trimming it still satisfies the requirements.
|
|
117
|
+
// @NOTE Check if after trimming it still satisfies the requirements.
|
|
89
118
|
spotlightItems.length >= requiredPosts &&
|
|
90
|
-
|
|
119
|
+
getNotificationsByTopActor(retainedItems, reason)[0]?.actorDid ===
|
|
120
|
+
actorDid,
|
|
91
121
|
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const
|
|
122
|
+
}
|
|
123
|
+
function getNotificationsByTopActor(items, reason) {
|
|
124
|
+
const collection = reason === NOTIFICATION_REASON.LIKE_VIA_REPOST ||
|
|
125
|
+
reason === NOTIFICATION_REASON.REPOST_VIA_REPOST
|
|
126
|
+
? app.bsky.feed.repost.$type
|
|
127
|
+
: app.bsky.feed.post.$type;
|
|
128
|
+
const itemsByActor = new Map();
|
|
95
129
|
for (const item of items) {
|
|
96
|
-
if (item.raw.reason !==
|
|
97
|
-
new AtUri(item.raw.reasonSubject).collection !==
|
|
130
|
+
if (item.raw.reason !== reason ||
|
|
131
|
+
new AtUri(item.raw.reasonSubject).collection !== collection) {
|
|
98
132
|
continue;
|
|
99
133
|
}
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
|
|
134
|
+
const actorItems = itemsByActor.get(item.actorDid);
|
|
135
|
+
if (actorItems)
|
|
136
|
+
actorItems.push(item);
|
|
103
137
|
else
|
|
104
|
-
|
|
138
|
+
itemsByActor.set(item.actorDid, [item]);
|
|
105
139
|
}
|
|
106
|
-
// Each actor likes a post once; insertion order breaks ties by newest
|
|
140
|
+
// @NOTE Each actor likes or reposts a post once per reason; insertion order breaks ties by newest notification.
|
|
107
141
|
let top = [];
|
|
108
|
-
for (const
|
|
109
|
-
if (
|
|
110
|
-
top =
|
|
142
|
+
for (const actorItems of itemsByActor.values()) {
|
|
143
|
+
if (actorItems.length > top.length)
|
|
144
|
+
top = actorItems;
|
|
111
145
|
}
|
|
112
146
|
return top;
|
|
113
|
-
}
|
|
147
|
+
}
|
|
114
148
|
//# sourceMappingURL=spotlight.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spotlight.js","sourceRoot":"","sources":["../../../../../../src/api/app/bsky/notification/grouping/spotlight.ts"],"names":[],"mappings":"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","sourcesContent":["import { mapDefined } from '@atproto/common'\nimport type { DidString } from '@atproto/lex'\nimport { AtUri } from '@atproto/syntax'\nimport { app } from '../../../../../lexicons/index.js'\nimport {\n APPVIEW_NOTIFICATION_REASON,\n NOTIFICATION_REASON,\n} from '../constants.js'\nimport type {\n GroupingResult,\n NotificationGroup,\n NotificationItem,\n} from './grouping.js'\nimport {\n MAX_GROUP_SIZE,\n compareNotificationGroupsNewestFirst,\n isNotificationRead,\n} from './shared.js'\n\nconst MULTI_POST_LIKE_RANGES = [\n { maxItems: 200, requiredPosts: 4 },\n { maxItems: 500, requiredPosts: 8 },\n]\n\ntype SpotlightCandidate = Pick<\n NotificationGroup,\n 'kind' | 'groupingKey' | 'items'\n> & {\n isEligibleAfterTrimming: (\n spotlightItems: NotificationItem[],\n retainedItems: NotificationItem[],\n ) => boolean\n}\n\nexport const buildSpotlight = (\n items: NotificationItem[],\n groups: NotificationGroup[],\n cursor: string | undefined,\n limit: number,\n seenAt: number | undefined,\n): GroupingResult => {\n const noSpotlightPage = { groups, cursor }\n const itemCount = groups.reduce(\n (count, group) => count + group.items.length,\n 0,\n )\n\n // Grouping consumes a continuous raw prefix, even when groups interleave.\n // items contains all fetched notifications, but grouping may stop before consuming them all;\n // itemCount counts notifications included in the ordinary groups, and pageItems contains the notifications on the page.\n const pageItems = items.slice(0, itemCount)\n const spotlight = selectMultiPostLikeSpotlight(pageItems)\n if (!spotlight) return noSpotlightPage\n\n const selectedItems = new Set(spotlight.items)\n const positions = new Map(pageItems.map((item, index) => [item, index]))\n const proposedGroups = mapDefined(\n [\n ...groups.map((group) => ({\n // Filtering may remove notifications from this group; build a new group with only the remaining items and their updated metadata.\n kind: group.kind,\n groupingKey: group.groupingKey,\n items: group.items.filter((item) => !selectedItems.has(item)),\n })),\n spotlight,\n ],\n (group): NotificationGroup | undefined => {\n const newest = group.items[0]\n const oldest = group.items.at(-1)\n if (!newest || !oldest) return\n\n // Filtering may remove notifications from this group; build a new group with only the remaining items and their updated metadata.\n return {\n kind: group.kind,\n groupingKey: group.groupingKey,\n id: newest.id,\n items: group.items,\n actorDids: group.items.map((item) => item.actorDid),\n itemCount: group.items.length,\n indexedAt: newest.raw.indexedAt,\n firstIndexedAt: oldest.raw.indexedAt,\n isRead: isNotificationRead(newest.raw.indexedAt, seenAt),\n }\n },\n ).sort(compareNotificationGroupsNewestFirst)\n\n let retainedItems = pageItems\n const extraGroup = proposedGroups[limit]\n if (extraGroup) {\n const cutoff = positions.get(extraGroup.items[0]!)!\n retainedItems = pageItems.slice(0, cutoff)\n cursor = pageItems[cutoff - 1]!.raw.indexedAt\n // Trimming every group at one raw position preserves the page prefix.\n for (const group of proposedGroups) {\n group.items = group.items.filter((item) => positions.get(item)! < cutoff)\n group.actorDids = group.items.map((item) => item.actorDid)\n group.itemCount = group.items.length\n const oldest = group.items.at(-1)\n if (oldest) group.firstIndexedAt = oldest.raw.indexedAt\n }\n }\n\n const spotlightItems = retainedItems.filter((item) => selectedItems.has(item))\n if (!spotlight.isEligibleAfterTrimming(spotlightItems, retainedItems)) {\n return noSpotlightPage\n }\n\n return {\n cursor,\n groups: proposedGroups.filter((group) => group.items.length > 0),\n }\n}\n\nconst selectMultiPostLikeSpotlight = (\n items: NotificationItem[],\n): SpotlightCandidate | undefined => {\n const range = MULTI_POST_LIKE_RANGES.find(\n ({ maxItems }) => items.length <= maxItems,\n )\n if (!range) return\n\n // Eligibility uses the ordinary page's volume, including after trimming.\n const { requiredPosts } = range\n const likesByTopLiker = getLikeNotificationsByTopLiker(items)\n if (likesByTopLiker.length < requiredPosts) return\n const actorDid = likesByTopLiker[0]!.actorDid\n return {\n kind: APPVIEW_NOTIFICATION_REASON.MULTI_POST_LIKE,\n groupingKey: actorDid,\n items: likesByTopLiker.slice(0, MAX_GROUP_SIZE),\n isEligibleAfterTrimming: (spotlightItems, retainedItems) =>\n // Check if after trimming it still satisfies the requirements.\n spotlightItems.length >= requiredPosts &&\n getLikeNotificationsByTopLiker(retainedItems)[0]?.actorDid === actorDid,\n }\n}\n\nconst getLikeNotificationsByTopLiker = (\n items: NotificationItem[],\n): NotificationItem[] => {\n const likesByActor = new Map<DidString, NotificationItem[]>()\n for (const item of items) {\n if (\n item.raw.reason !== NOTIFICATION_REASON.LIKE ||\n new AtUri(item.raw.reasonSubject).collection !== app.bsky.feed.post.$type\n ) {\n continue\n }\n const likes = likesByActor.get(item.actorDid)\n if (likes) likes.push(item)\n else likesByActor.set(item.actorDid, [item])\n }\n\n // Each actor likes a post once; insertion order breaks ties by newest like.\n let top: NotificationItem[] = []\n for (const likes of likesByActor.values()) {\n if (likes.length > top.length) top = likes\n }\n return top\n}\n"]}
|
|
1
|
+
{"version":3,"file":"spotlight.js","sourceRoot":"","sources":["../../../../../../src/api/app/bsky/notification/grouping/spotlight.ts"],"names":[],"mappings":"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","sourcesContent":["import { mapDefined } from '@atproto/common'\nimport type { DidString } from '@atproto/lex'\nimport { AtUri } from '@atproto/syntax'\nimport { app } from '../../../../../lexicons/index.js'\nimport {\n APPVIEW_NOTIFICATION_REASON,\n NOTIFICATION_REASON,\n} from '../constants.js'\nimport type {\n GroupingResult,\n NotificationGroup,\n NotificationItem,\n} from './grouping.js'\nimport {\n MAX_GROUP_SIZE,\n compareNotificationGroupsNewestFirst,\n isNotificationRead,\n} from './shared.js'\n\nconst MAX_SPOTLIGHTS = 3\nconst MULTI_POST_RANGES = [\n { maxItems: 200, requiredPosts: 4 },\n { maxItems: 500, requiredPosts: 8 },\n]\nconst SPOTLIGHT_KINDS = {\n [NOTIFICATION_REASON.LIKE]: APPVIEW_NOTIFICATION_REASON.MULTI_POST_LIKE,\n [NOTIFICATION_REASON.REPOST]: APPVIEW_NOTIFICATION_REASON.MULTI_POST_REPOST,\n [NOTIFICATION_REASON.LIKE_VIA_REPOST]:\n APPVIEW_NOTIFICATION_REASON.MULTI_POST_LIKE_VIA_REPOST,\n [NOTIFICATION_REASON.REPOST_VIA_REPOST]:\n APPVIEW_NOTIFICATION_REASON.MULTI_POST_REPOST_VIA_REPOST,\n} as const\ntype SpotlightReason = keyof typeof SPOTLIGHT_KINDS\nconst SPOTLIGHT_REASONS = Object.keys(SPOTLIGHT_KINDS) as SpotlightReason[]\n\ntype SpotlightCandidate = Pick<\n NotificationGroup,\n 'kind' | 'groupingKey' | 'items'\n> & {\n isEligibleAfterTrimming: (\n spotlightItems: NotificationItem[],\n retainedItems: NotificationItem[],\n ) => boolean\n}\n\nexport function buildSpotlight(\n items: NotificationItem[],\n groups: NotificationGroup[],\n cursor: string | undefined,\n limit: number,\n seenAt: number | undefined,\n): GroupingResult {\n // @NOTE Try one spotlight at a time, keeping the last page where all spotlights pass validation.\n let lastGoodPage: GroupingResult = { groups, cursor }\n const itemCount = groups.reduce(\n (count, group) => count + group.items.length,\n 0,\n )\n // @NOTE Grouping consumes a continuous raw prefix, even when groups interleave.\n // items contains all fetched notifications, but grouping may stop before consuming them all;\n // itemCount counts notifications included in the ordinary groups, and pageItems contains the notifications on the page.\n const pageItems = items.slice(0, itemCount)\n const positions = new Map(pageItems.map((item, index) => [item, index]))\n // @NOTE Rank each reason's top actor by full count, then recency, before capping spotlight size.\n const candidates = mapDefined(SPOTLIGHT_REASONS, (reason) =>\n selectSpotlight(pageItems, reason),\n )\n .sort(\n (left, right) =>\n right.items.length - left.items.length ||\n positions.get(left.items[0]!)! - positions.get(right.items[0]!)!,\n )\n .map((candidate) => ({\n ...candidate,\n items: candidate.items.slice(0, MAX_GROUP_SIZE),\n }))\n\n let lastGoodItems = pageItems\n const acceptedSpotlights: SpotlightCandidate[] = []\n for (const candidate of candidates) {\n if (acceptedSpotlights.length === MAX_SPOTLIGHTS) break\n // @NOTE 1. Tentatively add this spotlight using only notifications retained on the last accepted page.\n const candidateItems = candidate.items.filter(\n (item) => positions.get(item)! < lastGoodItems.length,\n )\n const selectedItems = new Set(candidateItems)\n const proposedGroups = mapDefined(\n [\n ...lastGoodPage.groups.map((group) => ({\n // @NOTE Filtering may remove notifications from this group; build a new group with only the remaining items and their updated metadata.\n kind: group.kind,\n groupingKey: group.groupingKey,\n items: group.items.filter((item) => !selectedItems.has(item)),\n })),\n { ...candidate, items: candidateItems },\n ],\n (group): NotificationGroup | undefined => {\n const newest = group.items[0]\n const oldest = group.items.at(-1)\n if (!newest || !oldest) return\n\n // @NOTE Filtering may remove notifications from this group; build a new group with only the remaining items and their updated metadata.\n return {\n kind: group.kind,\n groupingKey: group.groupingKey,\n id: newest.id,\n items: group.items,\n actorDids: group.items.map((item) => item.actorDid),\n itemCount: group.items.length,\n indexedAt: newest.raw.indexedAt,\n firstIndexedAt: oldest.raw.indexedAt,\n isRead: isNotificationRead(newest.raw.indexedAt, seenAt),\n }\n },\n ).sort(compareNotificationGroupsNewestFirst)\n\n // @NOTE 2. Trim the tentative page to the group limit without changing the saved state.\n let retainedItems = lastGoodItems\n let nextCursor = lastGoodPage.cursor\n const extraGroup = proposedGroups[limit]\n if (extraGroup) {\n const cutoff = positions.get(extraGroup.items[0]!)!\n retainedItems = lastGoodItems.slice(0, cutoff)\n nextCursor = pageItems[cutoff - 1]!.raw.indexedAt\n // @NOTE Trimming every group at one raw position preserves the page prefix.\n for (const group of proposedGroups) {\n group.items = group.items.filter(\n (item) => positions.get(item)! < cutoff,\n )\n group.actorDids = group.items.map((item) => item.actorDid)\n group.itemCount = group.items.length\n const oldest = group.items.at(-1)\n if (oldest) group.firstIndexedAt = oldest.raw.indexedAt\n }\n }\n\n const retained = new Set(retainedItems)\n // @NOTE 3. Reject the attempt if any spotlight fails, preserving the previously accepted page and cursor.\n const allEligible = [...acceptedSpotlights, candidate].every((spotlight) =>\n spotlight.isEligibleAfterTrimming(\n spotlight.items.filter((item) => retained.has(item)),\n retainedItems,\n ),\n )\n if (!allEligible) continue\n\n // @NOTE 4. Save the valid page, cursor, and remaining notifications for the next attempt.\n lastGoodPage = {\n cursor: nextCursor,\n groups: proposedGroups.filter((group) => group.items.length > 0),\n }\n lastGoodItems = retainedItems\n acceptedSpotlights.push(candidate)\n }\n\n return lastGoodPage\n}\n\nfunction selectSpotlight(\n items: NotificationItem[],\n reason: SpotlightReason,\n): SpotlightCandidate | undefined {\n const range = MULTI_POST_RANGES.find(\n ({ maxItems }) => items.length <= maxItems,\n )\n if (!range) return\n\n // @NOTE Eligibility uses the ordinary page's volume, including after trimming.\n const { requiredPosts } = range\n const topItems = getNotificationsByTopActor(items, reason)\n if (topItems.length < requiredPosts) return\n const actorDid = topItems[0]!.actorDid\n return {\n kind: SPOTLIGHT_KINDS[reason],\n groupingKey: actorDid,\n items: topItems,\n isEligibleAfterTrimming: (spotlightItems, retainedItems) =>\n // @NOTE Check if after trimming it still satisfies the requirements.\n spotlightItems.length >= requiredPosts &&\n getNotificationsByTopActor(retainedItems, reason)[0]?.actorDid ===\n actorDid,\n }\n}\n\nfunction getNotificationsByTopActor(\n items: NotificationItem[],\n reason: SpotlightReason,\n): NotificationItem[] {\n const collection =\n reason === NOTIFICATION_REASON.LIKE_VIA_REPOST ||\n reason === NOTIFICATION_REASON.REPOST_VIA_REPOST\n ? app.bsky.feed.repost.$type\n : app.bsky.feed.post.$type\n const itemsByActor = new Map<DidString, NotificationItem[]>()\n for (const item of items) {\n if (\n item.raw.reason !== reason ||\n new AtUri(item.raw.reasonSubject).collection !== collection\n ) {\n continue\n }\n const actorItems = itemsByActor.get(item.actorDid)\n if (actorItems) actorItems.push(item)\n else itemsByActor.set(item.actorDid, [item])\n }\n\n // @NOTE Each actor likes or reposts a post once per reason; insertion order breaks ties by newest notification.\n let top: NotificationItem[] = []\n for (const actorItems of itemsByActor.values()) {\n if (actorItems.length > top.length) top = actorItems\n }\n return top\n}\n"]}
|
|
@@ -47,8 +47,9 @@ const skeleton = async (input) => {
|
|
|
47
47
|
const hydration = async (input) => {
|
|
48
48
|
const { ctx, params, skeleton } = input;
|
|
49
49
|
const { dids } = skeleton;
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
return ctx.hydrator.hydrateProfiles(dids, params.hydrateCtx, {
|
|
51
|
+
activitySubscriptions: true,
|
|
52
|
+
});
|
|
52
53
|
};
|
|
53
54
|
const noBlocks = (input) => {
|
|
54
55
|
const { skeleton, hydration, ctx } = input;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listActivitySubscriptions.js","sourceRoot":"","sources":["../../../../../src/api/app/bsky/notification/listActivitySubscriptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAQ5C,OAAO,EAAE,GAAG,EAAE,MAAM,+BAA+B,CAAA;AACnD,OAAO,EAKL,cAAc,GACf,MAAM,yBAAyB,CAAA;AAEhC,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAEzE,MAAM,CAAC,OAAO,WAAW,MAAc,EAAE,GAAe;IACtD,MAAM,yBAAyB,GAAG,cAAc,CAC9C,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,YAAY,CACb,CAAA;IACD,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,yBAAyB,EAAE;QAC1D,IAAI,EAAE,GAAG,CAAC,YAAY,CAAC,QAAQ;QAC/B,OAAO,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE;YACvC,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAA;YACnC,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;YACrC,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,aAAa,CAAC;gBAClD,QAAQ;gBACR,MAAM;aACP,CAAC,CAAA;YAEF,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC;gBAC5B,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,CAC3B,yBAAyB,CACvB,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,EACxC,GAAG,CACJ;gBACH,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa;aAC9B,CAAC,CAAA;YAEF,OAAO;gBACL,QAAQ,EAAE,kBAAkB;gBAC5B,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE,UAAU,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,QAAQ,EAAE,CAAC;aACvD,CAAA;QACH,CAAC;KACF,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,QAAQ,GAAG,KAAK,EACpB,KAAuC,EACf,EAAE;IAC1B,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,CAAA;IAC7B,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAA;IACzC,IAAI,gBAAgB,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QACpC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,CAAA;IAC/B,CAAC;IACD,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GACpB,MAAM,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,2BAA2B,CAAC;QACvD,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC,MAAM;QAClC,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EAAE,MAAM,CAAC,MAAM;KACtB,CAAC,CAAA;IACJ,OAAO;QACL,QAAQ;QACR,IAAI,EAAE,IAAmB;QACzB,MAAM;KACP,CAAA;AACH,CAAC,CAAA;AAED,MAAM,SAAS,GAAG,KAAK,EACrB,KAAuD,EACvD,EAAE;IACF,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAA;IACvC,MAAM,EAAE,IAAI,EAAE,GAAG,QAAQ,CAAA;IACzB,
|
|
1
|
+
{"version":3,"file":"listActivitySubscriptions.js","sourceRoot":"","sources":["../../../../../src/api/app/bsky/notification/listActivitySubscriptions.ts"],"names":[],"mappings":"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","sourcesContent":["import { mapDefined } from '@atproto/common'\nimport type { DidString } from '@atproto/syntax'\nimport type { Server } from '@atproto/xrpc-server'\nimport type { AppContext } from '../../../../context.js'\nimport type {\n HydrateCtxWithViewer,\n Hydrator,\n} from '../../../../hydration/hydrator.js'\nimport { app } from '../../../../lexicons/index.js'\nimport {\n type HydrationFnInput,\n type PresentationFnInput,\n type RulesFnInput,\n type SkeletonFnInput,\n createPipeline,\n} from '../../../../pipeline.js'\nimport type { Views } from '../../../../views/index.js'\nimport { clearlyBadCursor, fillPage, resHeaders } from '../../../util.js'\n\nexport default function (server: Server, ctx: AppContext) {\n const listActivitySubscriptions = createPipeline(\n skeleton,\n hydration,\n noBlocks,\n presentation,\n )\n server.add(app.bsky.notification.listActivitySubscriptions, {\n auth: ctx.authVerifier.standard,\n handler: async ({ params, auth, req }) => {\n const viewer = auth.credentials.iss\n const labelers = ctx.reqLabelers(req)\n const hydrateCtx = await ctx.hydrator.createContext({\n labelers,\n viewer,\n })\n\n const result = await fillPage({\n cursor: params.cursor,\n limit: params.limit,\n fetch: ({ cursor, limit }) =>\n listActivitySubscriptions(\n { ...params, cursor, limit, hydrateCtx },\n ctx,\n ),\n items: (r) => r.subscriptions,\n })\n\n return {\n encoding: 'application/json',\n body: result,\n headers: resHeaders({ labelers: hydrateCtx.labelers }),\n }\n },\n })\n}\n\nconst skeleton = async (\n input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n const { params, ctx } = input\n const actorDid = params.hydrateCtx.viewer\n if (clearlyBadCursor(params.cursor)) {\n return { actorDid, dids: [] }\n }\n const { dids, cursor } =\n await ctx.hydrator.dataplane.getActivitySubscriptionDids({\n actorDid: params.hydrateCtx.viewer,\n limit: params.limit,\n cursor: params.cursor,\n })\n return {\n actorDid,\n dids: dids as DidString[],\n cursor,\n }\n}\n\nconst hydration = async (\n input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n const { ctx, params, skeleton } = input\n const { dids } = skeleton\n return ctx.hydrator.hydrateProfiles(dids, params.hydrateCtx, {\n activitySubscriptions: true,\n })\n}\n\nconst noBlocks = (input: RulesFnInput<Context, Params, SkeletonState>) => {\n const { skeleton, hydration, ctx } = input\n skeleton.dids = skeleton.dids.filter(\n (did) => !ctx.views.viewerBlockExists(did, hydration),\n )\n return skeleton\n}\n\nconst presentation = (\n input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n const { ctx, hydration, skeleton } = input\n const { dids, cursor } = skeleton\n const subscriptions = mapDefined(dids, (did) => {\n return ctx.views.profile(did, hydration)\n })\n return { subscriptions, cursor }\n}\n\ntype Context = {\n hydrator: Hydrator\n views: Views\n}\n\ntype Params = app.bsky.notification.listActivitySubscriptions.$Params & {\n hydrateCtx: HydrateCtxWithViewer\n}\n\ntype SkeletonState = {\n actorDid: DidString\n dids: DidString[]\n cursor?: string\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getAtmosphereExploreTab.d.ts","sourceRoot":"","sources":["../../../../../src/api/app/bsky/unspecced/getAtmosphereExploreTab.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,MAAM,EAGZ,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"getAtmosphereExploreTab.d.ts","sourceRoot":"","sources":["../../../../../src/api/app/bsky/unspecced/getAtmosphereExploreTab.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,MAAM,EAGZ,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AA0BxD,MAAM,CAAC,OAAO,WAAW,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,QAyHvD"}
|
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mapDefined } from '@atproto/common';
|
|
2
2
|
import { l } from '@atproto/lex';
|
|
3
3
|
import { AtUri } from '@atproto/syntax';
|
|
4
4
|
import { MethodNotImplementedError, UpstreamFailureError, UpstreamTimeoutError, } from '@atproto/xrpc-server';
|
|
5
5
|
import { safeFetchWrap } from '@atproto-labs/fetch-node';
|
|
6
|
-
import {
|
|
7
|
-
import { HydrationMap } from '../../../../hydration/util.js';
|
|
8
|
-
import { app, site, social } from '../../../../lexicons/index.js';
|
|
9
|
-
import { uriToDid } from '../../../../util/uris.js';
|
|
6
|
+
import { app } from '../../../../lexicons/index.js';
|
|
10
7
|
const MAX_CONFIG_BYTES = 512 * 1024;
|
|
11
8
|
const MAX_ATMOSPHERE_RECORDS = 500;
|
|
12
|
-
const MAX_GALLERY_ITEMS = 10;
|
|
13
|
-
const MAX_PREVIEW_ACTORS = 3;
|
|
14
9
|
const groupSchema = l.object({
|
|
15
10
|
featured: l.boolean(),
|
|
16
11
|
uris: l.array(l.unknown(), { maxLength: 100 }),
|
|
@@ -82,61 +77,42 @@ export default function (server, ctx) {
|
|
|
82
77
|
}
|
|
83
78
|
const articles = completeGroups(config.articles);
|
|
84
79
|
const publications = completeGroups(config.publications);
|
|
85
|
-
const photos = completeGroups(config.photos
|
|
86
|
-
...group,
|
|
87
|
-
// @NOTE Gallery membership comes from backlinks, not loose CMS photo URIs.
|
|
88
|
-
uris: mapDefined(group.uris, (input) => {
|
|
89
|
-
const uri = recordUri(input);
|
|
90
|
-
return uri &&
|
|
91
|
-
new AtUri(uri).collection === social.grain.gallery.$type
|
|
92
|
-
? uri
|
|
93
|
-
: undefined;
|
|
94
|
-
}),
|
|
95
|
-
})));
|
|
80
|
+
const photos = completeGroups(config.photos);
|
|
96
81
|
const livestreams = completeGroups(config.livestreams);
|
|
97
82
|
const uris = [
|
|
98
|
-
...new Set([...articles, ...publications, ...photos, ...livestreams].
|
|
83
|
+
...new Set([...articles, ...publications, ...photos, ...livestreams].map(({ uri }) => uri)),
|
|
99
84
|
];
|
|
100
85
|
const hydrateCtx = await ctx.hydrator.createContext({
|
|
101
86
|
viewer: auth.credentials.iss,
|
|
102
87
|
labelers: ctx.reqLabelers(req),
|
|
103
88
|
});
|
|
104
|
-
const state = await
|
|
89
|
+
const state = await ctx.hydrator.hydrateEmbedExternalViewFromUris(uris, hydrateCtx);
|
|
105
90
|
const now = Date.now();
|
|
106
91
|
const result = {
|
|
107
92
|
announcementBanner: config.announcementBanner,
|
|
108
|
-
articles: mapDefined(articles, ({ featured,
|
|
109
|
-
const
|
|
110
|
-
const view = uri
|
|
111
|
-
? ctx.views.externalRecordView(uri, state, now)
|
|
112
|
-
: undefined;
|
|
93
|
+
articles: mapDefined(articles, ({ featured, uri }) => {
|
|
94
|
+
const view = ctx.views.externalRecordView(uri, state, now);
|
|
113
95
|
return view && app.bsky.embed.external.viewArticle.$isTypeOf(view)
|
|
114
96
|
? { featured, view }
|
|
115
97
|
: undefined;
|
|
116
98
|
}),
|
|
117
|
-
publications: mapDefined(publications, ({ featured,
|
|
118
|
-
const view =
|
|
119
|
-
? ctx.views.externalRecordView(uris[0], state, now)
|
|
120
|
-
: undefined;
|
|
99
|
+
publications: mapDefined(publications, ({ featured, uri }) => {
|
|
100
|
+
const view = ctx.views.externalRecordView(uri, state, now);
|
|
121
101
|
return view &&
|
|
122
102
|
app.bsky.embed.external.viewArticlePublication.$isTypeOf(view)
|
|
123
103
|
? { featured, view }
|
|
124
104
|
: undefined;
|
|
125
105
|
}),
|
|
126
|
-
photos: mapDefined(photos, ({ featured,
|
|
127
|
-
const view =
|
|
128
|
-
? ctx.views.externalRecordView(uris[0], state, now)
|
|
129
|
-
: undefined;
|
|
106
|
+
photos: mapDefined(photos, ({ featured, uri }) => {
|
|
107
|
+
const view = ctx.views.externalRecordView(uri, state, now);
|
|
130
108
|
return view &&
|
|
131
109
|
app.bsky.embed.external.viewGallery.$isTypeOf(view) &&
|
|
132
110
|
view.items.length
|
|
133
111
|
? { featured, view }
|
|
134
112
|
: undefined;
|
|
135
113
|
}),
|
|
136
|
-
livestreams: mapDefined(livestreams, ({ featured,
|
|
137
|
-
const view =
|
|
138
|
-
? ctx.views.externalRecordView(uris[0], state, now)
|
|
139
|
-
: undefined;
|
|
114
|
+
livestreams: mapDefined(livestreams, ({ featured, uri }) => {
|
|
115
|
+
const view = ctx.views.externalRecordView(uri, state, now);
|
|
140
116
|
return view && app.bsky.embed.external.viewLivestream.$isTypeOf(view)
|
|
141
117
|
? { featured, view }
|
|
142
118
|
: undefined;
|
|
@@ -149,120 +125,12 @@ export default function (server, ctx) {
|
|
|
149
125
|
}
|
|
150
126
|
function completeGroups(groups = []) {
|
|
151
127
|
return mapDefined(groups, ({ featured, uris }) => {
|
|
152
|
-
const
|
|
153
|
-
if (!
|
|
128
|
+
const uri = recordUri(uris[0]);
|
|
129
|
+
if (!uri)
|
|
154
130
|
return;
|
|
155
|
-
return { featured,
|
|
131
|
+
return { featured, uri };
|
|
156
132
|
});
|
|
157
133
|
}
|
|
158
|
-
async function hydrateAtmosphere(uris, ctx, hydrator) {
|
|
159
|
-
const records = await hydrateRecords(uris, hydrator);
|
|
160
|
-
const available = uris.filter((uri) => records.get(uri));
|
|
161
|
-
const counts = await hydrator.external.getAtmosphereBacklinkCounts(available);
|
|
162
|
-
const backlinks = new HydrationMap();
|
|
163
|
-
// @NOTE Bound fan-out as well as each backlink page; never scan a whole collection.
|
|
164
|
-
for (const batch of chunkArray(available, 8)) {
|
|
165
|
-
await Promise.all(batch.map(async (uri) => {
|
|
166
|
-
const collection = new AtUri(uri).collection;
|
|
167
|
-
const sources = [];
|
|
168
|
-
if (collection === site.standard.document.$type) {
|
|
169
|
-
sources.push({
|
|
170
|
-
collection: site.standard.graph.recommend.$type,
|
|
171
|
-
limit: MAX_PREVIEW_ACTORS,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
else if (collection === site.standard.publication.$type) {
|
|
175
|
-
sources.push({
|
|
176
|
-
collection: site.standard.graph.subscription.$type,
|
|
177
|
-
limit: MAX_PREVIEW_ACTORS,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
else if (collection === social.grain.gallery.$type) {
|
|
181
|
-
sources.push({
|
|
182
|
-
collection: social.grain.favorite.$type,
|
|
183
|
-
limit: MAX_PREVIEW_ACTORS,
|
|
184
|
-
}, {
|
|
185
|
-
collection: social.grain.gallery.item.$type,
|
|
186
|
-
limit: MAX_GALLERY_ITEMS,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
const links = [];
|
|
190
|
-
for (const { collection, limit } of sources) {
|
|
191
|
-
const page = await hydrator.external.getAtmosphereBacklinks(uri, collection, { limit });
|
|
192
|
-
links.push(...mapDefined(page.backlinks.slice(0, limit), ({ uri: input }) => {
|
|
193
|
-
const uri = recordUri(input);
|
|
194
|
-
return uri && new AtUri(uri).collection === collection
|
|
195
|
-
? uri
|
|
196
|
-
: undefined;
|
|
197
|
-
}));
|
|
198
|
-
}
|
|
199
|
-
backlinks.set(uri, [...new Set(links)]);
|
|
200
|
-
}));
|
|
201
|
-
}
|
|
202
|
-
records.merge(await hydrateRecords([...backlinks.values()].flatMap((uris) => uris ?? []), hydrator));
|
|
203
|
-
const photoUris = [];
|
|
204
|
-
for (const [galleryUri, links] of backlinks) {
|
|
205
|
-
if (new AtUri(galleryUri).collection !== social.grain.gallery.$type)
|
|
206
|
-
continue;
|
|
207
|
-
for (const uri of links ?? []) {
|
|
208
|
-
const item = social.grain.gallery.item.$ifMatches(records.get(uri)?.record);
|
|
209
|
-
if (!item ||
|
|
210
|
-
item.gallery !== galleryUri ||
|
|
211
|
-
uriToDid(uri) !== uriToDid(galleryUri))
|
|
212
|
-
continue;
|
|
213
|
-
const photoUri = recordUri(item.item);
|
|
214
|
-
if (photoUri &&
|
|
215
|
-
new AtUri(photoUri).collection === social.grain.photo.$type)
|
|
216
|
-
photoUris.push(photoUri);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
records.merge(await hydrateRecords(photoUris, hydrator));
|
|
220
|
-
const allUris = [...records.keys()];
|
|
221
|
-
const dids = [...new Set(allUris.map(uriToDid))];
|
|
222
|
-
const states = [];
|
|
223
|
-
for (const batch of chunkArray(dids, 200)) {
|
|
224
|
-
states.push(await hydrator.hydrateProfilesBasic(batch, ctx));
|
|
225
|
-
}
|
|
226
|
-
for (const batch of chunkArray(allUris, MAX_ATMOSPHERE_RECORDS)) {
|
|
227
|
-
states.push({
|
|
228
|
-
labels: await hydrator.label.getLabelsForSubjects(batch, ctx.labelers),
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
return {
|
|
232
|
-
...mergeManyStates(...states),
|
|
233
|
-
ctx,
|
|
234
|
-
externalRecords: records,
|
|
235
|
-
externalRecordBacklinks: backlinks,
|
|
236
|
-
externalRecordBacklinkCounts: counts,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
async function hydrateRecords(uris, hydrator) {
|
|
240
|
-
const records = new HydrationMap();
|
|
241
|
-
for (const batch of chunkArray([...new Set(uris)], MAX_ATMOSPHERE_RECORDS)) {
|
|
242
|
-
records.merge(await hydrator.external.getRecordsByURI(batch));
|
|
243
|
-
}
|
|
244
|
-
return records;
|
|
245
|
-
}
|
|
246
|
-
// @NOTE A publication hydrated for another CMS group must not complete this article.
|
|
247
|
-
function articleUri(uris, state) {
|
|
248
|
-
const documents = uris.filter((uri) => new AtUri(uri).collection === site.standard.document.$type);
|
|
249
|
-
const publications = uris.filter((uri) => new AtUri(uri).collection === site.standard.publication.$type);
|
|
250
|
-
if (documents.length !== 1 ||
|
|
251
|
-
publications.length > 1 ||
|
|
252
|
-
documents.length + publications.length !== uris.length)
|
|
253
|
-
return;
|
|
254
|
-
const uri = documents[0];
|
|
255
|
-
const document = site.standard.document.$ifMatches(state.externalRecords?.get(uri)?.record);
|
|
256
|
-
if (!document)
|
|
257
|
-
return;
|
|
258
|
-
if (document.site.startsWith('at://')) {
|
|
259
|
-
if (document.site !== publications[0])
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
else if (publications.length)
|
|
263
|
-
return;
|
|
264
|
-
return uri;
|
|
265
|
-
}
|
|
266
134
|
function recordUri(value) {
|
|
267
135
|
if (!l.isAtUriString(value))
|
|
268
136
|
return;
|