A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at feat/pgpull 182 lines 5.2 kB view raw
1/* eslint-disable @typescript-eslint/no-explicit-any */ 2import { zodResolver } from "@hookform/resolvers/zod"; 3import { Button } from "baseui/button"; 4import { Spinner } from "baseui/spinner"; 5import { Textarea } from "baseui/textarea"; 6import { LabelLarge, LabelMedium } from "baseui/typography"; 7import { useAtomValue, useSetAtom } from "jotai"; 8import { useState } from "react"; 9import { Controller, useForm } from "react-hook-form"; 10import { useLocation, useParams } from "react-router"; 11import z from "zod"; 12import { profileAtom } from "../../atoms/profile"; 13import { shoutsAtom } from "../../atoms/shouts"; 14import { userAtom } from "../../atoms/user"; 15import useShout from "../../hooks/useShout"; 16import SignInModal from "../SignInModal"; 17import ShoutList from "./ShoutList"; 18 19const ShoutSchema = z.object({ 20 message: z.string().min(1).max(1000), 21}); 22 23interface ShoutProps { 24 type?: "album" | "artist" | "song" | "playlist" | "profile"; 25} 26 27function Shout(props: ShoutProps) { 28 props = { 29 type: "song", 30 ...props, 31 }; 32 const shouts = useAtomValue(shoutsAtom); 33 const setShouts = useSetAtom(shoutsAtom); 34 const [isOpen, setIsOpen] = useState(false); 35 const profile = useAtomValue(profileAtom); 36 const user = useAtomValue(userAtom); 37 const { shout, getShouts } = useShout(); 38 const { control, handleSubmit, watch, reset } = useForm< 39 z.infer<typeof ShoutSchema> 40 >({ 41 mode: "onChange", 42 resolver: zodResolver(ShoutSchema), 43 defaultValues: { 44 message: "", 45 }, 46 }); 47 const { did, rkey } = useParams<{ did: string; rkey: string }>(); 48 const location = useLocation(); 49 const [loading, setLoading] = useState(false); 50 51 const onShout = async ({ message }: z.infer<typeof ShoutSchema>) => { 52 setLoading(true); 53 let uri = ""; 54 55 if (location.pathname.startsWith("/profile")) { 56 uri = `at://${did}`; 57 } 58 59 if (location.pathname.includes("app.rocksky.song")) { 60 uri = `at://${did}/app.rocksky.song/${rkey}`; 61 } 62 63 if (location.pathname.includes("app.rocksky.album")) { 64 uri = `at://${did}/app.rocksky.album/${rkey}`; 65 } 66 67 if (location.pathname.includes("app.rocksky.artist")) { 68 uri = `at://${did}/app.rocksky.artist/${rkey}`; 69 } 70 71 if (location.pathname.includes("app.rocksky.scrobble")) { 72 uri = `at://${did}/app.rocksky.scrobble/${rkey}`; 73 } 74 75 await shout(uri, message); 76 77 const data = await getShouts(uri); 78 setShouts({ 79 ...shouts, 80 [location.pathname]: processShouts(data), 81 }); 82 83 setLoading(false); 84 85 reset(); 86 }; 87 88 const processShouts = (data: any) => { 89 const mapShouts = (parentId: string | null) => { 90 return data 91 .filter((x: any) => x.shouts.parent === parentId) 92 .map((x: any) => ({ 93 id: x.shouts.id, 94 uri: x.shouts.uri, 95 message: x.shouts.content, 96 date: x.shouts.createdAt, 97 liked: x.shouts.liked, 98 reported: x.shouts.reported, 99 likes: x.shouts.likes, 100 user: { 101 did: x.users.did, 102 avatar: x.users.avatar, 103 displayName: x.users.displayName, 104 handle: x.users.handle, 105 }, 106 replies: mapShouts(x.shouts.id).reverse(), 107 })); 108 }; 109 110 return mapShouts(null); 111 }; 112 113 return ( 114 <div style={{ marginTop: 150 }}> 115 <LabelLarge marginBottom={"10px"}>Shoutbox</LabelLarge> 116 {profile && ( 117 <> 118 <Controller 119 name="message" 120 control={control} 121 render={({ field }) => ( 122 <Textarea 123 {...field} 124 placeholder={ 125 props.type === "profile" 126 ? `@${profile?.handle}, leave a shout for @${user?.handle} ...` 127 : `@${profile?.handle}, share your thoughts about this ${props.type}` 128 } 129 resize="vertical" 130 overrides={{ 131 Input: { 132 style: { 133 width: "calc(95vw - 25px)", 134 }, 135 }, 136 }} 137 maxLength={1000} 138 /> 139 )} 140 /> 141 142 <div 143 style={{ 144 marginTop: 15, 145 display: "flex", 146 justifyContent: "flex-end", 147 }} 148 > 149 {!loading && ( 150 <Button 151 disabled={ 152 watch("message").length === 0 || 153 watch("message").length > 1000 154 } 155 onClick={handleSubmit(onShout)} 156 > 157 Post Shout 158 </Button> 159 )} 160 {loading && <Spinner $size={25} $color="rgb(255, 40, 118)" />} 161 </div> 162 </> 163 )} 164 {!profile && ( 165 <LabelMedium marginTop={"20px"}> 166 Want to share your thoughts?{" "} 167 <span 168 style={{ color: "rgb(255, 40, 118)", cursor: "pointer" }} 169 onClick={() => setIsOpen(true)} 170 > 171 Sign in 172 </span>{" "} 173 to leave a shout. 174 </LabelMedium> 175 )} 176 <ShoutList /> 177 <SignInModal isOpen={isOpen} onClose={() => setIsOpen(false)} /> 178 </div> 179 ); 180} 181 182export default Shout;