From 3e3d05fc26a634b17549c9473bd8aeebb7dec4ec Mon Sep 17 00:00:00 2001 From: Manti <67705577+mantikafasi@users.noreply.github.com> Date: Sun, 28 May 2023 23:03:06 +0300 Subject: ReviewDB: Add Review Modal & Pagination (#1174) Co-authored-by: V --- src/api/Settings.ts | 5 + src/components/ExpandableHeader.css | 12 ++ src/components/ExpandableHeader.tsx | 108 +++++++++++++ .../components/UserPermissions.tsx | 110 +++++-------- src/plugins/reviewDB/Utils/ReviewDBAPI.ts | 123 -------------- src/plugins/reviewDB/Utils/Utils.tsx | 89 ---------- src/plugins/reviewDB/components/MessageButton.tsx | 54 ++++--- src/plugins/reviewDB/components/ReviewBadge.tsx | 5 +- .../reviewDB/components/ReviewComponent.tsx | 75 +++++---- src/plugins/reviewDB/components/ReviewModal.tsx | 104 ++++++++++++ src/plugins/reviewDB/components/ReviewsView.tsx | 179 +++++++++++++-------- src/plugins/reviewDB/entities.ts | 76 +++++++++ src/plugins/reviewDB/entities/Badge.ts | 26 --- src/plugins/reviewDB/entities/Review.ts | 35 ---- src/plugins/reviewDB/entities/User.ts | 44 ----- src/plugins/reviewDB/index.tsx | 171 ++++++++++---------- src/plugins/reviewDB/reviewDbApi.ts | 143 ++++++++++++++++ src/plugins/reviewDB/settings.tsx | 82 ++++++++++ src/plugins/reviewDB/style.css | 50 +++++- src/plugins/reviewDB/utils.tsx | 92 +++++++++++ src/utils/react.tsx | 13 +- src/utils/types.ts | 2 + src/webpack/common/components.ts | 3 +- src/webpack/common/types/components.d.ts | 10 ++ 24 files changed, 1010 insertions(+), 601 deletions(-) create mode 100644 src/components/ExpandableHeader.css create mode 100644 src/components/ExpandableHeader.tsx delete mode 100644 src/plugins/reviewDB/Utils/ReviewDBAPI.ts delete mode 100644 src/plugins/reviewDB/Utils/Utils.tsx create mode 100644 src/plugins/reviewDB/components/ReviewModal.tsx create mode 100644 src/plugins/reviewDB/entities.ts delete mode 100644 src/plugins/reviewDB/entities/Badge.ts delete mode 100644 src/plugins/reviewDB/entities/Review.ts delete mode 100644 src/plugins/reviewDB/entities/User.ts create mode 100644 src/plugins/reviewDB/reviewDbApi.ts create mode 100644 src/plugins/reviewDB/settings.tsx create mode 100644 src/plugins/reviewDB/utils.tsx diff --git a/src/api/Settings.ts b/src/api/Settings.ts index e481e48..2f78668 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -266,7 +266,12 @@ export function definePluginSettings() { + return this as DefinedSettings & { store: T; }; + } }; + return definedSettings; } diff --git a/src/components/ExpandableHeader.css b/src/components/ExpandableHeader.css new file mode 100644 index 0000000..14e291b --- /dev/null +++ b/src/components/ExpandableHeader.css @@ -0,0 +1,12 @@ +.vc-expandableheader-center-flex { + display: flex; + justify-items: center; + align-items: center; +} + +.vc-expandableheader-btn { + all: unset; + cursor: pointer; + width: 24px; + height: 24px; +} diff --git a/src/components/ExpandableHeader.tsx b/src/components/ExpandableHeader.tsx new file mode 100644 index 0000000..1cbce4f --- /dev/null +++ b/src/components/ExpandableHeader.tsx @@ -0,0 +1,108 @@ +/* + * 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 . +*/ + +import { classNameFactory } from "@api/Styles"; +import { Text, Tooltip, useState } from "@webpack/common"; +export const cl = classNameFactory("vc-expandableheader-"); +import "./ExpandableHeader.css"; + +export interface ExpandableHeaderProps { + onMoreClick?: () => void; + moreTooltipText?: string; + onDropDownClick?: (state: boolean) => void; + defaultState?: boolean; + headerText: string; + children: React.ReactNode; + buttons?: React.ReactNode[]; +} + +export default function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) { + const [showContent, setShowContent] = useState(defaultState); + + return ( + <> +
+ + {headerText} + + +
+ { + buttons ?? null + } + + { + onMoreClick && // only show more button if callback is provided + + {tooltipProps => ( + + )} + + } + + + + {tooltipProps => ( + + )} + +
+
+ {showContent && children} + + ); +} diff --git a/src/plugins/permissionsViewer/components/UserPermissions.tsx b/src/plugins/permissionsViewer/components/UserPermissions.tsx index ab9bfea..37d9d0f 100644 --- a/src/plugins/permissionsViewer/components/UserPermissions.tsx +++ b/src/plugins/permissionsViewer/components/UserPermissions.tsx @@ -17,10 +17,11 @@ */ import ErrorBoundary from "@components/ErrorBoundary"; +import ExpandableHeader from "@components/ExpandableHeader"; import { proxyLazy } from "@utils/lazy"; import { classes } from "@utils/misc"; import { filters, findBulk } from "@webpack"; -import { i18n, PermissionsBits, Text, Tooltip, useMemo, UserStore, useState } from "@webpack/common"; +import { i18n, PermissionsBits, Text, Tooltip, useMemo, UserStore } from "@webpack/common"; import type { Guild, GuildMember } from "discord-types/general"; import { PermissionsSortOrder, settings } from ".."; @@ -47,7 +48,6 @@ const Classes = proxyLazy(() => { function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildMember: GuildMember; }) { const stns = settings.use(["permissionsSortOrder"]); - const [viewPermissions, setViewPermissions] = useState(settings.store.defaultPermissionsDropdownState); const [rolePermissions, userPermissions] = useMemo(() => { const userPermissions: UserPermissions = []; @@ -97,78 +97,40 @@ function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildM const { root, role, roleRemoveButton, roleNameOverflow, roles, rolePill, rolePillBorder, roleCircle, roleName } = Classes; return ( -
-
- Permissions - -
- - {tooltipProps => ( - - )} - - - - {tooltipProps => ( - - )} - - - - {tooltipProps => ( - - )} - -
-
- - {viewPermissions && userPermissions.length > 0 && ( + + + + )} + ) + ]}> + {userPermissions.length > 0 && (
{userPermissions.map(({ permission, roleColor }) => (
@@ -190,7 +152,7 @@ function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildM ))}
)} -
+ ); } diff --git a/src/plugins/reviewDB/Utils/ReviewDBAPI.ts b/src/plugins/reviewDB/Utils/ReviewDBAPI.ts deleted file mode 100644 index 62c89f5..0000000 --- a/src/plugins/reviewDB/Utils/ReviewDBAPI.ts +++ /dev/null @@ -1,123 +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 . -*/ - -import { Settings } from "@api/Settings"; - -import { Review } from "../entities/Review"; -import { ReviewDBUser } from "../entities/User"; -import { authorize, showToast } from "./Utils"; - -const API_URL = "https://manti.vendicated.dev"; - -const getToken = () => Settings.plugins.ReviewDB.token; - -interface Response { - success: boolean, - message: string; - reviews: Review[]; - updated: boolean; -} - -const WarningFlag = 0b00000010; - -export async function getReviews(id: string): Promise { - var flags = 0; - if (!Settings.plugins.ReviewDB.showWarning) flags |= WarningFlag; - const req = await fetch(API_URL + `/api/reviewdb/users/${id}/reviews?flags=${flags}`); - - 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: { - id: 0, - username: "Error", - profilePhoto: "https://cdn.discordapp.com/attachments/1045394533384462377/1084900598035513447/646808599204593683.png?size=128", - discordID: "0", - badges: [] - } - } - ]; - } - return res.reviews; -} - -export async function addReview(review: any): Promise { - review.token = getToken(); - - if (!review.token) { - showToast("Please authorize to add a review."); - authorize(); - return null; - } - - return fetch(API_URL + `/api/reviewdb/users/${review.userid}/reviews`, { - method: "PUT", - body: JSON.stringify(review), - headers: { - "Content-Type": "application/json", - } - }) - .then(r => r.json()) - .then(res => { - showToast(res.message); - return res ?? null; - }); -} - -export function deleteReview(id: number): Promise { - return fetch(API_URL + `/api/reviewdb/users/${id}/reviews`, { - method: "DELETE", - headers: new Headers({ - "Content-Type": "application/json", - Accept: "application/json", - }), - body: JSON.stringify({ - token: getToken(), - reviewid: id - }) - }).then(r => r.json()); -} - -export async function reportReview(id: number) { - const res = await fetch(API_URL + "/api/reviewdb/reports", { - method: "PUT", - headers: new Headers({ - "Content-Type": "application/json", - Accept: "application/json", - }), - body: JSON.stringify({ - reviewid: id, - token: getToken() - }) - }).then(r => r.json()) as Response; - showToast(await res.message); -} - -export function getCurrentUserInfo(token: string): Promise { - return fetch(API_URL + "/api/reviewdb/users", { - body: JSON.stringify({ token }), - method: "POST", - }) - .then(r => r.json()); -} diff --git a/src/plugins/reviewDB/Utils/Utils.tsx b/src/plugins/reviewDB/Utils/Utils.tsx deleted file mode 100644 index 42426b2..0000000 --- a/src/plugins/reviewDB/Utils/Utils.tsx +++ /dev/null @@ -1,89 +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 . -*/ - -import { Settings } from "@api/Settings"; -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"; - -export async function openUserProfileModal(userId: string) { - await UserUtils.fetchUser(userId); - - await FluxDispatcher.dispatch({ - type: "USER_PROFILE_MODAL_OPEN", - userId, - channelId: SelectedChannelStore.getChannelId(), - analyticsLocation: "Explosive Hotel" - }); -} - -export function authorize(callback?: any) { - const { OAuth2AuthorizeModal } = findByProps("OAuth2AuthorizeModal"); - - openModal((props: any) => - { - try { - const url = new URL(u); - url.searchParams.append("clientMod", "vencord"); - const res = await fetch(url, { - headers: new Headers({ Accept: "application/json" }) - }); - const { token, success } = await res.json(); - if (success) { - Settings.plugins.ReviewDB.token = token; - showToast("Successfully logged in!"); - callback?.(); - } else if (res.status === 1) { - showToast("An Error occurred while logging in."); - } - } catch (e) { - new Logger("ReviewDB").error("Failed to authorize", e); - } - }} - /> - ); -} - -export function showToast(text: string) { - Toasts.show({ - type: Toasts.Type.MESSAGE, - message: text, - id: Toasts.genId(), - options: { - position: Toasts.Position.BOTTOM - }, - }); -} - -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; -} 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" - ? ( -
- - - +export function DeleteButton({ onClick }: { onClick(): void; }) { + return ( + + {props => ( +
+ + +
- ) - : ( -
props.callback()}> - - + )} + + ); +} + +export function ReportButton({ onClick }: { onClick(): void; }) { + return ( + + {props => ( +
+ +
- ); - }; -}); + )} + + ); +} 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 }) => ( {badge.description} 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 . */ -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 ( -
-
- + +
{ > {review.sender.username} - {review.sender.badges.map(badge => )} - { - !Settings.plugins.ReviewDB.hideTimestamps && ( - - {dateFormat.format(review.timestamp * 1000)} - ) - } + {review.type === ReviewType.System && ( + + + System + + + )} +
+ {review.sender.badges.map(badge => )} -

- {review.comment} -

+ { + !settings.store.hideTimestamps && review.type !== ReviewType.System && ( + + {dateFormat.format(review.timestamp * 1000)} + ) + } + +

+ {review.comment} +

+ {review.id !== 0 && (
- + + {canDeleteReview(review, UserStore.getCurrentUser().id) && ( - + )}
-
+ )}
); }; 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 . +*/ + +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(); + const [signal, refetch] = useForceUpdater(true); + const [page, setPage] = useState(1); + + const ref = useRef(null); + + const reviewCount = data?.reviewCount; + const ownReview = data?.reviews.find(r => r.sender.discordID === settings.store.user?.discordID); + + return ( + + + + + {name}'s Reviews + {!!reviewCount && ({reviewCount} Reviews)} + + + + + +
+ ref.current?.scrollTo({ top: 0, behavior: "smooth" })} + hideOwnReview + /> +
+
+ + +
+ {ownReview && ( + + )} + + + {!!reviewCount && ( + + )} +
+
+
+
+ ); +} + +export function openReviewsModal(discordId: string, name: string) { + openModal(props => ( + + )); +} 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 . */ -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 ( + <> + + + {showInput && ( + r.sender.discordID === UserStore.getCurrentUser().id)} + /> + )} + + ); +} + +function ReviewList({ refetch, reviews, hideOwnReview }: { refetch(): void; reviews: Review[]; hideOwnReview: boolean; }) { + const myId = UserStore.getCurrentUser().id; + + return ( +
+ {reviews?.map(review => + (review.sender.discordID !== myId || !hideOwnReview) && + + )} + + {reviews?.length === 0 && ( + + Looks like nobody reviewed this user yet. You could be the first! + + )} +
+ ); +} - 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) { 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 ( -
- - User Reviews - - {reviews?.map(review => - - )} - {reviews?.length === 0 && ( - - Looks like nobody reviewed this user yet. You could be the first! - - )} -