Plugin

ReviewDB

Review other users (Adds a new settings to profiles)

Friends Servers
index.tsx
Download

Source

src/plugins/reviewDB/index.tsx
1import "./style.css";
2
3import { NavContextMenuPatchCallback } from "@api/ContextMenu";
4import ErrorBoundary from "@components/ErrorBoundary";
5import { OpenExternalIcon } from "@components/Icons";
6import { Paragraph } from "@components/Paragraph";
7import { Span } from "@components/Span";
8import { Devs } from "@utils/constants";
9import { classes } from "@utils/misc";
10import { useAwaiter } from "@utils/react";
11import definePlugin from "@utils/types";
12import { Guild, User } from "@vencord/discord-types";
13import { findCssClassesLazy } from "@webpack";
14import { Clickable, ConfirmModal, IconUtils, Menu, openModal, Parser } from "@webpack/common";
15
16import { Auth, initAuth, updateAuth } from "./auth";
17import { openReviewsModal } from "./components/ReviewModal";
18import { NotificationType, ReviewType } from "./entities";
19import { getCurrentUserInfo, getReviews, readNotification } from "./reviewDbApi";
20import { settings } from "./settings";
21import { cl, showToast } from "./utils";
22
23const DMSideBarClasses = findCssClassesLazy("widgetPreviews");
24const ProfileCardClasses = findCssClassesLazy("cardsList", "firstCardContainer", "card", "container");
25const ProfileCardContainerClasses = findCssClassesLazy("innerContainer", "icons", "icon", "displayCount", "displayCountText", "displayCountTextColor", "breadcrumb");
26const ProfileCardOverlayClasses = findCssClassesLazy("overlay", "isPrivate", "outer");
27
28const guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild, onClose(): void; }) => {
29 if (!guild) return;
30 children.push(
31 <Menu.MenuItem
32 label="View Reviews"
33 id="vc-rdb-server-reviews"
34 icon={OpenExternalIcon}
35 action={() => openReviewsModal(guild.id, guild.name, ReviewType.Server)}
36 />
37 );
38};
39
40const userContextPatch: NavContextMenuPatchCallback = (children, { user }: { user?: User, onClose(): void; }) => {
41 if (!user) return;
42 children.push(
43 <Menu.MenuItem
44 label="View Reviews"
45 id="vc-rdb-user-reviews"
46 icon={OpenExternalIcon}
47 action={() => openReviewsModal(user.id, user.username, ReviewType.User)}
48 />
49 );
50};
51
52export default definePlugin({
53 name: "ReviewDB",
54 description: "Review other users (Adds a new settings to profiles)",
55 tags: ["Friends", "Servers"],
56 authors: [Devs.mantikafasi, Devs.Ven],
57
58 settings,
59 contextMenus: {
60 "guild-header-popout": guildPopoutPatch,
61 "guild-context": guildPopoutPatch,
62 "user-context": userContextPatch,
63 "user-profile-actions": userContextPatch,
64 "user-profile-overflow-menu": userContextPatch
65 },
66
67 patches: [
68 {
69 // DM profile sidebar
70 find: ".SIDEBAR,disableToolbar:",
71 replacement: {
72 match: /user:(\i),widgets:.{0,100}?\}\),/,
73 replace: "$&$self.renderProfileComponent({user:$1,isSideBar:true}),"
74 }
75 },
76 {
77 // User popout
78 // Same find as ShowConnections
79 find: &#039;"UserProfilePopout");&#039;,
80 replacement: {
81 match: /user:(\i),widgets:.{0,100}?\}\),/,
82 replace: "$&$self.renderProfileComponent({user:$1}),"
83 }
84 }
85 ],
86
87 flux: {
88 CONNECTION_OPEN: initAuth,
89 },
90
91 async start() {
92 const s = settings.store;
93 const { lastReviewId, notifyReviews } = s;
94
95 await initAuth();
96
97 setTimeout(async () => {
98 if (!Auth.token) return;
99
100 const user = await getCurrentUserInfo();
101 if (user) {
102 updateAuth({ user });
103
104 if (notifyReviews) {
105 if (lastReviewId && lastReviewId < user.lastReviewID) {
106 s.lastReviewId = user.lastReviewID;
107 if (user.lastReviewID !== 0)
108 showToast("You have new reviews on your profile!");
109 }
110 }
111
112 const { notification } = user;
113 if (notification) {
114 const props = notification.type === NotificationType.Ban ? {
115 cancelText: "Appeal",
116 confirmText: "Ok",
117 onCancel: async () =>
118 VencordNative.native.openExternal(
119 "https:class="ts-cmt">//reviewdb.mantikafasi.dev/api/redirect?"
120 + new URLSearchParams({
121 token: Auth.token!,
122 page: "dashboard/appeal"
123 })
124 )
125 } : {};
126
127 openModal(modalProps => (
128 <ConfirmModal
129 {...modalProps}
130 title={notification.title}
131 confirmText={props.confirmText ?? "OK"}
132 cancelText={props.cancelText}
133 variant="primary"
134 onCancel={props.onCancel}
135 >
136 {Parser.parse(
137 notification.content,
138 false
139 )}
140 </ConfirmModal>
141 ));
142
143 readNotification(notification.id);
144 }
145 }
146 }, 4000);
147 },
148
149 renderProfileComponent: ErrorBoundary.wrap(({ user, isSideBar = false }: { user: User; isSideBar?: boolean; }) => {
150 const [reviewData] = useAwaiter(() => getReviews(user.id, { limit: 4 }), { deps: [user.id], fallbackValue: null });
151
152 // Discord are masters at using a crap ton of html elements and css classes to create a simple ui that could have
153 // been made with less than half of the number of elements, so we have to do this insanity to replicate their ui
154 const reviewsSection = (
155 <section className={ProfileCardClasses.container}>
156 <ul className={ProfileCardClasses.cardsList} tabIndex={-1}>
157 <li className={ProfileCardClasses.firstCardContainer}>
158 <Clickable
159 className={classes(ProfileCardContainerClasses.breadcrumb, reviewData?.hasOptedOut && cl("profile-popout-disabled"))}
160 onClick={() => !reviewData?.hasOptedOut && openReviewsModal(user.id, user.username, ReviewType.User)}
161 >
162 <div className={classes(ProfileCardOverlayClasses.overlay, ProfileCardContainerClasses.innerContainer, ProfileCardClasses.card)}>
163 <Paragraph size={isSideBar ? "sm" : "xs"} weight="medium">User Reviews</Paragraph>
164 {!!reviewData?.reviewCount
165 ? (
166 <div className={ProfileCardContainerClasses.icons}>
167 {reviewData.reviews
168 .filter(review => review.id !== 0)
169 .slice(0, 4)
170 .reverse()
171 .map((review, idx) => {
172 const showCount = idx === 3 && reviewData.reviewCount > 4;
173
174 return (
175 <div className={ProfileCardContainerClasses.icon} key={review.id}>
176 <img
177 src={review.sender.profilePhoto}
178 alt={review.sender.username}
179 className={showCount ? ProfileCardContainerClasses.displayCount : undefined}
180 onError={e => e.currentTarget.src = IconUtils.getDefaultAvatarURL(review.sender.discordID)}
181 />
182 {showCount && (
183 <div className={ProfileCardContainerClasses.displayCountText}>
184 <Span className={ProfileCardContainerClasses.displayCountTextColor} size="xs" weight="medium" defaultColor={false}>
185 +{reviewData.reviewCount - 3}
186 </Span>
187 </div>
188 )}
189 </div>
190 );
191 })}
192 </div>
193 )
194 : <Paragraph size={isSideBar ? "sm" : "xs"}>{reviewData?.hasOptedOut ? "User opted out" : "No reviews yet"}</Paragraph>
195 }
196 </div>
197 </Clickable>
198 </li>
199 </ul>
200 </section>
201 );
202
203 return isSideBar
204 ? <div className={DMSideBarClasses.widgetPreviews}>{reviewsSection}</div>
205 : reviewsSection;
206 }, { noop: true })
207});
208