Your music, beautifully tracked. All yours. (coming soon) teal.fm
teal-fm atproto

pfp thing init

+145 -71
+134 -62
apps/amethyst/components/actor/actorView.tsx
··· 1 - import { View, Image } from 'react-native'; 2 - import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; 3 - import { CardTitle } from '../../components/ui/card'; 4 - import { Text } from '@/components/ui/text'; 5 - import { useStore } from '@/stores/mainStore'; 6 7 - import ActorPlaysView from '@/components/play/actorPlaysView'; 8 - import { Button } from '@/components/ui/button'; 9 - import { Icon } from '@/lib/icons/iconWithClassName'; 10 - import { MoreHorizontal, Pen, Plus } from 'lucide-react-native'; 11 - import { Agent } from '@atproto/api'; 12 - import { useState, useEffect } from 'react'; 13 - import EditProfileModal from './editProfileView'; 14 15 - import { Record as ProfileRecord } from '@teal/lexicons/src/types/fm/teal/alpha/actor/profile'; 16 - import { OutputSchema as GetProfileOutputSchema } from '@teal/lexicons/src/types/fm/teal/alpha/actor/getProfile'; 17 - import getImageCdnLink from '@/lib/atp/getImageCdnLink'; 18 19 const GITHUB_AVATAR_URI = 20 - 'https://i.pinimg.com/originals/ef/a2/8d/efa28d18a04e7fa40ed49eeb0ab660db.jpg'; 21 22 export interface ActorViewProps { 23 actorDid: string; ··· 27 export 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); ··· 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 ··· 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) => ({ ··· 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 ··· 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, ··· 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 ··· 184 <View className="flex flex-col items-left justify-start text-left max-w-2xl w-screen gap-1 p-4 px-8"> 185 <View className="flex flex-row justify-between items-center"> 186 <View className="flex justify-between"> 187 - <Avatar alt="Rick Sanchez's Avatar" className="w-24 h-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="text-left flex w-full justify-between mt-2"> 204 - {profile.displayName ?? ' Richard'} 205 </CardTitle> 206 </View> 207 <View className="mt-2 flex-row gap-2"> ··· 236 </View> 237 <View> 238 {profile 239 - ? profile.description?.split('\n').map((str, i) => ( 240 <Text 241 className="text-start self-start place-self-start" 242 key={i} ··· 244 {str} 245 </Text> 246 )) || <Text>'A very mysterious person'</Text> 247 - : 'Loading...'} 248 </View> 249 </View> 250 <View className="max-w-2xl w-full gap-4 py-4 pl-8">
··· 1 + import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 2 + import { Text } from "@/components/ui/text"; 3 + import { useStore } from "@/stores/mainStore"; 4 + import { Image, View } from "react-native"; 5 + import { CardTitle } from "../../components/ui/card"; 6 7 + import ActorPlaysView from "@/components/play/actorPlaysView"; 8 + import { Button } from "@/components/ui/button"; 9 + import { Icon } from "@/lib/icons/iconWithClassName"; 10 + import { Agent } from "@atproto/api"; 11 + import { MoreHorizontal, Pen, Plus } from "lucide-react-native"; 12 + import { useEffect, useState } from "react"; 13 + import EditProfileModal from "./editProfileView"; 14 15 + import getImageCdnLink from "@/lib/atp/getImageCdnLink"; 16 + import { OutputSchema as GetProfileOutputSchema } from "@teal/lexicons/src/types/fm/teal/alpha/actor/getProfile"; 17 + import { Record as ProfileRecord } from "@teal/lexicons/src/types/fm/teal/alpha/actor/profile"; 18 + 19 + const topAlbums = [ 20 + { 21 + name: "Album 1", 22 + artist: "Artist 1", 23 + image: 24 + "https://at.uwu.wang/did:plc:tas6hj2xjrqben5653v5kohk/bafkreihxjfnq7r6tst33pf34lh6tojeh6tw6kt3p23dmhxmb2klxckjxx4", 25 + }, 26 + { 27 + name: "Album 2", 28 + artist: "Artist 2", 29 + image: 30 + "https://at.uwu.wang/did:plc:tas6hj2xjrqben5653v5kohk/bafkreihxjfnq7r6tst33pf34lh6tojeh6tw6kt3p23dmhxmb2klxckjxx4", 31 + }, 32 + { 33 + name: "Album 3", 34 + artist: "Artist 3", 35 + image: 36 + "https://at.uwu.wang/did:plc:tas6hj2xjrqben5653v5kohk/bafkreihxjfnq7r6tst33pf34lh6tojeh6tw6kt3p23dmhxmb2klxckjxx4", 37 + }, 38 + { 39 + name: "Album 4", 40 + artist: "Artist 4", 41 + image: 42 + "https://at.uwu.wang/did:plc:tas6hj2xjrqben5653v5kohk/bafkreihxjfnq7r6tst33pf34lh6tojeh6tw6kt3p23dmhxmb2klxckjxx4", 43 + }, 44 + { 45 + name: "Album 5", 46 + artist: "Artist 5", 47 + image: 48 + "https://at.uwu.wang/did:plc:tas6hj2xjrqben5653v5kohk/bafkreihxjfnq7r6tst33pf34lh6tojeh6tw6kt3p23dmhxmb2klxckjxx4", 49 + }, 50 + ]; 51 52 const GITHUB_AVATAR_URI = 53 + "https://i.pinimg.com/originals/ef/a2/8d/efa28d18a04e7fa40ed49eeb0ab660db.jpg"; 54 55 export interface ActorViewProps { 56 actorDid: string; ··· 60 export default function ActorView({ actorDid, pdsAgent }: ActorViewProps) { 61 const [isEditing, setIsEditing] = useState(false); 62 const [profile, setProfile] = useState< 63 + GetProfileOutputSchema["actor"] | null 64 >(null); 65 66 const tealDid = useStore((state) => state.tealDid); ··· 74 } 75 try { 76 let res = await pdsAgent.call( 77 + "fm.teal.alpha.actor.getProfile", 78 { actor: actorDid }, 79 {}, 80 + { headers: { "atproto-proxy": tealDid + "#teal_fm_appview" } } 81 ); 82 if (isMounted) { 83 + setProfile(res.data["actor"] as GetProfileOutputSchema["actor"]); 84 } 85 } catch (error) { 86 + console.error("Error fetching profile:", error); 87 } 88 }; 89 ··· 94 }; 95 }, [pdsAgent, actorDid, tealDid]); 96 97 + const isSelf = actorDid === (pdsAgent?.did || ""); 98 99 const handleSave = async ( 100 updatedProfile: { displayName: any; description: any }, 101 newAvatarUri: string, 102 + newBannerUri: string 103 ) => { 104 if (!pdsAgent) { 105 return; 106 } 107 // Implement your save logic here (e.g., update your database or state) 108 + console.log("Saving profile:", updatedProfile, newAvatarUri, newBannerUri); 109 110 // Update the local profile data 111 setProfile((prevProfile) => ({ ··· 120 let currentUser: ProfileRecord | undefined; 121 let cid: string | undefined; 122 try { 123 + const res = await pdsAgent.call("com.atproto.repo.getRecord", { 124 repo: pdsAgent.did, 125 + collection: "fm.teal.alpha.actor.profile", 126 + rkey: "self", 127 }); 128 currentUser = res.data.value; 129 cid = res.data.cid; 130 } catch (error) { 131 + console.error("Error fetching user profile:", error); 132 } 133 134 // upload blobs if necessary ··· 136 let newBannerBlob = currentUser?.banner ?? undefined; 137 if (newAvatarUri) { 138 // if it is http/s url then do nothing 139 + if (!newAvatarUri.startsWith("http")) { 140 + console.log("Uploading avatar"); 141 // its a b64 encoded data uri, decode it and get a blob 142 const data = await fetch(newAvatarUri).then((r) => r.blob()); 143 + const fileType = newAvatarUri.split(";")[0].split(":")[1]; 144 console.log(fileType); 145 const blob = new Blob([data], { type: fileType }); 146 newAvatarBlob = (await pdsAgent.uploadBlob(blob)).data.blob; 147 } 148 } 149 if (newBannerUri) { 150 + if (!newBannerUri.startsWith("http")) { 151 + console.log("Uploading banner"); 152 const data = await fetch(newBannerUri).then((r) => r.blob()); 153 + const fileType = newBannerUri.split(";")[0].split(":")[1]; 154 console.log(fileType); 155 const blob = new Blob([data], { type: fileType }); 156 newBannerBlob = (await pdsAgent.uploadBlob(blob)).data.blob; 157 } 158 } 159 160 + console.log("done uploading"); 161 162 let record: ProfileRecord = { 163 displayName: updatedProfile.displayName, ··· 170 171 if (cid) { 172 post = await pdsAgent.call( 173 + "com.atproto.repo.putRecord", 174 {}, 175 { 176 repo: pdsAgent.did, 177 + collection: "fm.teal.alpha.actor.profile", 178 + rkey: "self", 179 record, 180 swapRecord: cid, 181 + } 182 ); 183 } else { 184 post = await pdsAgent.call( 185 + "com.atproto.repo.createRecord", 186 {}, 187 { 188 repo: pdsAgent.did, 189 + collection: "fm.teal.alpha.actor.profile", 190 + rkey: "self", 191 record, 192 + } 193 ); 194 } 195 ··· 217 <View className="flex flex-col items-left justify-start text-left max-w-2xl w-screen gap-1 p-4 px-8"> 218 <View className="flex flex-row justify-between items-center"> 219 <View className="flex justify-between"> 220 + <View className="group"> 221 + <Avatar alt="Rick Sanchez's Avatar" className="w-24 h-24"> 222 + <AvatarImage 223 + source={{ 224 + uri: 225 + (profile.avatar && 226 + getImageCdnLink({ 227 + did: profile.did!, 228 + hash: profile.avatar, 229 + })) || 230 + GITHUB_AVATAR_URI, 231 + }} 232 + /> 233 + <AvatarFallback> 234 + <Text>{profile.displayName?.substring(0, 1) ?? "R"}</Text> 235 + </AvatarFallback> 236 + </Avatar> 237 + <View> 238 + {topAlbums.map((album, i: number) => ( 239 + <View 240 + key={album.name} 241 + className={`absolute ${ 242 + i === 0 243 + ? // #1, middle, yellow border 244 + `-z-10 group-hover:-translate-y-[9.3rem] group-hover:translate-x-5 group-hover:rotate-0 group-hover:bg-yellow-200 group-hover:p-[0.125rem]` 245 + : i === 1 246 + ? // #2, left of #1, silver border 247 + `-z-[11] group-hover:-translate-y-[8.8rem] group-hover:-translate-x-2 group-hover:-rotate-12 group-hover:bg-stone-200 group-hover:p-[0.125rem]` 248 + : i === 2 249 + ? // #3, right of #1, brown border 250 + `-z-[11] group-hover:-translate-y-[8.8rem] group-hover:translate-x-[3.25rem] group-hover:rotate-12 group-hover:bg-yellow-900 group-hover:p-[0.125rem]` 251 + : i === 3 252 + ? // #4, very left 253 + `-z-[12] group-hover:-translate-x-8 group-hover:-translate-y-[7.5rem] group-hover:-rotate-45` 254 + : // #5, very right 255 + `-z-[12] group-hover:-translate-y-[7.5rem] group-hover:translate-x-[5.5rem] group-hover:rotate-45` 256 + } -translate-y-[5.6rem] translate-x-5 rotate-0 rounded-xl opacity-0 transition-all duration-300 group-hover:opacity-100`} 257 + > 258 + {album.image ? ( 259 + <Image 260 + source={{ 261 + uri: album.image, 262 + width: 50, 263 + height: 50, 264 + }} 265 + alt={`${album.name} by ${album.artist}`} 266 + className="rounded-xl" 267 + /> 268 + ) : ( 269 + <Text>XD</Text> 270 + )} 271 + </View> 272 + ))} 273 + </View> 274 + </View> 275 <CardTitle className="text-left flex w-full justify-between mt-2"> 276 + {profile.displayName ?? " Richard"} 277 </CardTitle> 278 </View> 279 <View className="mt-2 flex-row gap-2"> ··· 308 </View> 309 <View> 310 {profile 311 + ? profile.description?.split("\n").map((str, i) => ( 312 <Text 313 className="text-start self-start place-self-start" 314 key={i} ··· 316 {str} 317 </Text> 318 )) || <Text>'A very mysterious person'</Text> 319 + : "Loading..."} 320 </View> 321 </View> 322 <View className="max-w-2xl w-full gap-4 py-4 pl-8">
+10 -8
compose.yaml
··· 1 services: 2 - aqua-api: 3 - image: ghcr.io/teal-fm/aqua:latest 4 - container_name: aqua-api 5 - # pass through db.sqlite 6 - volumes: 7 - - ./db.sqlite:/app/db.sqlite 8 ports: 9 - - "3000:3000" 10 env_file: 11 - - .env
··· 1 services: 2 + cadet: 3 + image: ghcr.io/mmattbtw/cadet:asdf 4 ports: 5 + - "9000:9000" 6 env_file: 7 + - .env 8 + satellite: 9 + image: ghcr.io/mmattbtw/satellite:asdf 10 + ports: 11 + - "3132:3000" 12 + env_file: 13 + - .env
+1 -1
package.json
··· 2 "name": "teal", 3 "private": true, 4 "version": "0.0.0", 5 - "packageManager": "pnpm@9.15.0+sha256.09a8fe31a34fda706354680619f4002f4ccef6dadff93240d24ef6c831f0fd28", 6 "scripts": { 7 "dev": "turbo dev", 8 "build": "pnpm turbo run build --filter='./packages/*' --filter='./apps/*'",
··· 2 "name": "teal", 3 "private": true, 4 "version": "0.0.0", 5 + "packageManager": "pnpm@10.10.0", 6 "scripts": { 7 "dev": "turbo dev", 8 "build": "pnpm turbo run build --filter='./packages/*' --filter='./apps/*'",