tangled
alpha
login
or
join now
whey.party
/
red-dwarf
83
fork
atom
an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
83
fork
atom
overview
issues
25
pulls
pipelines
post interactions
rimar1337
4 months ago
61ce2144
de4321b1
+625
-157
9 changed files
expand all
collapse all
unified
split
src
auto-imports.d.ts
components
UniversalPostRenderer.tsx
routeTree.gen.ts
routes
notifications.tsx
profile.$did
post.$rkey.liked-by.tsx
post.$rkey.quotes.tsx
post.$rkey.reposted-by.tsx
post.$rkey.tsx
utils
useQuery.ts
+1
src/auto-imports.d.ts
···
19
const IconMaterialSymbolsTag: typeof import('~icons/material-symbols/tag.jsx').default
20
const IconMdiAccountCircle: typeof import('~icons/mdi/account-circle.jsx').default
21
const IconMdiAccountPlus: typeof import('~icons/mdi/account-plus.jsx').default
0
22
const IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline.jsx').default
23
}
···
19
const IconMaterialSymbolsTag: typeof import('~icons/material-symbols/tag.jsx').default
20
const IconMdiAccountCircle: typeof import('~icons/mdi/account-circle.jsx').default
21
const IconMdiAccountPlus: typeof import('~icons/mdi/account-plus.jsx').default
22
+
const IconMdiMessageReplyTextOutline: typeof import('~icons/mdi/message-reply-text-outline.jsx').default
23
const IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline.jsx').default
24
}
+18
-4
src/components/UniversalPostRenderer.tsx
···
41
ref?: React.Ref<HTMLDivElement>;
42
dataIndexPropPass?: number;
43
nopics?: boolean;
0
44
lightboxCallback?: (d: LightboxProps) => void;
45
maxReplies?: number;
46
isQuote?: boolean;
···
152
ref,
153
dataIndexPropPass,
154
nopics,
0
155
lightboxCallback,
156
maxReplies,
157
isQuote,
···
536
ref={ref}
537
dataIndexPropPass={dataIndexPropPass}
538
nopics={nopics}
0
539
lightboxCallback={lightboxCallback}
540
maxReplies={maxReplies}
541
isQuote={isQuote}
···
567
ref={ref}
568
dataIndexPropPass={dataIndexPropPass}
569
nopics={nopics}
0
570
lightboxCallback={lightboxCallback}
571
maxReplies={
572
maxReplies && maxReplies > 0 ? maxReplies - 1 : undefined
···
636
ref,
637
dataIndexPropPass,
638
nopics,
0
639
lightboxCallback,
640
maxReplies,
641
isQuote,
···
657
ref?: React.Ref<HTMLDivElement>;
658
dataIndexPropPass?: number;
659
nopics?: boolean;
0
660
lightboxCallback?: (d: LightboxProps) => void;
661
maxReplies?: number;
662
isQuote?: boolean;
···
874
ref={ref}
875
dataIndexPropPass={dataIndexPropPass}
876
nopics={nopics}
0
877
lightboxCallback={lightboxCallback}
878
maxReplies={maxReplies}
879
isQuote={isQuote}
···
1327
ref,
1328
dataIndexPropPass,
1329
nopics,
0
1330
lightboxCallback,
1331
maxReplies,
1332
}: {
···
1353
ref?: React.Ref<HTMLDivElement>;
1354
dataIndexPropPass?: number;
1355
nopics?: boolean;
0
1356
lightboxCallback?: (d: LightboxProps) => void;
1357
maxReplies?: number;
1358
}) {
···
1759
<div
1760
style={{
1761
fontSize: 16,
1762
-
marginBottom: !post.embed /*|| depth > 0*/ ? 0 : 8,
1763
whiteSpace: "pre-wrap",
1764
textAlign: "left",
1765
overflowWrap: "anywhere",
1766
wordBreak: "break-word",
1767
-
//color: theme.text,
0
0
0
0
0
1768
}}
1769
className="text-gray-900 dark:text-gray-100"
1770
>
···
1787
</>
1788
)}
1789
</div>
1790
-
{post.embed && depth < 1 ? (
1791
<PostEmbeds
1792
embed={post.embed}
1793
//moderation={moderation}
···
1809
</div>
1810
</>
1811
)}
1812
-
<div style={{ paddingTop: post.embed && depth < 1 ? 4 : 0 }}>
1813
<>
1814
{expanded && (
1815
<div
···
41
ref?: React.Ref<HTMLDivElement>;
42
dataIndexPropPass?: number;
43
nopics?: boolean;
44
+
concise?: boolean;
45
lightboxCallback?: (d: LightboxProps) => void;
46
maxReplies?: number;
47
isQuote?: boolean;
···
153
ref,
154
dataIndexPropPass,
155
nopics,
156
+
concise,
157
lightboxCallback,
158
maxReplies,
159
isQuote,
···
538
ref={ref}
539
dataIndexPropPass={dataIndexPropPass}
540
nopics={nopics}
541
+
concise={concise}
542
lightboxCallback={lightboxCallback}
543
maxReplies={maxReplies}
544
isQuote={isQuote}
···
570
ref={ref}
571
dataIndexPropPass={dataIndexPropPass}
572
nopics={nopics}
573
+
concise={concise}
574
lightboxCallback={lightboxCallback}
575
maxReplies={
576
maxReplies && maxReplies > 0 ? maxReplies - 1 : undefined
···
640
ref,
641
dataIndexPropPass,
642
nopics,
643
+
concise,
644
lightboxCallback,
645
maxReplies,
646
isQuote,
···
662
ref?: React.Ref<HTMLDivElement>;
663
dataIndexPropPass?: number;
664
nopics?: boolean;
665
+
concise?: boolean;
666
lightboxCallback?: (d: LightboxProps) => void;
667
maxReplies?: number;
668
isQuote?: boolean;
···
880
ref={ref}
881
dataIndexPropPass={dataIndexPropPass}
882
nopics={nopics}
883
+
concise={concise}
884
lightboxCallback={lightboxCallback}
885
maxReplies={maxReplies}
886
isQuote={isQuote}
···
1334
ref,
1335
dataIndexPropPass,
1336
nopics,
1337
+
concise,
1338
lightboxCallback,
1339
maxReplies,
1340
}: {
···
1361
ref?: React.Ref<HTMLDivElement>;
1362
dataIndexPropPass?: number;
1363
nopics?: boolean;
1364
+
concise?: boolean;
1365
lightboxCallback?: (d: LightboxProps) => void;
1366
maxReplies?: number;
1367
}) {
···
1768
<div
1769
style={{
1770
fontSize: 16,
1771
+
marginBottom: !post.embed || concise ? 0 : 8,
1772
whiteSpace: "pre-wrap",
1773
textAlign: "left",
1774
overflowWrap: "anywhere",
1775
wordBreak: "break-word",
1776
+
...(concise && {
1777
+
display: "-webkit-box",
1778
+
WebkitBoxOrient: "vertical",
1779
+
WebkitLineClamp: 2,
1780
+
overflow: "hidden",
1781
+
}),
1782
}}
1783
className="text-gray-900 dark:text-gray-100"
1784
>
···
1801
</>
1802
)}
1803
</div>
1804
+
{post.embed && depth < 1 && !concise ? (
1805
<PostEmbeds
1806
embed={post.embed}
1807
//moderation={moderation}
···
1823
</div>
1824
</>
1825
)}
1826
+
<div style={{ paddingTop: post.embed && !concise && depth < 1 ? 4 : 0 }}>
1827
<>
1828
{expanded && (
1829
<div
+66
src/routeTree.gen.ts
···
21
import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
22
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
23
import { Route as ProfileDidPostRkeyRouteImport } from './routes/profile.$did/post.$rkey'
0
0
0
24
import { Route as ProfileDidPostRkeyImageIRouteImport } from './routes/profile.$did/post.$rkey.image.$i'
25
26
const SettingsRoute = SettingsRouteImport.update({
···
84
path: '/profile/$did/post/$rkey',
85
getParentRoute: () => rootRouteImport,
86
} as any)
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
87
const ProfileDidPostRkeyImageIRoute =
88
ProfileDidPostRkeyImageIRouteImport.update({
89
id: '/image/$i',
···
102
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
103
'/profile/$did': typeof ProfileDidIndexRoute
104
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
0
0
0
105
'/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute
106
}
107
export interface FileRoutesByTo {
···
115
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
116
'/profile/$did': typeof ProfileDidIndexRoute
117
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
0
0
0
118
'/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute
119
}
120
export interface FileRoutesById {
···
131
'/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
132
'/profile/$did/': typeof ProfileDidIndexRoute
133
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
0
0
0
134
'/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute
135
}
136
export interface FileRouteTypes {
···
146
| '/route-b'
147
| '/profile/$did'
148
| '/profile/$did/post/$rkey'
0
0
0
149
| '/profile/$did/post/$rkey/image/$i'
150
fileRoutesByTo: FileRoutesByTo
151
to:
···
159
| '/route-b'
160
| '/profile/$did'
161
| '/profile/$did/post/$rkey'
0
0
0
162
| '/profile/$did/post/$rkey/image/$i'
163
id:
164
| '__root__'
···
174
| '/_pathlessLayout/_nested-layout/route-b'
175
| '/profile/$did/'
176
| '/profile/$did/post/$rkey'
0
0
0
177
| '/profile/$did/post/$rkey/image/$i'
178
fileRoutesById: FileRoutesById
179
}
···
275
preLoaderRoute: typeof ProfileDidPostRkeyRouteImport
276
parentRoute: typeof rootRouteImport
277
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
278
'/profile/$did/post/$rkey/image/$i': {
279
id: '/profile/$did/post/$rkey/image/$i'
280
path: '/image/$i'
···
316
)
317
318
interface ProfileDidPostRkeyRouteChildren {
0
0
0
319
ProfileDidPostRkeyImageIRoute: typeof ProfileDidPostRkeyImageIRoute
320
}
321
322
const ProfileDidPostRkeyRouteChildren: ProfileDidPostRkeyRouteChildren = {
0
0
0
323
ProfileDidPostRkeyImageIRoute: ProfileDidPostRkeyImageIRoute,
324
}
325
···
21
import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
22
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
23
import { Route as ProfileDidPostRkeyRouteImport } from './routes/profile.$did/post.$rkey'
24
+
import { Route as ProfileDidPostRkeyRepostedByRouteImport } from './routes/profile.$did/post.$rkey.reposted-by'
25
+
import { Route as ProfileDidPostRkeyQuotesRouteImport } from './routes/profile.$did/post.$rkey.quotes'
26
+
import { Route as ProfileDidPostRkeyLikedByRouteImport } from './routes/profile.$did/post.$rkey.liked-by'
27
import { Route as ProfileDidPostRkeyImageIRouteImport } from './routes/profile.$did/post.$rkey.image.$i'
28
29
const SettingsRoute = SettingsRouteImport.update({
···
87
path: '/profile/$did/post/$rkey',
88
getParentRoute: () => rootRouteImport,
89
} as any)
90
+
const ProfileDidPostRkeyRepostedByRoute =
91
+
ProfileDidPostRkeyRepostedByRouteImport.update({
92
+
id: '/reposted-by',
93
+
path: '/reposted-by',
94
+
getParentRoute: () => ProfileDidPostRkeyRoute,
95
+
} as any)
96
+
const ProfileDidPostRkeyQuotesRoute =
97
+
ProfileDidPostRkeyQuotesRouteImport.update({
98
+
id: '/quotes',
99
+
path: '/quotes',
100
+
getParentRoute: () => ProfileDidPostRkeyRoute,
101
+
} as any)
102
+
const ProfileDidPostRkeyLikedByRoute =
103
+
ProfileDidPostRkeyLikedByRouteImport.update({
104
+
id: '/liked-by',
105
+
path: '/liked-by',
106
+
getParentRoute: () => ProfileDidPostRkeyRoute,
107
+
} as any)
108
const ProfileDidPostRkeyImageIRoute =
109
ProfileDidPostRkeyImageIRouteImport.update({
110
id: '/image/$i',
···
123
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
124
'/profile/$did': typeof ProfileDidIndexRoute
125
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
126
+
'/profile/$did/post/$rkey/liked-by': typeof ProfileDidPostRkeyLikedByRoute
127
+
'/profile/$did/post/$rkey/quotes': typeof ProfileDidPostRkeyQuotesRoute
128
+
'/profile/$did/post/$rkey/reposted-by': typeof ProfileDidPostRkeyRepostedByRoute
129
'/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute
130
}
131
export interface FileRoutesByTo {
···
139
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
140
'/profile/$did': typeof ProfileDidIndexRoute
141
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
142
+
'/profile/$did/post/$rkey/liked-by': typeof ProfileDidPostRkeyLikedByRoute
143
+
'/profile/$did/post/$rkey/quotes': typeof ProfileDidPostRkeyQuotesRoute
144
+
'/profile/$did/post/$rkey/reposted-by': typeof ProfileDidPostRkeyRepostedByRoute
145
'/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute
146
}
147
export interface FileRoutesById {
···
158
'/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
159
'/profile/$did/': typeof ProfileDidIndexRoute
160
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
161
+
'/profile/$did/post/$rkey/liked-by': typeof ProfileDidPostRkeyLikedByRoute
162
+
'/profile/$did/post/$rkey/quotes': typeof ProfileDidPostRkeyQuotesRoute
163
+
'/profile/$did/post/$rkey/reposted-by': typeof ProfileDidPostRkeyRepostedByRoute
164
'/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute
165
}
166
export interface FileRouteTypes {
···
176
| '/route-b'
177
| '/profile/$did'
178
| '/profile/$did/post/$rkey'
179
+
| '/profile/$did/post/$rkey/liked-by'
180
+
| '/profile/$did/post/$rkey/quotes'
181
+
| '/profile/$did/post/$rkey/reposted-by'
182
| '/profile/$did/post/$rkey/image/$i'
183
fileRoutesByTo: FileRoutesByTo
184
to:
···
192
| '/route-b'
193
| '/profile/$did'
194
| '/profile/$did/post/$rkey'
195
+
| '/profile/$did/post/$rkey/liked-by'
196
+
| '/profile/$did/post/$rkey/quotes'
197
+
| '/profile/$did/post/$rkey/reposted-by'
198
| '/profile/$did/post/$rkey/image/$i'
199
id:
200
| '__root__'
···
210
| '/_pathlessLayout/_nested-layout/route-b'
211
| '/profile/$did/'
212
| '/profile/$did/post/$rkey'
213
+
| '/profile/$did/post/$rkey/liked-by'
214
+
| '/profile/$did/post/$rkey/quotes'
215
+
| '/profile/$did/post/$rkey/reposted-by'
216
| '/profile/$did/post/$rkey/image/$i'
217
fileRoutesById: FileRoutesById
218
}
···
314
preLoaderRoute: typeof ProfileDidPostRkeyRouteImport
315
parentRoute: typeof rootRouteImport
316
}
317
+
'/profile/$did/post/$rkey/reposted-by': {
318
+
id: '/profile/$did/post/$rkey/reposted-by'
319
+
path: '/reposted-by'
320
+
fullPath: '/profile/$did/post/$rkey/reposted-by'
321
+
preLoaderRoute: typeof ProfileDidPostRkeyRepostedByRouteImport
322
+
parentRoute: typeof ProfileDidPostRkeyRoute
323
+
}
324
+
'/profile/$did/post/$rkey/quotes': {
325
+
id: '/profile/$did/post/$rkey/quotes'
326
+
path: '/quotes'
327
+
fullPath: '/profile/$did/post/$rkey/quotes'
328
+
preLoaderRoute: typeof ProfileDidPostRkeyQuotesRouteImport
329
+
parentRoute: typeof ProfileDidPostRkeyRoute
330
+
}
331
+
'/profile/$did/post/$rkey/liked-by': {
332
+
id: '/profile/$did/post/$rkey/liked-by'
333
+
path: '/liked-by'
334
+
fullPath: '/profile/$did/post/$rkey/liked-by'
335
+
preLoaderRoute: typeof ProfileDidPostRkeyLikedByRouteImport
336
+
parentRoute: typeof ProfileDidPostRkeyRoute
337
+
}
338
'/profile/$did/post/$rkey/image/$i': {
339
id: '/profile/$did/post/$rkey/image/$i'
340
path: '/image/$i'
···
376
)
377
378
interface ProfileDidPostRkeyRouteChildren {
379
+
ProfileDidPostRkeyLikedByRoute: typeof ProfileDidPostRkeyLikedByRoute
380
+
ProfileDidPostRkeyQuotesRoute: typeof ProfileDidPostRkeyQuotesRoute
381
+
ProfileDidPostRkeyRepostedByRoute: typeof ProfileDidPostRkeyRepostedByRoute
382
ProfileDidPostRkeyImageIRoute: typeof ProfileDidPostRkeyImageIRoute
383
}
384
385
const ProfileDidPostRkeyRouteChildren: ProfileDidPostRkeyRouteChildren = {
386
+
ProfileDidPostRkeyLikedByRoute: ProfileDidPostRkeyLikedByRoute,
387
+
ProfileDidPostRkeyQuotesRoute: ProfileDidPostRkeyQuotesRoute,
388
+
ProfileDidPostRkeyRepostedByRoute: ProfileDidPostRkeyRepostedByRoute,
389
ProfileDidPostRkeyImageIRoute: ProfileDidPostRkeyImageIRoute,
390
}
391
+96
-56
src/routes/notifications.tsx
···
1
import { AtUri } from "@atproto/api";
2
import * as TabsPrimitive from "@radix-ui/react-tabs";
3
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
4
-
import { createFileRoute, useNavigate } from "@tanstack/react-router";
5
import { useAtom } from "jotai";
6
import * as React from "react";
7
···
47
export const Route = createFileRoute("/notifications")({
48
component: NotificationsComponent,
49
});
50
-
51
52
export default function NotificationsTabs() {
53
const [activeTab, setActiveTab] = React.useState("mentions");
···
225
);
226
}
227
228
-
229
function PostInteractionsTab() {
230
const { agent } = useAuth();
231
const { data: identity } = useQueryIdentity(agent?.did);
···
274
);
275
}
276
0
0
0
0
0
0
0
277
function PostInteractionsItem({ uri }: { uri: string }) {
278
const { data: links } = useQueryConstellation({
279
method: "/links/all",
280
target: uri,
281
});
282
283
-
const interactions = React.useMemo(() => {
284
-
const likes =
285
-
links?.links?.["app.bsky.feed.like"]?.[".subject.uri"]?.records || 0;
286
-
const replies =
287
-
links?.links?.["app.bsky.feed.post"]?.[".reply.parent.uri"]?.records || 0;
288
-
const reposts =
289
-
links?.links?.["app.bsky.feed.repost"]?.[".subject.uri"]?.records || 0;
290
-
const quotes1 =
291
-
links?.links?.["app.bsky.feed.post"]?.[".embed.record.uri"]?.records || 0;
292
-
const quotes2 =
293
-
links?.links?.["app.bsky.feed.post"]?.[".embed.record.record.uri"]
294
-
?.records || 0;
295
296
-
const totals = {
297
-
likes,
298
-
replies,
299
-
reposts,
300
-
quotes: quotes1 + quotes2,
301
-
};
302
-
303
-
const list = (
304
-
[
305
-
["reply", totals.replies],
306
-
["repost", totals.reposts],
307
-
["like", totals.likes],
308
-
["quote", totals.quotes],
309
-
] as const
310
-
).filter(([, count]) => count > 0);
311
-
312
-
return { totals, list };
313
-
}, [links]);
314
315
return (
316
-
<div className="flex flex-col border-b pb-8">
317
-
<div className="border rounded-xl mx-4 mt-4 ">
318
<UniversalPostRendererATURILoader
319
isQuote
320
key={uri}
321
atUri={uri}
322
-
nopics
0
323
/>
324
-
</div>
325
-
<div className="flex flex-col">
326
-
{interactions.list.map(([type, count]) => (
327
-
<InteractionsButton key={type} type={type} uri={uri} count={count} />
328
-
))}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
329
</div>
330
</div>
331
);
···
340
uri: string;
341
count: number;
342
}) {
0
0
343
return (
344
-
<div className="flex-1 border-t py-2 px-4 flex flex-row items-center gap-2">
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
345
{type === "like" ? (
346
<MdiCardsHeartOutline height={22} width={22} />
347
) : type === "repost" ? (
348
<MdiRepeat height={22} width={22} />
349
) : type === "reply" ? (
350
<MdiCommentOutline height={22} width={22} />
0
0
0
0
0
0
351
) : (
352
<></>
353
)}
354
{type}
355
{/* bad grammar replys */}
356
{count > 1 ? "s" : ""} <div className="flex-1" /> {count}
357
-
</div>
358
);
359
}
360
361
-
function NotificationItem({ notification }: { notification: string }) {
362
const aturi = new AtUri(notification);
363
const navigate = useNavigate();
364
const { data: identity } = useQueryIdentity(aturi.host);
···
381
382
return (
383
<div
384
-
className="flex items-center gap-3 p-4 cursor-pointer border-b flex-row"
385
onClick={() =>
386
aturi &&
387
navigate({
···
390
})
391
}
392
>
393
-
<div>
394
{aturi.collection === "app.bsky.graph.follow" ? (
395
<IconMdiAccountPlus />
0
0
396
) : (
397
<></>
398
)}
399
-
</div>
400
{profile ? (
401
<img
402
src={avatar || defaultpfp}
···
406
) : (
407
<div className="w-10 h-10 rounded-full bg-gray-300 dark:bg-gray-700" />
408
)}
409
-
<div className="flex flex-col">
410
-
<div className="flex flex-row gap-2">
411
-
<span className="font-medium text-gray-900 dark:text-gray-100">
412
{profile?.displayName || identity?.handle || "Someone"}
413
</span>
414
-
<span className="text-gray-700 dark:text-gray-400">
415
@{identity?.handle}
416
</span>
417
</div>
···
428
);
429
}
430
431
-
432
-
const EmptyState = ({ text }: { text: string }) => (
433
<div className="py-10 text-center text-gray-500 dark:text-gray-400">
434
{text}
435
</div>
436
);
437
438
-
const LoadingState = ({ text }: { text: string }) => (
439
<div className="py-10 text-center text-gray-500 dark:text-gray-400 italic">
440
{text}
441
</div>
442
);
443
444
-
const ErrorState = ({ error }: { error: unknown }) => (
445
<div className="py-10 text-center text-red-600 dark:text-red-400">
446
Error: {(error as Error)?.message || "Something went wrong."}
447
</div>
448
-
);
···
1
import { AtUri } from "@atproto/api";
2
import * as TabsPrimitive from "@radix-ui/react-tabs";
3
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
4
+
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
5
import { useAtom } from "jotai";
6
import * as React from "react";
7
···
47
export const Route = createFileRoute("/notifications")({
48
component: NotificationsComponent,
49
});
0
50
51
export default function NotificationsTabs() {
52
const [activeTab, setActiveTab] = React.useState("mentions");
···
224
);
225
}
226
0
227
function PostInteractionsTab() {
228
const { agent } = useAuth();
229
const { data: identity } = useQueryIdentity(agent?.did);
···
272
);
273
}
274
275
+
const ORDER: ("like" | "repost" | "reply" | "quote")[] = [
276
+
"like",
277
+
"repost",
278
+
"reply",
279
+
"quote",
280
+
];
281
+
282
function PostInteractionsItem({ uri }: { uri: string }) {
283
const { data: links } = useQueryConstellation({
284
method: "/links/all",
285
target: uri,
286
});
287
288
+
const likes =
289
+
links?.links?.["app.bsky.feed.like"]?.[".subject.uri"]?.records || 0;
290
+
const replies =
291
+
links?.links?.["app.bsky.feed.post"]?.[".reply.parent.uri"]?.records || 0;
292
+
const reposts =
293
+
links?.links?.["app.bsky.feed.repost"]?.[".subject.uri"]?.records || 0;
294
+
const quotes1 =
295
+
links?.links?.["app.bsky.feed.post"]?.[".embed.record.uri"]?.records || 0;
296
+
const quotes2 =
297
+
links?.links?.["app.bsky.feed.post"]?.[".embed.record.record.uri"]
298
+
?.records || 0;
299
+
const quotes = quotes1 + quotes2;
300
301
+
const all = likes + replies + reposts + quotes;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
302
303
return (
304
+
<div className="flex flex-col">
305
+
<div className="border rounded-xl mx-4 mt-4 overflow-hidden">
306
<UniversalPostRendererATURILoader
307
isQuote
308
key={uri}
309
atUri={uri}
310
+
nopics={true}
311
+
concise={true}
312
/>
313
+
<div className="flex flex-col divide-x">
314
+
<InteractionsButton
315
+
key={likes}
316
+
type={"like"}
317
+
uri={uri}
318
+
count={likes}
319
+
/>
320
+
<InteractionsButton
321
+
key={reposts}
322
+
type={"repost"}
323
+
uri={uri}
324
+
count={reposts}
325
+
/>
326
+
<InteractionsButton
327
+
key={replies}
328
+
type={"reply"}
329
+
uri={uri}
330
+
count={replies}
331
+
/>
332
+
<InteractionsButton
333
+
key={quotes}
334
+
type={"quote"}
335
+
uri={uri}
336
+
count={quotes}
337
+
/>
338
+
{!all && (
339
+
<div className="text-center text-gray-500 dark:text-gray-400 pb-3 pt-2 border-t">
340
+
No interactions yet.
341
+
</div>
342
+
)}
343
+
</div>
344
</div>
345
</div>
346
);
···
355
uri: string;
356
count: number;
357
}) {
358
+
if (!count) return <></>;
359
+
const aturi = new AtUri(uri);
360
return (
361
+
<Link
362
+
to={
363
+
`/profile/$did/post/$rkey` +
364
+
(type === "like"
365
+
? "/liked-by"
366
+
: type === "repost"
367
+
? "/reposted-by"
368
+
: type === "quote"
369
+
? "/quotes"
370
+
: "")
371
+
}
372
+
params={{
373
+
did: aturi.host,
374
+
rkey: aturi.rkey,
375
+
}}
376
+
className="flex-1 border-t py-2 px-4 flex flex-row items-center gap-2 transition-colors hover:bg-gray-100 hover:dark:bg-gray-800"
377
+
>
378
{type === "like" ? (
379
<MdiCardsHeartOutline height={22} width={22} />
380
) : type === "repost" ? (
381
<MdiRepeat height={22} width={22} />
382
) : type === "reply" ? (
383
<MdiCommentOutline height={22} width={22} />
384
+
) : type === "quote" ? (
385
+
<IconMdiMessageReplyTextOutline
386
+
height={22}
387
+
width={22}
388
+
className=" text-gray-400"
389
+
/>
390
) : (
391
<></>
392
)}
393
{type}
394
{/* bad grammar replys */}
395
{count > 1 ? "s" : ""} <div className="flex-1" /> {count}
396
+
</Link>
397
);
398
}
399
400
+
export function NotificationItem({ notification }: { notification: string }) {
401
const aturi = new AtUri(notification);
402
const navigate = useNavigate();
403
const { data: identity } = useQueryIdentity(aturi.host);
···
420
421
return (
422
<div
423
+
className="flex items-center p-4 cursor-pointer gap-3 justify-around border-b flex-row"
424
onClick={() =>
425
aturi &&
426
navigate({
···
429
})
430
}
431
>
432
+
{/* <div>
433
{aturi.collection === "app.bsky.graph.follow" ? (
434
<IconMdiAccountPlus />
435
+
) : aturi.collection === "app.bsky.feed.like" ? (
436
+
<MdiCardsHeart />
437
) : (
438
<></>
439
)}
440
+
</div> */}
441
{profile ? (
442
<img
443
src={avatar || defaultpfp}
···
447
) : (
448
<div className="w-10 h-10 rounded-full bg-gray-300 dark:bg-gray-700" />
449
)}
450
+
<div className="flex flex-col min-w-0">
451
+
<div className="flex flex-row gap-2 overflow-hidden text-ellipsis whitespace-nowrap min-w-0">
452
+
<span className="font-medium text-gray-900 dark:text-gray-100 truncate">
453
{profile?.displayName || identity?.handle || "Someone"}
454
</span>
455
+
<span className="text-gray-700 dark:text-gray-400 truncate">
456
@{identity?.handle}
457
</span>
458
</div>
···
469
);
470
}
471
472
+
export const EmptyState = ({ text }: { text: string }) => (
0
473
<div className="py-10 text-center text-gray-500 dark:text-gray-400">
474
{text}
475
</div>
476
);
477
478
+
export const LoadingState = ({ text }: { text: string }) => (
479
<div className="py-10 text-center text-gray-500 dark:text-gray-400 italic">
480
{text}
481
</div>
482
);
483
484
+
export const ErrorState = ({ error }: { error: unknown }) => (
485
<div className="py-10 text-center text-red-600 dark:text-red-400">
486
Error: {(error as Error)?.message || "Something went wrong."}
487
</div>
488
+
);
+100
src/routes/profile.$did/post.$rkey.liked-by.tsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { useInfiniteQuery } from "@tanstack/react-query";
2
+
import { createFileRoute } from "@tanstack/react-router";
3
+
import { useAtom } from "jotai";
4
+
import React from "react";
5
+
6
+
import { Header } from "~/components/Header";
7
+
import { constellationURLAtom } from "~/utils/atoms";
8
+
import { useQueryIdentity, yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks } from "~/utils/useQuery";
9
+
10
+
import {
11
+
EmptyState,
12
+
ErrorState,
13
+
LoadingState,
14
+
NotificationItem,
15
+
} from "../notifications";
16
+
17
+
export const Route = createFileRoute("/profile/$did/post/$rkey/liked-by")({
18
+
component: RouteComponent,
19
+
});
20
+
21
+
function RouteComponent() {
22
+
const { did, rkey } = Route.useParams();
23
+
const { data: identity } = useQueryIdentity(did);
24
+
const atUri = identity?.did && rkey ? `at://${decodeURIComponent(identity.did)}/app.bsky.feed.post/${rkey}` : '';
25
+
26
+
const [constellationurl] = useAtom(constellationURLAtom);
27
+
const infinitequeryresults = useInfiniteQuery({
28
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
29
+
{
30
+
constellation: constellationurl,
31
+
method: "/links",
32
+
target: atUri,
33
+
collection: "app.bsky.feed.like",
34
+
path: ".subject.uri",
35
+
}
36
+
),
37
+
enabled: !!atUri,
38
+
});
39
+
40
+
const {
41
+
data: infiniteLikesData,
42
+
fetchNextPage,
43
+
hasNextPage,
44
+
isFetchingNextPage,
45
+
isLoading,
46
+
isError,
47
+
error,
48
+
} = infinitequeryresults;
49
+
50
+
const likesAturis = React.useMemo(() => {
51
+
// Get all replies from the standard infinite query
52
+
return (
53
+
infiniteLikesData?.pages.flatMap(
54
+
(page) =>
55
+
page?.linking_records.map(
56
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`
57
+
) ?? []
58
+
) ?? []
59
+
);
60
+
}, [infiniteLikesData]);
61
+
62
+
return (
63
+
<>
64
+
<Header
65
+
title={`Liked By`}
66
+
backButtonCallback={() => {
67
+
if (window.history.length > 1) {
68
+
window.history.back();
69
+
} else {
70
+
window.location.assign("/");
71
+
}
72
+
}}
73
+
/>
74
+
75
+
<>
76
+
{(() => {
77
+
if (isLoading) return <LoadingState text="Loading likes..." />;
78
+
if (isError) return <ErrorState error={error} />;
79
+
80
+
if (!likesAturis?.length)
81
+
return <EmptyState text="No likes yet." />;
82
+
})()}
83
+
</>
84
+
85
+
{likesAturis.map((m) => (
86
+
<NotificationItem key={m} notification={m} />
87
+
))}
88
+
89
+
{hasNextPage && (
90
+
<button
91
+
onClick={() => fetchNextPage()}
92
+
disabled={isFetchingNextPage}
93
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50"
94
+
>
95
+
{isFetchingNextPage ? "Loading..." : "Load More"}
96
+
</button>
97
+
)}
98
+
</>
99
+
);
100
+
}
+141
src/routes/profile.$did/post.$rkey.quotes.tsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { useInfiniteQuery } from "@tanstack/react-query";
2
+
import { createFileRoute } from "@tanstack/react-router";
3
+
import { useAtom } from "jotai";
4
+
import React from "react";
5
+
6
+
import { Header } from "~/components/Header";
7
+
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
8
+
import { constellationURLAtom } from "~/utils/atoms";
9
+
import { type linksRecord,useQueryIdentity, yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks } from "~/utils/useQuery";
10
+
11
+
import {
12
+
EmptyState,
13
+
ErrorState,
14
+
LoadingState,
15
+
} from "../notifications";
16
+
17
+
export const Route = createFileRoute("/profile/$did/post/$rkey/quotes")({
18
+
component: RouteComponent,
19
+
});
20
+
21
+
function RouteComponent() {
22
+
const { did, rkey } = Route.useParams();
23
+
const { data: identity } = useQueryIdentity(did);
24
+
const atUri = identity?.did && rkey ? `at://${decodeURIComponent(identity.did)}/app.bsky.feed.post/${rkey}` : '';
25
+
26
+
const [constellationurl] = useAtom(constellationURLAtom);
27
+
const infinitequeryresultsWithoutMedia = useInfiniteQuery({
28
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
29
+
{
30
+
constellation: constellationurl,
31
+
method: "/links",
32
+
target: atUri,
33
+
collection: "app.bsky.feed.post",
34
+
path: ".embed.record.uri", // embed.record.record.uri and embed.record.uri
35
+
}
36
+
),
37
+
enabled: !!atUri,
38
+
});
39
+
const infinitequeryresultsWithMedia = useInfiniteQuery({
40
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
41
+
{
42
+
constellation: constellationurl,
43
+
method: "/links",
44
+
target: atUri,
45
+
collection: "app.bsky.feed.post",
46
+
path: ".embed.record.record.uri", // embed.record.record.uri and embed.record.uri
47
+
}
48
+
),
49
+
enabled: !!atUri,
50
+
});
51
+
52
+
const {
53
+
data: infiniteQuotesDataWithoutMedia,
54
+
fetchNextPage: fetchNextPageWithoutMedia,
55
+
hasNextPage: hasNextPageWithoutMedia,
56
+
isFetchingNextPage: isFetchingNextPageWithoutMedia,
57
+
isLoading: isLoadingWithoutMedia,
58
+
isError: isErrorWithoutMedia,
59
+
error: errorWithoutMedia,
60
+
} = infinitequeryresultsWithoutMedia;
61
+
const {
62
+
data: infiniteQuotesDataWithMedia,
63
+
fetchNextPage: fetchNextPageWithMedia,
64
+
hasNextPage: hasNextPageWithMedia,
65
+
isFetchingNextPage: isFetchingNextPageWithMedia,
66
+
isLoading: isLoadingWithMedia,
67
+
isError: isErrorWithMedia,
68
+
error: errorWithMedia,
69
+
} = infinitequeryresultsWithMedia;
70
+
71
+
const fetchNextPage = async () => {
72
+
await Promise.all([
73
+
hasNextPageWithMedia && fetchNextPageWithMedia(),
74
+
hasNextPageWithoutMedia && fetchNextPageWithoutMedia(),
75
+
]);
76
+
};
77
+
78
+
const hasNextPage = hasNextPageWithMedia || hasNextPageWithoutMedia;
79
+
const isFetchingNextPage = isFetchingNextPageWithMedia || isFetchingNextPageWithoutMedia;
80
+
const isLoading = isLoadingWithMedia || isLoadingWithoutMedia;
81
+
82
+
const allQuotes = React.useMemo(() => {
83
+
const withPages = infiniteQuotesDataWithMedia?.pages ?? [];
84
+
const withoutPages = infiniteQuotesDataWithoutMedia?.pages ?? [];
85
+
const maxLen = Math.max(withPages.length, withoutPages.length);
86
+
const merged: linksRecord[] = [];
87
+
88
+
for (let i = 0; i < maxLen; i++) {
89
+
const a = withPages[i]?.linking_records ?? [];
90
+
const b = withoutPages[i]?.linking_records ?? [];
91
+
const mergedPage = [...a, ...b].sort((b, a) => a.rkey.localeCompare(b.rkey));
92
+
merged.push(...mergedPage);
93
+
}
94
+
95
+
return merged;
96
+
}, [infiniteQuotesDataWithMedia?.pages, infiniteQuotesDataWithoutMedia?.pages]);
97
+
98
+
const quotesAturis = React.useMemo(() => {
99
+
return allQuotes.flatMap((r) => `at://${r.did}/${r.collection}/${r.rkey}`);
100
+
}, [allQuotes]);
101
+
102
+
return (
103
+
<>
104
+
<Header
105
+
title={`Quotes`}
106
+
backButtonCallback={() => {
107
+
if (window.history.length > 1) {
108
+
window.history.back();
109
+
} else {
110
+
window.location.assign("/");
111
+
}
112
+
}}
113
+
/>
114
+
115
+
<>
116
+
{(() => {
117
+
if (isLoading) return <LoadingState text="Loading quotes..." />;
118
+
if (isErrorWithMedia) return <ErrorState error={errorWithMedia} />;
119
+
if (isErrorWithoutMedia) return <ErrorState error={errorWithoutMedia} />;
120
+
121
+
if (!quotesAturis?.length)
122
+
return <EmptyState text="No quotes yet." />;
123
+
})()}
124
+
</>
125
+
126
+
{quotesAturis.map((m) => (
127
+
<UniversalPostRendererATURILoader key={m} atUri={m} />
128
+
))}
129
+
130
+
{hasNextPage && (
131
+
<button
132
+
onClick={() => fetchNextPage()}
133
+
disabled={isFetchingNextPage}
134
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50"
135
+
>
136
+
{isFetchingNextPage ? "Loading..." : "Load More"}
137
+
</button>
138
+
)}
139
+
</>
140
+
);
141
+
}
+100
src/routes/profile.$did/post.$rkey.reposted-by.tsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { useInfiniteQuery } from "@tanstack/react-query";
2
+
import { createFileRoute } from "@tanstack/react-router";
3
+
import { useAtom } from "jotai";
4
+
import React from "react";
5
+
6
+
import { Header } from "~/components/Header";
7
+
import { constellationURLAtom } from "~/utils/atoms";
8
+
import { useQueryIdentity, yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks } from "~/utils/useQuery";
9
+
10
+
import {
11
+
EmptyState,
12
+
ErrorState,
13
+
LoadingState,
14
+
NotificationItem,
15
+
} from "../notifications";
16
+
17
+
export const Route = createFileRoute("/profile/$did/post/$rkey/reposted-by")({
18
+
component: RouteComponent,
19
+
});
20
+
21
+
function RouteComponent() {
22
+
const { did, rkey } = Route.useParams();
23
+
const { data: identity } = useQueryIdentity(did);
24
+
const atUri = identity?.did && rkey ? `at://${decodeURIComponent(identity.did)}/app.bsky.feed.post/${rkey}` : '';
25
+
26
+
const [constellationurl] = useAtom(constellationURLAtom);
27
+
const infinitequeryresults = useInfiniteQuery({
28
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
29
+
{
30
+
constellation: constellationurl,
31
+
method: "/links",
32
+
target: atUri,
33
+
collection: "app.bsky.feed.repost",
34
+
path: ".subject.uri",
35
+
}
36
+
),
37
+
enabled: !!atUri,
38
+
});
39
+
40
+
const {
41
+
data: infiniteRepostsData,
42
+
fetchNextPage,
43
+
hasNextPage,
44
+
isFetchingNextPage,
45
+
isLoading,
46
+
isError,
47
+
error,
48
+
} = infinitequeryresults;
49
+
50
+
const repostsAturis = React.useMemo(() => {
51
+
// Get all replies from the standard infinite query
52
+
return (
53
+
infiniteRepostsData?.pages.flatMap(
54
+
(page) =>
55
+
page?.linking_records.map(
56
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`
57
+
) ?? []
58
+
) ?? []
59
+
);
60
+
}, [infiniteRepostsData]);
61
+
62
+
return (
63
+
<>
64
+
<Header
65
+
title={`Reposted By`}
66
+
backButtonCallback={() => {
67
+
if (window.history.length > 1) {
68
+
window.history.back();
69
+
} else {
70
+
window.location.assign("/");
71
+
}
72
+
}}
73
+
/>
74
+
75
+
<>
76
+
{(() => {
77
+
if (isLoading) return <LoadingState text="Loading reposts..." />;
78
+
if (isError) return <ErrorState error={error} />;
79
+
80
+
if (!repostsAturis?.length)
81
+
return <EmptyState text="No reposts yet." />;
82
+
})()}
83
+
</>
84
+
85
+
{repostsAturis.map((m) => (
86
+
<NotificationItem key={m} notification={m} />
87
+
))}
88
+
89
+
{hasNextPage && (
90
+
<button
91
+
onClick={() => fetchNextPage()}
92
+
disabled={isFetchingNextPage}
93
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50"
94
+
>
95
+
{isFetchingNextPage ? "Loading..." : "Load More"}
96
+
</button>
97
+
)}
98
+
</>
99
+
);
100
+
}
+98
-92
src/routes/profile.$did/post.$rkey.tsx
···
1
import { AtUri } from "@atproto/api";
2
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
3
-
import { createFileRoute, Outlet } from "@tanstack/react-router";
4
import { useAtom } from "jotai";
5
import React, { useLayoutEffect } from "react";
6
···
52
nopics?: boolean;
53
lightboxCallback?: (d: LightboxProps) => void;
54
}) {
0
0
0
55
//const { get, set } = usePersistentStore();
56
const queryClient = useQueryClient();
57
// const [resolvedDid, setResolvedDid] = React.useState<string | null>(null);
···
190
data: identity,
191
isLoading: isIdentityLoading,
192
error: identityError,
193
-
} = useQueryIdentity(did);
194
195
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
196
197
const atUri = React.useMemo(
198
() =>
199
-
resolvedDid
200
? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}`
201
: undefined,
202
-
[resolvedDid, rkey]
203
);
204
205
-
const { data: mainPost } = useQueryPost(atUri);
206
207
console.log("atUri",atUri)
208
···
215
);
216
217
// @ts-expect-error i hate overloads
218
-
const { data: links } = useQueryConstellation(atUri?{
219
method: "/links/all",
220
target: atUri,
221
} : {
···
248
}, [links]);
249
250
const { data: opreplies } = useQueryConstellation(
251
-
!!opdid && replyCount && replyCount >= 25
252
? {
253
method: "/links",
254
target: atUri,
···
289
path: ".reply.parent.uri",
290
}
291
),
292
-
enabled: !!atUri,
293
});
294
295
const {
···
371
const [layoutReady, setLayoutReady] = React.useState(false);
372
373
useLayoutEffect(() => {
0
374
if (parents.length > 0 && !layoutReady && mainPostRef.current) {
375
const mainPostElement = mainPostRef.current;
376
···
389
// eslint-disable-next-line react-hooks/set-state-in-effect
390
setLayoutReady(true);
391
}
392
-
}, [parents, layoutReady]);
393
394
395
const [slingshoturl] = useAtom(slingshotURLAtom)
396
397
React.useEffect(() => {
398
-
if (parentsLoading) {
399
setLayoutReady(false);
400
}
401
···
403
setLayoutReady(true);
404
hasPerformedInitialLayout.current = true;
405
}
406
-
}, [parentsLoading, mainPost]);
407
408
React.useEffect(() => {
409
if (!mainPost?.value?.reply?.parent?.uri) {
···
444
return () => {
445
ignore = true;
446
};
447
-
}, [mainPost, queryClient]);
448
449
-
if (!did || !rkey) return <div>Invalid post URI</div>;
450
-
if (isIdentityLoading) return <div>Resolving handle...</div>;
451
-
if (identityError)
452
return <div style={{ color: "red" }}>{identityError.message}</div>;
453
-
if (!atUri) return <div>Could not construct post URI.</div>;
454
455
return (
456
<>
457
<Outlet />
458
-
<Header
459
-
title={`Post`}
460
-
backButtonCallback={() => {
461
-
if (window.history.length > 1) {
462
-
window.history.back();
463
-
} else {
464
-
window.location.assign("/");
465
-
}
466
-
}}
467
-
/>
0
468
469
-
{parentsLoading && (
470
-
<div className="text-center text-gray-500 dark:text-gray-400 flex flex-row">
471
-
<div className="ml-4 w-[42px] flex justify-center">
472
-
<div
473
-
style={{ width: 2, height: "100%", opacity: 0.5 }}
474
-
className="bg-gray-500 dark:bg-gray-400"
475
-
></div>
0
0
476
</div>
477
-
Loading conversation...
0
0
0
0
0
0
0
0
0
0
0
0
478
</div>
479
-
)}
480
-
481
-
{/* we should use the reply lines here thats provided by UPR*/}
482
-
<div style={{ maxWidth: 600, padding: 0 }}>
483
-
{parents.map((parent, index) => (
484
<UniversalPostRendererATURILoader
485
-
key={parent.uri}
486
-
atUri={parent.uri}
487
-
topReplyLine={index > 0}
488
-
bottomReplyLine={true}
489
-
bottomBorder={false}
490
/>
491
-
))}
492
-
</div>
493
-
<div ref={mainPostRef}>
494
-
<UniversalPostRendererATURILoader
495
-
atUri={atUri}
496
-
detailed={true}
497
-
topReplyLine={parentsLoading || parents.length > 0}
498
-
nopics={!!nopics}
499
-
lightboxCallback={lightboxCallback}
500
-
/>
501
-
</div>
502
-
<div
503
-
style={{
504
-
maxWidth: 600,
505
-
//margin: "0px auto 0",
506
-
padding: 0,
507
-
minHeight: "80dvh",
508
-
paddingBottom: "20dvh",
509
-
}}
510
-
>
511
<div
512
-
className="text-gray-500 dark:text-gray-400 text-sm font-bold"
513
style={{
514
-
fontSize: 18,
515
-
margin: "12px 16px 12px 16px",
516
-
fontWeight: 600,
0
0
517
}}
518
>
519
-
Replies
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
520
</div>
521
-
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
522
-
{replyAturis.length > 0 &&
523
-
replyAturis.map((reply) => {
524
-
//const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`;
525
-
return (
526
-
<UniversalPostRendererATURILoader
527
-
key={reply}
528
-
atUri={reply}
529
-
maxReplies={4}
530
-
/>
531
-
);
532
-
})}
533
-
{hasNextPage && (
534
-
<button
535
-
onClick={() => fetchNextPage()}
536
-
disabled={isFetchingNextPage}
537
-
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50"
538
-
>
539
-
{isFetchingNextPage ? "Loading..." : "Load More"}
540
-
</button>
541
-
)}
542
-
</div>
543
-
</div>
544
</>
545
);
546
}
···
1
import { AtUri } from "@atproto/api";
2
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
3
+
import { createFileRoute, Outlet, useMatchRoute } from "@tanstack/react-router";
4
import { useAtom } from "jotai";
5
import React, { useLayoutEffect } from "react";
6
···
52
nopics?: boolean;
53
lightboxCallback?: (d: LightboxProps) => void;
54
}) {
55
+
const matchRoute = useMatchRoute()
56
+
const showMainPostRoute = !!matchRoute({ to: '/profile/$did/post/$rkey' }) || !!matchRoute({ to: '/profile/$did/post/$rkey/image/$i' })
57
+
58
//const { get, set } = usePersistentStore();
59
const queryClient = useQueryClient();
60
// const [resolvedDid, setResolvedDid] = React.useState<string | null>(null);
···
193
data: identity,
194
isLoading: isIdentityLoading,
195
error: identityError,
196
+
} = useQueryIdentity(showMainPostRoute ? did : undefined);
197
198
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
199
200
const atUri = React.useMemo(
201
() =>
202
+
resolvedDid && showMainPostRoute
203
? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}`
204
: undefined,
205
+
[resolvedDid, rkey, showMainPostRoute]
206
);
207
208
+
const { data: mainPost } = useQueryPost(showMainPostRoute ? atUri : undefined);
209
210
console.log("atUri",atUri)
211
···
218
);
219
220
// @ts-expect-error i hate overloads
221
+
const { data: links } = useQueryConstellation(atUri&&showMainPostRoute?{
222
method: "/links/all",
223
target: atUri,
224
} : {
···
251
}, [links]);
252
253
const { data: opreplies } = useQueryConstellation(
254
+
showMainPostRoute && !!opdid && replyCount && replyCount >= 25
255
? {
256
method: "/links",
257
target: atUri,
···
292
path: ".reply.parent.uri",
293
}
294
),
295
+
enabled: !!atUri && showMainPostRoute,
296
});
297
298
const {
···
374
const [layoutReady, setLayoutReady] = React.useState(false);
375
376
useLayoutEffect(() => {
377
+
if (!showMainPostRoute) return
378
if (parents.length > 0 && !layoutReady && mainPostRef.current) {
379
const mainPostElement = mainPostRef.current;
380
···
393
// eslint-disable-next-line react-hooks/set-state-in-effect
394
setLayoutReady(true);
395
}
396
+
}, [parents, layoutReady, showMainPostRoute]);
397
398
399
const [slingshoturl] = useAtom(slingshotURLAtom)
400
401
React.useEffect(() => {
402
+
if (parentsLoading || !showMainPostRoute) {
403
setLayoutReady(false);
404
}
405
···
407
setLayoutReady(true);
408
hasPerformedInitialLayout.current = true;
409
}
410
+
}, [parentsLoading, mainPost, showMainPostRoute]);
411
412
React.useEffect(() => {
413
if (!mainPost?.value?.reply?.parent?.uri) {
···
448
return () => {
449
ignore = true;
450
};
451
+
}, [mainPost, queryClient, slingshoturl]);
452
453
+
if ((!did || !rkey) && showMainPostRoute) return <div>Invalid post URI</div>;
454
+
if (isIdentityLoading && showMainPostRoute) return <div>Resolving handle...</div>;
455
+
if (identityError && showMainPostRoute)
456
return <div style={{ color: "red" }}>{identityError.message}</div>;
457
+
if (!atUri && showMainPostRoute) return <div>Could not construct post URI.</div>;
458
459
return (
460
<>
461
<Outlet />
462
+
{showMainPostRoute && (<>
463
+
<Header
464
+
title={`Post`}
465
+
backButtonCallback={() => {
466
+
if (window.history.length > 1) {
467
+
window.history.back();
468
+
} else {
469
+
window.location.assign("/");
470
+
}
471
+
}}
472
+
/>
473
474
+
{parentsLoading && (
475
+
<div className="text-center text-gray-500 dark:text-gray-400 flex flex-row">
476
+
<div className="ml-4 w-[42px] flex justify-center">
477
+
<div
478
+
style={{ width: 2, height: "100%", opacity: 0.5 }}
479
+
className="bg-gray-500 dark:bg-gray-400"
480
+
></div>
481
+
</div>
482
+
Loading conversation...
483
</div>
484
+
)}
485
+
486
+
{/* we should use the reply lines here thats provided by UPR*/}
487
+
<div style={{ maxWidth: 600, padding: 0 }}>
488
+
{parents.map((parent, index) => (
489
+
<UniversalPostRendererATURILoader
490
+
key={parent.uri}
491
+
atUri={parent.uri}
492
+
topReplyLine={index > 0}
493
+
bottomReplyLine={true}
494
+
bottomBorder={false}
495
+
/>
496
+
))}
497
</div>
498
+
<div ref={mainPostRef}>
0
0
0
0
499
<UniversalPostRendererATURILoader
500
+
atUri={atUri!}
501
+
detailed={true}
502
+
topReplyLine={parentsLoading || parents.length > 0}
503
+
nopics={!!nopics}
504
+
lightboxCallback={lightboxCallback}
505
/>
506
+
</div>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
507
<div
0
508
style={{
509
+
maxWidth: 600,
510
+
//margin: "0px auto 0",
511
+
padding: 0,
512
+
minHeight: "80dvh",
513
+
paddingBottom: "20dvh",
514
}}
515
>
516
+
<div
517
+
className="text-gray-500 dark:text-gray-400 text-sm font-bold"
518
+
style={{
519
+
fontSize: 18,
520
+
margin: "12px 16px 12px 16px",
521
+
fontWeight: 600,
522
+
}}
523
+
>
524
+
Replies
525
+
</div>
526
+
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
527
+
{replyAturis.length > 0 &&
528
+
replyAturis.map((reply) => {
529
+
//const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`;
530
+
return (
531
+
<UniversalPostRendererATURILoader
532
+
key={reply}
533
+
atUri={reply}
534
+
maxReplies={4}
535
+
/>
536
+
);
537
+
})}
538
+
{hasNextPage && (
539
+
<button
540
+
onClick={() => fetchNextPage()}
541
+
disabled={isFetchingNextPage}
542
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50"
543
+
>
544
+
{isFetchingNextPage ? "Loading..." : "Load More"}
545
+
</button>
546
+
)}
547
+
</div>
548
</div>
549
+
</>)}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
550
</>
551
);
552
}
+5
-5
src/utils/useQuery.ts
···
352
);
353
}
354
355
-
type linksRecord = {
356
did: string;
357
collection: string;
358
rkey: string;
···
634
collection: string
635
path: string
636
}) {
637
-
console.log(
638
-
'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks',
639
-
query,
640
-
)
641
642
return infiniteQueryOptions({
643
enabled: !!query?.target,
···
352
);
353
}
354
355
+
export type linksRecord = {
356
did: string;
357
collection: string;
358
rkey: string;
···
634
collection: string
635
path: string
636
}) {
637
+
// console.log(
638
+
// 'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks',
639
+
// query,
640
+
// )
641
642
return infiniteQueryOptions({
643
enabled: !!query?.target,