Plugin
ReviewDB
Review other users (Adds a new settings to profiles)
1
import "./style.css";2
3
import { NavContextMenuPatchCallback } from "@api/ContextMenu";4
import ErrorBoundary from "@components/ErrorBoundary";5
import { OpenExternalIcon } from "@components/Icons";6
import { Paragraph } from "@components/Paragraph";7
import { Span } from "@components/Span";8
import { Devs } from "@utils/constants";9
import { classes } from "@utils/misc";10
import { useAwaiter } from "@utils/react";11
import definePlugin from "@utils/types";12
import { Guild, User } from "@vencord/discord-types";13
import { findCssClassesLazy } from "@webpack";14
import { Clickable, ConfirmModal, IconUtils, Menu, openModal, Parser } from "@webpack/common";15
16
import { Auth, initAuth, updateAuth } from "./auth";17
import { openReviewsModal } from "./components/ReviewModal";18
import { NotificationType, ReviewType } from "./entities";19
import { getCurrentUserInfo, getReviews, readNotification } from "./reviewDbApi";20
import { settings } from "./settings";21
import { cl, showToast } from "./utils";22
23
const DMSideBarClasses = findCssClassesLazy("widgetPreviews");24
const ProfileCardClasses = findCssClassesLazy("cardsList", "firstCardContainer", "card", "container");25
const ProfileCardContainerClasses = findCssClassesLazy("innerContainer", "icons", "icon", "displayCount", "displayCountText", "displayCountTextColor", "breadcrumb");26
const ProfileCardOverlayClasses = findCssClassesLazy("overlay", "isPrivate", "outer");27
28
const guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild, onClose(): void; }) => {29
if (!guild) return;30
children.push(31
<Menu.MenuItem32
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
40
const userContextPatch: NavContextMenuPatchCallback = (children, { user }: { user?: User, onClose(): void; }) => {41
if (!user) return;42
children.push(43
<Menu.MenuItem44
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
52
export 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": userContextPatch65
},66
67
patches: [68
{69
// DM profile sidebar70
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 popout78
// Same find as ShowConnections79
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
<ConfirmModal129
{...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
false139
)}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 have153
// been made with less than half of the number of elements, so we have to do this insanity to replicate their ui154
const reviewsSection = (155
<section className={ProfileCardClasses.container}>156
<ul className={ProfileCardClasses.cardsList} tabIndex={-1}>157
<li className={ProfileCardClasses.firstCardContainer}>158
<Clickable159
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?.reviewCount165
? (166
<div className={ProfileCardContainerClasses.icons}>167
{reviewData.reviews168
.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
<img177
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 isSideBar204
? <div className={DMSideBarClasses.widgetPreviews}>{reviewsSection}</div>205
: reviewsSection;206
}, { noop: true })207
});208