+10
package-lock.json
+10
package-lock.json
···
29
"react": "^19.0.0",
30
"react-dom": "^19.0.0",
31
"react-player": "^3.3.2",
32
"tailwindcss": "^4.0.6",
33
"tanstack-router-keepalive": "^1.0.0"
34
},
···
12543
"csstype": "^3.1.0",
12544
"seroval": "~1.3.0",
12545
"seroval-plugins": "~1.3.0"
12546
}
12547
},
12548
"node_modules/source-map": {
···
29
"react": "^19.0.0",
30
"react-dom": "^19.0.0",
31
"react-player": "^3.3.2",
32
+
"sonner": "^2.0.7",
33
"tailwindcss": "^4.0.6",
34
"tanstack-router-keepalive": "^1.0.0"
35
},
···
12544
"csstype": "^3.1.0",
12545
"seroval": "~1.3.0",
12546
"seroval-plugins": "~1.3.0"
12547
+
}
12548
+
},
12549
+
"node_modules/sonner": {
12550
+
"version": "2.0.7",
12551
+
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
12552
+
"integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
12553
+
"peerDependencies": {
12554
+
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
12555
+
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
12556
}
12557
},
12558
"node_modules/source-map": {
+1
package.json
+1
package.json
+1
src/auto-imports.d.ts
+1
src/auto-imports.d.ts
···
20
const IconMdiAccountCircle: typeof import('~icons/mdi/account-circle.jsx').default
21
const IconMdiAccountPlus: typeof import('~icons/mdi/account-plus.jsx').default
22
const IconMdiCheck: typeof import('~icons/mdi/check.jsx').default
23
const IconMdiMessageReplyTextOutline: typeof import('~icons/mdi/message-reply-text-outline.jsx').default
24
const IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline.jsx').default
25
}
···
20
const IconMdiAccountCircle: typeof import('~icons/mdi/account-circle.jsx').default
21
const IconMdiAccountPlus: typeof import('~icons/mdi/account-plus.jsx').default
22
const IconMdiCheck: typeof import('~icons/mdi/check.jsx').default
23
+
const IconMdiClose: typeof import('~icons/mdi/close.jsx').default
24
const IconMdiMessageReplyTextOutline: typeof import('~icons/mdi/message-reply-text-outline.jsx').default
25
const IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline.jsx').default
26
}
+6
src/providers/LikeMutationQueueProvider.tsx
+6
src/providers/LikeMutationQueueProvider.tsx
···
5
import React, { createContext, use, useCallback, useEffect, useRef } from "react";
6
7
import { useAuth } from "~/providers/UnifiedAuthProvider";
8
import { constellationURLAtom, internalLikedPostsAtom } from "~/utils/atoms";
9
import { constructArbitraryQuery, constructConstellationQuery, type linksRecordsResponse } from "~/utils/useQuery";
10
···
125
}
126
} catch (err) {
127
console.error("Like mutation failed, reverting:", err);
128
if (mutation.type === 'like') {
129
setFastState(mutation.target, null);
130
} else if (mutation.type === 'unlike') {
···
5
import React, { createContext, use, useCallback, useEffect, useRef } from "react";
6
7
import { useAuth } from "~/providers/UnifiedAuthProvider";
8
+
import { renderSnack } from "~/routes/__root";
9
import { constellationURLAtom, internalLikedPostsAtom } from "~/utils/atoms";
10
import { constructArbitraryQuery, constructConstellationQuery, type linksRecordsResponse } from "~/utils/useQuery";
11
···
126
}
127
} catch (err) {
128
console.error("Like mutation failed, reverting:", err);
129
+
renderSnack({
130
+
title: 'Like Mutation Failed',
131
+
description: 'Please try again.',
132
+
//button: { label: 'Try Again', onClick: () => console.log('whatever') },
133
+
})
134
if (mutation.type === 'like') {
135
setFastState(mutation.target, null);
136
} else if (mutation.type === 'unlike') {
+138
-8
src/routes/__root.tsx
+138
-8
src/routes/__root.tsx
···
14
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
15
import { useAtom } from "jotai";
16
import * as React from "react";
17
import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive";
18
19
import { Composer } from "~/components/Composer";
···
83
<LikeMutationQueueProvider>
84
<RootDocument>
85
<KeepAliveProvider>
86
<KeepAliveOutlet />
87
</KeepAliveProvider>
88
</RootDocument>
···
91
);
92
}
93
94
function RootDocument({ children }: { children: React.ReactNode }) {
95
useAtomCssVar(hueAtom, "--tw-gray-hue");
96
const location = useLocation();
···
134
<div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950">
135
<nav className="hidden lg:flex h-screen w-[250px] flex-col gap-0 p-4 dark:border-gray-800 sticky top-0 self-start">
136
<div className="flex items-center gap-3 mb-4">
137
-
<FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} />
138
<span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100">
139
Red Dwarf{" "}
140
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
···
235
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
236
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
237
//active={true}
238
-
onClickCallbback={() => setComposerPost({ kind: 'root' })}
239
text="Post"
240
/>
241
</div>
···
373
374
<nav className="hidden sm:flex items-center lg:hidden h-screen flex-col gap-2 p-4 dark:border-gray-800 sticky top-0 self-start">
375
<div className="flex items-center gap-3 mb-4">
376
-
<FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} />
377
</div>
378
<MaterialNavItem
379
small
···
475
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
476
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
477
//active={true}
478
-
onClickCallbback={() => setComposerPost({ kind: 'root' })}
479
text="Post"
480
/>
481
</div>
···
485
<button
486
className="lg:hidden fixed bottom-22 right-4 z-50 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 rounded-2xl w-14 h-14 flex items-center justify-center transition-all"
487
style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }}
488
-
onClick={() => setComposerPost({ kind: 'root' })}
489
type="button"
490
aria-label="Create Post"
491
>
···
502
</main>
503
504
<aside className="hidden lg:flex h-screen w-[250px] sticky top-0 self-start flex-col">
505
-
<div className="px-4 pt-4"><Import /></div>
506
<Login />
507
508
<div className="flex-1"></div>
509
<p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4">
510
-
Red Dwarf is a Bluesky client that does not rely on any Bluesky API App Servers. Instead, it uses Microcosm to fetch records directly from each users' PDS (via Slingshot) and connect them using backlinks (via Constellation)
511
</p>
512
</aside>
513
</div>
···
683
) : (
684
<div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-0 shadow border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 z-10">
685
<div className="flex items-center gap-2">
686
-
<FluentEmojiHighContrastGlowingStar className="h-6 w-6" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} />
687
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
688
Red Dwarf{" "}
689
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
···
14
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
15
import { useAtom } from "jotai";
16
import * as React from "react";
17
+
import { toast as sonnerToast } from "sonner";
18
+
import { Toaster } from "sonner";
19
import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive";
20
21
import { Composer } from "~/components/Composer";
···
85
<LikeMutationQueueProvider>
86
<RootDocument>
87
<KeepAliveProvider>
88
+
<AppToaster />
89
<KeepAliveOutlet />
90
</KeepAliveProvider>
91
</RootDocument>
···
94
);
95
}
96
97
+
export function AppToaster() {
98
+
return (
99
+
<Toaster
100
+
position="bottom-center"
101
+
toastOptions={{
102
+
duration: 4000,
103
+
}}
104
+
/>
105
+
);
106
+
}
107
+
108
+
export function renderSnack({
109
+
title,
110
+
description,
111
+
button,
112
+
}: Omit<ToastProps, "id">) {
113
+
return sonnerToast.custom((id) => (
114
+
<Snack
115
+
id={id}
116
+
title={title}
117
+
description={description}
118
+
button={
119
+
button?.label
120
+
? {
121
+
label: button?.label,
122
+
onClick: () => {
123
+
button?.onClick?.();
124
+
},
125
+
}
126
+
: undefined
127
+
}
128
+
/>
129
+
));
130
+
}
131
+
132
+
function Snack(props: ToastProps) {
133
+
const { title, description, button, id } = props;
134
+
135
+
return (
136
+
<div
137
+
role="status"
138
+
aria-live="polite"
139
+
className="
140
+
w-full md:max-w-[520px]
141
+
flex items-center justify-between
142
+
rounded-md
143
+
px-4 py-3
144
+
shadow-sm
145
+
dark:bg-gray-300 dark:text-gray-900
146
+
bg-gray-700 text-gray-100
147
+
ring-1 dark:ring-gray-200 ring-gray-800
148
+
"
149
+
>
150
+
<div className="flex-1 min-w-0">
151
+
<p className="text-sm font-medium truncate">{title}</p>
152
+
{description ? (
153
+
<p className="mt-1 text-sm dark:text-gray-600 text-gray-300 truncate">
154
+
{description}
155
+
</p>
156
+
) : null}
157
+
</div>
158
+
159
+
{button ? (
160
+
<div className="ml-4 flex-shrink-0">
161
+
<button
162
+
className="
163
+
text-sm font-medium
164
+
px-3 py-1 rounded-md
165
+
bg-gray-200 text-gray-900
166
+
hover:bg-gray-300
167
+
dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700
168
+
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 dark:focus:ring-gray-700
169
+
"
170
+
onClick={() => {
171
+
button.onClick();
172
+
sonnerToast.dismiss(id);
173
+
}}
174
+
>
175
+
{button.label}
176
+
</button>
177
+
</div>
178
+
) : null}
179
+
<button className=" ml-4"
180
+
onClick={() => {
181
+
sonnerToast.dismiss(id);
182
+
}}
183
+
>
184
+
<IconMdiClose />
185
+
</button>
186
+
</div>
187
+
);
188
+
}
189
+
190
+
/* Types */
191
+
interface ToastProps {
192
+
id: string | number;
193
+
title: string;
194
+
description?: string;
195
+
button?: {
196
+
label: string;
197
+
onClick: () => void;
198
+
};
199
+
}
200
+
201
function RootDocument({ children }: { children: React.ReactNode }) {
202
useAtomCssVar(hueAtom, "--tw-gray-hue");
203
const location = useLocation();
···
241
<div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950">
242
<nav className="hidden lg:flex h-screen w-[250px] flex-col gap-0 p-4 dark:border-gray-800 sticky top-0 self-start">
243
<div className="flex items-center gap-3 mb-4">
244
+
<FluentEmojiHighContrastGlowingStar
245
+
className="h-8 w-8"
246
+
style={{
247
+
color:
248
+
"oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))",
249
+
}}
250
+
/>
251
<span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100">
252
Red Dwarf{" "}
253
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
···
348
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
349
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
350
//active={true}
351
+
onClickCallbback={() => setComposerPost({ kind: "root" })}
352
text="Post"
353
/>
354
</div>
···
486
487
<nav className="hidden sm:flex items-center lg:hidden h-screen flex-col gap-2 p-4 dark:border-gray-800 sticky top-0 self-start">
488
<div className="flex items-center gap-3 mb-4">
489
+
<FluentEmojiHighContrastGlowingStar
490
+
className="h-8 w-8"
491
+
style={{
492
+
color:
493
+
"oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))",
494
+
}}
495
+
/>
496
</div>
497
<MaterialNavItem
498
small
···
594
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
595
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
596
//active={true}
597
+
onClickCallbback={() => setComposerPost({ kind: "root" })}
598
text="Post"
599
/>
600
</div>
···
604
<button
605
className="lg:hidden fixed bottom-22 right-4 z-50 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 rounded-2xl w-14 h-14 flex items-center justify-center transition-all"
606
style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }}
607
+
onClick={() => setComposerPost({ kind: "root" })}
608
type="button"
609
aria-label="Create Post"
610
>
···
621
</main>
622
623
<aside className="hidden lg:flex h-screen w-[250px] sticky top-0 self-start flex-col">
624
+
<div className="px-4 pt-4">
625
+
<Import />
626
+
</div>
627
<Login />
628
629
<div className="flex-1"></div>
630
<p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4">
631
+
Red Dwarf is a Bluesky client that does not rely on any Bluesky API
632
+
App Servers. Instead, it uses Microcosm to fetch records directly
633
+
from each users' PDS (via Slingshot) and connect them using
634
+
backlinks (via Constellation)
635
</p>
636
</aside>
637
</div>
···
807
) : (
808
<div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-0 shadow border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 z-10">
809
<div className="flex items-center gap-2">
810
+
<FluentEmojiHighContrastGlowingStar
811
+
className="h-6 w-6"
812
+
style={{
813
+
color:
814
+
"oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))",
815
+
}}
816
+
/>
817
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
818
Red Dwarf{" "}
819
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
+99
-39
src/routes/profile.$did/index.tsx
+99
-39
src/routes/profile.$did/index.tsx
···
33
useQueryProfile,
34
} from "~/utils/useQuery";
35
36
import { Chip } from "../notifications";
37
38
export const Route = createFileRoute("/profile/$did/")({
···
81
82
const isReady = !!resolvedDid && !isIdentityLoading && !!profileRecord;
83
84
-
const resultwhateversure = useQueryConstellationLinksCountDistinctDids(resolvedDid ? {
85
-
method: "/links/count/distinct-dids",
86
-
collection: "app.bsky.graph.follow",
87
-
target: resolvedDid,
88
-
path: ".subject"
89
-
} : undefined)
90
91
const followercount = resultwhateversure?.data?.total;
92
···
152
also save it persistently
153
*/}
154
<FollowButton targetdidorhandle={did} />
155
-
<button className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]">
156
... {/* todo: icon */}
157
</button>
158
</div>
···
165
{handle}
166
</div>
167
<div className="flex flex-row gap-2 text-md text-gray-500 dark:text-gray-400 mb-2">
168
-
<Link to="/profile/$did/followers" params={{did: did}}>{followercount && (<span className="mr-1 text-gray-900 dark:text-gray-200 font-medium">{followercount}</span>)}Followers</Link>
169
-
170
-
<Link to="/profile/$did/follows" params={{did: did}}>Follows</Link>
171
</div>
172
{description && (
173
<div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]">
···
212
}
213
214
export type ProfilePostsFilter = {
215
-
posts: boolean,
216
-
replies: boolean,
217
-
mediaOnly: boolean,
218
-
}
219
export const defaultProfilePostsFilter: ProfilePostsFilter = {
220
posts: true,
221
replies: true,
222
mediaOnly: false,
223
-
}
224
225
-
function ProfilePostsFilterChipBar({filters, toggle}:{filters: ProfilePostsFilter | null, toggle: (key: keyof ProfilePostsFilter) => void}) {
226
-
const empty = (!filters?.replies && !filters?.posts);
227
-
const almostEmpty = (!filters?.replies && filters?.posts);
228
229
useEffect(() => {
230
if (empty) {
231
-
toggle("posts")
232
}
233
}, [empty, toggle]);
234
···
237
<Chip
238
state={filters?.posts ?? true}
239
text="Posts"
240
-
onClick={() => almostEmpty ? null : toggle("posts")}
241
/>
242
<Chip
243
state={filters?.replies ?? true}
···
258
const [filterses, setFilterses] = useAtom(profileChipsAtom);
259
const filters = filterses?.[did];
260
const setFilters = (obj: ProfilePostsFilter) => {
261
-
setFilterses((prev)=>{
262
-
return{
263
...prev,
264
-
[did]: obj
265
-
}
266
-
})
267
-
}
268
-
useEffect(()=>{
269
if (!filters) {
270
setFilters(defaultProfilePostsFilter);
271
}
272
-
})
273
useReusableTabScrollRestore(`Profile` + did);
274
const queryClient = useQueryClient();
275
const {
···
306
);
307
308
const toggle = (key: keyof ProfilePostsFilter) => {
309
-
setFilterses(prev => {
310
-
const existing = prev[did] ?? { posts: false, replies: false, mediaOnly: false }; // default
311
312
return {
313
...prev,
···
805
)}
806
</>
807
) : (
808
-
<button className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]">
809
Edit Profile
810
</button>
811
)}
···
822
const { data: identity } = useQueryIdentity(targetdidorhandle);
823
const [show] = useAtom(enableBitesAtom);
824
825
-
if (!show) return
826
827
return (
828
<>
829
<button
830
-
onClick={(e) => {
831
e.stopPropagation();
832
-
sendBite({
833
agent: agent || undefined,
834
targetDid: identity?.did,
835
});
···
842
);
843
}
844
845
-
function sendBite({
846
agent,
847
targetDid,
848
}: {
849
agent?: Agent;
850
targetDid?: string;
851
}) {
852
-
if (!agent?.did || !targetDid) return;
853
const newRecord = {
854
repo: agent.did,
855
collection: "net.wafrn.feed.bite",
856
rkey: TID.next().toString(),
857
record: {
858
$type: "net.wafrn.feed.bite",
859
-
subject: "at://"+targetDid,
860
createdAt: new Date().toISOString(),
861
},
862
};
863
864
-
agent.com.atproto.repo.createRecord(newRecord).catch((err) => {
865
console.error("Bite failed:", err);
866
-
});
867
}
868
-
869
870
export function Mutual({ targetdidorhandle }: { targetdidorhandle: string }) {
871
const { agent } = useAuth();
···
33
useQueryProfile,
34
} from "~/utils/useQuery";
35
36
+
import { renderSnack } from "../__root";
37
import { Chip } from "../notifications";
38
39
export const Route = createFileRoute("/profile/$did/")({
···
82
83
const isReady = !!resolvedDid && !isIdentityLoading && !!profileRecord;
84
85
+
const resultwhateversure = useQueryConstellationLinksCountDistinctDids(
86
+
resolvedDid
87
+
? {
88
+
method: "/links/count/distinct-dids",
89
+
collection: "app.bsky.graph.follow",
90
+
target: resolvedDid,
91
+
path: ".subject",
92
+
}
93
+
: undefined
94
+
);
95
96
const followercount = resultwhateversure?.data?.total;
97
···
157
also save it persistently
158
*/}
159
<FollowButton targetdidorhandle={did} />
160
+
<button
161
+
className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]"
162
+
onClick={(e) => {
163
+
renderSnack({
164
+
title: "Not Implemented Yet",
165
+
description: "Sorry...",
166
+
//button: { label: 'Try Again', onClick: () => console.log('whatever') },
167
+
});
168
+
}}
169
+
>
170
... {/* todo: icon */}
171
</button>
172
</div>
···
179
{handle}
180
</div>
181
<div className="flex flex-row gap-2 text-md text-gray-500 dark:text-gray-400 mb-2">
182
+
<Link to="/profile/$did/followers" params={{ did: did }}>
183
+
{followercount && (
184
+
<span className="mr-1 text-gray-900 dark:text-gray-200 font-medium">
185
+
{followercount}
186
+
</span>
187
+
)}
188
+
Followers
189
+
</Link>
190
-
191
+
<Link to="/profile/$did/follows" params={{ did: did }}>
192
+
Follows
193
+
</Link>
194
</div>
195
{description && (
196
<div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]">
···
235
}
236
237
export type ProfilePostsFilter = {
238
+
posts: boolean;
239
+
replies: boolean;
240
+
mediaOnly: boolean;
241
+
};
242
export const defaultProfilePostsFilter: ProfilePostsFilter = {
243
posts: true,
244
replies: true,
245
mediaOnly: false,
246
+
};
247
248
+
function ProfilePostsFilterChipBar({
249
+
filters,
250
+
toggle,
251
+
}: {
252
+
filters: ProfilePostsFilter | null;
253
+
toggle: (key: keyof ProfilePostsFilter) => void;
254
+
}) {
255
+
const empty = !filters?.replies && !filters?.posts;
256
+
const almostEmpty = !filters?.replies && filters?.posts;
257
258
useEffect(() => {
259
if (empty) {
260
+
toggle("posts");
261
}
262
}, [empty, toggle]);
263
···
266
<Chip
267
state={filters?.posts ?? true}
268
text="Posts"
269
+
onClick={() => (almostEmpty ? null : toggle("posts"))}
270
/>
271
<Chip
272
state={filters?.replies ?? true}
···
287
const [filterses, setFilterses] = useAtom(profileChipsAtom);
288
const filters = filterses?.[did];
289
const setFilters = (obj: ProfilePostsFilter) => {
290
+
setFilterses((prev) => {
291
+
return {
292
...prev,
293
+
[did]: obj,
294
+
};
295
+
});
296
+
};
297
+
useEffect(() => {
298
if (!filters) {
299
setFilters(defaultProfilePostsFilter);
300
}
301
+
});
302
useReusableTabScrollRestore(`Profile` + did);
303
const queryClient = useQueryClient();
304
const {
···
335
);
336
337
const toggle = (key: keyof ProfilePostsFilter) => {
338
+
setFilterses((prev) => {
339
+
const existing = prev[did] ?? {
340
+
posts: false,
341
+
replies: false,
342
+
mediaOnly: false,
343
+
}; // default
344
345
return {
346
...prev,
···
838
)}
839
</>
840
) : (
841
+
<button
842
+
className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]"
843
+
onClick={(e) => {
844
+
renderSnack({
845
+
title: "Not Implemented Yet",
846
+
description: "Sorry...",
847
+
//button: { label: 'Try Again', onClick: () => console.log('whatever') },
848
+
});
849
+
}}
850
+
>
851
Edit Profile
852
</button>
853
)}
···
864
const { data: identity } = useQueryIdentity(targetdidorhandle);
865
const [show] = useAtom(enableBitesAtom);
866
867
+
if (!show) return;
868
869
return (
870
<>
871
<button
872
+
onClick={async (e) => {
873
e.stopPropagation();
874
+
await sendBite({
875
agent: agent || undefined,
876
targetDid: identity?.did,
877
});
···
884
);
885
}
886
887
+
async function sendBite({
888
agent,
889
targetDid,
890
}: {
891
agent?: Agent;
892
targetDid?: string;
893
}) {
894
+
if (!agent?.did || !targetDid) {
895
+
renderSnack({
896
+
title: "Bite Failed",
897
+
description: "You must be logged-in to bite someone.",
898
+
//button: { label: 'Try Again', onClick: () => console.log('whatever') },
899
+
});
900
+
return;
901
+
}
902
const newRecord = {
903
repo: agent.did,
904
collection: "net.wafrn.feed.bite",
905
rkey: TID.next().toString(),
906
record: {
907
$type: "net.wafrn.feed.bite",
908
+
subject: "at://" + targetDid,
909
createdAt: new Date().toISOString(),
910
},
911
};
912
913
+
try {
914
+
await agent.com.atproto.repo.createRecord(newRecord);
915
+
renderSnack({
916
+
title: "Bite Sent",
917
+
description: "Your bite was delivered.",
918
+
//button: { label: 'Undo', onClick: () => console.log('Undo clicked') },
919
+
});
920
+
} catch (err) {
921
console.error("Bite failed:", err);
922
+
renderSnack({
923
+
title: "Bite Failed",
924
+
description: "Your bite failed to be delivered.",
925
+
//button: { label: 'Try Again', onClick: () => console.log('whatever') },
926
+
});
927
+
}
928
}
929
930
export function Mutual({ targetdidorhandle }: { targetdidorhandle: string }) {
931
const { agent } = useAuth();