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