Your music, beautifully tracked. All yours. (coming soon) teal.fm
teal-fm atproto
at main 266 lines 8.6 kB view raw
1import { useEffect, useState } from "react"; 2import { Image, View } from "react-native"; 3import ActorPlaysView from "@/components/play/actorPlaysView"; 4import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 5import { Button } from "@/components/ui/button"; 6import { Text } from "@/components/ui/text"; 7import getImageCdnLink from "@/lib/atp/getImageCdnLink"; 8import { Icon } from "@/lib/icons/iconWithClassName"; 9import { useStore } from "@/stores/mainStore"; 10import { Agent } from "@atproto/api"; 11import { MoreHorizontal, Pen, Plus } from "lucide-react-native"; 12 13import { OutputSchema as GetProfileOutputSchema } from "@teal/lexicons/src/types/fm/teal/alpha/actor/getProfile"; 14import { Record as ProfileRecord } from "@teal/lexicons/src/types/fm/teal/alpha/actor/profile"; 15 16import { CardTitle } from "../../components/ui/card"; 17import EditProfileModal from "./editProfileView"; 18 19const GITHUB_AVATAR_URI = 20 "https://i.pinimg.com/originals/ef/a2/8d/efa28d18a04e7fa40ed49eeb0ab660db.jpg"; 21 22export interface ActorViewProps { 23 actorDid: string; 24 pdsAgent: Agent | null; 25} 26 27export default function ActorView({ actorDid, pdsAgent }: ActorViewProps) { 28 const [isEditing, setIsEditing] = useState(false); 29 const [profile, setProfile] = useState< 30 GetProfileOutputSchema["actor"] | null 31 >(null); 32 33 const tealDid = useStore((state) => state.tealDid); 34 35 useEffect(() => { 36 let isMounted = true; 37 38 const fetchProfile = async () => { 39 if (!pdsAgent) { 40 return; 41 } 42 try { 43 let res = await pdsAgent.call( 44 "fm.teal.alpha.actor.getProfile", 45 { actor: actorDid }, 46 {}, 47 { headers: { "atproto-proxy": tealDid + "#teal_fm_appview" } }, 48 ); 49 if (isMounted) { 50 setProfile(res.data["actor"] as GetProfileOutputSchema["actor"]); 51 } 52 } catch (error) { 53 console.error("Error fetching profile:", error); 54 } 55 }; 56 57 fetchProfile(); 58 59 return () => { 60 isMounted = false; 61 }; 62 }, [pdsAgent, actorDid, tealDid]); 63 64 const isSelf = actorDid === (pdsAgent?.did || ""); 65 66 const handleSave = async ( 67 updatedProfile: { displayName: any; description: any }, 68 newAvatarUri: string, 69 newBannerUri: string, 70 ) => { 71 if (!pdsAgent) { 72 return; 73 } 74 // Implement your save logic here (e.g., update your database or state) 75 console.log("Saving profile:", updatedProfile, newAvatarUri, newBannerUri); 76 77 // Update the local profile data 78 setProfile((prevProfile) => ({ 79 ...prevProfile, 80 displayName: updatedProfile.displayName, 81 description: updatedProfile.description, 82 avatar: newAvatarUri, 83 banner: newBannerUri, 84 })); 85 86 // get the current user's profile (getRecord) 87 let currentUser: ProfileRecord | undefined; 88 let cid: string | undefined; 89 try { 90 const res = await pdsAgent.call("com.atproto.repo.getRecord", { 91 repo: pdsAgent.did, 92 collection: "fm.teal.alpha.actor.profile", 93 rkey: "self", 94 }); 95 currentUser = res.data.value; 96 cid = res.data.cid; 97 } catch (error) { 98 console.error("Error fetching user profile:", error); 99 } 100 101 // upload blobs if necessary 102 let newAvatarBlob = currentUser?.avatar ?? undefined; 103 let newBannerBlob = currentUser?.banner ?? undefined; 104 if (newAvatarUri) { 105 // if it is http/s url then do nothing 106 if (!newAvatarUri.startsWith("http")) { 107 console.log("Uploading avatar"); 108 // its a b64 encoded data uri, decode it and get a blob 109 const data = await fetch(newAvatarUri).then((r) => r.blob()); 110 const fileType = newAvatarUri.split(";")[0].split(":")[1]; 111 console.log(fileType); 112 const blob = new Blob([data], { type: fileType }); 113 newAvatarBlob = (await pdsAgent.uploadBlob(blob)).data.blob; 114 } 115 } 116 if (newBannerUri) { 117 if (!newBannerUri.startsWith("http")) { 118 console.log("Uploading banner"); 119 const data = await fetch(newBannerUri).then((r) => r.blob()); 120 const fileType = newBannerUri.split(";")[0].split(":")[1]; 121 console.log(fileType); 122 const blob = new Blob([data], { type: fileType }); 123 newBannerBlob = (await pdsAgent.uploadBlob(blob)).data.blob; 124 } 125 } 126 127 console.log("done uploading"); 128 129 let record: ProfileRecord = { 130 displayName: updatedProfile.displayName, 131 description: updatedProfile.description, 132 avatar: newAvatarBlob, 133 banner: newBannerBlob, 134 }; 135 136 let post; 137 138 if (cid) { 139 post = await pdsAgent.call( 140 "com.atproto.repo.putRecord", 141 {}, 142 { 143 repo: pdsAgent.did, 144 collection: "fm.teal.alpha.actor.profile", 145 rkey: "self", 146 record, 147 swapRecord: cid, 148 }, 149 ); 150 } else { 151 post = await pdsAgent.call( 152 "com.atproto.repo.createRecord", 153 {}, 154 { 155 repo: pdsAgent.did, 156 collection: "fm.teal.alpha.actor.profile", 157 rkey: "self", 158 record, 159 }, 160 ); 161 } 162 163 setIsEditing(false); // Close the modal after saving 164 }; 165 166 if (!profile) { 167 return null; 168 } 169 170 return ( 171 <> 172 {profile.banner ? ( 173 <Image 174 className="-mb-6 h-32 w-full max-w-[100vh] scale-[1.32] rounded-xl md:h-44" 175 source={{ 176 uri: 177 getImageCdnLink({ did: profile.did!, hash: profile.banner }) ?? 178 GITHUB_AVATAR_URI, 179 }} 180 /> 181 ) : ( 182 <View className="-mb-6 h-32 w-full max-w-[100vh] scale-[1.32] rounded-xl bg-background md:h-44" /> 183 )} 184 <View className="items-left flex w-screen max-w-2xl flex-col justify-start gap-1 p-4 px-8 text-left"> 185 <View className="flex flex-row items-center justify-between"> 186 <View className="flex justify-between"> 187 <Avatar alt="Rick Sanchez's Avatar" className="h-24 w-24"> 188 <AvatarImage 189 source={{ 190 uri: 191 (profile.avatar && 192 getImageCdnLink({ 193 did: profile.did!, 194 hash: profile.avatar, 195 })) || 196 GITHUB_AVATAR_URI, 197 }} 198 /> 199 <AvatarFallback> 200 <Text>{profile.displayName?.substring(0, 1) ?? "R"}</Text> 201 </AvatarFallback> 202 </Avatar> 203 <CardTitle className="mt-2 flex w-full justify-between text-left"> 204 {profile.displayName ?? " Richard"} 205 </CardTitle> 206 </View> 207 <View className="mt-2 flex-row gap-2"> 208 {isSelf ? ( 209 <Button 210 variant="outline" 211 size="sm" 212 className="flex-row items-center justify-center gap-2 rounded-xl" 213 onPress={() => setIsEditing(true)} 214 > 215 <Icon icon={Pen} size={18} /> 216 <Text>Edit</Text> 217 </Button> 218 ) : ( 219 <Button 220 variant="outline" 221 size="sm" 222 className="flex-row items-center justify-center gap-2 rounded-xl" 223 > 224 <Icon icon={Plus} size={18} /> 225 <Text>Follow</Text> 226 </Button> 227 )} 228 <Button 229 variant="outline" 230 size="sm" 231 className="flex aspect-square flex-row items-center justify-center gap-2 rounded-full p-0 text-white" 232 > 233 <Icon icon={MoreHorizontal} size={18} /> 234 </Button> 235 </View> 236 </View> 237 <View> 238 {profile 239 ? profile.description?.split("\n").map((str, i) => ( 240 <Text 241 className="place-self-start self-start text-start" 242 key={i} 243 > 244 {str} 245 </Text> 246 )) || <Text>'A very mysterious person'</Text> 247 : "Loading..."} 248 </View> 249 </View> 250 <View className="w-full max-w-2xl gap-4 py-4 pl-8"> 251 <Text className="-ml-2 mr-6 border-b border-b-muted-foreground/30 pl-2 text-left text-2xl"> 252 Stamps 253 </Text> 254 <ActorPlaysView repo={actorDid} pdsAgent={pdsAgent} /> 255 </View> 256 {isSelf && ( 257 <EditProfileModal 258 isVisible={isEditing} 259 onClose={() => setIsEditing(false)} 260 profile={profile} // Pass the profile data 261 onSave={handleSave} // Pass the save handler 262 /> 263 )} 264 </> 265 ); 266}