tangled
alpha
login
or
join now
whey.party
/
red-dwarf
82
fork
atom
an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
82
fork
atom
overview
issues
25
pulls
pipelines
background like mutation
rimar1337
4 months ago
2f1eae19
24efdc83
+266
-64
6 changed files
expand all
collapse all
unified
split
src
components
UniversalPostRenderer.tsx
providers
LikeMutationQueueProvider.tsx
routes
__root.tsx
profile.$did
index.tsx
utils
atoms.ts
likeMutationQueue.ts
+16
-39
src/components/UniversalPostRenderer.tsx
···
10
composerAtom,
11
constellationURLAtom,
12
imgCDNAtom,
13
-
likedPostsAtom,
14
} from "~/utils/atoms";
15
import { useHydratedEmbed } from "~/utils/useHydrated";
16
import {
···
38
feedviewpost?: boolean;
39
repostedby?: string;
40
style?: React.CSSProperties;
41
-
ref?: React.Ref<HTMLDivElement>;
42
dataIndexPropPass?: number;
43
nopics?: boolean;
44
concise?: boolean;
···
659
feedviewpost?: boolean;
660
repostedby?: string;
661
style?: React.CSSProperties;
662
-
ref?: React.Ref<HTMLDivElement>;
663
dataIndexPropPass?: number;
664
nopics?: boolean;
665
concise?: boolean;
···
1206
import { useAuth } from "~/providers/UnifiedAuthProvider";
1207
import { FeedItemRenderAturiLoader, FollowButton, Mutual } from "~/routes/profile.$did";
1208
import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i";
0
1209
// import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed";
1210
// import type {
1211
// ViewRecord,
···
1358
depth?: number;
1359
repostedby?: string;
1360
style?: React.CSSProperties;
1361
-
ref?: React.Ref<HTMLDivElement>;
1362
dataIndexPropPass?: number;
1363
nopics?: boolean;
1364
concise?: boolean;
···
1367
}) {
1368
const parsed = new AtUri(post.uri);
1369
const navigate = useNavigate();
1370
-
const [likedPosts, setLikedPosts] = useAtom(likedPostsAtom);
1371
const [hasRetweeted, setHasRetweeted] = useState<boolean>(
1372
post.viewer?.repost ? true : false
1373
);
1374
-
const [hasLiked, setHasLiked] = useState<boolean>(
1375
-
post.uri in likedPosts || post.viewer?.like ? true : false
1376
-
);
1377
const [, setComposerPost] = useAtom(composerAtom);
1378
const { agent } = useAuth();
1379
-
const [likeUri, setLikeUri] = useState<string | undefined>(post.viewer?.like);
1380
const [retweetUri, setRetweetUri] = useState<string | undefined>(
1381
post.viewer?.repost
1382
);
1383
-
1384
-
const likeOrUnlikePost = async () => {
1385
-
const newLikedPosts = { ...likedPosts };
1386
-
if (!agent) {
1387
-
console.error("Agent is null or undefined");
1388
-
return;
1389
-
}
1390
-
if (hasLiked) {
1391
-
if (post.uri in likedPosts) {
1392
-
const likeUri = likedPosts[post.uri];
1393
-
setLikeUri(likeUri);
1394
-
}
1395
-
if (likeUri) {
1396
-
await agent.deleteLike(likeUri);
1397
-
setHasLiked(false);
1398
-
delete newLikedPosts[post.uri];
1399
-
}
1400
-
} else {
1401
-
const { uri } = await agent.like(post.uri, post.cid);
1402
-
setLikeUri(uri);
1403
-
setHasLiked(true);
1404
-
newLikedPosts[post.uri] = uri;
1405
-
}
1406
-
setLikedPosts(newLikedPosts);
1407
-
};
1408
1409
const repostOrUnrepostPost = async () => {
1410
if (!agent) {
···
1442
const isMainItem = false;
1443
const setMainItem = (any: any) => {};
1444
// eslint-disable-next-line react-hooks/refs
1445
-
console.log("Received ref in UniversalPostRenderer:", ref);
1446
return (
1447
<div ref={ref} style={style} data-index={dataIndexPropPass}>
1448
<div
···
1919
</DropdownMenu.Root>
1920
<HitSlopButton
1921
onClick={() => {
1922
-
likeOrUnlikePost();
1923
}}
1924
style={{
1925
...btnstyle,
1926
-
...(hasLiked ? { color: "#EC4899" } : {}),
1927
}}
1928
>
1929
-
{hasLiked ? <MdiCardsHeart /> : <MdiCardsHeartOutline />}
1930
-
{(post.likeCount || 0) + (hasLiked ? 1 : 0)}
1931
</HitSlopButton>
1932
<div style={{ display: "flex", gap: 8 }}>
1933
<HitSlopButton
···
10
composerAtom,
11
constellationURLAtom,
12
imgCDNAtom,
0
13
} from "~/utils/atoms";
14
import { useHydratedEmbed } from "~/utils/useHydrated";
15
import {
···
37
feedviewpost?: boolean;
38
repostedby?: string;
39
style?: React.CSSProperties;
40
+
ref?: React.RefObject<HTMLDivElement>;
41
dataIndexPropPass?: number;
42
nopics?: boolean;
43
concise?: boolean;
···
658
feedviewpost?: boolean;
659
repostedby?: string;
660
style?: React.CSSProperties;
661
+
ref?: React.RefObject<HTMLDivElement>;
662
dataIndexPropPass?: number;
663
nopics?: boolean;
664
concise?: boolean;
···
1205
import { useAuth } from "~/providers/UnifiedAuthProvider";
1206
import { FeedItemRenderAturiLoader, FollowButton, Mutual } from "~/routes/profile.$did";
1207
import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i";
1208
+
import { useFastLike } from "~/utils/likeMutationQueue";
1209
// import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed";
1210
// import type {
1211
// ViewRecord,
···
1358
depth?: number;
1359
repostedby?: string;
1360
style?: React.CSSProperties;
1361
+
ref?: React.RefObject<HTMLDivElement>;
1362
dataIndexPropPass?: number;
1363
nopics?: boolean;
1364
concise?: boolean;
···
1367
}) {
1368
const parsed = new AtUri(post.uri);
1369
const navigate = useNavigate();
0
1370
const [hasRetweeted, setHasRetweeted] = useState<boolean>(
1371
post.viewer?.repost ? true : false
1372
);
0
0
0
1373
const [, setComposerPost] = useAtom(composerAtom);
1374
const { agent } = useAuth();
0
1375
const [retweetUri, setRetweetUri] = useState<string | undefined>(
1376
post.viewer?.repost
1377
);
1378
+
const { liked, toggle, backfill } = useFastLike(post.uri, post.cid);
1379
+
// const bovref = useBackfillOnView(post.uri, post.cid);
1380
+
// React.useLayoutEffect(()=>{
1381
+
// if (expanded && !isQuote) {
1382
+
// backfill();
1383
+
// }
1384
+
// },[backfill, expanded, isQuote])
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1385
1386
const repostOrUnrepostPost = async () => {
1387
if (!agent) {
···
1419
const isMainItem = false;
1420
const setMainItem = (any: any) => {};
1421
// eslint-disable-next-line react-hooks/refs
1422
+
//console.log("Received ref in UniversalPostRenderer:", usedref);
1423
return (
1424
<div ref={ref} style={style} data-index={dataIndexPropPass}>
1425
<div
···
1896
</DropdownMenu.Root>
1897
<HitSlopButton
1898
onClick={() => {
1899
+
toggle();
1900
}}
1901
style={{
1902
...btnstyle,
1903
+
...(liked ? { color: "#EC4899" } : {}),
1904
}}
1905
>
1906
+
{liked ? <MdiCardsHeart /> : <MdiCardsHeartOutline />}
1907
+
{(post.likeCount || 0) + (liked ? 1 : 0)}
1908
</HitSlopButton>
1909
<div style={{ display: "flex", gap: 8 }}>
1910
<HitSlopButton
+157
src/providers/LikeMutationQueueProvider.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
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { AtUri } from "@atproto/api";
2
+
import { TID } from "@atproto/common-web";
3
+
import { useQueryClient } from "@tanstack/react-query";
4
+
import { useAtom } from "jotai";
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
+
11
+
export type LikeRecord = { uri: string; target: string; cid: string };
12
+
export type LikeMutation = { type: 'like'; target: string; cid: string };
13
+
export type UnlikeMutation = { type: 'unlike'; likeRecordUri: string; target: string, originalRecord: LikeRecord };
14
+
export type Mutation = LikeMutation | UnlikeMutation;
15
+
16
+
interface LikeMutationQueueContextType {
17
+
fastState: (target: string) => LikeRecord | null | undefined;
18
+
fastToggle: (target:string, cid:string) => void;
19
+
backfillState: (target: string, user: string) => Promise<void>;
20
+
}
21
+
22
+
const LikeMutationQueueContext = createContext<LikeMutationQueueContextType | undefined>(undefined);
23
+
24
+
export function LikeMutationQueueProvider({ children }: { children: React.ReactNode }) {
25
+
const { agent } = useAuth();
26
+
const queryClient = useQueryClient();
27
+
const [likedPosts, setLikedPosts] = useAtom(internalLikedPostsAtom);
28
+
const [constellationurl] = useAtom(constellationURLAtom);
29
+
30
+
const likedPostsRef = useRef(likedPosts);
31
+
useEffect(() => {
32
+
likedPostsRef.current = likedPosts;
33
+
}, [likedPosts]);
34
+
35
+
const queueRef = useRef<Mutation[]>([]);
36
+
const runningRef = useRef(false);
37
+
38
+
const fastState = (target: string) => likedPosts[target];
39
+
40
+
const setFastState = useCallback(
41
+
(target: string, record: LikeRecord | null) =>
42
+
setLikedPosts((prev) => ({ ...prev, [target]: record })),
43
+
[setLikedPosts]
44
+
);
45
+
46
+
const enqueue = (mutation: Mutation) => queueRef.current.push(mutation);
47
+
48
+
const fastToggle = useCallback((target: string, cid: string) => {
49
+
const likedRecord = likedPostsRef.current[target];
50
+
51
+
if (likedRecord) {
52
+
setFastState(target, null);
53
+
if (likedRecord.uri !== 'pending') {
54
+
enqueue({ type: "unlike", likeRecordUri: likedRecord.uri, target, originalRecord: likedRecord });
55
+
}
56
+
} else {
57
+
setFastState(target, { uri: "pending", target, cid });
58
+
enqueue({ type: "like", target, cid });
59
+
}
60
+
}, [setFastState]);
61
+
62
+
/**
63
+
*
64
+
* @deprecated dont use it yet, will cause infinite rerenders
65
+
*/
66
+
const backfillState = async (target: string, user: string) => {
67
+
const query = constructConstellationQuery({
68
+
constellation: constellationurl,
69
+
method: "/links",
70
+
target,
71
+
collection: "app.bsky.feed.like",
72
+
path: ".subject.uri",
73
+
dids: [user],
74
+
});
75
+
const data = await queryClient.fetchQuery(query);
76
+
const likes = (data as linksRecordsResponse)?.linking_records?.slice(0, 50) ?? [];
77
+
const found = likes.find((r) => r.did === user);
78
+
if (found) {
79
+
const uri = `at://${found.did}/${found.collection}/${found.rkey}`;
80
+
const ciddata = await queryClient.fetchQuery(
81
+
constructArbitraryQuery(uri)
82
+
);
83
+
if (ciddata?.cid)
84
+
setFastState(target, { uri, target, cid: ciddata?.cid });
85
+
} else {
86
+
setFastState(target, null);
87
+
}
88
+
};
89
+
90
+
91
+
useEffect(() => {
92
+
if (!agent?.did) return;
93
+
94
+
const processQueue = async () => {
95
+
if (runningRef.current || queueRef.current.length === 0) return;
96
+
runningRef.current = true;
97
+
98
+
while (queueRef.current.length > 0) {
99
+
const mutation = queueRef.current.shift()!;
100
+
try {
101
+
if (mutation.type === "like") {
102
+
const newRecord = {
103
+
repo: agent.did!,
104
+
collection: "app.bsky.feed.like",
105
+
rkey: TID.next().toString(),
106
+
record: {
107
+
$type: "app.bsky.feed.like",
108
+
subject: { uri: mutation.target, cid: mutation.cid },
109
+
createdAt: new Date().toISOString(),
110
+
},
111
+
};
112
+
const response = await agent.com.atproto.repo.createRecord(newRecord);
113
+
if (!response.success) throw new Error("createRecord failed");
114
+
115
+
const uri = `at://${agent.did}/${newRecord.collection}/${newRecord.rkey}`;
116
+
setFastState(mutation.target, {
117
+
uri,
118
+
target: mutation.target,
119
+
cid: mutation.cid,
120
+
});
121
+
} else if (mutation.type === "unlike") {
122
+
const aturi = new AtUri(mutation.likeRecordUri);
123
+
await agent.com.atproto.repo.deleteRecord({ repo: agent.did!, collection: aturi.collection, rkey: aturi.rkey });
124
+
setFastState(mutation.target, null);
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') {
131
+
setFastState(mutation.target, mutation.originalRecord);
132
+
}
133
+
}
134
+
}
135
+
runningRef.current = false;
136
+
};
137
+
138
+
const interval = setInterval(processQueue, 1000);
139
+
return () => clearInterval(interval);
140
+
}, [agent, setFastState]);
141
+
142
+
const value = { fastState, fastToggle, backfillState };
143
+
144
+
return (
145
+
<LikeMutationQueueContext value={value}>
146
+
{children}
147
+
</LikeMutationQueueContext>
148
+
);
149
+
}
150
+
151
+
export function useLikeMutationQueue() {
152
+
const context = use(LikeMutationQueueContext);
153
+
if (context === undefined) {
154
+
throw new Error('useLikeMutationQueue must be used within a LikeMutationQueueProvider');
155
+
}
156
+
return context;
157
+
}
+8
-5
src/routes/__root.tsx
···
22
import Login from "~/components/Login";
23
import { NotFound } from "~/components/NotFound";
24
import { FluentEmojiHighContrastGlowingStar } from "~/components/Star";
0
25
import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider";
26
import { composerAtom, hueAtom, useAtomCssVar } from "~/utils/atoms";
27
import { seo } from "~/utils/seo";
···
79
function RootComponent() {
80
return (
81
<UnifiedAuthProvider>
82
-
<RootDocument>
83
-
<KeepAliveProvider>
84
-
<KeepAliveOutlet />
85
-
</KeepAliveProvider>
86
-
</RootDocument>
0
0
87
</UnifiedAuthProvider>
88
);
89
}
···
22
import Login from "~/components/Login";
23
import { NotFound } from "~/components/NotFound";
24
import { FluentEmojiHighContrastGlowingStar } from "~/components/Star";
25
+
import { LikeMutationQueueProvider } from "~/providers/LikeMutationQueueProvider";
26
import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider";
27
import { composerAtom, hueAtom, useAtomCssVar } from "~/utils/atoms";
28
import { seo } from "~/utils/seo";
···
80
function RootComponent() {
81
return (
82
<UnifiedAuthProvider>
83
+
<LikeMutationQueueProvider>
84
+
<RootDocument>
85
+
<KeepAliveProvider>
86
+
<KeepAliveOutlet />
87
+
</KeepAliveProvider>
88
+
</RootDocument>
89
+
</LikeMutationQueueProvider>
90
</UnifiedAuthProvider>
91
);
92
}
+40
-20
src/routes/profile.$did/index.tsx
···
22
useGetFollowState,
23
useGetOneToOneState,
24
} from "~/utils/followState";
0
25
import {
26
useInfiniteQueryAuthorFeed,
27
useQueryArbitrary,
···
454
}
455
456
const { data: likes } = useQueryConstellation(
457
-
// @ts-expect-error overloads sucks
458
!listmode
459
? {
460
target: feed.uri,
···
470
className={`px-4 py-4 ${!disableBottomBorder && "border-b"} flex flex-col gap-1`}
471
to="/profile/$did/feed/$rkey"
472
params={{ did: aturi.host, rkey: aturi.rkey }}
473
-
onClick={(e)=>{e.stopPropagation();}}
0
0
474
>
475
<div className="flex flex-row gap-3">
476
<div className="min-w-10 min-h-10">
···
574
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
575
576
const {
577
-
data: repostsData,
578
fetchNextPage,
579
hasNextPage,
580
isFetchingNextPage,
···
585
"app.bsky.feed.like"
586
);
587
588
-
const reposts = React.useMemo(
589
-
() => repostsData?.pages.flatMap((page) => page.records) ?? [],
590
-
[repostsData]
591
);
592
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
593
return (
594
<>
595
<div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
596
Likes
597
</div>
598
<div>
599
-
{reposts.map((repost) => {
600
if (
601
-
!repost ||
602
-
!repost?.value ||
603
-
!repost?.value?.subject ||
604
// @ts-expect-error blehhhhh
605
-
!repost?.value?.subject?.uri
606
)
607
return;
608
-
const repostRecord =
609
-
repost.value as unknown as ATPAPI.AppBskyFeedLike.Record;
610
return (
611
<UniversalPostRendererATURILoader
612
-
key={repostRecord.subject.uri}
613
-
atUri={repostRecord.subject.uri}
614
feedviewpost={true}
615
/>
616
);
···
618
</div>
619
620
{/* Loading and "Load More" states */}
621
-
{arePostsLoading && reposts.length === 0 && (
622
-
<div className="p-4 text-center text-gray-500">Loading posts...</div>
623
)}
624
{isFetchingNextPage && (
625
<div className="p-4 text-center text-gray-500">Loading more...</div>
···
629
onClick={() => fetchNextPage()}
630
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"
631
>
632
-
Load More Posts
633
</button>
634
)}
635
-
{reposts.length === 0 && !arePostsLoading && (
636
-
<div className="p-4 text-center text-gray-500">No posts found.</div>
637
)}
638
</>
639
);
···
22
useGetFollowState,
23
useGetOneToOneState,
24
} from "~/utils/followState";
25
+
import { useFastSetLikesFromFeed } from "~/utils/likeMutationQueue";
26
import {
27
useInfiniteQueryAuthorFeed,
28
useQueryArbitrary,
···
455
}
456
457
const { data: likes } = useQueryConstellation(
458
+
// @ts-expect-error overloads sucks
459
!listmode
460
? {
461
target: feed.uri,
···
471
className={`px-4 py-4 ${!disableBottomBorder && "border-b"} flex flex-col gap-1`}
472
to="/profile/$did/feed/$rkey"
473
params={{ did: aturi.host, rkey: aturi.rkey }}
474
+
onClick={(e) => {
475
+
e.stopPropagation();
476
+
}}
477
>
478
<div className="flex flex-row gap-3">
479
<div className="min-w-10 min-h-10">
···
577
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
578
579
const {
580
+
data: likesData,
581
fetchNextPage,
582
hasNextPage,
583
isFetchingNextPage,
···
588
"app.bsky.feed.like"
589
);
590
591
+
const likes = React.useMemo(
592
+
() => likesData?.pages.flatMap((page) => page.records) ?? [],
593
+
[likesData]
594
);
595
596
+
const { setFastState } = useFastSetLikesFromFeed();
597
+
const seededRef = React.useRef(new Set<string>());
598
+
599
+
useEffect(() => {
600
+
for (const like of likes) {
601
+
if (!seededRef.current.has(like.uri)) {
602
+
seededRef.current.add(like.uri);
603
+
const record = like.value as unknown as ATPAPI.AppBskyFeedLike.Record;
604
+
setFastState(record.subject.uri, {
605
+
target: record.subject.uri,
606
+
uri: like.uri,
607
+
cid: like.cid,
608
+
});
609
+
}
610
+
}
611
+
}, [likes, setFastState]);
612
+
613
return (
614
<>
615
<div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
616
Likes
617
</div>
618
<div>
619
+
{likes.map((like) => {
620
if (
621
+
!like ||
622
+
!like?.value ||
623
+
!like?.value?.subject ||
624
// @ts-expect-error blehhhhh
625
+
!like?.value?.subject?.uri
626
)
627
return;
628
+
const likeRecord =
629
+
like.value as unknown as ATPAPI.AppBskyFeedLike.Record;
630
return (
631
<UniversalPostRendererATURILoader
632
+
key={likeRecord.subject.uri}
633
+
atUri={likeRecord.subject.uri}
634
feedviewpost={true}
635
/>
636
);
···
638
</div>
639
640
{/* Loading and "Load More" states */}
641
+
{arePostsLoading && likes.length === 0 && (
642
+
<div className="p-4 text-center text-gray-500">Loading likes...</div>
643
)}
644
{isFetchingNextPage && (
645
<div className="p-4 text-center text-gray-500">Loading more...</div>
···
649
onClick={() => fetchNextPage()}
650
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"
651
>
652
+
Load More Likes
653
</button>
654
)}
655
+
{likes.length === 0 && !arePostsLoading && (
656
+
<div className="p-4 text-center text-gray-500">No likes found.</div>
657
)}
658
</>
659
);
+11
src/utils/atoms.ts
···
59
{}
60
);
61
0
0
0
0
0
0
0
0
0
0
0
62
export const defaultconstellationURL = "constellation.microcosm.blue";
63
export const constellationURLAtom = atomWithStorage<string>(
64
"constellationURL",
···
59
{}
60
);
61
62
+
export type LikeRecord = {
63
+
uri: string; // at://did/collection/rkey
64
+
target: string;
65
+
cid: string;
66
+
};
67
+
68
+
export const internalLikedPostsAtom = atomWithStorage<Record<string, LikeRecord | null>>(
69
+
"internal-liked-posts",
70
+
{}
71
+
);
72
+
73
export const defaultconstellationURL = "constellation.microcosm.blue";
74
export const constellationURLAtom = atomWithStorage<string>(
75
"constellationURL",
+34
src/utils/likeMutationQueue.ts
···
0
0
0
0
0
0
0
0
0
0
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 { useAtom } from "jotai";
2
+
import { useCallback } from "react";
3
+
4
+
import { type LikeRecord,useLikeMutationQueue as useLikeMutationQueueFromProvider } from "~/providers/LikeMutationQueueProvider";
5
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
6
+
7
+
import { internalLikedPostsAtom } from "./atoms";
8
+
9
+
export function useFastLike(target: string, cid: string) {
10
+
const { agent } = useAuth();
11
+
const { fastState, fastToggle, backfillState } = useLikeMutationQueueFromProvider();
12
+
13
+
const liked = fastState(target);
14
+
const toggle = () => fastToggle(target, cid);
15
+
/**
16
+
*
17
+
* @deprecated dont use it yet, will cause infinite rerenders
18
+
*/
19
+
const backfill = () => agent?.did && backfillState(target, agent.did);
20
+
21
+
return { liked, toggle, backfill };
22
+
}
23
+
24
+
export function useFastSetLikesFromFeed() {
25
+
const [_, setLikedPosts] = useAtom(internalLikedPostsAtom);
26
+
27
+
const setFastState = useCallback(
28
+
(target: string, record: LikeRecord | null) =>
29
+
setLikedPosts((prev) => ({ ...prev, [target]: record })),
30
+
[setLikedPosts]
31
+
);
32
+
33
+
return { setFastState };
34
+
}