an appview-less Bluesky client using Constellation and PDS Queries
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
1import { AtUri, type Agent } from "@atproto/api";
2import { useQueryConstellation, type linksRecordsResponse } from "./useQuery";
3import type { QueryClient } from "@tanstack/react-query";
4import { TID } from "@atproto/common-web";
5
6export function useGetFollowState({
7 target,
8 user,
9}: {
10 target: string;
11 user?: string;
12}): string[] | undefined {
13 const { data: followData } = useQueryConstellation(
14 user
15 ? {
16 method: "/links",
17 target: target,
18 // @ts-expect-error overloading sucks so much
19 collection: "app.bsky.graph.follow",
20 path: ".subject",
21 dids: [user],
22 }
23 : { method: "undefined", target: "whatever" }
24 // overloading sucks so much
25 ) as { data: linksRecordsResponse | undefined };
26 const follows = followData?.linking_records.slice(0, 50) ?? [];
27
28 if (follows.length > 0) {
29 return follows.map((linksRecord) => {
30 return `at://${linksRecord.did}/${linksRecord.collection}/${linksRecord.rkey}`;
31 });
32 }
33
34 return undefined;
35}
36
37export function toggleFollow({
38 agent,
39 targetDid,
40 followRecords,
41 queryClient,
42}: {
43 agent?: Agent;
44 targetDid?: string;
45 followRecords: undefined | string[];
46 queryClient: QueryClient;
47}) {
48 if (!agent?.did || !targetDid) return;
49
50 const queryKey = [
51 "constellation",
52 "/links",
53 targetDid,
54 "app.bsky.graph.follow",
55 ".subject",
56 undefined,
57 [agent.did],
58 ] as const;
59
60 const updateCache = (
61 updater: (
62 oldData: linksRecordsResponse | undefined
63 ) => linksRecordsResponse | undefined
64 ) => {
65 queryClient.setQueryData(
66 queryKey,
67 (oldData: linksRecordsResponse | undefined) => updater(oldData)
68 );
69 };
70
71 if (typeof followRecords === "undefined") {
72 const newRecord = {
73 repo: agent.did,
74 collection: "app.bsky.graph.follow",
75 rkey: TID.next().toString(),
76 record: {
77 $type: "app.bsky.graph.follow",
78 subject: targetDid,
79 createdAt: new Date().toISOString(),
80 },
81 };
82
83 updateCache((old) => {
84 const newLinkingRecords = [newRecord, ...(old?.linking_records ?? [])];
85 return {
86 ...old,
87 linking_records: newLinkingRecords,
88 } as linksRecordsResponse;
89 });
90
91 agent.com.atproto.repo.createRecord(newRecord).catch((err) => {
92 console.error("Follow failed, reverting cache:", err);
93 // rollback cache
94 updateCache((old) => {
95 return {
96 ...old,
97 linking_records:
98 old?.linking_records.filter((r) => r.rkey !== newRecord.rkey) ?? [],
99 } as linksRecordsResponse;
100 });
101 });
102
103 return;
104 }
105
106 followRecords.forEach((followRecord) => {
107 const aturi = new AtUri(followRecord);
108 agent.com.atproto.repo
109 .deleteRecord({
110 repo: agent.did!,
111 collection: "app.bsky.graph.follow",
112 rkey: aturi.rkey,
113 })
114 .catch(console.error);
115 });
116
117 updateCache((old) => {
118 if (!old?.linking_records) return old;
119 return {
120 ...old,
121 linking_records: old.linking_records.filter(
122 (rec) =>
123 !followRecords.includes(
124 `at://${rec.did}/${rec.collection}/${rec.rkey}`
125 )
126 ),
127 };
128 });
129}