Live video on the AT Protocol
at next 139 lines 4.1 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 { useStore } from "store"; 6import { useStreamplaceUrl } from "store/hooks"; 7 8/** 9 * FollowButton component for following/unfollowing a streamer. 10 * 11 * Props: 12 * - streamerDID: string — The DID of the streamer to follow/unfollow 13 * - currentUserDID?: string — The DID of the current user (optional) 14 * - onFollowChange?: (isFollowing: boolean) => void — Optional callback when follow state changes 15 */ 16interface FollowButtonProps { 17 streamerDID: string; 18 currentUserDID?: string; 19 onFollowChange?: (isFollowing: boolean) => void; 20} 21 22const FollowButton: React.FC<FollowButtonProps> = ({ 23 streamerDID, 24 currentUserDID, 25 onFollowChange, 26}) => { 27 const [isFollowing, setIsFollowing] = useState<boolean | null>(null); 28 const [error, setError] = useState<string | null>(null); 29 const [followUri, setFollowUri] = useState<string | null>(null); 30 const streamplaceUrl = useStreamplaceUrl(); 31 const followUser = useStore((state) => state.followUser); 32 const unfollowUser = useStore((state) => state.unfollowUser); 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]); 78 79 const handleFollow = async () => { 80 setError(null); 81 setIsFollowing(true); // Optimistic 82 try { 83 await followUser(streamerDID); 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 unfollowUser(streamerDID, followUri ?? undefined); 99 setIsFollowing(false); 100 setFollowUri(null); 101 onFollowChange?.(false); 102 } catch (err) { 103 setIsFollowing(true); 104 setError( 105 `Failed to unfollow: ${err instanceof Error ? err.message : "Unknown error"}`, 106 ); 107 } 108 }; 109 110 return ( 111 <View 112 style={[ 113 { flexDirection: "row" }, 114 { alignItems: "center" }, 115 zero.gap.all[2], 116 ]} 117 > 118 <Button 119 onPress={isFollowing ? handleUnfollow : handleFollow} 120 variant={isFollowing ? "secondary" : "primary"} 121 size="pill" 122 width="min" 123 disabled={isFollowing === null} 124 loading={isFollowing === null} 125 leftIcon={!isFollowing && <Icon icon={Plus} size="sm" />} 126 hoverStyle={isFollowing ? { backgroundColor: "#dc2626" } : undefined} 127 > 128 {isFollowing === null 129 ? "Loading..." 130 : isFollowing 131 ? "Following" 132 : "Follow"} 133 </Button> 134 {error && <Text style={[{ color: "#c00" }, zero.ml[2]]}>{error}</Text>} 135 </View> 136 ); 137}; 138 139export default FollowButton;