BlueSky & more on desktop lazurite.stormlightlabs.org/
tauri rust typescript bluesky appview atproto solid
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 141 lines 5.1 kB view raw
1import type { NotificationView } from "$/lib/types"; 2import { describe, expect, it } from "vitest"; 3import { 4 buildAllNotificationsFeed, 5 groupActivityNotifications, 6 splitByReadState, 7 toSingleFeedItems, 8} from "../notification-grouping"; 9 10function createNotification(reason: string, overrides: Partial<NotificationView> = {}): NotificationView { 11 return { 12 author: { did: `did:plc:${reason}`, displayName: `${reason} author`, handle: `${reason}.test` }, 13 cid: `cid-${reason}`, 14 indexedAt: "2026-03-29T12:00:00.000Z", 15 isRead: false, 16 reason, 17 record: { text: `${reason} text` }, 18 uri: `at://did:plc:${reason}/app.bsky.notification/${reason}`, 19 ...overrides, 20 }; 21} 22 23describe("notification-grouping", () => { 24 it("groups activity by reason + reasonSubject", () => { 25 const notifications = [ 26 createNotification("like", { 27 author: { did: "did:plc:alice", displayName: "Alice", handle: "alice.test" }, 28 indexedAt: "2026-03-29T12:10:00.000Z", 29 reasonSubject: "at://did:plc:post/app.bsky.feed.post/1", 30 uri: "at://did:plc:alice/app.bsky.notification/1", 31 }), 32 createNotification("like", { 33 author: { did: "did:plc:bob", displayName: "Bob", handle: "bob.test" }, 34 indexedAt: "2026-03-29T12:08:00.000Z", 35 reasonSubject: "at://did:plc:post/app.bsky.feed.post/1", 36 uri: "at://did:plc:bob/app.bsky.notification/2", 37 }), 38 createNotification("repost", { 39 author: { did: "did:plc:carol", displayName: "Carol", handle: "carol.test" }, 40 indexedAt: "2026-03-29T12:09:00.000Z", 41 reasonSubject: "at://did:plc:post/app.bsky.feed.post/1", 42 uri: "at://did:plc:carol/app.bsky.notification/3", 43 }), 44 ]; 45 46 const grouped = groupActivityNotifications(notifications); 47 48 expect(grouped).toHaveLength(2); 49 const likeGroup = grouped.find((item) => item.kind === "group" && item.reason === "like"); 50 if (!likeGroup || likeGroup.kind !== "group") { 51 throw new Error("expected grouped like activity"); 52 } 53 54 expect(likeGroup.count).toBe(2); 55 expect(likeGroup.actorCount).toBe(2); 56 expect(grouped.some((item) => item.kind === "single")).toBe(true); 57 }); 58 59 it("does not group notifications without reasonSubject", () => { 60 const notifications = [ 61 createNotification("like", { uri: "at://did:plc:alice/app.bsky.notification/1" }), 62 createNotification("like", { uri: "at://did:plc:bob/app.bsky.notification/2" }), 63 ]; 64 65 const grouped = groupActivityNotifications(notifications); 66 67 expect(grouped).toHaveLength(2); 68 expect(grouped.every((item) => item.kind === "single")).toBe(true); 69 }); 70 71 it("propagates unread state and chooses the newest timestamp for grouped activity", () => { 72 const notifications = [ 73 createNotification("like", { 74 indexedAt: "2026-03-29T12:10:00.000Z", 75 isRead: true, 76 reasonSubject: "at://did:plc:post/app.bsky.feed.post/1", 77 uri: "at://did:plc:alice/app.bsky.notification/1", 78 }), 79 createNotification("like", { 80 indexedAt: "2026-03-29T12:08:00.000Z", 81 isRead: false, 82 reasonSubject: "at://did:plc:post/app.bsky.feed.post/1", 83 uri: "at://did:plc:bob/app.bsky.notification/2", 84 }), 85 ]; 86 87 const grouped = groupActivityNotifications(notifications); 88 89 expect(grouped).toHaveLength(1); 90 expect(grouped[0].kind).toBe("group"); 91 expect(grouped[0].isUnread).toBe(true); 92 expect(grouped[0].latestIndexedAt).toBe("2026-03-29T12:10:00.000Z"); 93 }); 94 95 it("sorts all feed items by newest timestamp across mentions and grouped activity", () => { 96 const mentions = [ 97 createNotification("mention", { 98 indexedAt: "2026-03-29T12:12:00.000Z", 99 uri: "at://did:plc:mention/app.bsky.notification/1", 100 }), 101 ]; 102 103 const activity = groupActivityNotifications([ 104 createNotification("like", { 105 indexedAt: "2026-03-29T12:10:00.000Z", 106 reasonSubject: "at://did:plc:post/app.bsky.feed.post/1", 107 uri: "at://did:plc:alice/app.bsky.notification/2", 108 }), 109 createNotification("like", { 110 indexedAt: "2026-03-29T12:08:00.000Z", 111 reasonSubject: "at://did:plc:post/app.bsky.feed.post/1", 112 uri: "at://did:plc:bob/app.bsky.notification/3", 113 }), 114 ]); 115 116 const all = buildAllNotificationsFeed(mentions, activity); 117 118 expect(all).toHaveLength(2); 119 if (all[0].kind !== "single") { 120 throw new Error("expected mention row first"); 121 } 122 123 if (all[1].kind !== "group") { 124 throw new Error("expected grouped activity row second"); 125 } 126 127 expect(all[0].notification.reason).toBe("mention"); 128 }); 129 130 it("splits feed rows into new and earlier sections", () => { 131 const items = toSingleFeedItems([ 132 createNotification("mention", { isRead: false, uri: "at://did:plc:mention/app.bsky.notification/1" }), 133 createNotification("reply", { isRead: true, uri: "at://did:plc:reply/app.bsky.notification/2" }), 134 ]); 135 136 const sections = splitByReadState(items); 137 138 expect(sections.newer).toHaveLength(1); 139 expect(sections.earlier).toHaveLength(1); 140 }); 141});