Live video on the AT Protocol
79
fork

Configure Feed

Select the types of activity you want to include in your feed.

at eli/rtmprec-2 232 lines 6.0 kB view raw
1import { AppBskyGraphFollow } from "@atproto/api"; 2import { useEffect, useState } from "react"; 3import { useStreamplaceStore } from "./streamplace-store"; 4import { usePDSAgent } from "./xrpc"; 5 6export function useCreateFollowRecord() { 7 let agent = usePDSAgent(); 8 const [isLoading, setIsLoading] = useState(false); 9 10 const createFollow = async (subjectDID: string) => { 11 if (!agent) { 12 throw new Error("No PDS agent found"); 13 } 14 15 if (!agent.did) { 16 throw new Error("No user DID found, assuming not logged in"); 17 } 18 19 setIsLoading(true); 20 try { 21 const record: AppBskyGraphFollow.Record = { 22 $type: "app.bsky.graph.follow", 23 subject: subjectDID, 24 createdAt: new Date().toISOString(), 25 }; 26 const result = await agent.com.atproto.repo.createRecord({ 27 repo: agent.did, 28 collection: "app.bsky.graph.follow", 29 record, 30 }); 31 return result; 32 } finally { 33 setIsLoading(false); 34 } 35 }; 36 37 return { createFollow, isLoading }; 38} 39 40export function useDeleteFollowRecord() { 41 let agent = usePDSAgent(); 42 const [isLoading, setIsLoading] = useState(false); 43 44 const deleteFollow = async (followRecordUri: string) => { 45 if (!agent) { 46 throw new Error("No PDS agent found"); 47 } 48 49 if (!agent.did) { 50 throw new Error("No user DID found, assuming not logged in"); 51 } 52 53 setIsLoading(true); 54 try { 55 const result = await agent.com.atproto.repo.deleteRecord({ 56 repo: agent.did, 57 collection: "app.bsky.graph.follow", 58 rkey: followRecordUri.split("/").pop()!, 59 }); 60 return result; 61 } finally { 62 setIsLoading(false); 63 } 64 }; 65 66 return { deleteFollow, isLoading }; 67} 68 69interface GraphManagerState { 70 isFollowing: boolean | null; 71 followUri: string | null; 72 isLoading: boolean; 73 error: string | null; 74} 75 76interface GraphManagerActions { 77 follow: () => Promise<void>; 78 unfollow: () => Promise<void>; 79 refresh: () => Promise<void>; 80} 81 82export function useGraphManager( 83 subjectDID: string | null | undefined, 84): GraphManagerState & GraphManagerActions { 85 const agent = usePDSAgent(); 86 const [isFollowing, setIsFollowing] = useState<boolean | null>(null); 87 const [followUri, setFollowUri] = useState<string | null>(null); 88 const [isLoading, setIsLoading] = useState(false); 89 const [error, setError] = useState<string | null>(null); 90 91 const userDID = agent?.did; 92 93 const streamplaceUrl = useStreamplaceStore((state) => state.url); 94 95 const fetchFollowStatus = async () => { 96 if (!userDID || !subjectDID || !streamplaceUrl) { 97 setIsFollowing(null); 98 setFollowUri(null); 99 return; 100 } 101 102 setIsLoading(true); 103 setError(null); 104 try { 105 const res = await fetch( 106 `${streamplaceUrl}/xrpc/place.stream.graph.getFollowingUser?subjectDID=${encodeURIComponent(subjectDID)}&userDID=${encodeURIComponent(userDID)}`, 107 { 108 credentials: "include", 109 }, 110 ); 111 112 if (!res.ok) { 113 const errorText = await res.text(); 114 throw new Error(`Failed to fetch follow status: ${errorText}`); 115 } 116 117 const data = await res.json(); 118 119 if (data.follow) { 120 setIsFollowing(true); 121 setFollowUri(data.follow.uri); 122 } else { 123 setIsFollowing(false); 124 setFollowUri(null); 125 } 126 } catch (err) { 127 setError( 128 `Could not determine follow state: ${err instanceof Error ? err.message : `Unknown error: ${err}`}`, 129 ); 130 setIsFollowing(null); 131 } finally { 132 setIsLoading(false); 133 } 134 }; 135 136 useEffect(() => { 137 if (!userDID || !subjectDID) { 138 setIsFollowing(null); 139 setFollowUri(null); 140 setError(null); 141 return; 142 } 143 144 fetchFollowStatus(); 145 }, [userDID, subjectDID, streamplaceUrl]); 146 147 const follow = async () => { 148 if (!agent || !subjectDID) { 149 throw new Error("Cannot follow: not logged in or no subject DID"); 150 } 151 152 if (!agent.did) { 153 throw new Error("No user DID found, assuming not logged in"); 154 } 155 156 setIsLoading(true); 157 setError(null); 158 const previousState = isFollowing; 159 setIsFollowing(true); // Optimistic 160 161 try { 162 const record: AppBskyGraphFollow.Record = { 163 $type: "app.bsky.graph.follow", 164 subject: subjectDID, 165 createdAt: new Date().toISOString(), 166 }; 167 const result = await agent.com.atproto.repo.createRecord({ 168 repo: agent.did, 169 collection: "app.bsky.graph.follow", 170 record, 171 }); 172 setFollowUri(result.data.uri); 173 setIsFollowing(true); 174 } catch (err) { 175 setIsFollowing(previousState); 176 const errorMsg = `Failed to follow: ${err instanceof Error ? err.message : "Unknown error"}`; 177 setError(errorMsg); 178 throw new Error(errorMsg); 179 } finally { 180 setIsLoading(false); 181 } 182 }; 183 184 const unfollow = async () => { 185 if (!agent || !subjectDID) { 186 throw new Error("Cannot unfollow: not logged in or no subject DID"); 187 } 188 189 if (!agent.did) { 190 throw new Error("No user DID found, assuming not logged in"); 191 } 192 193 if (!followUri) { 194 throw new Error("Cannot unfollow: no follow URI found"); 195 } 196 197 setIsLoading(true); 198 setError(null); 199 const previousState = isFollowing; 200 const previousUri = followUri; 201 setIsFollowing(false); // Optimistic 202 setFollowUri(null); 203 204 try { 205 await agent.com.atproto.repo.deleteRecord({ 206 repo: agent.did, 207 collection: "app.bsky.graph.follow", 208 rkey: followUri.split("/").pop()!, 209 }); 210 setIsFollowing(false); 211 setFollowUri(null); 212 } catch (err) { 213 setIsFollowing(previousState); 214 setFollowUri(previousUri); 215 const errorMsg = `Failed to unfollow: ${err instanceof Error ? err.message : "Unknown error"}`; 216 setError(errorMsg); 217 throw new Error(errorMsg); 218 } finally { 219 setIsLoading(false); 220 } 221 }; 222 223 return { 224 isFollowing, 225 followUri, 226 isLoading, 227 error, 228 follow, 229 unfollow, 230 refresh: fetchFollowStatus, 231 }; 232}