diff options
Diffstat (limited to 'src/plugins/reviewDB')
-rw-r--r-- | src/plugins/reviewDB/components/MessageButton.tsx | 54 | ||||
-rw-r--r-- | src/plugins/reviewDB/components/ReviewBadge.tsx | 5 | ||||
-rw-r--r-- | src/plugins/reviewDB/components/ReviewComponent.tsx | 75 | ||||
-rw-r--r-- | src/plugins/reviewDB/components/ReviewModal.tsx | 104 | ||||
-rw-r--r-- | src/plugins/reviewDB/components/ReviewsView.tsx | 179 | ||||
-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.ts | 26 | ||||
-rw-r--r-- | src/plugins/reviewDB/entities/Review.ts | 35 | ||||
-rw-r--r-- | src/plugins/reviewDB/index.tsx | 171 | ||||
-rw-r--r-- | src/plugins/reviewDB/reviewDbApi.ts (renamed from src/plugins/reviewDB/Utils/ReviewDBAPI.ts) | 82 | ||||
-rw-r--r-- | src/plugins/reviewDB/settings.tsx | 82 | ||||
-rw-r--r-- | src/plugins/reviewDB/style.css | 50 | ||||
-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 + ); } |