diff options
Diffstat (limited to 'src/plugins/reviewDB/components')
| -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 |
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(); + } + }} + /> ); } |
