Your music, beautifully tracked. All yours. (coming soon)
teal.fm
teal-fm
atproto
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}