aboutsummaryrefslogtreecommitdiff
path: root/src/plugins/reviewDB/components
diff options
context:
space:
mode:
authorManti <67705577+mantikafasi@users.noreply.github.com>2023-05-28 23:03:06 +0300
committerGitHub <noreply@github.com>2023-05-28 22:03:06 +0200
commit3e3d05fc26a634b17549c9473bd8aeebb7dec4ec (patch)
treea3591b0b0dea888f24d1ce8e58584b4447f62dde /src/plugins/reviewDB/components
parent6300198a5463ab38da81906bda634addf4c8a369 (diff)
downloadVencord-3e3d05fc26a634b17549c9473bd8aeebb7dec4ec.tar.gz
Vencord-3e3d05fc26a634b17549c9473bd8aeebb7dec4ec.tar.bz2
Vencord-3e3d05fc26a634b17549c9473bd8aeebb7dec4ec.zip
ReviewDB: Add Review Modal & Pagination (#1174)
Co-authored-by: V <vendicated@riseup.net>
Diffstat (limited to 'src/plugins/reviewDB/components')
-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
5 files changed, 295 insertions, 122 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();
+ }
+ }}
+ />
);
}