Plugin
ReviewDB
Review other users (Adds a new settings to profiles)
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import "./style.css";8
9
import { NavContextMenuPatchCallback } from "@api/ContextMenu";10
import ErrorBoundary from "@components/ErrorBoundary";11
import { OpenExternalIcon } from "@components/Icons";12
import { Paragraph } from "@components/Paragraph";13
import { Span } from "@components/Span";14
import { Devs } from "@utils/constants";15
import { classes } from "@utils/misc";16
import { useAwaiter } from "@utils/react";17
import definePlugin from "@utils/types";18
import { Guild, User } from "@vencord/discord-types";19
import { findCssClassesLazy } from "@webpack";20
import { Clickable, ConfirmModal, IconUtils, Menu, openModal, Parser } from "@webpack/common";21
22
import { Auth, initAuth, updateAuth } from "./auth";23
import { openReviewsModal } from "./components/ReviewModal";24
import { NotificationType, ReviewType } from "./entities";25
import { getCurrentUserInfo, getReviews, readNotification } from "./reviewDbApi";26
import { settings } from "./settings";27
import { cl, showToast } from "./utils";28
29
const DMSideBarClasses = findCssClassesLazy("widgetPreviews");30
const ProfileCardClasses = findCssClassesLazy("cardsList", "firstCardContainer", "card", "container");31
const ProfileCardContainerClasses = findCssClassesLazy("innerContainer", "icons", "icon", "displayCount", "displayCountText", "displayCountTextColor", "breadcrumb");32
const ProfileCardOverlayClasses = findCssClassesLazy("overlay", "isPrivate", "outer");33
34
const guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild, onClose(): void; }) => {35
if (!guild) return;36
children.push(37
<Menu.MenuItem38
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
46
const userContextPatch: NavContextMenuPatchCallback = (children, { user }: { user?: User, onClose(): void; }) => {47
if (!user) return;48
children.push(49
<Menu.MenuItem50
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
58
export 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": userContextPatch71
},72
73
patches: [74
{75
// DM profile sidebar76
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 popout84
// Same find as ShowConnections85
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
<ConfirmModal135
{...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
false145
)}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 have159
// been made with less than half of the number of elements, so we have to do this insanity to replicate their ui160
const reviewsSection = (161
<section className={ProfileCardClasses.container}>162
<ul className={ProfileCardClasses.cardsList} tabIndex={-1}>163
<li className={ProfileCardClasses.firstCardContainer}>164
<Clickable165
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?.reviewCount171
? (172
<div className={ProfileCardContainerClasses.icons}>173
{reviewData.reviews174
.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
<img183
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 isSideBar210
? <div className={DMSideBarClasses.widgetPreviews}>{reviewsSection}</div>211
: reviewsSection;212
}, { noop: true })213
});214