+1
-1
README.md
+1
-1
README.md
···
8
8
## running dev and build
9
9
in the `vite.config.ts` file you should change these values
10
10
```ts
11
-
const PROD_URL = "https://reddwarf.whey.party"
11
+
const PROD_URL = "https://reddwarf.app"
12
12
const DEV_URL = "https://local3768forumtest.whey.party"
13
13
```
14
14
the PROD_URL is what will compile your oauth client metadata so it is very important to change that. same for DEV_URL if you are using a tunnel for dev work
+2
-1
src/components/Import.tsx
+2
-1
src/components/Import.tsx
···
46
46
// parse text
47
47
/**
48
48
* text might be
49
-
* 1. bsky dot app url (deer.social, reddwarf.whey.party, main.bsky.dev, catskys.social) (reddwarf link segments might be uri encoded,)
49
+
* 1. bsky dot app url (reddwarf link segments might be uri encoded,)
50
50
* 2. aturi
51
51
* 3. plain handle
52
52
* 4. plain did
···
60
60
"social.daniela.lol",
61
61
"deer.social",
62
62
"reddwarf.whey.party",
63
+
"reddwarf.app",
63
64
"main.bsky.dev",
64
65
"catsky.social",
65
66
"blacksky.community",
+32
-6
src/components/InfiniteCustomFeed.tsx
+32
-6
src/components/InfiniteCustomFeed.tsx
···
1
+
import { useQueryClient } from "@tanstack/react-query";
1
2
import * as React from "react";
2
3
3
4
//import { useInView } from "react-intersection-observer";
···
37
38
isFetchingNextPage,
38
39
refetch,
39
40
isRefetching,
41
+
queryKey,
40
42
} = useInfiniteQueryFeedSkeleton({
41
43
feedUri: feedUri,
42
44
agent: agent ?? undefined,
···
44
46
pdsUrl: pdsUrl,
45
47
feedServiceDid: feedServiceDid,
46
48
});
49
+
const queryClient = useQueryClient();
50
+
47
51
48
52
const handleRefresh = () => {
53
+
queryClient.removeQueries({queryKey: queryKey});
54
+
//queryClient.invalidateQueries(["infinite-feed", feedUri] as const);
49
55
refetch();
50
56
};
51
57
58
+
const allPosts = React.useMemo(() => {
59
+
const flattenedPosts = data?.pages.flatMap((page) => page?.feed) ?? [];
60
+
61
+
const seenUris = new Set<string>();
62
+
63
+
return flattenedPosts.filter((item) => {
64
+
if (!item?.post) return false;
65
+
66
+
if (seenUris.has(item.post)) {
67
+
return false;
68
+
}
69
+
70
+
seenUris.add(item.post);
71
+
72
+
return true;
73
+
});
74
+
}, [data]);
75
+
52
76
//const { ref, inView } = useInView();
53
77
54
78
// React.useEffect(() => {
···
67
91
);
68
92
}
69
93
70
-
const allPosts =
71
-
data?.pages.flatMap((page) => {
72
-
if (page) return page.feed;
73
-
}) ?? [];
94
+
// const allPosts =
95
+
// data?.pages.flatMap((page) => {
96
+
// if (page) return page.feed;
97
+
// }) ?? [];
74
98
75
99
if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) {
76
100
return (
···
116
140
className="sticky lg:bottom-4 bottom-22 ml-4 w-[42px] h-[42px] z-10 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:dark:bg-gray-900 disabled:bg-gray-100 disabled:cursor-not-allowed"
117
141
aria-label="Refresh feed"
118
142
>
119
-
<RefreshIcon className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`} />
143
+
<RefreshIcon
144
+
className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`}
145
+
/>
120
146
</button>
121
147
</>
122
148
);
···
139
165
d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"
140
166
></path>
141
167
</svg>
142
-
);
168
+
);
+1
src/routes/index.tsx
+1
src/routes/index.tsx
+14
-7
src/routes/profile.$did/index.tsx
+14
-7
src/routes/profile.$did/index.tsx
···
10
10
UniversalPostRendererATURILoader,
11
11
} from "~/components/UniversalPostRenderer";
12
12
import { useAuth } from "~/providers/UnifiedAuthProvider";
13
-
import { imgCDNAtom } from "~/utils/atoms";
13
+
import { aturiListServiceAtom, imgCDNAtom } from "~/utils/atoms";
14
14
import {
15
15
toggleFollow,
16
16
useGetFollowState,
17
17
useGetOneToOneState,
18
18
} from "~/utils/followState";
19
19
import {
20
-
useInfiniteQueryAuthorFeed,
20
+
useInfiniteQueryAturiList,
21
21
useQueryIdentity,
22
22
useQueryProfile,
23
23
} from "~/utils/useQuery";
···
29
29
function ProfileComponent() {
30
30
// booo bad this is not always the did it might be a handle, use identity.did instead
31
31
const { did } = Route.useParams();
32
-
const navigate = useNavigate();
32
+
//const navigate = useNavigate();
33
33
const queryClient = useQueryClient();
34
34
const {
35
35
data: identity,
···
39
39
40
40
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
41
41
const resolvedHandle = did.startsWith("did:") ? identity?.handle : did;
42
-
const pdsUrl = identity?.pds;
42
+
//const pdsUrl = identity?.pds;
43
43
44
44
const profileUri = resolvedDid
45
45
? `at://${resolvedDid}/app.bsky.actor.profile/self`
···
47
47
const { data: profileRecord } = useQueryProfile(profileUri);
48
48
const profile = profileRecord?.value;
49
49
50
+
const [aturilistservice] = useAtom(aturiListServiceAtom);
51
+
50
52
const {
51
53
data: postsData,
52
54
fetchNextPage,
53
55
hasNextPage,
54
56
isFetchingNextPage,
55
57
isLoading: arePostsLoading,
56
-
} = useInfiniteQueryAuthorFeed(resolvedDid, pdsUrl);
58
+
} = useInfiniteQueryAturiList({
59
+
aturilistservice: aturilistservice,
60
+
did: resolvedDid,
61
+
collection: "app.bsky.feed.post",
62
+
reverse: true
63
+
});
57
64
58
65
React.useEffect(() => {
59
66
if (postsData) {
60
67
postsData.pages.forEach((page) => {
61
-
page.records.forEach((record) => {
68
+
page.forEach((record) => {
62
69
if (!queryClient.getQueryData(["post", record.uri])) {
63
70
queryClient.setQueryData(["post", record.uri], record);
64
71
}
···
68
75
}, [postsData, queryClient]);
69
76
70
77
const posts = React.useMemo(
71
-
() => postsData?.pages.flatMap((page) => page.records) ?? [],
78
+
() => postsData?.pages.flatMap((page) => page) ?? [],
72
79
[postsData]
73
80
);
74
81
+8
src/routes/settings.tsx
+8
src/routes/settings.tsx
···
5
5
import { Header } from "~/components/Header";
6
6
import Login from "~/components/Login";
7
7
import {
8
+
aturiListServiceAtom,
8
9
constellationURLAtom,
10
+
defaultaturilistservice,
9
11
defaultconstellationURL,
10
12
defaulthue,
11
13
defaultImgCDN,
···
51
53
title={"Slingshot"}
52
54
description={"Customize the Slingshot instance to be used by Red Dwarf"}
53
55
init={defaultslingshotURL}
56
+
/>
57
+
<TextInputSetting
58
+
atom={aturiListServiceAtom}
59
+
title={"AtUriListService"}
60
+
description={"Customize the AtUriListService instance to be used by Red Dwarf"}
61
+
init={defaultaturilistservice}
54
62
/>
55
63
<TextInputSetting
56
64
atom={imgCDNAtom}
+5
src/utils/atoms.ts
+5
src/utils/atoms.ts
···
32
32
"slingshotURL",
33
33
defaultslingshotURL
34
34
);
35
+
export const defaultaturilistservice = "aturilistservice.reddwarf.app";
36
+
export const aturiListServiceAtom = atomWithStorage<string>(
37
+
"aturilistservice",
38
+
defaultaturilistservice
39
+
);
35
40
export const defaultImgCDN = "cdn.bsky.app";
36
41
export const imgCDNAtom = atomWithStorage<string>("imgcdnurl", defaultImgCDN);
37
42
export const defaultVideoCDN = "video.bsky.app";
+82
-2
src/utils/useQuery.ts
+82
-2
src/utils/useQuery.ts
···
565
565
});
566
566
}
567
567
568
+
export const ATURI_PAGE_LIMIT = 100;
569
+
570
+
export interface AturiDirectoryAturisItem {
571
+
uri: string;
572
+
cid: string;
573
+
rkey: string;
574
+
}
575
+
576
+
export type AturiDirectoryAturis = AturiDirectoryAturisItem[];
577
+
578
+
export function constructAturiListQuery(aturilistservice: string, did: string, collection: string, reverse?: boolean) {
579
+
return queryOptions({
580
+
// A unique key for this query, including all parameters that affect the data.
581
+
queryKey: ["aturiList", did, collection, { reverse }],
582
+
583
+
// The function that fetches the data.
584
+
queryFn: async ({ pageParam }: QueryFunctionContext) => {
585
+
const cursor = pageParam as string | undefined;
586
+
587
+
// Use URLSearchParams for safe and clean URL construction.
588
+
const params = new URLSearchParams({
589
+
did,
590
+
collection,
591
+
});
592
+
593
+
if (cursor) {
594
+
params.set("cursor", cursor);
595
+
}
596
+
597
+
// Add the reverse parameter if it's true
598
+
if (reverse) {
599
+
params.set("reverse", "true");
600
+
}
601
+
602
+
const url = `https://${aturilistservice}/aturis?${params.toString()}`;
603
+
604
+
const res = await fetch(url);
605
+
if (!res.ok) {
606
+
// You can add more specific error handling here
607
+
throw new Error(`Failed to fetch AT-URI list for ${did}`);
608
+
}
609
+
610
+
return res.json() as Promise<AturiDirectoryAturis>;
611
+
},
612
+
});
613
+
}
614
+
615
+
export function useInfiniteQueryAturiList({aturilistservice, did, collection, reverse}:{aturilistservice: string, did: string | undefined, collection: string | undefined, reverse?: boolean}) {
616
+
// We only enable the query if both `did` and `collection` are provided.
617
+
const isEnabled = !!did && !!collection;
618
+
619
+
const { queryKey, queryFn } = constructAturiListQuery(aturilistservice, did!, collection!, reverse);
620
+
621
+
return useInfiniteQuery({
622
+
queryKey,
623
+
queryFn,
624
+
initialPageParam: undefined as never, // ???? what is this shit
625
+
626
+
// @ts-expect-error i wouldve used as null | undefined, anyways
627
+
getNextPageParam: (lastPage: AturiDirectoryAturis) => {
628
+
// If the last page returned no records, we're at the end.
629
+
if (!lastPage || lastPage.length === 0) {
630
+
return undefined;
631
+
}
632
+
633
+
// If the number of records is less than our page limit, it must be the last page.
634
+
if (lastPage.length < ATURI_PAGE_LIMIT) {
635
+
return undefined;
636
+
}
637
+
638
+
// The cursor for the next page is the `rkey` of the last item we received.
639
+
const lastItem = lastPage[lastPage.length - 1];
640
+
return lastItem.rkey;
641
+
},
642
+
643
+
enabled: isEnabled,
644
+
});
645
+
}
646
+
647
+
568
648
type FeedSkeletonPage = ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
569
649
570
650
export function constructInfiniteFeedSkeletonQuery(options: {
···
615
695
}) {
616
696
const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options);
617
697
618
-
return useInfiniteQuery({
698
+
return {...useInfiniteQuery({
619
699
queryKey,
620
700
queryFn,
621
701
initialPageParam: undefined as never,
···
623
703
staleTime: Infinity,
624
704
refetchOnWindowFocus: false,
625
705
enabled: !!options.feedUri && (options.isAuthed ? !!options.agent && !!options.pdsUrl && !!options.feedServiceDid : true),
626
-
});
706
+
}), queryKey: queryKey};
627
707
}
628
708
629
709
+1
-1
vite.config.ts
+1
-1
vite.config.ts