diff --git a/src/api/Settings.ts b/src/api/Settings.ts index e481e48c..2f786681 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 00000000..14e291b0 --- /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 00000000..1cbce4f2 --- /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 ab9bfeab..37d9d0f4 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/components/MessageButton.tsx b/src/plugins/reviewDB/components/MessageButton.tsx index 3b8308ab..176f4d62 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 8c013cd0..e65dff24 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 ac09b4cc..ddcae0aa 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 00000000..6e85dc29 --- /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 ff46ccaa..bd264fa6 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! - - )} -