forked from
rocksky.app/rocksky
A decentralized music tracking and discovery platform built on AT Protocol 馃幍
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;