aboutsummaryrefslogtreecommitdiff
path: root/src/plugins/reviewDB
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/reviewDB')
-rw-r--r--src/plugins/reviewDB/components/MessageButton.tsx54
-rw-r--r--src/plugins/reviewDB/components/ReviewBadge.tsx5
-rw-r--r--src/plugins/reviewDB/components/ReviewComponent.tsx75
-rw-r--r--src/plugins/reviewDB/components/ReviewModal.tsx104
-rw-r--r--src/plugins/reviewDB/components/ReviewsView.tsx179
-rw-r--r--src/plugins/reviewDB/entities.ts (renamed from src/plugins/reviewDB/entities/User.ts)54
-rw-r--r--src/plugins/reviewDB/entities/Badge.ts26
-rw-r--r--src/plugins/reviewDB/entities/Review.ts35
-rw-r--r--src/plugins/reviewDB/index.tsx171
-rw-r--r--src/plugins/reviewDB/reviewDbApi.ts (renamed from src/plugins/reviewDB/Utils/ReviewDBAPI.ts)82
-rw-r--r--src/plugins/reviewDB/settings.tsx82
-rw-r--r--src/plugins/reviewDB/style.css50
-rw-r--r--src/plugins/reviewDB/utils.tsx (renamed from src/plugins/reviewDB/Utils/Utils.tsx)17
13 files changed, 617 insertions, 317 deletions
diff --git a/src/plugins/reviewDB/components/MessageButton.tsx b/src/plugins/reviewDB/components/MessageButton.tsx
index 3b8308a..176f4d6 100644
--- a/src/plugins/reviewDB/components/MessageButton.tsx
+++ b/src/plugins/reviewDB/components/MessageButton.tsx
@@ -17,28 +17,44 @@
*/
import { classes } from "@utils/misc";
-import { LazyComponent } from "@utils/react";
-import { findByProps } from "@webpack";
+import { findByPropsLazy } from "@webpack";
+import { Tooltip } from "@webpack/common";
-export default LazyComponent(() => {
- const { button, dangerous } = findByProps("button", "wrapper", "disabled", "separator");
+const iconClasses = findByPropsLazy("button", "wrapper", "disabled", "separator");
- return function MessageButton(props) {
- return props.type === "delete"
- ? (
- <div className={classes(button, dangerous)} aria-label="Delete Review" onClick={props.callback}>
- <svg aria-hidden="false" width="16" height="16" viewBox="0 0 20 20">
- <path fill="currentColor" d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z"></path>
- <path fill="currentColor" d="M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z"></path>
+export function DeleteButton({ onClick }: { onClick(): void; }) {
+ return (
+ <Tooltip text="Delete Review">
+ {props => (
+ <div
+ {...props}
+ className={classes(iconClasses.button, iconClasses.dangerous)}
+ onClick={onClick}
+ >
+ <svg width="16" height="16" viewBox="0 0 20 20">
+ <path fill="currentColor" d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z" />
+ <path fill="currentColor" d="M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z" />
</svg>
</div>
- )
- : (
- <div className={button} aria-label="Report Review" onClick={() => props.callback()}>
- <svg aria-hidden="false" width="16" height="16" viewBox="0 0 20 20">
- <path fill="currentColor" d="M20,6.002H14V3.002C14,2.45 13.553,2.002 13,2.002H4C3.447,2.002 3,2.45 3,3.002V22.002H5V14.002H10.586L8.293,16.295C8.007,16.581 7.922,17.011 8.076,17.385C8.23,17.759 8.596,18.002 9,18.002H20C20.553,18.002 21,17.554 21,17.002V7.002C21,6.45 20.553,6.002 20,6.002Z"></path>
+ )}
+ </Tooltip>
+ );
+}
+
+export function ReportButton({ onClick }: { onClick(): void; }) {
+ return (
+ <Tooltip text="Report Review">
+ {props => (
+ <div
+ {...props}
+ className={iconClasses.button}
+ onClick={onClick}
+ >
+ <svg width="16" height="16" viewBox="0 0 20 20">
+ <path fill="currentColor" d="M20,6.002H14V3.002C14,2.45 13.553,2.002 13,2.002H4C3.447,2.002 3,2.45 3,3.002V22.002H5V14.002H10.586L8.293,16.295C8.007,16.581 7.922,17.011 8.076,17.385C8.23,17.759 8.596,18.002 9,18.002H20C20.553,18.002 21,17.554 21,17.002V7.002C21,6.45 20.553,6.002 20,6.002Z" />
</svg>
</div>
- );
- };
-});
+ )}
+ </Tooltip>
+ );
+}
diff --git a/src/plugins/reviewDB/components/ReviewBadge.tsx b/src/plugins/reviewDB/components/ReviewBadge.tsx
index 8c013cd..e65dff2 100644
--- a/src/plugins/reviewDB/components/ReviewBadge.tsx
+++ b/src/plugins/reviewDB/components/ReviewBadge.tsx
@@ -18,7 +18,8 @@
import { MaskedLinkStore, Tooltip } from "@webpack/common";
-import { Badge } from "../entities/Badge";
+import { Badge } from "../entities";
+import { cl } from "../utils";
export default function ReviewBadge(badge: Badge) {
return (
@@ -26,13 +27,13 @@ export default function ReviewBadge(badge: Badge) {
text={badge.name}>
{({ onMouseEnter, onMouseLeave }) => (
<img
+ className={cl("badge")}
width="24px"
height="24px"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
src={badge.icon}
alt={badge.description}
- style={{ verticalAlign: "middle", marginLeft: "4px" }}
onClick={() =>
MaskedLinkStore.openUntrustedLink({
href: badge.redirectURL,
diff --git a/src/plugins/reviewDB/components/ReviewComponent.tsx b/src/plugins/reviewDB/components/ReviewComponent.tsx
index ac09b4c..ddcae0a 100644
--- a/src/plugins/reviewDB/components/ReviewComponent.tsx
+++ b/src/plugins/reviewDB/components/ReviewComponent.tsx
@@ -16,16 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { Settings } from "@api/Settings";
import { classes } from "@utils/misc";
import { LazyComponent } from "@utils/react";
import { filters, findBulk } from "@webpack";
import { Alerts, moment, Timestamp, UserStore } from "@webpack/common";
-import { Review } from "../entities/Review";
-import { deleteReview, reportReview } from "../Utils/ReviewDBAPI";
-import { canDeleteReview, openUserProfileModal, showToast } from "../Utils/Utils";
-import MessageButton from "./MessageButton";
+import { Review, ReviewType } from "../entities";
+import { deleteReview, reportReview } from "../reviewDbApi";
+import { settings } from "../settings";
+import { canDeleteReview, cl, openUserProfileModal, showToast } from "../utils";
+import { DeleteButton, ReportButton } from "./MessageButton";
import ReviewBadge from "./ReviewBadge";
export default LazyComponent(() => {
@@ -36,11 +36,13 @@ export default LazyComponent(() => {
{ container, isHeader },
{ avatar, clickable, username, messageContent, wrapper, cozy },
buttonClasses,
+ botTag
] = findBulk(
p("cozyMessage"),
p("container", "isHeader"),
p("avatar", "zalgo"),
p("button", "wrapper", "selected"),
+ p("botTag")
);
const dateFormat = new Intl.DateTimeFormat();
@@ -79,21 +81,21 @@ export default LazyComponent(() => {
}
return (
- <div className={classes(cozyMessage, wrapper, message, groupStart, cozy, "user-review")} style={
+ <div className={classes(cozyMessage, wrapper, message, groupStart, cozy, cl("review"))} style={
{
marginLeft: "0px",
- paddingLeft: "52px",
+ paddingLeft: "52px", // wth is this
paddingRight: "16px"
}
}>
- <div>
- <img
- className={classes(avatar, clickable)}
- onClick={openModal}
- src={review.sender.profilePhoto || "/assets/1f0bfc0865d324c2587920a7d80c609b.png?size=128"}
- style={{ left: "0px" }}
- />
+ <img
+ className={classes(avatar, clickable)}
+ onClick={openModal}
+ src={review.sender.profilePhoto || "/assets/1f0bfc0865d324c2587920a7d80c609b.png?size=128"}
+ style={{ left: "0px" }}
+ />
+ <div style={{ display: "inline-flex", justifyContent: "center", alignItems: "center" }}>
<span
className={classes(clickable, username)}
style={{ color: "var(--channels-default)", fontSize: "14px" }}
@@ -101,32 +103,45 @@ export default LazyComponent(() => {
>
{review.sender.username}
</span>
- {review.sender.badges.map(badge => <ReviewBadge {...badge} />)}
- {
- !Settings.plugins.ReviewDB.hideTimestamps && (
- <Timestamp timestamp={moment(review.timestamp * 1000)} >
- {dateFormat.format(review.timestamp * 1000)}
- </Timestamp>)
- }
+ {review.type === ReviewType.System && (
+ <span
+ className={classes(botTag.botTagVerified, botTag.botTagRegular, botTag.botTag, botTag.px, botTag.rem)}
+ style={{ marginLeft: "4px" }}>
+ <span className={botTag.botText}>
+ System
+ </span>
+ </span>
+ )}
+ </div>
+ {review.sender.badges.map(badge => <ReviewBadge {...badge} />)}
- <p
- className={classes(messageContent)}
- style={{ fontSize: 15, marginTop: 4, color: "var(--text-normal)" }}
- >
- {review.comment}
- </p>
+ {
+ !settings.store.hideTimestamps && review.type !== ReviewType.System && (
+ <Timestamp timestamp={moment(review.timestamp * 1000)} >
+ {dateFormat.format(review.timestamp * 1000)}
+ </Timestamp>)
+ }
+
+ <p
+ className={classes(messageContent)}
+ style={{ fontSize: 15, marginTop: 4, color: "var(--text-normal)" }}
+ >
+ {review.comment}
+ </p>
+ {review.id !== 0 && (
<div className={classes(container, isHeader, buttons)} style={{
padding: "0px",
}}>
<div className={buttonClasses.wrapper} >
- <MessageButton type="report" callback={reportRev} />
+ <ReportButton onClick={reportRev} />
+
{canDeleteReview(review, UserStore.getCurrentUser().id) && (
- <MessageButton type="delete" callback={delReview} />
+ <DeleteButton onClick={delReview} />
)}
</div>
</div>
- </div>
+ )}
</div>
);
};
diff --git a/src/plugins/reviewDB/components/ReviewModal.tsx b/src/plugins/reviewDB/components/ReviewModal.tsx
new file mode 100644
index 0000000..6e85dc2
--- /dev/null
+++ b/src/plugins/reviewDB/components/ReviewModal.tsx
@@ -0,0 +1,104 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import ErrorBoundary from "@components/ErrorBoundary";
+import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
+import { useForceUpdater } from "@utils/react";
+import { Paginator, Text, useRef, useState } from "@webpack/common";
+
+import { Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
+import { settings } from "../settings";
+import { cl } from "../utils";
+import ReviewComponent from "./ReviewComponent";
+import ReviewsView, { ReviewsInputComponent } from "./ReviewsView";
+
+function Modal({ modalProps, discordId, name }: { modalProps: any; discordId: string; name: string; }) {
+ const [data, setData] = useState<Response>();
+ const [signal, refetch] = useForceUpdater(true);
+ const [page, setPage] = useState(1);
+
+ const ref = useRef<HTMLDivElement>(null);
+
+ const reviewCount = data?.reviewCount;
+ const ownReview = data?.reviews.find(r => r.sender.discordID === settings.store.user?.discordID);
+
+ return (
+ <ErrorBoundary>
+ <ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
+ <ModalHeader>
+ <Text variant="heading-lg/semibold" className={cl("modal-header")}>
+ {name}'s Reviews
+ {!!reviewCount && <span> ({reviewCount} Reviews)</span>}
+ </Text>
+ <ModalCloseButton onClick={modalProps.onClose} />
+ </ModalHeader>
+
+ <ModalContent scrollerRef={ref}>
+ <div className={cl("modal-reviews")}>
+ <ReviewsView
+ discordId={discordId}
+ name={name}
+ page={page}
+ refetchSignal={signal}
+ onFetchReviews={setData}
+ scrollToTop={() => ref.current?.scrollTo({ top: 0, behavior: "smooth" })}
+ hideOwnReview
+ />
+ </div>
+ </ModalContent>
+
+ <ModalFooter className={cl("modal-footer")}>
+ <div>
+ {ownReview && (
+ <ReviewComponent
+ refetch={refetch}
+ review={ownReview}
+ />
+ )}
+ <ReviewsInputComponent
+ isAuthor={ownReview != null}
+ discordId={discordId}
+ name={name}
+ refetch={refetch}
+ />
+
+ {!!reviewCount && (
+ <Paginator
+ currentPage={page}
+ maxVisiblePages={5}
+ pageSize={REVIEWS_PER_PAGE}
+ totalCount={reviewCount}
+ onPageChange={setPage}
+ />
+ )}
+ </div>
+ </ModalFooter>
+ </ModalRoot>
+ </ErrorBoundary>
+ );
+}
+
+export function openReviewsModal(discordId: string, name: string) {
+ openModal(props => (
+ <Modal
+ modalProps={props}
+ discordId={discordId}
+ name={name}
+ />
+ ));
+}
diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx
index ff46cca..bd264fa 100644
--- a/src/plugins/reviewDB/components/ReviewsView.tsx
+++ b/src/plugins/reviewDB/components/ReviewsView.tsx
@@ -16,42 +16,113 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { Settings } from "@api/Settings";
import { classes } from "@utils/misc";
-import { useAwaiter } from "@utils/react";
+import { useAwaiter, useForceUpdater } from "@utils/react";
import { findByPropsLazy } from "@webpack";
-import { Forms, React, Text, UserStore } from "@webpack/common";
+import { Forms, React, UserStore } from "@webpack/common";
import type { KeyboardEvent } from "react";
-import { addReview, getReviews } from "../Utils/ReviewDBAPI";
-import { authorize, showToast } from "../Utils/Utils";
+import { Review } from "../entities";
+import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
+import { settings } from "../settings";
+import { authorize, cl, showToast } from "../utils";
import ReviewComponent from "./ReviewComponent";
const Classes = findByPropsLazy("inputDefault", "editable");
-export default function ReviewsView({ userId }: { userId: string; }) {
- const { token } = Settings.plugins.ReviewDB;
- const [refetchCount, setRefetchCount] = React.useState(0);
- const [reviews, _, isLoading] = useAwaiter(() => getReviews(userId), {
- fallbackValue: [],
- deps: [refetchCount],
+interface UserProps {
+ discordId: string;
+ name: string;
+}
+
+interface Props extends UserProps {
+ onFetchReviews(data: Response): void;
+ refetchSignal?: unknown;
+ showInput?: boolean;
+ page?: number;
+ scrollToTop?(): void;
+ hideOwnReview?: boolean;
+}
+
+export default function ReviewsView({
+ discordId,
+ name,
+ onFetchReviews,
+ refetchSignal,
+ scrollToTop,
+ page = 1,
+ showInput = false,
+ hideOwnReview = false,
+}: Props) {
+ const [signal, refetch] = useForceUpdater(true);
+
+ const [reviewData] = useAwaiter(() => getReviews(discordId, (page - 1) * REVIEWS_PER_PAGE), {
+ fallbackValue: null,
+ deps: [refetchSignal, signal, page],
+ onSuccess: data => {
+ scrollToTop?.();
+ onFetchReviews(data!);
+ }
});
- const username = UserStore.getUser(userId)?.username ?? "";
- const dirtyRefetch = () => setRefetchCount(refetchCount + 1);
+ if (!reviewData) return null;
+
+ return (
+ <>
+ <ReviewList
+ refetch={refetch}
+ reviews={reviewData!.reviews}
+ hideOwnReview={hideOwnReview}
+ />
+
+ {showInput && (
+ <ReviewsInputComponent
+ name={name}
+ discordId={discordId}
+ refetch={refetch}
+ isAuthor={reviewData!.reviews?.some(r => r.sender.discordID === UserStore.getCurrentUser().id)}
+ />
+ )}
+ </>
+ );
+}
+
+function ReviewList({ refetch, reviews, hideOwnReview }: { refetch(): void; reviews: Review[]; hideOwnReview: boolean; }) {
+ const myId = UserStore.getCurrentUser().id;
+
+ return (
+ <div className={cl("view")}>
+ {reviews?.map(review =>
+ (review.sender.discordID !== myId || !hideOwnReview) &&
+ <ReviewComponent
+ key={review.id}
+ review={review}
+ refetch={refetch}
+ />
+ )}
+
+ {reviews?.length === 0 && (
+ <Forms.FormText className={cl("placeholder")}>
+ Looks like nobody reviewed this user yet. You could be the first!
+ </Forms.FormText>
+ )}
+ </div>
+ );
+}
- if (isLoading) return null;
+export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) {
+ const { token } = settings.store;
function onKeyPress({ key, target }: KeyboardEvent<HTMLTextAreaElement>) {
if (key === "Enter") {
addReview({
- userid: userId,
+ userid: discordId,
comment: (target as HTMLInputElement).value,
star: -1
}).then(res => {
if (res?.success) {
(target as HTMLInputElement).value = ""; // clear the input
- dirtyRefetch();
+ refetch();
} else if (res?.message) {
showToast(res.message);
}
@@ -60,61 +131,27 @@ export default function ReviewsView({ userId }: { userId: string; }) {
}
return (
- <div className="vc-reviewdb-view">
- <Text
- tag="h2"
- variant="eyebrow"
- style={{
- marginBottom: "8px",
- color: "var(--header-primary)"
- }}
- >
- User Reviews
- </Text>
- {reviews?.map(review =>
- <ReviewComponent
- key={review.id}
- review={review}
- refetch={dirtyRefetch}
- />
- )}
- {reviews?.length === 0 && (
- <Forms.FormText style={{ paddingRight: "12px", paddingTop: "0px", paddingLeft: "0px", paddingBottom: "4px", fontWeight: "bold", fontStyle: "italic" }}>
- Looks like nobody reviewed this user yet. You could be the first!
- </Forms.FormText>
- )}
- <textarea
- className={classes(Classes.inputDefault, "enter-comment")}
- onKeyDownCapture={e => {
- if (e.key === "Enter") {
- e.preventDefault(); // prevent newlines
- }
- }}
- placeholder={
- token
- ? (reviews?.some(r => r.sender.discordID === UserStore.getCurrentUser().id)
- ? `Update review for @${username}`
- : `Review @${username}`)
- : "You need to authorize to review users!"
+ <textarea
+ className={classes(Classes.inputDefault, "enter-comment", cl("input"))}
+ onKeyDownCapture={e => {
+ if (e.key === "Enter") {
+ e.preventDefault(); // prevent newlines
}
- onKeyDown={onKeyPress}
- onClick={() => {
- if (!token) {
- showToast("Opening authorization window...");
- authorize();
- }
- }}
-
- style={{
- marginTop: "6px",
- resize: "none",
- marginBottom: "12px",
- overflow: "hidden",
- background: "transparent",
- border: "1px solid var(--profile-message-input-border-color)",
- fontSize: "14px",
- }}
- />
- </div>
+ }}
+ placeholder={
+ !token
+ ? "You need to authorize to review users!"
+ : isAuthor
+ ? `Update review for @${name}`
+ : `Review @${name}`
+ }
+ onKeyDown={onKeyPress}
+ onClick={() => {
+ if (!token) {
+ showToast("Opening authorization window...");
+ authorize();
+ }
+ }}
+ />
);
}
diff --git a/src/plugins/reviewDB/entities/User.ts b/src/plugins/reviewDB/entities.ts
index 2c59992..1415101 100644
--- a/src/plugins/reviewDB/entities/User.ts
+++ b/src/plugins/reviewDB/entities.ts
@@ -22,23 +22,55 @@ export const enum UserType {
Admin = 1
}
+export const enum ReviewType {
+ User = 0,
+ Server = 1,
+ Support = 2,
+ System = 3
+}
+
+export interface Badge {
+ name: string;
+ description: string;
+ icon: string;
+ redirectURL: string;
+ type: number;
+}
+
export interface BanInfo {
id: string;
discordID: string;
reviewID: number;
reviewContent: string;
- banEndDate: string;
+ banEndDate: number;
}
export interface ReviewDBUser {
- ID: number
- discordID: string
- username: string
- profilePhoto: string
- clientMod: string
- warningCount: number
- badges: any[]
- banInfo: BanInfo | null
- lastReviewID: number
- type: UserType
+ ID: number;
+ discordID: string;
+ username: string;
+ profilePhoto: string;
+ clientMod: string;
+ warningCount: number;
+ badges: any[];
+ banInfo: BanInfo | null;
+ lastReviewID: number;
+ type: UserType;
+}
+
+export interface ReviewAuthor {
+ id: number,
+ discordID: string,
+ username: string,
+ profilePhoto: string,
+ badges: Badge[];
+}
+
+export interface Review {
+ comment: string,
+ id: number,
+ star: number,
+ sender: ReviewAuthor,
+ timestamp: number;
+ type?: ReviewType;
}
diff --git a/src/plugins/reviewDB/entities/Badge.ts b/src/plugins/reviewDB/entities/Badge.ts
deleted file mode 100644
index ac33715..0000000
--- a/src/plugins/reviewDB/entities/Badge.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-
-export interface Badge {
- name: string;
- description: string;
- icon: string;
- redirectURL : string;
- type: number;
-}
diff --git a/src/plugins/reviewDB/entities/Review.ts b/src/plugins/reviewDB/entities/Review.ts
deleted file mode 100644
index e1f8380..0000000
--- a/src/plugins/reviewDB/entities/Review.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Badge } from "./Badge";
-
-export interface Sender {
- id : number,
- discordID: string,
- username: string,
- profilePhoto: string,
- badges: Badge[]
-}
-
-export interface Review {
- comment: string,
- id: number,
- star: number,
- sender: Sender,
- timestamp: number
-}
diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx
index 52ddb3b..0dcb7cd 100644
--- a/src/plugins/reviewDB/index.tsx
+++ b/src/plugins/reviewDB/index.tsx
@@ -18,23 +18,40 @@
import "./style.css";
-import { Settings } from "@api/Settings";
+import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import ErrorBoundary from "@components/ErrorBoundary";
+import ExpandableHeader from "@components/ExpandableHeader";
+import { OpenExternalIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
-import definePlugin, { OptionType } from "@utils/types";
-import { Alerts, Button } from "@webpack/common";
-import { User } from "discord-types/general";
+import definePlugin from "@utils/types";
+import { Alerts, Menu, useState } from "@webpack/common";
+import { Guild, User } from "discord-types/general";
+import { openReviewsModal } from "./components/ReviewModal";
import ReviewsView from "./components/ReviewsView";
-import { UserType } from "./entities/User";
-import { getCurrentUserInfo } from "./Utils/ReviewDBAPI";
-import { authorize, showToast } from "./Utils/Utils";
+import { UserType } from "./entities";
+import { getCurrentUserInfo } from "./reviewDbApi";
+import { settings } from "./settings";
+import { showToast } from "./utils";
+
+const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => {
+ children.push(
+ <Menu.MenuItem
+ label="View Reviews"
+ id="vc-rdb-server-reviews"
+ icon={OpenExternalIcon}
+ action={() => openReviewsModal(props.guild.id, props.guild.name)}
+ />
+ );
+};
export default definePlugin({
name: "ReviewDB",
description: "Review other users (Adds a new settings to profiles)",
authors: [Devs.mantikafasi, Devs.Ven],
+ settings,
+
patches: [
{
find: "disableBorderColor:!0",
@@ -45,100 +62,86 @@ export default definePlugin({
}
],
- options: {
- authorize: {
- type: OptionType.COMPONENT,
- description: "Authorize with ReviewDB",
- component: () => (
- <Button onClick={authorize}>
- Authorize with ReviewDB
- </Button>
- )
- },
- notifyReviews: {
- type: OptionType.BOOLEAN,
- description: "Notify about new reviews on startup",
- default: true,
- },
- showWarning: {
- type: OptionType.BOOLEAN,
- description: "Display warning to be respectful at the top of the reviews list",
- default: true,
- },
- hideTimestamps: {
- type: OptionType.BOOLEAN,
- description: "Hide timestamps on reviews",
- default: false,
- },
- website: {
- type: OptionType.COMPONENT,
- description: "ReviewDB website",
- component: () => (
- <Button onClick={() => {
- window.open("https://reviewdb.mantikafasi.dev");
- }}>
- ReviewDB website
- </Button>
- )
- },
- supportServer: {
- type: OptionType.COMPONENT,
- description: "ReviewDB Support Server",
- component: () => (
- <Button onClick={() => {
- window.open("https://discord.gg/eWPBSbvznt");
- }}>
- ReviewDB Support Server
- </Button>
- )
- },
- },
-
async start() {
- const settings = Settings.plugins.ReviewDB;
- if (!settings.notifyReviews || !settings.token) return;
+ const s = settings.store;
+ const { token, lastReviewId, notifyReviews } = s;
+
+ if (!notifyReviews || !token) return;
setTimeout(async () => {
- const user = await getCurrentUserInfo(settings.token);
- if (settings.lastReviewId < user.lastReviewID) {
- settings.lastReviewId = user.lastReviewID;
+ const user = await getCurrentUserInfo(token);
+ if (lastReviewId && lastReviewId < user.lastReviewID) {
+ s.lastReviewId = user.lastReviewID;
if (user.lastReviewID !== 0)
showToast("You have new reviews on your profile!");
}
- if (user.banInfo) {
- const endDate = new Date(user.banInfo.banEndDate);
- if (endDate > new Date() && (settings.user?.banInfo?.banEndDate ?? 0) < endDate) {
+ addContextMenuPatch("guild-header-popout", guildPopoutPatch);
+ if (user.banInfo) {
+ const endDate = new Date(user.banInfo.banEndDate).getTime();
+ if (endDate > Date.now() && (s.user?.banInfo?.banEndDate ?? 0) < endDate) {
Alerts.show({
title: "You have been banned from ReviewDB",
- body: <>
- <p>
- You are banned from ReviewDB {(user.type === UserType.Banned) ? "permanently" : "until " + endDate.toLocaleString()}
- </p>
- <p>
- Offending Review: {user.banInfo.reviewContent}
- </p>
- <p>
- Continued offenses will result in a permanent ban.
- </p>
- </>,
+ body: (
+ <>
+ <p>
+ You are banned from ReviewDB {
+ user.type === UserType.Banned
+ ? "permanently"
+ : "until " + endDate.toLocaleString()
+ }
+ </p>
+ {user.banInfo.reviewContent && (
+ <p>Offending Review: {user.banInfo.reviewContent}</p>
+ )}
+ <p>Continued offenses will result in a permanent ban.</p>
+ </>
+ ),
cancelText: "Appeal",
confirmText: "Ok",
- onCancel: () => {
- window.open("https://forms.gle/Thj3rDYaMdKoMMuq6");
- }
+ onCancel: () =>
+ VencordNative.native.openExternal(
+ "https://reviewdb.mantikafasi.dev/api/redirect?"
+ + new URLSearchParams({
+ token: settings.store.token!,
+ page: "dashboard/appeal"
+ })
+ )
});
}
}
- settings.user = user;
+ s.user = user;
}, 4000);
},
- getReviewsComponent: (user: User) => (
- <ErrorBoundary message="Failed to render Reviews">
- <ReviewsView userId={user.id} />
- </ErrorBoundary>
- )
+ stop() {
+ removeContextMenuPatch("guild-header-popout", guildPopoutPatch);
+ },
+
+ getReviewsComponent: ErrorBoundary.wrap((user: User) => {
+ const [reviewCount, setReviewCount] = useState<number>();
+
+ return (
+ <ExpandableHeader
+ headerText="User Reviews"
+ onMoreClick={() => openReviewsModal(user.id, user.username)}
+ moreTooltipText={
+ reviewCount && reviewCount > 50
+ ? `View all ${reviewCount} reviews`
+ : "Open Review Modal"
+ }
+ onDropDownClick={state => settings.store.reviewsDropdownState = !state}
+ defaultState={settings.store.reviewsDropdownState}
+ >
+ <ReviewsView
+ discordId={user.id}
+ name={user.username}
+ onFetchReviews={r => setReviewCount(r.reviewCount)}
+ showInput
+ />
+ </ExpandableHeader>
+ );
+ }, { message: "Failed to render Reviews" })
});
diff --git a/src/plugins/reviewDB/Utils/ReviewDBAPI.ts b/src/plugins/reviewDB/reviewDbApi.ts
index 62c89f5..e3ab8a8 100644
--- a/src/plugins/reviewDB/Utils/ReviewDBAPI.ts
+++ b/src/plugins/reviewDB/reviewDbApi.ts
@@ -16,54 +16,74 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { Settings } from "@api/Settings";
-import { Review } from "../entities/Review";
-import { ReviewDBUser } from "../entities/User";
-import { authorize, showToast } from "./Utils";
+import { Review, ReviewDBUser } from "./entities";
+import { settings } from "./settings";
+import { authorize, showToast } from "./utils";
const API_URL = "https://manti.vendicated.dev";
-const getToken = () => Settings.plugins.ReviewDB.token;
+export const REVIEWS_PER_PAGE = 50;
-interface Response {
+export interface Response {
success: boolean,
message: string;
reviews: Review[];
updated: boolean;
+ hasNextPage: boolean;
+ reviewCount: number;
}
const WarningFlag = 0b00000010;
-export async function getReviews(id: string): Promise<Review[]> {
- var flags = 0;
- if (!Settings.plugins.ReviewDB.showWarning) flags |= WarningFlag;
- const req = await fetch(API_URL + `/api/reviewdb/users/${id}/reviews?flags=${flags}`);
+export async function getReviews(id: string, offset = 0): Promise<Response> {
+ let flags = 0;
+ if (!settings.store.showWarning) flags |= WarningFlag;
+
+ const params = new URLSearchParams({
+ flags: String(flags),
+ offset: String(offset)
+ });
+ const req = await fetch(`${API_URL}/api/reviewdb/users/${id}/reviews?${params}`);
+
+ const res = (req.status === 200)
+ ? await req.json() as Response
+ : {
+ success: false,
+ message: "An Error occured while fetching reviews. Please try again later.",
+ reviews: [],
+ updated: false,
+ hasNextPage: false,
+ reviewCount: 0
+ };
- const res = (req.status === 200) ? await req.json() as Response : { success: false, message: "An Error occured while fetching reviews. Please try again later.", reviews: [], updated: false };
if (!res.success) {
showToast(res.message);
- return [
- {
- id: 0,
- comment: "An Error occured while fetching reviews. Please try again later.",
- star: 0,
- timestamp: 0,
- sender: {
+ return {
+ ...res,
+ reviews: [
+ {
id: 0,
- username: "Error",
- profilePhoto: "https://cdn.discordapp.com/attachments/1045394533384462377/1084900598035513447/646808599204593683.png?size=128",
- discordID: "0",
- badges: []
+ comment: "An Error occured while fetching reviews. Please try again later.",
+ star: 0,
+ timestamp: 0,
+ sender: {
+ id: 0,
+ username: "Error",
+ profilePhoto: "https://cdn.discordapp.com/attachments/1045394533384462377/1084900598035513447/646808599204593683.png?size=128",
+ discordID: "0",
+ badges: []
+ }
}
- }
- ];
+ ]
+ };
}
- return res.reviews;
+
+ return res;
}
export async function addReview(review: any): Promise<Response | null> {
- review.token = getToken();
+ review.token = settings.store.token;
if (!review.token) {
showToast("Please authorize to add a review.");
@@ -93,7 +113,7 @@ export function deleteReview(id: number): Promise<Response> {
Accept: "application/json",
}),
body: JSON.stringify({
- token: getToken(),
+ token: settings.store.token,
reviewid: id
})
}).then(r => r.json());
@@ -108,16 +128,16 @@ export async function reportReview(id: number) {
}),
body: JSON.stringify({
reviewid: id,
- token: getToken()
+ token: settings.store.token
})
}).then(r => r.json()) as Response;
- showToast(await res.message);
+
+ showToast(res.message);
}
export function getCurrentUserInfo(token: string): Promise<ReviewDBUser> {
return fetch(API_URL + "/api/reviewdb/users", {
body: JSON.stringify({ token }),
method: "POST",
- })
- .then(r => r.json());
+ }).then(r => r.json());
}
diff --git a/src/plugins/reviewDB/settings.tsx b/src/plugins/reviewDB/settings.tsx
new file mode 100644
index 0000000..fb5b362
--- /dev/null
+++ b/src/plugins/reviewDB/settings.tsx
@@ -0,0 +1,82 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { definePluginSettings } from "@api/Settings";
+import { OptionType } from "@utils/types";
+import { Button } from "@webpack/common";
+
+import { ReviewDBUser } from "./entities";
+import { authorize } from "./utils";
+
+export const settings = definePluginSettings({
+ authorize: {
+ type: OptionType.COMPONENT,
+ description: "Authorize with ReviewDB",
+ component: () => (
+ <Button onClick={authorize}>
+ Authorize with ReviewDB
+ </Button>
+ )
+ },
+ notifyReviews: {
+ type: OptionType.BOOLEAN,
+ description: "Notify about new reviews on startup",
+ default: true,
+ },
+ showWarning: {
+ type: OptionType.BOOLEAN,
+ description: "Display warning to be respectful at the top of the reviews list",
+ default: true,
+ },
+ hideTimestamps: {
+ type: OptionType.BOOLEAN,
+ description: "Hide timestamps on reviews",
+ default: false,
+ },
+ website: {
+ type: OptionType.COMPONENT,
+ description: "ReviewDB website",
+ component: () => (
+ <Button onClick={() => {
+ let url = "https://reviewdb.mantikafasi.dev/";
+ if (settings.store.token)
+ url += "/api/redirect?token=" + encodeURIComponent(settings.store.token);
+
+ VencordNative.native.openExternal(url);
+ }}>
+ ReviewDB website
+ </Button>
+ )
+ },
+ supportServer: {
+ type: OptionType.COMPONENT,
+ description: "ReviewDB Support Server",
+ component: () => (
+ <Button onClick={() => {
+ VencordNative.native.openExternal("https://discord.gg/eWPBSbvznt");
+ }}>
+ ReviewDB Support Server
+ </Button>
+ )
+ }
+}).withPrivateSettings<{
+ token?: string;
+ user?: ReviewDBUser;
+ lastReviewId?: number;
+ reviewsDropdownState?: boolean;
+}>();
diff --git a/src/plugins/reviewDB/style.css b/src/plugins/reviewDB/style.css
index 6171abe..83cf087 100644
--- a/src/plugins/reviewDB/style.css
+++ b/src/plugins/reviewDB/style.css
@@ -1,3 +1,51 @@
-[class|="section"]:not([class|="lastSection"]) + .vc-reviewdb-view {
+[class|="section"]:not([class|="lastSection"]) + .vc-rdb-view {
margin-top: 12px;
}
+
+.vc-rdb-badge {
+ vertical-align: middle;
+ margin-left: 4px;
+}
+
+.vc-rdb-input {
+ margin-top: 6px;
+ margin-bottom: 12px;
+ resize: none;
+ overflow: hidden;
+ background: transparent;
+ border: 1px solid var(--profile-message-input-border-color);
+ font-size: 14px;
+}
+
+.vc-rdb-placeholder {
+ margin-bottom: 4px;
+ font-weight: bold;
+ font-style: italic;
+ color: var(--text-muted);
+}
+
+.vc-rdb-modal-footer {
+ padding: 0;
+}
+
+.vc-rdb-modal-footer > div {
+ width: 100%;
+ margin: 6px 16px;
+}
+
+.vc-rdb-modal-footer .vc-rdb-input {
+ margin-bottom: 0;
+ background: var(--input-background);
+}
+
+.vc-rdb-modal-footer [class|="pageControlContainer"] {
+ margin-top: 0;
+}
+
+.vc-rdb-modal-header {
+ flex-grow: 1;
+}
+
+.vc-rdb-modal-reviews {
+ margin-top: 16px;
+}
diff --git a/src/plugins/reviewDB/Utils/Utils.tsx b/src/plugins/reviewDB/utils.tsx
index 42426b2..67d043d 100644
--- a/src/plugins/reviewDB/Utils/Utils.tsx
+++ b/src/plugins/reviewDB/utils.tsx
@@ -16,14 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { Settings } from "@api/Settings";
+import { classNameFactory } from "@api/Styles";
import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal";
import { findByProps } from "@webpack";
import { FluxDispatcher, React, SelectedChannelStore, Toasts, UserUtils } from "@webpack/common";
-import { Review } from "../entities/Review";
-import { UserType } from "../entities/User";
+import { Review, UserType } from "./entities";
+import { settings } from "./settings";
+
+export const cl = classNameFactory("vc-rdb-");
export async function openUserProfileModal(userId: string) {
await UserUtils.fetchUser(userId);
@@ -57,7 +59,7 @@ export function authorize(callback?: any) {
});
const { token, success } = await res.json();
if (success) {
- Settings.plugins.ReviewDB.token = token;
+ settings.store.token = token;
showToast("Successfully logged in!");
callback?.();
} else if (res.status === 1) {
@@ -82,8 +84,9 @@ export function showToast(text: string) {
});
}
-export const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
-
export function canDeleteReview(review: Review, userId: string) {
- if (review.sender.discordID === userId || Settings.plugins.ReviewDB.user?.type === UserType.Admin) return true;
+ return (
+ review.sender.discordID === userId
+ || settings.store.user?.type === UserType.Admin
+ );
}