Live video on the AT Protocol
79
fork

Configure Feed

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

at eli/localhost-dev-is-back 142 lines 4.2 kB view raw
1import { Button, Icon, Text, zero } from "@streamplace/components"; 2import { Plus } from "lucide-react-native"; 3import React, { useEffect, useState } from "react"; 4import { View } from "react-native"; 5import { followUser, unfollowUser } from "../features/bluesky/blueskySlice"; 6import { selectStreamplace } from "../features/streamplace/streamplaceSlice"; 7import { useAppDispatch, useAppSelector } from "../store/hooks"; 8 9/** 10 * FollowButton component for following/unfollowing a streamer. 11 * 12 * Props: 13 * - streamerDID: string — The DID of the streamer to follow/unfollow 14 * - currentUserDID?: string — The DID of the current user (optional) 15 * - onFollowChange?: (isFollowing: boolean) => void — Optional callback when follow state changes 16 */ 17interface FollowButtonProps { 18 streamerDID: string; 19 currentUserDID?: string; 20 onFollowChange?: (isFollowing: boolean) => void; 21} 22 23const FollowButton: React.FC<FollowButtonProps> = ({ 24 streamerDID, 25 currentUserDID, 26 onFollowChange, 27}) => { 28 const [isFollowing, setIsFollowing] = useState<boolean | null>(null); 29 const [error, setError] = useState<string | null>(null); 30 const [followUri, setFollowUri] = useState<string | null>(null); 31 const { url: streamplaceUrl } = useAppSelector(selectStreamplace); 32 const dispatch = useAppDispatch(); 33 34 // Hide button if not logged in or viewing own stream 35 if (!currentUserDID || currentUserDID === streamerDID) return null; 36 37 // Fetch initial follow state using xrpc 38 useEffect(() => { 39 let cancelled = false; 40 41 const fetchFollowStatus = async () => { 42 if (!currentUserDID || !streamerDID) return; 43 44 setError(null); 45 try { 46 const res = await fetch( 47 `${streamplaceUrl}/xrpc/place.stream.graph.getFollowingUser?subjectDID=${encodeURIComponent(streamerDID)}&userDID=${encodeURIComponent(currentUserDID)}`, 48 { 49 credentials: "include", 50 }, 51 ); 52 53 if (!res.ok) { 54 const errorText = await res.text(); 55 throw new Error(`Failed to fetch follow status: ${errorText}`); 56 } 57 58 const data = await res.json(); 59 if (cancelled) return; 60 61 if (data.follow) { 62 setIsFollowing(true); 63 setFollowUri(data.follow.uri); 64 } else { 65 setIsFollowing(false); 66 setFollowUri(null); 67 } 68 } catch (err) { 69 if (!cancelled) setError("Could not determine follow state"); 70 } 71 }; 72 73 fetchFollowStatus(); 74 return () => { 75 cancelled = true; 76 }; 77 }, [currentUserDID, streamerDID, streamplaceUrl]); 78 79 const handleFollow = async () => { 80 setError(null); 81 setIsFollowing(true); // Optimistic 82 try { 83 await dispatch(followUser(streamerDID)).unwrap(); 84 setIsFollowing(true); 85 onFollowChange?.(true); 86 } catch (err) { 87 setIsFollowing(false); 88 setError( 89 `Failed to follow: ${err instanceof Error ? err.message : "Unknown error"}`, 90 ); 91 } 92 }; 93 94 const handleUnfollow = async () => { 95 setError(null); 96 setIsFollowing(false); // Optimistic 97 try { 98 await dispatch( 99 unfollowUser({ 100 subjectDID: streamerDID, 101 ...(followUri ? { followUri } : {}), 102 }), 103 ).unwrap(); 104 setIsFollowing(false); 105 setFollowUri(null); 106 onFollowChange?.(false); 107 } catch (err) { 108 setIsFollowing(true); 109 setError( 110 `Failed to unfollow: ${err instanceof Error ? err.message : "Unknown error"}`, 111 ); 112 } 113 }; 114 115 return ( 116 <View 117 style={[ 118 { flexDirection: "row" }, 119 { alignItems: "center" }, 120 zero.gap.all[2], 121 ]} 122 > 123 <Button 124 onPress={isFollowing ? handleUnfollow : handleFollow} 125 variant={isFollowing ? "secondary" : "primary"} 126 size="pill" 127 disabled={isFollowing === null} 128 loading={isFollowing === null} 129 leftIcon={!isFollowing && <Icon icon={Plus} size="sm" />} 130 > 131 {isFollowing === null 132 ? "Loading..." 133 : isFollowing 134 ? "Unfollow" 135 : "Follow"} 136 </Button> 137 {error && <Text style={[{ color: "#c00" }, zero.ml[2]]}>{error}</Text>} 138 </View> 139 ); 140}; 141 142export default FollowButton;