Plugin

ReviewDB

Review other users (Adds a new settings to profiles)

Friends Servers
index.tsx
Download

Source

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