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 { useParams } from "@tanstack/react-router";
4import { Button } from "baseui/button";
5import { Spinner } from "baseui/spinner";
6import { Textarea } from "baseui/textarea";
7import { LabelLarge, LabelMedium } from "baseui/typography";
8import { useAtomValue, useSetAtom } from "jotai";
9import { useState } from "react";
10import { Controller, useForm } from "react-hook-form";
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({ strict: false });
48 const location = window.location;
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("/song/")) {
60 uri = `at://${did}/app.rocksky.song/${rkey}`;
61 }
62
63 if (location.pathname.includes("/album/")) {
64 uri = `at://${did}/app.rocksky.album/${rkey}`;
65 }
66
67 if (location.pathname.includes("/artist/")) {
68 uri = `at://${did}/app.rocksky.artist/${rkey}`;
69 }
70
71 if (location.pathname.includes("/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 className="mt-[150px]">
115 <LabelLarge marginBottom={"10px"} className="!text-[var(--color-text)]">
116 Shoutbox
117 </LabelLarge>
118 {profile && (
119 <>
120 <Controller
121 name="message"
122 control={control}
123 render={({ field }) => (
124 <Textarea
125 {...field}
126 placeholder={
127 props.type === "profile"
128 ? `@${profile?.handle}, leave a shout for @${user?.handle} ...`
129 : `@${profile?.handle}, share your thoughts about this ${props.type}`
130 }
131 resize="vertical"
132 overrides={{
133 Input: {
134 style: {
135 width: "770px",
136 color: "var(--color-text)",
137 backgroundColor: "var(--color-input-background)",
138 caretColor: "var(--color-text)",
139 },
140 },
141 InputContainer: {
142 style: {
143 backgroundColor: "var(--color-input-background)",
144 borderColor: "var(--color-input-background)",
145 },
146 },
147 Root: {
148 style: {
149 backgroundColor: "var(--color-input-background)",
150 border: "none !important",
151 },
152 },
153 }}
154 maxLength={1000}
155 />
156 )}
157 />
158
159 <div className="mt-[15px] flex justify-end">
160 {!loading && (
161 <Button
162 disabled={
163 watch("message").length === 0 ||
164 watch("message").length > 1000
165 }
166 onClick={handleSubmit(onShout)}
167 overrides={{
168 BaseButton: {
169 style: ({ $disabled }) => ({
170 backgroundColor: "var(--color-purple) !important",
171 opacity: $disabled ? 0.4 : 1,
172 color: "var(--color-button-text) !important",
173 borderRadius: "2px",
174 }),
175 },
176 }}
177 >
178 Post Shout
179 </Button>
180 )}
181 {loading && <Spinner $size={25} $color="rgb(255, 40, 118)" />}
182 </div>
183 </>
184 )}
185 {!profile && (
186 <LabelMedium marginTop={"20px"} className="!text-[var(--color-text)]">
187 Want to share your thoughts?{" "}
188 <span
189 className="text-[var(--color-primary)] cursor-pointer"
190 onClick={() => setIsOpen(true)}
191 >
192 Sign in
193 </span>{" "}
194 to leave a shout.
195 </LabelMedium>
196 )}
197 <ShoutList />
198 <SignInModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
199 </div>
200 );
201}
202
203export default Shout;