From 89a6c575c9cfe6a6fa72def404220817e008cfea Mon Sep 17 00:00:00 2001 From: V Date: Thu, 18 May 2023 05:11:06 +0200 Subject: [PATCH 001/735] lastfm: Fix discord application --- src/plugins/lastfm.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/lastfm.tsx b/src/plugins/lastfm.tsx index 380d1ea97..19620e0c0 100644 --- a/src/plugins/lastfm.tsx +++ b/src/plugins/lastfm.tsx @@ -72,7 +72,7 @@ enum ActivityFlag { INSTANCE = 1 << 0, } -const applicationId = "1043533871037284423"; +const applicationId = "1108588077900898414"; const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f"; const logger = new Logger("LastFMRichPresence"); @@ -167,6 +167,7 @@ export default definePlugin({ settings, start() { + this.updatePresence(); this.updateInterval = setInterval(() => { this.updatePresence(); }, 16000); }, @@ -198,7 +199,7 @@ export default definePlugin({ const trackData = json.recenttracks?.track[0]; - if (!trackData || !trackData["@attr"]?.nowplaying) + if (!trackData?.["@attr"]?.nowplaying) return null; // why does the json api have xml structure From ec091a79591cf9619dd589102f5b338827b70ad8 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 19 May 2023 21:24:56 -0300 Subject: [PATCH 002/735] Fix SHC broken patches; Sort PermViewer channel overwrites roles (#1166) --- src/plugins/permissionsViewer/index.tsx | 6 +++--- src/plugins/permissionsViewer/utils.ts | 16 +++++++++++++++- .../components/HiddenChannelLockScreen.tsx | 5 +++-- src/plugins/showHiddenChannels/index.tsx | 10 +++++----- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/plugins/permissionsViewer/index.tsx b/src/plugins/permissionsViewer/index.tsx index 208fef381..480efc150 100644 --- a/src/plugins/permissionsViewer/index.tsx +++ b/src/plugins/permissionsViewer/index.tsx @@ -27,7 +27,7 @@ import type { Guild, GuildMember } from "discord-types/general"; import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "./components/RolesAndUsersPermissions"; import UserPermissions from "./components/UserPermissions"; -import { getSortedRoles } from "./utils"; +import { getSortedRoles, sortPermissionOverwrites } from "./utils"; export const enum PermissionsSortOrder { HighestRole, @@ -94,12 +94,12 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) { case MenuItemParentType.Channel: { const channel = ChannelStore.getChannel(id!); - permissions = Object.values(channel.permissionOverwrites).map(({ id, allow, deny, type }) => ({ + permissions = sortPermissionOverwrites(Object.values(channel.permissionOverwrites).map(({ id, allow, deny, type }) => ({ type: type as PermissionType, id, overwriteAllow: allow, overwriteDeny: deny - })); + })), guildId); header = channel.name; diff --git a/src/plugins/permissionsViewer/utils.ts b/src/plugins/permissionsViewer/utils.ts index b74714797..06f293b03 100644 --- a/src/plugins/permissionsViewer/utils.ts +++ b/src/plugins/permissionsViewer/utils.ts @@ -18,11 +18,12 @@ import { classNameFactory } from "@api/Styles"; import { wordsToTitle } from "@utils/text"; -import { i18n, Parser } from "@webpack/common"; +import { GuildStore, i18n, Parser } from "@webpack/common"; import { Guild, GuildMember, Role } from "discord-types/general"; import type { ReactNode } from "react"; import { PermissionsSortOrder, settings } from "."; +import { PermissionType } from "./components/RolesAndUsersPermissions"; export const cl = classNameFactory("vc-permviewer-"); @@ -82,3 +83,16 @@ export function sortUserRoles(roles: Role[]) { return roles; } } + +export function sortPermissionOverwrites(overwrites: T[], guildId: string) { + const guild = GuildStore.getGuild(guildId); + + return overwrites.sort((a, b) => { + if (a.type !== PermissionType.Role || b.type !== PermissionType.Role) return 0; + + const roleA = guild.roles[a.id]; + const roleB = guild.roles[b.id]; + + return roleB.position - roleA.position; + }); +} diff --git a/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx index 506fbe7b6..d01efecf5 100644 --- a/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx +++ b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx @@ -26,6 +26,7 @@ import type { Channel } from "discord-types/general"; import type { ComponentType } from "react"; import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "../../permissionsViewer/components/RolesAndUsersPermissions"; +import { sortPermissionOverwrites } from "../../permissionsViewer/utils"; import { settings, VIEW_CHANNEL } from ".."; enum SortOrderTypes { @@ -169,12 +170,12 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) { } if (Settings.plugins.PermissionsViewer.enabled) { - setPermissions(Object.values(permissionOverwrites).map(overwrite => ({ + setPermissions(sortPermissionOverwrites(Object.values(permissionOverwrites).map(overwrite => ({ type: overwrite.type as PermissionType, id: overwrite.id, overwriteAllow: overwrite.allow, overwriteDeny: overwrite.deny - }))); + })), guild_id)); } }, [channelId]); diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index 667f710e3..634b5cd63 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -107,13 +107,13 @@ export default definePlugin({ }, { // Prevent Discord from trying to connect to hidden channels - match: /(?=\|\|\i\.default\.selectVoiceChannel\((\i)\.id\))/, - replace: (_, channel) => `||$self.isHiddenChannel(${channel})` + match: /if\(!\i&&!\i(?=.{0,50}?selectVoiceChannel\((\i)\.id\))/, + replace: (m, channel) => `${m}&&!$self.isHiddenChannel(${channel})` }, { // Make Discord show inside the channel if clicking on a hidden or locked channel - match: /(?<=\|\|\i\.default\.selectVoiceChannel\((\i)\.id\);!__OVERLAY__&&\()/, - replace: (_, channel) => `$self.isHiddenChannel(${channel},true)||` + match: /!__OVERLAY__&&\((?<=selectVoiceChannel\((\i)\.id\).+?)/, + replace: (m, channel) => `${m}$self.isHiddenChannel(${channel},true)||` } ] }, @@ -195,7 +195,7 @@ export default definePlugin({ replace: (_, pushNotificationButtonExpression, channel) => `if($self.isHiddenChannel(${channel})){${pushNotificationButtonExpression}break;}` }, { - match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_FORUM:if\(!\i\){)(?=.+?;(.+?{channel:(\i)},"notifications"\)\)))/, + match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_FORUM:.+?if\(!\i\){)(?=.+?;(.+?{channel:(\i)},"notifications"\)\)))/, replace: (_, pushNotificationButtonExpression, channel) => `if($self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}` }, { From 184c03b28e6df72f301f91862dc8bdfb5f074fcb Mon Sep 17 00:00:00 2001 From: V Date: Tue, 23 May 2023 01:55:39 +0200 Subject: [PATCH 003/735] PluginModal: Anonymise authors (#1176) --- src/components/PluginSettings/PluginModal.tsx | 28 +++++++++++++++---- .../PluginSettings/userPopoutHideBotTag.css | 3 ++ src/plugins/apiBadges.tsx | 6 ++-- src/plugins/supportHelper.tsx | 4 +-- src/utils/constants.ts | 27 ++++++++++++++++-- src/utils/misc.tsx | 4 +++ 6 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 src/components/PluginSettings/userPopoutHideBotTag.css diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index 1818a8bcf..7079ebfc6 100644 --- a/src/components/PluginSettings/PluginModal.tsx +++ b/src/components/PluginSettings/PluginModal.tsx @@ -18,6 +18,7 @@ import { generateId } from "@api/Commands"; import { useSettings } from "@api/Settings"; +import { disableStyle, enableStyle } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { proxyLazy } from "@utils/lazy"; @@ -40,6 +41,7 @@ import { SettingSliderComponent, SettingTextComponent } from "./components"; +import hideBotTagStyle from "./userPopoutHideBotTag.css?managed"; const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers")); const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"); @@ -50,11 +52,12 @@ interface PluginModalProps extends ModalProps { onRestartNeeded(): void; } -/** To stop discord making unwanted requests... */ -function makeDummyUser(user: { name: string, id: BigInt; }) { +function makeDummyUser(user: { username: string; id?: string; avatar?: string; }) { const newUser = new UserRecord({ - username: user.name, - id: generateId(), + username: user.username, + id: user.id ?? generateId(), + avatar: user.avatar, + /** To stop discord making unwanted requests... */ bot: true, }); FluxDispatcher.dispatch({ @@ -89,14 +92,27 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti const hasSettings = Boolean(pluginSettings && plugin.options); React.useEffect(() => { + enableStyle(hideBotTagStyle); + + let originalUser: User; (async () => { for (const user of plugin.authors.slice(0, 6)) { const author = user.id - ? await UserUtils.fetchUser(`${user.id}`).catch(() => makeDummyUser(user)) - : makeDummyUser(user); + ? await UserUtils.fetchUser(`${user.id}`) + // only show name & pfp and no actions so users cannot harass plugin devs for support (send dms, add as friend, etc) + .then(u => (originalUser = u, makeDummyUser(u))) + .catch(() => makeDummyUser({ username: user.name })) + : makeDummyUser({ username: user.name }); + setAuthors(a => [...a, author]); } })(); + + return () => { + disableStyle(hideBotTagStyle); + if (originalUser) + FluxDispatcher.dispatch({ type: "USER_UPDATE", user: originalUser }); + }; }, []); async function saveAndClose() { diff --git a/src/components/PluginSettings/userPopoutHideBotTag.css b/src/components/PluginSettings/userPopoutHideBotTag.css new file mode 100644 index 000000000..5e33e4b32 --- /dev/null +++ b/src/components/PluginSettings/userPopoutHideBotTag.css @@ -0,0 +1,3 @@ +[class|="userPopoutOuter"] [class*="botTag"] { + display: none; +} diff --git a/src/plugins/apiBadges.tsx b/src/plugins/apiBadges.tsx index 23ed6de2b..5f44d9826 100644 --- a/src/plugins/apiBadges.tsx +++ b/src/plugins/apiBadges.tsx @@ -24,15 +24,13 @@ import { Heart } from "@components/Heart"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; +import { isPluginDev } from "@utils/misc"; import { closeModal, Modals, openModal } from "@utils/modal"; import definePlugin from "@utils/types"; import { Forms, Toasts } from "@webpack/common"; const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/attachments/1033680203433660458/1092089947126780035/favicon.png"; -/** List of vencord contributor IDs */ -const contributorIds: string[] = Object.values(Devs).map(d => d.id.toString()); - const ContributorBadge: ProfileBadge = { description: "Vencord Contributor", image: CONTRIBUTOR_BADGE, @@ -43,7 +41,7 @@ const ContributorBadge: ProfileBadge = { transform: "scale(0.9)" // The image is a bit too big compared to default badges } }, - shouldShow: ({ user }) => contributorIds.includes(user.id), + shouldShow: ({ user }) => isPluginDev(user.id), link: "https://github.com/Vendicated/Vencord" }; diff --git a/src/plugins/supportHelper.tsx b/src/plugins/supportHelper.tsx index db38b375e..8b0880d19 100644 --- a/src/plugins/supportHelper.tsx +++ b/src/plugins/supportHelper.tsx @@ -18,6 +18,7 @@ import { DataStore } from "@api/index"; import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants"; +import { isPluginDev } from "@utils/misc"; import { makeCodeblock } from "@utils/text"; import definePlugin from "@utils/types"; import { isOutdated } from "@utils/updater"; @@ -74,8 +75,7 @@ ${makeCodeblock(Object.keys(plugins).filter(Vencord.Plugins.isPluginEnabled).joi async CHANNEL_SELECT({ channelId }) { if (channelId !== SUPPORT_CHANNEL_ID) return; - const myId = BigInt(UserStore.getCurrentUser().id); - if (Object.values(Devs).some(d => d.id === myId)) return; + if (isPluginDev(UserStore.getCurrentUser().id)) return; if (isOutdated && gitHash !== await DataStore.get(REMEMBER_DISMISS_KEY)) { const rememberDismiss = () => DataStore.set(REMEMBER_DISMISS_KEY, gitHash); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 1c7047090..9671ac398 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -29,7 +29,18 @@ export const REACT_GLOBAL = "Vencord.Webpack.Common.React"; export const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : ""}`; export const SUPPORT_CHANNEL_ID = "1026515880080842772"; -// Add yourself here if you made a plugin +export interface Dev { + name: string; + id: bigint; + badge?: boolean; +} + +/** + * If you made a plugin or substantial contribution, add yourself here. + * This object is used for the plugin author list, as well as to add a contributor badge to your profile. + * If you wish to stay fully anonymous, feel free to set ID to 0n. + * If you are fine with attribution but don't want the badge, add badge: false + */ export const Devs = /* #__PURE__*/ Object.freeze({ Ven: { name: "Vendicated", @@ -201,7 +212,8 @@ export const Devs = /* #__PURE__*/ Object.freeze({ }, nick: { name: "nick", - id: 347884694408265729n + id: 347884694408265729n, + badge: false }, whqwert: { name: "whqwert", @@ -295,4 +307,13 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "outfoxxed", id: 837425748435796060n }, -}); +} satisfies Record); + +// iife so #__PURE__ works correctly +export const DevsById = /* #__PURE__*/ (() => + Object.freeze(Object.fromEntries( + Object.entries(Devs) + .filter(d => d[1].id !== 0n) + .map(([_, v]) => [v.id, v] as const) + )) +)() as Record; diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index 59475cbd7..ec612a91d 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -18,6 +18,8 @@ import { Clipboard, Toasts } from "@webpack/common"; +import { DevsById } from "./constants"; + /** * Recursively merges defaults into an object and returns the same object * @param obj Object @@ -100,3 +102,5 @@ export function identity(value: T): T { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#mobile_tablet_or_desktop // "In summary, we recommend looking for the string Mobi anywhere in the User Agent to detect a mobile device." export const isMobile = navigator.userAgent.includes("Mobi"); + +export const isPluginDev = (id: string) => Object.hasOwn(DevsById, id); From 5219fb700fb481bfbfa3a036d5c10fb7b834ade5 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Mon, 22 May 2023 21:22:25 -0300 Subject: [PATCH 004/735] PermViewer: Add ability to change sort order; Properly center (#1182) --- .../components/UserPermissions.tsx | 28 +++++++++++++++++-- src/plugins/permissionsViewer/styles.css | 23 +++++++++++++-- src/plugins/typingIndicator.tsx | 2 +- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/plugins/permissionsViewer/components/UserPermissions.tsx b/src/plugins/permissionsViewer/components/UserPermissions.tsx index acffa787f..ab9bfeab4 100644 --- a/src/plugins/permissionsViewer/components/UserPermissions.tsx +++ b/src/plugins/permissionsViewer/components/UserPermissions.tsx @@ -23,7 +23,7 @@ import { filters, findBulk } from "@webpack"; import { i18n, PermissionsBits, Text, Tooltip, useMemo, UserStore, useState } from "@webpack/common"; import type { Guild, GuildMember } from "discord-types/general"; -import { settings } from ".."; +import { PermissionsSortOrder, settings } from ".."; import { cl, getPermissionString, getSortedRoles, sortUserRoles } from "../utils"; import openRolesAndUsersPermissionsModal, { PermissionType, type RoleOrUserPermission } from "./RolesAndUsersPermissions"; @@ -46,6 +46,7 @@ const Classes = proxyLazy(() => { }) as Record<"roles" | "rolePill" | "rolePillBorder" | "desaturateUserColors" | "flex" | "alignCenter" | "justifyCenter" | "svg" | "background" | "dot" | "dotBorderColor" | "roleCircle" | "dotBorderBase" | "flex" | "alignCenter" | "justifyCenter" | "wrap" | "root" | "role" | "roleRemoveButton" | "roleDot" | "roleFlowerStar" | "roleRemoveIcon" | "roleRemoveIconFocused" | "roleVerifiedIcon" | "roleName" | "roleNameOverflow" | "actionButton" | "overflowButton" | "addButton" | "addButtonIcon" | "overflowRolesPopout" | "overflowRolesPopoutArrowWrapper" | "overflowRolesPopoutArrow" | "popoutBottom" | "popoutTop" | "overflowRolesPopoutHeader" | "overflowRolesPopoutHeaderIcon" | "overflowRolesPopoutHeaderText" | "roleIcon", string>; function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildMember: GuildMember; }) { + const stns = settings.use(["permissionsSortOrder"]); const [viewPermissions, setViewPermissions] = useState(settings.store.defaultPermissionsDropdownState); const [rolePermissions, userPermissions] = useMemo(() => { @@ -91,7 +92,7 @@ function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildM userPermissions.sort((a, b) => b.rolePosition - a.rolePosition); return [rolePermissions, userPermissions]; - }, []); + }, [stns.permissionsSortOrder]); const { root, role, roleRemoveButton, roleNameOverflow, roles, rolePill, rolePillBorder, roleCircle, roleName } = Classes; @@ -100,7 +101,28 @@ function UserPermissionsComponent({ guild, guildMember }: { guild: Guild; guildM
Permissions -
+
+ + {tooltipProps => ( + + )} + + {tooltipProps => (
)} - {GuildMemberStore.getNick(guildId!, user.id) || !guildId && RelationshipStore.getNickname(user.id) || user.globalName || user.username} + {GuildMemberStore.getNick(guildId!, user.id) + || (!guildId && RelationshipStore.getNickname(user.id)) + || (user as any).globalName + || user.username + } ); }, { noop: true }); From bc46bfa4675bb78d9044edf8793653358652548d Mon Sep 17 00:00:00 2001 From: UwU <61664271+UwUDev@users.noreply.github.com> Date: Tue, 23 May 2023 03:32:27 +0200 Subject: [PATCH 009/735] =?UTF-8?q?New=20plugin=20:=20Party=20mode=20?= =?UTF-8?q?=F0=9F=8E=89=20(#1161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ArjixWasTaken <53124886+ArjixWasTaken@users.noreply.github.com> Co-authored-by: V --- src/plugins/partyMode.ts | 105 +++++++++++++++++++++++++++++++++++ src/utils/constants.ts | 4 ++ src/webpack/common/stores.ts | 3 +- 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 src/plugins/partyMode.ts diff --git a/src/plugins/partyMode.ts b/src/plugins/partyMode.ts new file mode 100644 index 000000000..bb822f5a0 --- /dev/null +++ b/src/plugins/partyMode.ts @@ -0,0 +1,105 @@ +/* + * 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 { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findStoreLazy } from "@webpack"; +import { GenericStore } from "@webpack/common"; + +const PoggerModeSettingsStore: GenericStore = findStoreLazy("PoggermodeSettingsStore"); + +const enum Intensity { + Normal, + Better, + ProjectX, +} + +const settings = definePluginSettings({ + superIntensePartyMode: { + description: "Party intensity", + type: OptionType.SELECT, + options: [ + { label: "Normal", value: Intensity.Normal, default: true }, + { label: "Better", value: Intensity.Better }, + { label: "Project X", value: Intensity.ProjectX }, + ], + restartNeeded: false, + onChange: setSettings + }, +}); + +export default definePlugin({ + name: "Party mode 🎉", + description: "Allows you to use party mode cause the party never ends ✨", + authors: [Devs.UwUDev], + settings, + + start() { + setPoggerState(true); + setSettings(settings.store.superIntensePartyMode); + }, + + stop() { + setPoggerState(false); + }, +}); + +function setPoggerState(state: boolean) { + Object.assign(PoggerModeSettingsStore.__getLocalVars().state, { + enabled: state, + settingsVisible: state + }); +} + +function setSettings(intensity: Intensity) { + const state = { + screenshakeEnabledLocations: { 0: true, 1: true, 2: true }, + shakeIntensity: 1, + confettiSize: 16, + confettiCount: 5, + combosRequiredCount: 1 + }; + + switch (intensity) { + case Intensity.Normal: { + Object.assign(state, { + screenshakeEnabledLocations: { 0: true, 1: false, 2: false }, + combosRequiredCount: 5 + }); + break; + } + case Intensity.Better: { + Object.assign(state, { + confettiSize: 12, + confettiCount: 8, + }); + break; + } + case Intensity.ProjectX: { + Object.assign(state, { + shakeIntensity: 20, + confettiSize: 25, + confettiCount: 15, + }); + break; + } + } + + Object.assign(PoggerModeSettingsStore.__getLocalVars().state, state); +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 91c5929ca..7e1f95fd1 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -311,6 +311,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "outfoxxed", id: 837425748435796060n }, + UwUDev: { + name: "UwU", + id: 691413039156690994n, + }, } satisfies Record); // iife so #__PURE__ works correctly diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index f31629995..05d525429 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -25,7 +25,7 @@ import * as t from "./types/stores"; export const Flux: t.Flux = findByPropsLazy("connectStores"); -type GenericStore = t.FluxStore & Record; +export type GenericStore = t.FluxStore & Record; export let MessageStore: Omit & { getMessages(chanId: string): any; @@ -37,6 +37,7 @@ export let PermissionStore: GenericStore; export let GuildChannelStore: GenericStore; export let ReadStateStore: GenericStore; export let PresenceStore: GenericStore; +export let PoggerModeSettingsStore: GenericStore; export let GuildStore: Stores.GuildStore & t.FluxStore; export let UserStore: Stores.UserStore & t.FluxStore; From 368d2bcdbb5489123f0810c75ea092311e20ff75 Mon Sep 17 00:00:00 2001 From: V Date: Tue, 23 May 2023 03:47:09 +0200 Subject: [PATCH 010/735] DiscordUtils: Add sendMessage --- src/utils/discord.ts | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/utils/discord.ts b/src/utils/discord.ts index dfd20e6f1..228e2b4b2 100644 --- a/src/utils/discord.ts +++ b/src/utils/discord.ts @@ -16,11 +16,13 @@ * along with this program. If not, see . */ -import { findLazy } from "@webpack"; +import { MessageObject } from "@api/MessageEvents"; +import { findByPropsLazy, findLazy } from "@webpack"; import { ChannelStore, ComponentDispatch, GuildStore, PrivateChannelsStore, SelectedChannelStore } from "@webpack/common"; -import { Guild } from "discord-types/general"; +import { Guild, Message } from "discord-types/general"; const PreloadedUserSettings = findLazy(m => m.ProtoClass?.typeName.endsWith("PreloadedUserSettings")); +const MessageActions = findByPropsLazy("editMessage", "sendMessage"); export function getCurrentChannel() { return ChannelStore.getChannel(SelectedChannelStore.getChannelId()); @@ -49,3 +51,29 @@ export function insertTextIntoChatInputBox(text: string) { plainText: text }); } + +interface MessageExtra { + messageReference: Message["messageReference"]; + allowedMentions: { + parse: string[]; + replied_user: boolean; + }; + stickerIds: string[]; +} + +export function sendMessage( + channelId: string, + data: Partial, + waitForChannelReady?: boolean, + extra?: Partial +) { + const messageData = { + content: "", + invalidEmojis: [], + tts: false, + validNonShortcutEmojis: [], + ...data + }; + + return MessageActions.sendMessage(channelId, messageData, waitForChannelReady, extra); +} From a94787a9f373653da1f116a6c1ce03c2c5111160 Mon Sep 17 00:00:00 2001 From: V Date: Tue, 23 May 2023 03:50:21 +0200 Subject: [PATCH 011/735] Bump to 1.2.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f9aacc47..b5e92cac6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.2.4", + "version": "1.2.5", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From d888a0a291e6915a770fb8fffeb8fd44c2865122 Mon Sep 17 00:00:00 2001 From: V Date: Tue, 23 May 2023 04:42:06 +0200 Subject: [PATCH 012/735] [skip ci] Fix plugin json generation --- scripts/generatePluginList.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/generatePluginList.ts b/scripts/generatePluginList.ts index 8442e4290..7c273de70 100644 --- a/scripts/generatePluginList.ts +++ b/scripts/generatePluginList.ts @@ -19,6 +19,7 @@ import { Dirent, readdirSync, readFileSync, writeFileSync } from "fs"; import { access, readFile } from "fs/promises"; import { join } from "path"; +import { isSatisfiesExpression } from "typescript"; import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript"; interface Dev { @@ -66,9 +67,9 @@ function parseDevs() { const value = devsDeclaration.initializer.arguments[0]; - if (!isObjectLiteralExpression(value)) return; + if (!isSatisfiesExpression(value) || !isObjectLiteralExpression(value.expression)) throw new Error("Failed to parse devs: not an object literal"); - for (const prop of value.properties) { + for (const prop of value.expression.properties) { const name = (prop.name as Identifier).text; const value = isPropertyAssignment(prop) ? prop.initializer : prop; From 458c7ed4c5035e125a497c192e71737f946ff302 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 23 May 2023 00:25:48 -0300 Subject: [PATCH 013/735] Make Fake Nitro transformations support the new markdown (#911) --- scripts/generatePluginList.ts | 7 +- src/plugins/fakeNitro.ts | 129 ++++++++++++++++++++++++++-------- 2 files changed, 104 insertions(+), 32 deletions(-) diff --git a/scripts/generatePluginList.ts b/scripts/generatePluginList.ts index 7c273de70..87c32ab62 100644 --- a/scripts/generatePluginList.ts +++ b/scripts/generatePluginList.ts @@ -19,8 +19,7 @@ import { Dirent, readdirSync, readFileSync, writeFileSync } from "fs"; import { access, readFile } from "fs/promises"; import { join } from "path"; -import { isSatisfiesExpression } from "typescript"; -import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript"; +import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript"; interface Dev { name: string; @@ -131,7 +130,9 @@ async function parseFile(fileName: string) { if (!isArrayLiteralExpression(value)) throw fail("authors is not an array literal"); data.authors = value.elements.map(e => { if (!isPropertyAccessExpression(e)) throw fail("authors array contains non-property access expressions"); - return devs[getName(e)!]; + const d = devs[getName(e)!]; + if (!d) throw fail(`couldn't look up author ${getName(e)}`); + return d; }); break; case "tags": diff --git a/src/plugins/fakeNitro.ts b/src/plugins/fakeNitro.ts index 0d69538c8..34cd63b8a 100644 --- a/src/plugins/fakeNitro.ts +++ b/src/plugins/fakeNitro.ts @@ -22,11 +22,12 @@ import { Devs } from "@utils/constants"; import { ApngBlendOp, ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies"; import { getCurrentGuild } from "@utils/discord"; import { proxyLazy } from "@utils/lazy"; +import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack"; import { ChannelStore, EmojiStore, FluxDispatcher, Parser, PermissionStore, UserStore } from "@webpack/common"; import type { Message } from "discord-types/general"; -import type { ReactNode } from "react"; +import type { ReactElement, ReactNode } from "react"; const DRAFT_TYPE = 0; const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR"); @@ -392,70 +393,137 @@ export default definePlugin({ }); }, - patchFakeNitroEmojisOrRemoveStickersLinks(content: Array, inline: boolean) { - if (content.length > 1 && !settings.store.transformCompoundSentence) return content; + trimContent(content: Array) { + const firstContent = content[0]; + if (typeof firstContent === "string") content[0] = firstContent.trimStart(); + if (content[0] === "") content.shift(); - const newContent: Array = []; + const lastIndex = content.length - 1; + const lastContent = content[lastIndex]; + if (typeof lastContent === "string") content[lastIndex] = lastContent.trimEnd(); + if (content[lastIndex] === "") content.pop(); + }, + + clearEmptyArrayItems(array: Array) { + return array.filter(item => item != null); + }, + + ensureChildrenIsArray(child: ReactElement) { + if (!Array.isArray(child.props.children)) child.props.children = [child.props.children]; + }, + + patchFakeNitroEmojisOrRemoveStickersLinks(content: Array, inline: boolean) { + // If content has more than one child or it's a single ReactElement like a header or list + if ((content.length > 1 || typeof content[0]?.type === "string") && !settings.store.transformCompoundSentence) return content; let nextIndex = content.length; - for (const element of content) { - if (element.props?.trusted == null) { - newContent.push(element); - continue; - } - + const transformLinkChild = (child: ReactElement) => { if (settings.store.transformEmojis) { - const fakeNitroMatch = element.props.href.match(fakeNitroEmojiRegex); + const fakeNitroMatch = child.props.href.match(fakeNitroEmojiRegex); if (fakeNitroMatch) { let url: URL | null = null; try { - url = new URL(element.props.href); + url = new URL(child.props.href); } catch { } const emojiName = EmojiStore.getCustomEmojiById(fakeNitroMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroEmoji"; - newContent.push(Parser.defaultRules.customEmoji.react({ - jumboable: !inline && content.length === 1, + return Parser.defaultRules.customEmoji.react({ + jumboable: !inline && content.length === 1 && typeof content[0].type !== "string", animated: fakeNitroMatch[2] === "gif", emojiId: fakeNitroMatch[1], name: emojiName, fake: true - }, void 0, { key: String(nextIndex++) })); - - continue; + }, void 0, { key: String(nextIndex++) }); } } if (settings.store.transformStickers) { - if (fakeNitroStickerRegex.test(element.props.href)) continue; + if (fakeNitroStickerRegex.test(child.props.href)) return null; - const gifMatch = element.props.href.match(fakeNitroGifStickerRegex); + const gifMatch = child.props.href.match(fakeNitroGifStickerRegex); if (gifMatch) { // There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker - if (StickerStore.getStickerById(gifMatch[1])) continue; + if (StickerStore.getStickerById(gifMatch[1])) return null; } } - newContent.push(element); + return child; + }; + + const transformChild = (child: ReactElement) => { + if (child?.props?.trusted != null) return transformLinkChild(child); + if (child?.props?.children != null) { + if (!Array.isArray(child.props.children)) { + child.props.children = modifyChild(child.props.children); + return child; + } + + child.props.children = modifyChildren(child.props.children); + if (child.props.children.length === 0) return null; + return child; + } + + return child; + }; + + const modifyChild = (child: ReactElement) => { + const newChild = transformChild(child); + + if (newChild?.type === "ul" || newChild?.type === "ol") { + this.ensureChildrenIsArray(newChild); + if (newChild.props.children.length === 0) return null; + + let listHasAnItem = false; + for (const [index, child] of newChild.props.children.entries()) { + if (child == null) { + delete newChild.props.children[index]; + continue; + } + + this.ensureChildrenIsArray(child); + if (child.props.children.length > 0) listHasAnItem = true; + else delete newChild.props.children[index]; + } + + if (!listHasAnItem) return null; + + newChild.props.children = this.clearEmptyArrayItems(newChild.props.children); + } + + return newChild; + }; + + const modifyChildren = (children: Array) => { + for (const [index, child] of children.entries()) children[index] = modifyChild(child); + + children = this.clearEmptyArrayItems(children); + this.trimContent(children); + + return children; + }; + + try { + return modifyChildren(window._.cloneDeep(content)); + } catch (err) { + new Logger("FakeNitro").error(err); + return content; } - - const firstContent = newContent[0]; - if (typeof firstContent === "string") newContent[0] = firstContent.trimStart(); - - return newContent; }, patchFakeNitroStickers(stickers: Array, message: Message) { const itemsToMaybePush: Array = []; const contentItems = message.content.split(/\s/); - if (contentItems.length === 1 && !settings.store.transformCompoundSentence) itemsToMaybePush.push(contentItems[0]); - else itemsToMaybePush.push(...contentItems); + if (settings.store.transformCompoundSentence) itemsToMaybePush.push(...contentItems); + else if (contentItems.length === 1) itemsToMaybePush.push(contentItems[0]); itemsToMaybePush.push(...message.attachments.filter(attachment => attachment.content_type === "image/gif").map(attachment => attachment.url)); for (const item of itemsToMaybePush) { + if (!settings.store.transformCompoundSentence && !item.startsWith("http")) continue; + const imgMatch = item.match(fakeNitroStickerRegex); if (imgMatch) { let url: URL | null = null; @@ -492,10 +560,13 @@ export default definePlugin({ }, shouldIgnoreEmbed(embed: Message["embeds"][number], message: Message) { - if (message.content.split(/\s/).length > 1 && !settings.store.transformCompoundSentence) return false; + const contentItems = message.content.split(/\s/); + if (contentItems.length > 1 && !settings.store.transformCompoundSentence) return false; switch (embed.type) { case "image": { + if (!settings.store.transformCompoundSentence && !contentItems.includes(embed.url!) && !contentItems.includes(embed.image!.proxyURL)) return false; + if (settings.store.transformEmojis) { if (fakeNitroEmojiRegex.test(embed.url!)) return true; } From 6300198a5463ab38da81906bda634addf4c8a369 Mon Sep 17 00:00:00 2001 From: V Date: Sun, 28 May 2023 21:59:22 +0200 Subject: [PATCH 014/735] MessageLinkEmbeds: Fix niche unclaimed account bug This plugin fires MESSAGE_UPDATE events for messages containing message links (to rerender them). If the updated message is an interaction, it contains message.interaction.user. If the one who ran the command is you, message.interaction.user will be you and the email in this data is always set to null. Discord seems to update the local user data with this user. So essentially, in the above described edge case it would update the current user to have no email (only locally, in memory. There is 0 risk for your account, it was just a temporary visual bug) which would cause the unclaimed account banner to appear. This commit fixes this by simply omitting the interaction field from the MESSAGE_UPDATE event --- src/plugins/hideAttachments.tsx | 8 +------- src/plugins/messageLinkEmbeds.tsx | 2 ++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/plugins/hideAttachments.tsx b/src/plugins/hideAttachments.tsx index 944da6539..83a924eaa 100644 --- a/src/plugins/hideAttachments.tsx +++ b/src/plugins/hideAttachments.tsx @@ -20,7 +20,7 @@ import { get, set } from "@api/DataStore"; import { addButton, removeButton } from "@api/MessagePopover"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; -import { ChannelStore, FluxDispatcher } from "@webpack/common"; +import { ChannelStore } from "@webpack/common"; let style: HTMLStyleElement; @@ -101,11 +101,5 @@ export default definePlugin({ await saveHiddenMessages(ids); await this.buildCss(); - - // update is necessary to rerender the PopOver - FluxDispatcher.dispatch({ - type: "MESSAGE_UPDATE", - message: { id } - }); } }); diff --git a/src/plugins/messageLinkEmbeds.tsx b/src/plugins/messageLinkEmbeds.tsx index d1fcbf357..5fae23458 100644 --- a/src/plugins/messageLinkEmbeds.tsx +++ b/src/plugins/messageLinkEmbeds.tsx @@ -225,6 +225,8 @@ function MessageEmbedAccessory({ message }: { message: Message; }) { } else { const msg = { ...message } as any; delete msg.embeds; + delete msg.interaction; + messageFetchQueue.push(() => fetchMessage(channelID, messageID) .then(m => m && FluxDispatcher.dispatch({ type: "MESSAGE_UPDATE", 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: [PATCH 015/735] 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 ++++------- .../reviewDB/components/MessageButton.tsx | 54 ++++-- .../reviewDB/components/ReviewBadge.tsx | 5 +- .../reviewDB/components/ReviewComponent.tsx | 75 +++++--- .../reviewDB/components/ReviewModal.tsx | 104 ++++++++++ .../reviewDB/components/ReviewsView.tsx | 179 +++++++++++------- .../{entities/User.ts => entities.ts} | 54 ++++-- src/plugins/reviewDB/entities/Badge.ts | 26 --- src/plugins/reviewDB/entities/Review.ts | 35 ---- src/plugins/reviewDB/index.tsx | 171 +++++++++-------- .../{Utils/ReviewDBAPI.ts => reviewDbApi.ts} | 82 +++++--- src/plugins/reviewDB/settings.tsx | 82 ++++++++ src/plugins/reviewDB/style.css | 50 ++++- .../reviewDB/{Utils/Utils.tsx => utils.tsx} | 17 +- src/utils/react.tsx | 13 +- src/utils/types.ts | 2 + src/webpack/common/components.ts | 3 +- src/webpack/common/types/components.d.ts | 10 + 21 files changed, 803 insertions(+), 394 deletions(-) create mode 100644 src/components/ExpandableHeader.css create mode 100644 src/components/ExpandableHeader.tsx create mode 100644 src/plugins/reviewDB/components/ReviewModal.tsx rename src/plugins/reviewDB/{entities/User.ts => entities.ts} (55%) delete mode 100644 src/plugins/reviewDB/entities/Badge.ts delete mode 100644 src/plugins/reviewDB/entities/Review.ts rename src/plugins/reviewDB/{Utils/ReviewDBAPI.ts => reviewDbApi.ts} (59%) create mode 100644 src/plugins/reviewDB/settings.tsx rename src/plugins/reviewDB/{Utils/Utils.tsx => utils.tsx} (88%) diff --git a/src/api/Settings.ts b/src/api/Settings.ts index e481e48c0..2f7866819 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 000000000..14e291b06 --- /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 000000000..1cbce4f2e --- /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 ab9bfeab4..37d9d0f42 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 3b8308ab1..176f4d624 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 8c013cd0a..e65dff247 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 ac09b4cc5..ddcae0aac 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 000000000..6e85dc295 --- /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 ff46ccaa8..bd264fa65 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; - if (isLoading) 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! + + )} +
+ ); +} + +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! - - )} -