mirror of
https://github.com/Vendicated/Vencord.git
synced 2025-01-10 18:06:22 +00:00
Merge branch 'Vendicated:main' into main
This commit is contained in:
commit
db2b08d2fb
46 changed files with 2271 additions and 386 deletions
2
.github/workflows/codeberg-mirror.yml
vendored
2
.github/workflows/codeberg-mirror.yml
vendored
|
@ -18,5 +18,5 @@ jobs:
|
|||
fetch-depth: 0
|
||||
- uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1
|
||||
with:
|
||||
target_repo_url: "git@codeberg.org:Ven/cord.git"
|
||||
target_repo_url: "git@codeberg.org:Vee/cord.git"
|
||||
ssh_private_key: ${{ secrets.CODEBERG_SSH_PRIVATE_KEY }}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.6.5",
|
||||
"version": "1.6.7",
|
||||
"description": "The cutest Discord client mod",
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
|
|
|
@ -44,7 +44,7 @@ async function syncSettings() {
|
|||
// pre-check for local shared settings
|
||||
if (
|
||||
Settings.cloud.authenticated &&
|
||||
await dsGet("Vencord_cloudSecret") === null // this has been enabled due to local settings share or some other bug
|
||||
!await dsGet("Vencord_cloudSecret") // this has been enabled due to local settings share or some other bug
|
||||
) {
|
||||
// show a notification letting them know and tell them how to fix it
|
||||
showNotification({
|
||||
|
@ -145,4 +145,3 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
}));
|
||||
}
|
||||
}, { once: true });
|
||||
|
||||
|
|
|
@ -21,9 +21,11 @@ import { classNameFactory } from "@api/Styles";
|
|||
import { Flex } from "@components/Flex";
|
||||
import { DeleteIcon } from "@components/Icons";
|
||||
import { Link } from "@components/Link";
|
||||
import PluginModal from "@components/PluginSettings/PluginModal";
|
||||
import { openInviteModal } from "@utils/discord";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { openModal } from "@utils/modal";
|
||||
import { showItemInFolder } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { findByPropsLazy, findLazy } from "@webpack";
|
||||
|
@ -248,6 +250,21 @@ function ThemesTab() {
|
|||
>
|
||||
Edit QuickCSS
|
||||
</Button>
|
||||
|
||||
{Vencord.Settings.plugins.ClientTheme.enabled && (
|
||||
<Button
|
||||
onClick={() => openModal(modalProps => (
|
||||
<PluginModal
|
||||
{...modalProps}
|
||||
plugin={Vencord.Plugins.plugins.ClientTheme}
|
||||
onRestartNeeded={() => { }}
|
||||
/>
|
||||
))}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
Edit ClientTheme
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
</Card>
|
||||
|
||||
|
|
|
@ -81,9 +81,12 @@ function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string
|
|||
|
||||
function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
|
||||
return (
|
||||
<Card style={{ padding: ".5em" }}>
|
||||
<Card style={{ padding: "0 0.5em" }}>
|
||||
{updates.map(({ hash, author, message }) => (
|
||||
<div>
|
||||
<div style={{
|
||||
marginTop: "0.5em",
|
||||
marginBottom: "0.5em"
|
||||
}}>
|
||||
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
|
||||
<span style={{
|
||||
marginLeft: "0.5em",
|
||||
|
@ -113,7 +116,7 @@ function Updatable(props: CommonProps) {
|
|||
</>
|
||||
) : (
|
||||
<Forms.FormText className={Margins.bottom8}>
|
||||
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
|
||||
{isOutdated ? (updates.length === 1 ? "There is 1 Update" : `There are ${updates.length} Updates`) : "Up to Date!"}
|
||||
</Forms.FormText>
|
||||
)}
|
||||
|
||||
|
|
|
@ -139,8 +139,15 @@ export function initIpc(mainWindow: BrowserWindow) {
|
|||
}
|
||||
|
||||
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
||||
const title = "Vencord QuickCSS Editor";
|
||||
const existingWindow = BrowserWindow.getAllWindows().find(w => w.title === title);
|
||||
if (existingWindow && !existingWindow.isDestroyed()) {
|
||||
existingWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const win = new BrowserWindow({
|
||||
title: "Vencord QuickCSS Editor",
|
||||
title,
|
||||
autoHideMenuBar: true,
|
||||
darkTheme: true,
|
||||
webPreferences: {
|
||||
|
|
|
@ -73,6 +73,8 @@ async function build() {
|
|||
const command = isFlatpak ? "flatpak-spawn" : "node";
|
||||
const args = isFlatpak ? ["--host", "node", "scripts/build/build.mjs"] : ["scripts/build/build.mjs"];
|
||||
|
||||
if (IS_DEV) args.push("--dev");
|
||||
|
||||
const res = await execFile(command, args, opts);
|
||||
|
||||
return !res.stderr.includes("Build failed");
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Settings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
const enum Methods {
|
||||
Random,
|
||||
Consistent,
|
||||
Timestamp,
|
||||
}
|
||||
|
||||
const tarExtMatcher = /\.tar\.\w+$/;
|
||||
|
||||
export default definePlugin({
|
||||
name: "AnonymiseFileNames",
|
||||
authors: [Devs.obscurity],
|
||||
description: "Anonymise uploaded file names",
|
||||
patches: [
|
||||
{
|
||||
find: "instantBatchUpload:function",
|
||||
replacement: {
|
||||
match: /uploadFiles:(.{1,2}),/,
|
||||
replace:
|
||||
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f.filename)),$1(...args)),",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
options: {
|
||||
method: {
|
||||
description: "Anonymising method",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ label: "Random Characters", value: Methods.Random, default: true },
|
||||
{ label: "Consistent", value: Methods.Consistent },
|
||||
{ label: "Timestamp (4chan-like)", value: Methods.Timestamp },
|
||||
],
|
||||
},
|
||||
randomisedLength: {
|
||||
description: "Random characters length",
|
||||
type: OptionType.NUMBER,
|
||||
default: 7,
|
||||
disabled: () => Settings.plugins.AnonymiseFileNames.method !== Methods.Random,
|
||||
},
|
||||
consistent: {
|
||||
description: "Consistent filename",
|
||||
type: OptionType.STRING,
|
||||
default: "image",
|
||||
disabled: () => Settings.plugins.AnonymiseFileNames.method !== Methods.Consistent,
|
||||
},
|
||||
},
|
||||
|
||||
anonymise(file: string) {
|
||||
let name = "image";
|
||||
const tarMatch = tarExtMatcher.exec(file);
|
||||
const extIdx = tarMatch?.index ?? file.lastIndexOf(".");
|
||||
const ext = extIdx !== -1 ? file.slice(extIdx) : "";
|
||||
|
||||
switch (Settings.plugins.AnonymiseFileNames.method) {
|
||||
case Methods.Random:
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
name = Array.from(
|
||||
{ length: Settings.plugins.AnonymiseFileNames.randomisedLength },
|
||||
() => chars[Math.floor(Math.random() * chars.length)]
|
||||
).join("");
|
||||
break;
|
||||
case Methods.Consistent:
|
||||
name = Settings.plugins.AnonymiseFileNames.consistent;
|
||||
break;
|
||||
case Methods.Timestamp:
|
||||
// UNIX timestamp in nanos, i could not find a better dependency-less way
|
||||
name = `${Math.floor(Date.now() / 1000)}${Math.floor(window.performance.now())}`;
|
||||
break;
|
||||
}
|
||||
return name + ext;
|
||||
},
|
||||
});
|
130
src/plugins/anonymiseFileNames/index.tsx
Normal file
130
src/plugins/anonymiseFileNames/index.tsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Upload } from "@api/MessageEvents";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||
|
||||
type AnonUpload = Upload & { anonymise?: boolean; };
|
||||
|
||||
const ActionBarIcon = findByCodeLazy(".actionBarIcon)");
|
||||
const UploadDraft = findByPropsLazy("popFirstFile", "update");
|
||||
|
||||
const enum Methods {
|
||||
Random,
|
||||
Consistent,
|
||||
Timestamp,
|
||||
}
|
||||
|
||||
const tarExtMatcher = /\.tar\.\w+$/;
|
||||
|
||||
const settings = definePluginSettings({
|
||||
anonymiseByDefault: {
|
||||
description: "Whether to anonymise file names by default",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
method: {
|
||||
description: "Anonymising method",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ label: "Random Characters", value: Methods.Random, default: true },
|
||||
{ label: "Consistent", value: Methods.Consistent },
|
||||
{ label: "Timestamp", value: Methods.Timestamp },
|
||||
],
|
||||
},
|
||||
randomisedLength: {
|
||||
description: "Random characters length",
|
||||
type: OptionType.NUMBER,
|
||||
default: 7,
|
||||
disabled: () => settings.store.method !== Methods.Random,
|
||||
},
|
||||
consistent: {
|
||||
description: "Consistent filename",
|
||||
type: OptionType.STRING,
|
||||
default: "image",
|
||||
disabled: () => settings.store.method !== Methods.Consistent,
|
||||
},
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "AnonymiseFileNames",
|
||||
authors: [Devs.obscurity],
|
||||
description: "Anonymise uploaded file names",
|
||||
patches: [
|
||||
{
|
||||
find: "instantBatchUpload:function",
|
||||
replacement: {
|
||||
match: /uploadFiles:(.{1,2}),/,
|
||||
replace:
|
||||
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
|
||||
},
|
||||
},
|
||||
{
|
||||
find: ".Messages.ATTACHMENT_UTILITIES_SPOILER",
|
||||
replacement: {
|
||||
match: /(?<=children:\[)(?=.{10,80}tooltip:\i\.\i\.Messages\.ATTACHMENT_UTILITIES_SPOILER)/,
|
||||
replace: "arguments[0].canEdit!==false?$self.renderIcon(arguments[0]):null,"
|
||||
},
|
||||
},
|
||||
],
|
||||
settings,
|
||||
|
||||
renderIcon: ErrorBoundary.wrap(({ upload, channelId, draftType }: { upload: AnonUpload; draftType: unknown; channelId: string; }) => {
|
||||
const anonymise = upload.anonymise ?? settings.store.anonymiseByDefault;
|
||||
return (
|
||||
<ActionBarIcon
|
||||
tooltip={anonymise ? "Using anonymous file name" : "Using normal file name"}
|
||||
onClick={() => {
|
||||
upload.anonymise = !anonymise;
|
||||
UploadDraft.update(channelId, upload.id, draftType, {}); // dummy update so component rerenders
|
||||
}}
|
||||
>
|
||||
{anonymise
|
||||
? <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M17.06 13C15.2 13 13.64 14.33 13.24 16.1C12.29 15.69 11.42 15.8 10.76 16.09C10.35 14.31 8.79 13 6.94 13C4.77 13 3 14.79 3 17C3 19.21 4.77 21 6.94 21C9 21 10.68 19.38 10.84 17.32C11.18 17.08 12.07 16.63 13.16 17.34C13.34 19.39 15 21 17.06 21C19.23 21 21 19.21 21 17C21 14.79 19.23 13 17.06 13M6.94 19.86C5.38 19.86 4.13 18.58 4.13 17S5.39 14.14 6.94 14.14C8.5 14.14 9.75 15.42 9.75 17S8.5 19.86 6.94 19.86M17.06 19.86C15.5 19.86 14.25 18.58 14.25 17S15.5 14.14 17.06 14.14C18.62 14.14 19.88 15.42 19.88 17S18.61 19.86 17.06 19.86M22 10.5H2V12H22V10.5M15.53 2.63C15.31 2.14 14.75 1.88 14.22 2.05L12 2.79L9.77 2.05L9.72 2.04C9.19 1.89 8.63 2.17 8.43 2.68L6 9H18L15.56 2.68L15.53 2.63Z" /></svg>
|
||||
: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style={{ transform: "scale(-1,1)" }}><path fill="currentColor" d="M22.11 21.46L2.39 1.73L1.11 3L6.31 8.2L6 9H7.11L8.61 10.5H2V12H10.11L13.5 15.37C13.38 15.61 13.3 15.85 13.24 16.1C12.29 15.69 11.41 15.8 10.76 16.09C10.35 14.31 8.79 13 6.94 13C4.77 13 3 14.79 3 17C3 19.21 4.77 21 6.94 21C9 21 10.68 19.38 10.84 17.32C11.18 17.08 12.07 16.63 13.16 17.34C13.34 19.39 15 21 17.06 21C17.66 21 18.22 20.86 18.72 20.61L20.84 22.73L22.11 21.46M6.94 19.86C5.38 19.86 4.13 18.58 4.13 17C4.13 15.42 5.39 14.14 6.94 14.14C8.5 14.14 9.75 15.42 9.75 17C9.75 18.58 8.5 19.86 6.94 19.86M17.06 19.86C15.5 19.86 14.25 18.58 14.25 17C14.25 16.74 14.29 16.5 14.36 16.25L17.84 19.73C17.59 19.81 17.34 19.86 17.06 19.86M22 12H15.2L13.7 10.5H22V12M17.06 13C19.23 13 21 14.79 21 17C21 17.25 20.97 17.5 20.93 17.73L19.84 16.64C19.68 15.34 18.66 14.32 17.38 14.17L16.29 13.09C16.54 13.03 16.8 13 17.06 13M12.2 9L7.72 4.5L8.43 2.68C8.63 2.17 9.19 1.89 9.72 2.04L9.77 2.05L12 2.79L14.22 2.05C14.75 1.88 15.32 2.14 15.54 2.63L15.56 2.68L18 9H12.2Z" /></svg>
|
||||
}
|
||||
</ActionBarIcon>
|
||||
);
|
||||
}, { noop: true }),
|
||||
|
||||
anonymise(upload: AnonUpload) {
|
||||
if ((upload.anonymise ?? settings.store.anonymiseByDefault) === false) return upload.filename;
|
||||
|
||||
const file = upload.filename;
|
||||
const tarMatch = tarExtMatcher.exec(file);
|
||||
const extIdx = tarMatch?.index ?? file.lastIndexOf(".");
|
||||
const ext = extIdx !== -1 ? file.slice(extIdx) : "";
|
||||
|
||||
switch (settings.store.method) {
|
||||
case Methods.Random:
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
return Array.from(
|
||||
{ length: settings.store.randomisedLength },
|
||||
() => chars[Math.floor(Math.random() * chars.length)]
|
||||
).join("") + ext;
|
||||
case Methods.Consistent:
|
||||
return settings.store.consistent + ext;
|
||||
case Methods.Timestamp:
|
||||
return Date.now() + ext;
|
||||
}
|
||||
},
|
||||
});
|
23
src/plugins/betterGifPicker/index.ts
Normal file
23
src/plugins/betterGifPicker/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterGifPicker",
|
||||
description: "Makes the gif picker open the favourite category by default",
|
||||
authors: [Devs.Samwich],
|
||||
patches: [
|
||||
{
|
||||
find: ".GIFPickerResultTypes.SEARCH",
|
||||
replacement: [{
|
||||
match: "this.state={resultType:null}",
|
||||
replace: 'this.state={resultType:"Favorites"}'
|
||||
}]
|
||||
}
|
||||
]
|
||||
});
|
|
@ -82,7 +82,7 @@ export const streamContextPatch: NavContextMenuPatchCallback = (children, { stre
|
|||
};
|
||||
|
||||
export const userContextPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => {
|
||||
return addViewStreamContext(children, { userId: user.id });
|
||||
if (user) return addViewStreamContext(children, { userId: user.id });
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
|
|
|
@ -153,5 +153,6 @@ export const defaultRules = [
|
|||
"utm_term",
|
||||
"si@open.spotify.com",
|
||||
"igshid",
|
||||
"igsh",
|
||||
"share_id@reddit.com",
|
||||
];
|
||||
|
|
|
@ -19,6 +19,16 @@
|
|||
border: thin solid var(--background-modifier-accent) !important;
|
||||
}
|
||||
|
||||
.client-theme-warning {
|
||||
.client-theme-warning * {
|
||||
color: var(--text-danger);
|
||||
}
|
||||
|
||||
.client-theme-contrast-warning {
|
||||
background-color: var(--background-primary);
|
||||
padding: 0.5rem;
|
||||
border-radius: .5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
@ -8,19 +8,19 @@ import "./clientTheme.css";
|
|||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getTheme, Theme } from "@utils/discord";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { Button, Forms } from "@webpack/common";
|
||||
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Button, Forms, lodash as _, useStateFromStores } from "@webpack/common";
|
||||
|
||||
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
|
||||
|
||||
const colorPresets = [
|
||||
"#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D",
|
||||
"#3A483D", "#344242", "#313D4B", "#2D2F47", "#322B42",
|
||||
"#3C2E42", "#422938"
|
||||
"#3C2E42", "#422938", "#b6908f", "#bfa088", "#d3c77d",
|
||||
"#86ac86", "#88aab3", "#8693b5", "#8a89ba", "#ad94bb",
|
||||
];
|
||||
|
||||
function onPickColor(color: number) {
|
||||
|
@ -30,9 +30,35 @@ function onPickColor(color: number) {
|
|||
updateColorVars(hexColor);
|
||||
}
|
||||
|
||||
const { saveClientTheme } = findByPropsLazy("saveClientTheme");
|
||||
|
||||
function setTheme(theme: string) {
|
||||
saveClientTheme({ theme });
|
||||
}
|
||||
|
||||
const ThemeStore = findStoreLazy("ThemeStore");
|
||||
const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore");
|
||||
|
||||
function ThemeSettings() {
|
||||
const lightnessWarning = hexToLightness(settings.store.color) > 45;
|
||||
const lightModeWarning = getTheme() === Theme.Light;
|
||||
const theme = useStateFromStores([ThemeStore], () => ThemeStore.theme);
|
||||
const isLightTheme = theme === "light";
|
||||
const oppositeTheme = isLightTheme ? "dark" : "light";
|
||||
|
||||
const nitroTheme = useStateFromStores([NitroThemeStore], () => NitroThemeStore.gradientPreset);
|
||||
const nitroThemeEnabled = nitroTheme !== undefined;
|
||||
|
||||
const selectedLuminance = relativeLuminance(settings.store.color);
|
||||
|
||||
let contrastWarning = false, fixableContrast = true;
|
||||
if ((isLightTheme && selectedLuminance < 0.26) || !isLightTheme && selectedLuminance > 0.12)
|
||||
contrastWarning = true;
|
||||
if (selectedLuminance < 0.26 && selectedLuminance > 0.12)
|
||||
fixableContrast = false;
|
||||
// light mode with values greater than 65 leads to background colors getting crushed together and poor text contrast for muted channels
|
||||
if (isLightTheme && selectedLuminance > 0.65) {
|
||||
contrastWarning = true;
|
||||
fixableContrast = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="client-theme-settings">
|
||||
|
@ -48,15 +74,18 @@ function ThemeSettings() {
|
|||
suggestedColors={colorPresets}
|
||||
/>
|
||||
</div>
|
||||
{lightnessWarning || lightModeWarning
|
||||
? <div>
|
||||
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} />
|
||||
<Forms.FormText className="client-theme-warning">Your theme won't look good:</Forms.FormText>
|
||||
{lightnessWarning && <Forms.FormText className="client-theme-warning">Selected color is very light</Forms.FormText>}
|
||||
{lightModeWarning && <Forms.FormText className="client-theme-warning">Light mode isn't supported</Forms.FormText>}
|
||||
{(contrastWarning || nitroThemeEnabled) && (<>
|
||||
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} />
|
||||
<div className={`client-theme-contrast-warning ${contrastWarning ? (isLightTheme ? "theme-dark" : "theme-light") : ""}`}>
|
||||
<div className="client-theme-warning">
|
||||
<Forms.FormText>Warning, your theme won't look good:</Forms.FormText>
|
||||
{contrastWarning && <Forms.FormText>Selected color won't contrast well with text</Forms.FormText>}
|
||||
{nitroThemeEnabled && <Forms.FormText>Nitro themes aren't supported</Forms.FormText>}
|
||||
</div>
|
||||
{(contrastWarning && fixableContrast) && <Button onClick={() => setTheme(oppositeTheme)} color={Button.Colors.RED}>Switch to {oppositeTheme} mode</Button>}
|
||||
{(nitroThemeEnabled) && <Button onClick={() => setTheme(theme)} color={Button.Colors.RED}>Disable Nitro Theme</Button>}
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -87,9 +116,12 @@ export default definePlugin({
|
|||
settings,
|
||||
|
||||
startAt: StartAt.DOMContentLoaded,
|
||||
start() {
|
||||
async start() {
|
||||
updateColorVars(settings.store.color);
|
||||
generateColorOffsets();
|
||||
|
||||
const styles = await getStyles();
|
||||
generateColorOffsets(styles);
|
||||
generateLightModeFixes(styles);
|
||||
},
|
||||
|
||||
stop() {
|
||||
|
@ -98,56 +130,86 @@ export default definePlugin({
|
|||
}
|
||||
});
|
||||
|
||||
const variableRegex = /(--primary-[5-9]\d{2}-hsl):.*?(\S*)%;/g;
|
||||
const variableRegex = /(--primary-\d{3}-hsl):.*?(\S*)%;/g;
|
||||
const lightVariableRegex = /^--primary-[1-5]\d{2}-hsl/g;
|
||||
const darkVariableRegex = /^--primary-[5-9]\d{2}-hsl/g;
|
||||
|
||||
async function generateColorOffsets() {
|
||||
|
||||
const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]');
|
||||
const variableLightness = {} as Record<string, number>;
|
||||
|
||||
// Search all stylesheets for color variables
|
||||
for (const styleLinkNode of styleLinkNodes) {
|
||||
const cssLink = styleLinkNode.getAttribute("href");
|
||||
if (!cssLink) continue;
|
||||
|
||||
const res = await fetch(cssLink);
|
||||
const cssString = await res.text();
|
||||
|
||||
// Get lightness values of --primary variables >=500
|
||||
let variableMatch = variableRegex.exec(cssString);
|
||||
while (variableMatch !== null) {
|
||||
const [, variable, lightness] = variableMatch;
|
||||
variableLightness[variable] = parseFloat(lightness);
|
||||
variableMatch = variableRegex.exec(cssString);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate offsets
|
||||
const lightnessOffsets = Object.entries(variableLightness)
|
||||
// generates variables per theme by:
|
||||
// - matching regex (so we can limit what variables are included in light/dark theme, otherwise text becomes unreadable)
|
||||
// - offset from specified center (light/dark theme get different offsets because light uses 100 for background-primary, while dark uses 600)
|
||||
function genThemeSpecificOffsets(variableLightness: Record<string, number>, regex: RegExp, centerVariable: string): string {
|
||||
return Object.entries(variableLightness).filter(([key]) => key.search(regex) > -1)
|
||||
.map(([key, lightness]) => {
|
||||
const lightnessOffset = lightness - variableLightness["--primary-600-hsl"];
|
||||
const lightnessOffset = lightness - variableLightness[centerVariable];
|
||||
const plusOrMinus = lightnessOffset >= 0 ? "+" : "-";
|
||||
return `${key}: var(--theme-h) var(--theme-s) calc(var(--theme-l) ${plusOrMinus} ${Math.abs(lightnessOffset).toFixed(2)}%);`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.setAttribute("id", "clientThemeOffsets");
|
||||
style.textContent = `:root:root {
|
||||
${lightnessOffsets}
|
||||
}`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
function generateColorOffsets(styles) {
|
||||
const variableLightness = {} as Record<string, number>;
|
||||
|
||||
// Get lightness values of --primary variables
|
||||
let variableMatch = variableRegex.exec(styles);
|
||||
while (variableMatch !== null) {
|
||||
const [, variable, lightness] = variableMatch;
|
||||
variableLightness[variable] = parseFloat(lightness);
|
||||
variableMatch = variableRegex.exec(styles);
|
||||
}
|
||||
|
||||
createStyleSheet("clientThemeOffsets", [
|
||||
`.theme-light {\n ${genThemeSpecificOffsets(variableLightness, lightVariableRegex, "--primary-345-hsl")} \n}`,
|
||||
`.theme-dark {\n ${genThemeSpecificOffsets(variableLightness, darkVariableRegex, "--primary-600-hsl")} \n}`,
|
||||
].join("\n\n"));
|
||||
}
|
||||
|
||||
function generateLightModeFixes(styles) {
|
||||
const groupLightUsesW500Regex = /\.theme-light[^{]*\{[^}]*var\(--white-500\)[^}]*}/gm;
|
||||
// get light capturing groups that mention --white-500
|
||||
const relevantStyles = [...styles.matchAll(groupLightUsesW500Regex)].flat();
|
||||
|
||||
const groupBackgroundRegex = /^([^{]*)\{background:var\(--white-500\)/m;
|
||||
const groupBackgroundColorRegex = /^([^{]*)\{background-color:var\(--white-500\)/m;
|
||||
// find all capturing groups that assign background or background-color directly to w500
|
||||
const backgroundGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundRegex)).join(",\n");
|
||||
const backgroundColorGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundColorRegex)).join(",\n");
|
||||
// create css to reassign them to --primary-100
|
||||
const reassignBackgrounds = `${backgroundGroups} {\n background: var(--primary-100) \n}`;
|
||||
const reassignBackgroundColors = `${backgroundColorGroups} {\n background-color: var(--primary-100) \n}`;
|
||||
|
||||
const groupBgVarRegex = /\.theme-light\{([^}]*--[^:}]*(?:background|bg)[^:}]*:var\(--white-500\)[^}]*)\}/m;
|
||||
const bgVarRegex = /^(--[^:]*(?:background|bg)[^:]*):var\(--white-500\)/m;
|
||||
// get all global variables used for backgrounds
|
||||
const lightVars = mapReject(relevantStyles, style => captureOne(style, groupBgVarRegex)) // get the insides of capture groups that have at least one background var with w500
|
||||
.map(str => str.split(";")).flat(); // captureGroupInsides[] -> cssRule[]
|
||||
const lightBgVars = mapReject(lightVars, variable => captureOne(variable, bgVarRegex)); // remove vars that aren't for backgrounds or w500
|
||||
// create css to reassign every var
|
||||
const reassignVariables = `.theme-light {\n ${lightBgVars.map(variable => `${variable}: var(--primary-100);`).join("\n")} \n}`;
|
||||
|
||||
createStyleSheet("clientThemeLightModeFixes", [
|
||||
reassignBackgrounds,
|
||||
reassignBackgroundColors,
|
||||
reassignVariables,
|
||||
].join("\n\n"));
|
||||
}
|
||||
|
||||
function captureOne(str, regex) {
|
||||
const result = str.match(regex);
|
||||
return (result === null) ? null : result[1];
|
||||
}
|
||||
|
||||
function mapReject(arr, mapFunc, rejectFunc = _.isNull) {
|
||||
return _.reject(arr.map(mapFunc), rejectFunc);
|
||||
}
|
||||
|
||||
function updateColorVars(color: string) {
|
||||
const { hue, saturation, lightness } = hexToHSL(color);
|
||||
|
||||
let style = document.getElementById("clientThemeVars");
|
||||
if (!style) {
|
||||
style = document.createElement("style");
|
||||
style.setAttribute("id", "clientThemeVars");
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
if (!style)
|
||||
style = createStyleSheet("clientThemeVars");
|
||||
|
||||
style.textContent = `:root {
|
||||
--theme-h: ${hue};
|
||||
|
@ -156,6 +218,28 @@ function updateColorVars(color: string) {
|
|||
}`;
|
||||
}
|
||||
|
||||
function createStyleSheet(id, content = "") {
|
||||
const style = document.createElement("style");
|
||||
style.setAttribute("id", id);
|
||||
style.textContent = content.split("\n").map(line => line.trim()).join("\n");
|
||||
document.body.appendChild(style);
|
||||
return style;
|
||||
}
|
||||
|
||||
// returns all of discord's native styles in a single string
|
||||
async function getStyles(): Promise<string> {
|
||||
let out = "";
|
||||
const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]');
|
||||
for (const styleLinkNode of styleLinkNodes) {
|
||||
const cssLink = styleLinkNode.getAttribute("href");
|
||||
if (!cssLink) continue;
|
||||
|
||||
const res = await fetch(cssLink);
|
||||
out += await res.text();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// https://css-tricks.com/converting-color-spaces-in-javascript/
|
||||
function hexToHSL(hexCode: string) {
|
||||
// Hex => RGB normalized to 0-1
|
||||
|
@ -198,17 +282,14 @@ function hexToHSL(hexCode: string) {
|
|||
return { hue, saturation, lightness };
|
||||
}
|
||||
|
||||
// Minimized math just for lightness, lowers lag when changing colors
|
||||
function hexToLightness(hexCode: string) {
|
||||
// Hex => RGB normalized to 0-1
|
||||
const r = parseInt(hexCode.substring(0, 2), 16) / 255;
|
||||
const g = parseInt(hexCode.substring(2, 4), 16) / 255;
|
||||
const b = parseInt(hexCode.substring(4, 6), 16) / 255;
|
||||
// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
|
||||
function relativeLuminance(hexCode: string) {
|
||||
const normalize = (x: number) =>
|
||||
x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
|
||||
|
||||
const cMax = Math.max(r, g, b);
|
||||
const cMin = Math.min(r, g, b);
|
||||
const r = normalize(parseInt(hexCode.substring(0, 2), 16) / 255);
|
||||
const g = normalize(parseInt(hexCode.substring(2, 4), 16) / 255);
|
||||
const b = normalize(parseInt(hexCode.substring(4, 6), 16) / 255);
|
||||
|
||||
const lightness = 100 * ((cMax + cMin) / 2);
|
||||
|
||||
return lightness;
|
||||
return r * 0.2126 + g * 0.7152 + b * 0.0722;
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@ interface UserContextProps {
|
|||
}
|
||||
|
||||
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => {
|
||||
if (!user) return;
|
||||
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
id="vc-copy-user-url"
|
||||
|
|
|
@ -6,21 +6,17 @@
|
|||
|
||||
import "./ui/styles.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Link } from "@components/Link";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { closeAllModals } from "@utils/modal";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { FluxDispatcher, Forms, UserStore } from "@webpack/common";
|
||||
import { UserStore } from "@webpack/common";
|
||||
|
||||
import { CDN_URL, RAW_SKU_ID, SKU_ID } from "./lib/constants";
|
||||
import { useAuthorizationStore } from "./lib/stores/AuthorizationStore";
|
||||
import { useCurrentUserDecorationsStore } from "./lib/stores/CurrentUserDecorationsStore";
|
||||
import { useUserDecorAvatarDecoration, useUsersDecorationsStore } from "./lib/stores/UsersDecorationsStore";
|
||||
import { settings } from "./settings";
|
||||
import { setDecorationGridDecoration, setDecorationGridItem } from "./ui/components";
|
||||
import DecorSection from "./ui/components/DecorSection";
|
||||
|
||||
|
@ -30,27 +26,6 @@ export interface AvatarDecoration {
|
|||
skuId: string;
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
changeDecoration: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Change your avatar decoration",
|
||||
component() {
|
||||
return <div>
|
||||
<DecorSection hideTitle hideDivider noMargin />
|
||||
<Forms.FormText type="description" className={classes(Margins.top8, Margins.bottom8)}>
|
||||
You can also access Decor decorations from the <Link
|
||||
href="/settings/profile-customization"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
closeAllModals();
|
||||
FluxDispatcher.dispatch({ type: "USER_SETTINGS_MODAL_SET_SECTION", section: "Profile Customization" });
|
||||
}}
|
||||
>Profiles</Link> page.
|
||||
</Forms.FormText>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
});
|
||||
export default definePlugin({
|
||||
name: "Decor",
|
||||
description: "Create and use your own custom avatar decorations, or pick your favorite from the presets.",
|
||||
|
|
47
src/plugins/decor/settings.tsx
Normal file
47
src/plugins/decor/settings.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Link } from "@components/Link";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { closeAllModals } from "@utils/modal";
|
||||
import { OptionType } from "@utils/types";
|
||||
import { FluxDispatcher, Forms } from "@webpack/common";
|
||||
|
||||
import DecorSection from "./ui/components/DecorSection";
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
changeDecoration: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Change your avatar decoration",
|
||||
component() {
|
||||
if (!Vencord.Plugins.plugins.Decor.started) return <Forms.FormText>
|
||||
Enable Decor and restart your client to change your avatar decoration.
|
||||
</Forms.FormText>;
|
||||
|
||||
return <div>
|
||||
<DecorSection hideTitle hideDivider noMargin />
|
||||
<Forms.FormText type="description" className={classes(Margins.top8, Margins.bottom8)}>
|
||||
You can also access Decor decorations from the <Link
|
||||
href="/settings/profile-customization"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
closeAllModals();
|
||||
FluxDispatcher.dispatch({ type: "USER_SETTINGS_MODAL_SET_SECTION", section: "Profile Customization" });
|
||||
}}
|
||||
>Profiles</Link> page.
|
||||
</Forms.FormText>
|
||||
</div>;
|
||||
}
|
||||
},
|
||||
agreedToGuidelines: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Agreed to guidelines",
|
||||
hidden: true,
|
||||
default: false
|
||||
}
|
||||
});
|
|
@ -19,7 +19,7 @@ export let DecorationGridItem: DecorationGridItemComponent;
|
|||
export const setDecorationGridItem = v => DecorationGridItem = v;
|
||||
|
||||
export const AvatarDecorationModalPreview = LazyComponentWebpack(() => {
|
||||
const component = findComponentByCode("AvatarDecorationModalPreview");
|
||||
const component = findComponentByCode(".shopPreviewBanner");
|
||||
return React.memo(component);
|
||||
});
|
||||
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { extractAndLoadChunksLazy } from "@webpack";
|
||||
import { extractAndLoadChunksLazy, findByPropsLazy } from "@webpack";
|
||||
|
||||
export const cl = classNameFactory("vc-decor-");
|
||||
export const DecorationModalStyles = findByPropsLazy("modalFooterShopButton");
|
||||
|
||||
export const requireAvatarDecorationModal = extractAndLoadChunksLazy(["openAvatarDecorationModal:"]);
|
||||
export const requireCreateStickerModal = extractAndLoadChunksLazy(["stickerInspected]:"]);
|
||||
|
|
|
@ -4,12 +4,13 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { openInviteModal } from "@utils/discord";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { Alerts, Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Parser, Text, Tooltip, useEffect, UserStore, UserUtils, useState } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
||||
|
@ -18,16 +19,17 @@ import { GUILD_ID, INVITE_KEY } from "../../lib/constants";
|
|||
import { useAuthorizationStore } from "../../lib/stores/AuthorizationStore";
|
||||
import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore";
|
||||
import { decorationToAvatarDecoration } from "../../lib/utils/decoration";
|
||||
import { cl, requireAvatarDecorationModal } from "../";
|
||||
import { settings } from "../../settings";
|
||||
import { cl, DecorationModalStyles, requireAvatarDecorationModal } from "../";
|
||||
import { AvatarDecorationModalPreview } from "../components";
|
||||
import DecorationGridCreate from "../components/DecorationGridCreate";
|
||||
import DecorationGridNone from "../components/DecorationGridNone";
|
||||
import DecorDecorationGridDecoration from "../components/DecorDecorationGridDecoration";
|
||||
import SectionedGridList from "../components/SectionedGridList";
|
||||
import { openCreateDecorationModal } from "./CreateDecorationModal";
|
||||
import { openGuidelinesModal } from "./GuidelinesModal";
|
||||
|
||||
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
|
||||
const DecorationModalStyles = findByPropsLazy("modalFooterShopButton");
|
||||
|
||||
function usePresets() {
|
||||
const [presets, setPresets] = useState<Preset[]>([]);
|
||||
|
@ -83,7 +85,7 @@ function SectionHeader({ section }: { section: Section; }) {
|
|||
</div>;
|
||||
}
|
||||
|
||||
export default function ChangeDecorationModal(props: any) {
|
||||
function ChangeDecorationModal(props: ModalProps) {
|
||||
// undefined = not trying, null = none, Decoration = selected
|
||||
const [tryingDecoration, setTryingDecoration] = useState<Decoration | null | undefined>(undefined);
|
||||
const isTryingDecoration = typeof tryingDecoration !== "undefined";
|
||||
|
@ -116,6 +118,7 @@ export default function ChangeDecorationModal(props: any) {
|
|||
const data = [
|
||||
{
|
||||
title: "Your Decorations",
|
||||
subtitle: "You can delete your own decorations by right clicking on them.",
|
||||
sectionKey: "ownDecorations",
|
||||
items: ["none", ...ownDecorations, "create"]
|
||||
},
|
||||
|
@ -148,60 +151,62 @@ export default function ChangeDecorationModal(props: any) {
|
|||
className={cl("change-decoration-modal-content")}
|
||||
scrollbarType="none"
|
||||
>
|
||||
<SectionedGridList
|
||||
renderItem={item => {
|
||||
if (typeof item === "string") {
|
||||
switch (item) {
|
||||
case "none":
|
||||
return <DecorationGridNone
|
||||
className={cl("change-decoration-modal-decoration")}
|
||||
isSelected={activeSelectedDecoration === null}
|
||||
onSelect={() => setTryingDecoration(null)}
|
||||
/>;
|
||||
case "create":
|
||||
return <Tooltip text="You already have a decoration pending review" shouldShow={hasDecorationPendingReview}>
|
||||
{tooltipProps => <DecorationGridCreate
|
||||
<ErrorBoundary>
|
||||
<SectionedGridList
|
||||
renderItem={item => {
|
||||
if (typeof item === "string") {
|
||||
switch (item) {
|
||||
case "none":
|
||||
return <DecorationGridNone
|
||||
className={cl("change-decoration-modal-decoration")}
|
||||
isSelected={activeSelectedDecoration === null}
|
||||
onSelect={() => setTryingDecoration(null)}
|
||||
/>;
|
||||
case "create":
|
||||
return <Tooltip text="You already have a decoration pending review" shouldShow={hasDecorationPendingReview}>
|
||||
{tooltipProps => <DecorationGridCreate
|
||||
className={cl("change-decoration-modal-decoration")}
|
||||
{...tooltipProps}
|
||||
onSelect={!hasDecorationPendingReview ? (settings.store.agreedToGuidelines ? openCreateDecorationModal : openGuidelinesModal) : () => { }}
|
||||
/>}
|
||||
</Tooltip>;
|
||||
}
|
||||
} else {
|
||||
return <Tooltip text={"Pending review"} shouldShow={item.reviewed === false}>
|
||||
{tooltipProps => (
|
||||
<DecorDecorationGridDecoration
|
||||
{...tooltipProps}
|
||||
onSelect={!hasDecorationPendingReview ? openCreateDecorationModal : () => { }}
|
||||
/>}
|
||||
</Tooltip>;
|
||||
className={cl("change-decoration-modal-decoration")}
|
||||
onSelect={item.reviewed !== false ? () => setTryingDecoration(item) : () => { }}
|
||||
isSelected={activeSelectedDecoration?.hash === item.hash}
|
||||
decoration={item}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>;
|
||||
}
|
||||
} else {
|
||||
return <Tooltip text={"Pending review"} shouldShow={item.reviewed === false}>
|
||||
{tooltipProps => (
|
||||
<DecorDecorationGridDecoration
|
||||
{...tooltipProps}
|
||||
className={cl("change-decoration-modal-decoration")}
|
||||
onSelect={item.reviewed !== false ? () => setTryingDecoration(item) : () => { }}
|
||||
isSelected={activeSelectedDecoration?.hash === item.hash}
|
||||
decoration={item}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>;
|
||||
}
|
||||
}}
|
||||
getItemKey={item => typeof item === "string" ? item : item.hash}
|
||||
getSectionKey={section => section.sectionKey}
|
||||
renderSectionHeader={section => <SectionHeader section={section} />}
|
||||
sections={data}
|
||||
/>
|
||||
<div className={cl("change-decoration-modal-preview")}>
|
||||
<AvatarDecorationModalPreview
|
||||
avatarDecorationOverride={avatarDecorationOverride}
|
||||
user={UserStore.getCurrentUser()}
|
||||
}}
|
||||
getItemKey={item => typeof item === "string" ? item : item.hash}
|
||||
getSectionKey={section => section.sectionKey}
|
||||
renderSectionHeader={section => <SectionHeader section={section} />}
|
||||
sections={data}
|
||||
/>
|
||||
{isActiveDecorationPreset && <Forms.FormTitle className="">Part of the {activeDecorationPreset.name} Preset</Forms.FormTitle>}
|
||||
{typeof activeSelectedDecoration === "object" &&
|
||||
<Text
|
||||
variant="text-sm/semibold"
|
||||
color="header-primary"
|
||||
>
|
||||
{activeSelectedDecoration?.alt}
|
||||
</Text>
|
||||
}
|
||||
{activeDecorationHasAuthor && <Text key={`createdBy-${activeSelectedDecoration.authorId}`}>Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}</Text>}
|
||||
</div>
|
||||
<div className={cl("change-decoration-modal-preview")}>
|
||||
<AvatarDecorationModalPreview
|
||||
avatarDecorationOverride={avatarDecorationOverride}
|
||||
user={UserStore.getCurrentUser()}
|
||||
/>
|
||||
{isActiveDecorationPreset && <Forms.FormTitle className="">Part of the {activeDecorationPreset.name} Preset</Forms.FormTitle>}
|
||||
{typeof activeSelectedDecoration === "object" &&
|
||||
<Text
|
||||
variant="text-sm/semibold"
|
||||
color="header-primary"
|
||||
>
|
||||
{activeSelectedDecoration?.alt}
|
||||
</Text>
|
||||
}
|
||||
{activeDecorationHasAuthor && <Text key={`createdBy-${activeSelectedDecoration.authorId}`}>Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}</Text>}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</ModalContent>
|
||||
<ModalFooter className={classes(cl("change-decoration-modal-footer", cl("modal-footer")))}>
|
||||
<div className={cl("change-decoration-modal-footer-btn-container")}>
|
||||
|
|
|
@ -4,23 +4,23 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Link } from "@components/Link";
|
||||
import { openInviteModal } from "@utils/discord";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Text, TextInput, useEffect, useMemo, UserStore, useState } from "@webpack/common";
|
||||
|
||||
import { GUILD_ID, INVITE_KEY, RAW_SKU_ID } from "../../lib/constants";
|
||||
import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore";
|
||||
import { cl, requireAvatarDecorationModal, requireCreateStickerModal } from "../";
|
||||
import { cl, DecorationModalStyles, requireAvatarDecorationModal, requireCreateStickerModal } from "../";
|
||||
import { AvatarDecorationModalPreview } from "../components";
|
||||
|
||||
|
||||
const DecorationModalStyles = findByPropsLazy("modalFooterShopButton");
|
||||
|
||||
const FileUpload = findComponentByCodeLazy("fileUploadInput,");
|
||||
|
||||
const { default: HelpMessage, HelpMessageTypes } = findByPropsLazy("HelpMessageTypes");
|
||||
|
||||
function useObjectURL(object: Blob | MediaSource | null) {
|
||||
const [url, setUrl] = useState<string | null>(null);
|
||||
|
||||
|
@ -39,7 +39,7 @@ function useObjectURL(object: Blob | MediaSource | null) {
|
|||
return url;
|
||||
}
|
||||
|
||||
export default function CreateDecorationModal(props) {
|
||||
function CreateDecorationModal(props: ModalProps) {
|
||||
const [name, setName] = useState("");
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
@ -75,65 +75,69 @@ export default function CreateDecorationModal(props) {
|
|||
className={cl("create-decoration-modal-content")}
|
||||
scrollbarType="none"
|
||||
>
|
||||
<div className={cl("create-decoration-modal-form-preview-container")}>
|
||||
<div className={cl("create-decoration-modal-form")}>
|
||||
{error !== null && <Text color="text-danger" variant="text-xs/normal">{error.message}</Text>}
|
||||
<Forms.FormSection title="File">
|
||||
<FileUpload
|
||||
filename={file?.name}
|
||||
placeholder="Choose a file"
|
||||
buttonText="Browse"
|
||||
filters={[{ name: "Decoration file", extensions: ["png", "apng"] }]}
|
||||
onFileSelect={setFile}
|
||||
<ErrorBoundary>
|
||||
<HelpMessage messageType={HelpMessageTypes.WARNING}>
|
||||
Make sure your decoration does not violate <Link
|
||||
href="https://github.com/decor-discord/.github/blob/main/GUIDELINES.md"
|
||||
>
|
||||
the guidelines
|
||||
</Link> before submitting it.
|
||||
</HelpMessage>
|
||||
<div className={cl("create-decoration-modal-form-preview-container")}>
|
||||
<div className={cl("create-decoration-modal-form")}>
|
||||
{error !== null && <Text color="text-danger" variant="text-xs/normal">{error.message}</Text>}
|
||||
<Forms.FormSection title="File">
|
||||
<FileUpload
|
||||
filename={file?.name}
|
||||
placeholder="Choose a file"
|
||||
buttonText="Browse"
|
||||
filters={[{ name: "Decoration file", extensions: ["png", "apng"] }]}
|
||||
onFileSelect={setFile}
|
||||
/>
|
||||
<Forms.FormText type="description" className={Margins.top8}>
|
||||
File should be APNG or PNG.
|
||||
</Forms.FormText>
|
||||
</Forms.FormSection>
|
||||
<Forms.FormSection title="Name">
|
||||
<TextInput
|
||||
placeholder="Companion Cube"
|
||||
value={name}
|
||||
onChange={setName}
|
||||
/>
|
||||
<Forms.FormText type="description" className={Margins.top8}>
|
||||
This name will be used when referring to this decoration.
|
||||
</Forms.FormText>
|
||||
</Forms.FormSection>
|
||||
</div>
|
||||
<div>
|
||||
<AvatarDecorationModalPreview
|
||||
avatarDecorationOverride={decoration}
|
||||
user={UserStore.getCurrentUser()}
|
||||
/>
|
||||
<Forms.FormText type="description" className={Margins.top8}>
|
||||
File should be APNG or PNG.
|
||||
</Forms.FormText>
|
||||
</Forms.FormSection>
|
||||
<Forms.FormSection title="Name">
|
||||
<TextInput
|
||||
placeholder="Companion Cube"
|
||||
value={name}
|
||||
onChange={setName}
|
||||
/>
|
||||
<Forms.FormText type="description" className={Margins.top8}>
|
||||
This name will be used when referring to this decoration.
|
||||
</Forms.FormText>
|
||||
</Forms.FormSection>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<AvatarDecorationModalPreview
|
||||
avatarDecorationOverride={decoration}
|
||||
user={UserStore.getCurrentUser()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Forms.FormText type="description" className={Margins.bottom16}>
|
||||
Make sure your decoration does not violate <Link
|
||||
href="https://github.com/decor-discord/.github/blob/main/GUIDELINES.md"
|
||||
>
|
||||
the guidelines
|
||||
</Link> before creating your decoration.
|
||||
<br />You can receive updates on your decoration's review by joining <Link
|
||||
href={`https://discord.gg/${INVITE_KEY}`}
|
||||
onClick={async e => {
|
||||
e.preventDefault();
|
||||
if (!GuildStore.getGuild(GUILD_ID)) {
|
||||
const inviteAccepted = await openInviteModal(INVITE_KEY);
|
||||
if (inviteAccepted) {
|
||||
<Forms.FormText type="description" className={Margins.bottom16}>
|
||||
<br />You can receive updates on your decoration's review by joining <Link
|
||||
href={`https://discord.gg/${INVITE_KEY}`}
|
||||
onClick={async e => {
|
||||
e.preventDefault();
|
||||
if (!GuildStore.getGuild(GUILD_ID)) {
|
||||
const inviteAccepted = await openInviteModal(INVITE_KEY);
|
||||
if (inviteAccepted) {
|
||||
closeAllModals();
|
||||
FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" });
|
||||
}
|
||||
} else {
|
||||
closeAllModals();
|
||||
FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" });
|
||||
NavigationRouter.transitionToGuild(GUILD_ID);
|
||||
}
|
||||
} else {
|
||||
closeAllModals();
|
||||
FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" });
|
||||
NavigationRouter.transitionToGuild(GUILD_ID);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Decor's Discord server
|
||||
</Link>.
|
||||
</Forms.FormText>
|
||||
}}
|
||||
>
|
||||
Decor's Discord server
|
||||
</Link>.
|
||||
</Forms.FormText>
|
||||
</ErrorBoundary>
|
||||
</ModalContent>
|
||||
<ModalFooter className={cl("modal-footer")}>
|
||||
<Button
|
||||
|
@ -145,7 +149,7 @@ export default function CreateDecorationModal(props) {
|
|||
disabled={!file || !name}
|
||||
submitting={submitting}
|
||||
>
|
||||
Create
|
||||
Submit for Review
|
||||
</Button>
|
||||
<Button
|
||||
onClick={props.onClose}
|
||||
|
|
65
src/plugins/decor/ui/modals/GuidelinesModal.tsx
Normal file
65
src/plugins/decor/ui/modals/GuidelinesModal.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Link } from "@components/Link";
|
||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { Button, Forms, Text } from "@webpack/common";
|
||||
|
||||
import { settings } from "../../settings";
|
||||
import { cl, DecorationModalStyles, requireAvatarDecorationModal } from "../";
|
||||
import { openCreateDecorationModal } from "./CreateDecorationModal";
|
||||
|
||||
function GuidelinesModal(props: ModalProps) {
|
||||
return <ModalRoot
|
||||
{...props}
|
||||
size={ModalSize.SMALL}
|
||||
className={DecorationModalStyles.modal}
|
||||
>
|
||||
<ModalHeader separator={false} className={cl("modal-header")}>
|
||||
<Text
|
||||
color="header-primary"
|
||||
variant="heading-lg/semibold"
|
||||
tag="h1"
|
||||
style={{ flexGrow: 1 }}
|
||||
>
|
||||
Hold on
|
||||
</Text>
|
||||
<ModalCloseButton onClick={props.onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent
|
||||
scrollbarType="none"
|
||||
>
|
||||
<Forms.FormText>
|
||||
By submitting a decoration, you agree to <Link
|
||||
href="https://github.com/decor-discord/.github/blob/main/GUIDELINES.md"
|
||||
>
|
||||
the guidelines
|
||||
</Link>. Not reading these guidelines may get your account suspended from creating more decorations in the future.
|
||||
</Forms.FormText>
|
||||
</ModalContent>
|
||||
<ModalFooter className={cl("modal-footer")}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
settings.store.agreedToGuidelines = true;
|
||||
props.onClose();
|
||||
openCreateDecorationModal();
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
<Button
|
||||
onClick={props.onClose}
|
||||
color={Button.Colors.PRIMARY}
|
||||
look={Button.Looks.LINK}
|
||||
>
|
||||
Go Back
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>;
|
||||
}
|
||||
|
||||
export const openGuidelinesModal = () =>
|
||||
requireAvatarDecorationModal().then(() => openModal(props => <GuidelinesModal {...props} />));
|
|
@ -8,7 +8,7 @@
|
|||
display: flex;
|
||||
border-radius: 5px 5px 0 0;
|
||||
padding: 0 16px;
|
||||
gap: 4px
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.vc-decor-change-decoration-modal-preview {
|
||||
|
@ -72,7 +72,7 @@
|
|||
.vc-decor-sectioned-grid-list-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vc-decor-section-remove-margin {
|
||||
|
|
35
src/plugins/fixCodeblockGap/index.ts
Normal file
35
src/plugins/fixCodeblockGap/index.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "FixCodeblockGap",
|
||||
description: "Removes the gap between codeblocks and text below it",
|
||||
authors: [Devs.Grzesiek11],
|
||||
patches: [
|
||||
{
|
||||
find: ".default.Messages.DELETED_ROLE_PLACEHOLDER",
|
||||
replacement: {
|
||||
match: String.raw`/^${"```"}(?:([a-z0-9_+\-.#]+?)\n)?\n*([^\n][^]*?)\n*${"```"}`,
|
||||
replace: "$&\\n?",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
|
@ -126,7 +126,9 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
|
|||
|
||||
function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback {
|
||||
return (children, props) => () => {
|
||||
if (!props || (type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild)) return children;
|
||||
if (!props) return;
|
||||
if ((type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild) || (type === MenuItemParentType.Channel && (!props.channel || !props.guild)))
|
||||
return children;
|
||||
|
||||
const group = findGroupChildrenByChildId(childId, children);
|
||||
|
||||
|
|
|
@ -36,62 +36,68 @@ function search(src: string, engine: string) {
|
|||
open(engine + encodeURIComponent(src), "_blank");
|
||||
}
|
||||
|
||||
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
|
||||
if (!props) return;
|
||||
const { reverseImageSearchType, itemHref, itemSrc } = props;
|
||||
function makeSearchItem(src: string) {
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
label="Search Image"
|
||||
key="search-image"
|
||||
id="search-image"
|
||||
>
|
||||
{Object.keys(Engines).map((engine, i) => {
|
||||
const key = "search-image-" + engine;
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
key={key}
|
||||
id={key}
|
||||
label={
|
||||
<Flex style={{ alignItems: "center", gap: "0.5em" }}>
|
||||
<img
|
||||
style={{
|
||||
borderRadius: i >= 3 // Do not round Google, Yandex & SauceNAO
|
||||
? "50%"
|
||||
: void 0
|
||||
}}
|
||||
aria-hidden="true"
|
||||
height={16}
|
||||
width={16}
|
||||
src={new URL("/favicon.ico", Engines[engine]).toString().replace("lens.", "")}
|
||||
/>
|
||||
{engine}
|
||||
</Flex>
|
||||
}
|
||||
action={() => search(src, Engines[engine])}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Menu.MenuItem
|
||||
key="search-image-all"
|
||||
id="search-image-all"
|
||||
label={
|
||||
<Flex style={{ alignItems: "center", gap: "0.5em" }}>
|
||||
<OpenExternalIcon height={16} width={16} />
|
||||
All
|
||||
</Flex>
|
||||
}
|
||||
action={() => Object.values(Engines).forEach(e => search(src, e))}
|
||||
/>
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (!reverseImageSearchType || reverseImageSearchType !== "img") return;
|
||||
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
|
||||
if (props?.reverseImageSearchType !== "img") return;
|
||||
|
||||
const src = itemHref ?? itemSrc;
|
||||
const src = props.itemHref ?? props.itemSrc;
|
||||
|
||||
const group = findGroupChildrenByChildId("copy-link", children);
|
||||
if (group) {
|
||||
group.push((
|
||||
<Menu.MenuItem
|
||||
label="Search Image"
|
||||
key="search-image"
|
||||
id="search-image"
|
||||
>
|
||||
{Object.keys(Engines).map((engine, i) => {
|
||||
const key = "search-image-" + engine;
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
key={key}
|
||||
id={key}
|
||||
label={
|
||||
<Flex style={{ alignItems: "center", gap: "0.5em" }}>
|
||||
<img
|
||||
style={{
|
||||
borderRadius: i >= 3 // Do not round Google, Yandex & SauceNAO
|
||||
? "50%"
|
||||
: void 0
|
||||
}}
|
||||
aria-hidden="true"
|
||||
height={16}
|
||||
width={16}
|
||||
src={new URL("/favicon.ico", Engines[engine]).toString().replace("lens.", "")}
|
||||
/>
|
||||
{engine}
|
||||
</Flex>
|
||||
}
|
||||
action={() => search(src, Engines[engine])}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Menu.MenuItem
|
||||
key="search-image-all"
|
||||
id="search-image-all"
|
||||
label={
|
||||
<Flex style={{ alignItems: "center", gap: "0.5em" }}>
|
||||
<OpenExternalIcon height={16} width={16} />
|
||||
All
|
||||
</Flex>
|
||||
}
|
||||
action={() => Object.values(Engines).forEach(e => search(src, e))}
|
||||
/>
|
||||
</Menu.MenuItem>
|
||||
));
|
||||
}
|
||||
group?.push(makeSearchItem(src));
|
||||
};
|
||||
|
||||
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
|
||||
if (!props?.src) return;
|
||||
|
||||
const group = findGroupChildrenByChildId("copy-native-link", children) ?? children;
|
||||
group.push(makeSearchItem(props.src));
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
|
@ -111,10 +117,12 @@ export default definePlugin({
|
|||
],
|
||||
|
||||
start() {
|
||||
addContextMenuPatch("message", imageContextMenuPatch);
|
||||
addContextMenuPatch("message", messageContextMenuPatch);
|
||||
addContextMenuPatch("image-context", imageContextMenuPatch);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeContextMenuPatch("message", imageContextMenuPatch);
|
||||
removeContextMenuPatch("message", messageContextMenuPatch);
|
||||
removeContextMenuPatch("image-context", imageContextMenuPatch);
|
||||
}
|
||||
});
|
||||
|
|
78
src/plugins/reviewDB/auth.tsx
Normal file
78
src/plugins/reviewDB/auth.tsx
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { DataStore } from "@api/index";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { openModal } from "@utils/modal";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { showToast, Toasts, UserStore } from "@webpack/common";
|
||||
|
||||
import { ReviewDBAuth } from "./entities";
|
||||
|
||||
const DATA_STORE_KEY = "rdb-auth";
|
||||
|
||||
const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal");
|
||||
|
||||
export let Auth: ReviewDBAuth = {};
|
||||
|
||||
export async function initAuth() {
|
||||
Auth = await getAuth() ?? {};
|
||||
}
|
||||
|
||||
export async function getAuth(): Promise<ReviewDBAuth | undefined> {
|
||||
const auth = await DataStore.get(DATA_STORE_KEY);
|
||||
return auth?.[UserStore.getCurrentUser()?.id];
|
||||
}
|
||||
|
||||
export async function getToken() {
|
||||
const auth = await getAuth();
|
||||
return auth?.token;
|
||||
}
|
||||
|
||||
export async function updateAuth(newAuth: ReviewDBAuth) {
|
||||
return DataStore.update(DATA_STORE_KEY, auth => {
|
||||
auth ??= {};
|
||||
Auth = auth[UserStore.getCurrentUser().id] ??= {};
|
||||
|
||||
if (newAuth.token) Auth.token = newAuth.token;
|
||||
if (newAuth.user) Auth.user = newAuth.user;
|
||||
|
||||
return auth;
|
||||
});
|
||||
}
|
||||
|
||||
export function authorize(callback?: any) {
|
||||
openModal(props =>
|
||||
<OAuth2AuthorizeModal
|
||||
{...props}
|
||||
scopes={["identify"]}
|
||||
responseType="code"
|
||||
redirectUri="https://manti.vendicated.dev/api/reviewdb/auth"
|
||||
permissions={0n}
|
||||
clientId="915703782174752809"
|
||||
cancelCompletesFlow={false}
|
||||
callback={async (response: any) => {
|
||||
try {
|
||||
const url = new URL(response.location);
|
||||
url.searchParams.append("clientMod", "vencord");
|
||||
const res = await fetch(url, {
|
||||
headers: new Headers({ Accept: "application/json" })
|
||||
});
|
||||
const { token, success } = await res.json();
|
||||
if (success) {
|
||||
updateAuth({ token });
|
||||
showToast("Successfully logged in!", Toasts.Type.SUCCESS);
|
||||
callback?.();
|
||||
} else if (res.status === 1) {
|
||||
showToast("An Error occurred while logging in.", Toasts.Type.FAILURE);
|
||||
}
|
||||
} catch (e) {
|
||||
new Logger("ReviewDB").error("Failed to authorize", e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
99
src/plugins/reviewDB/components/BlockedUserModal.tsx
Normal file
99
src/plugins/reviewDB/components/BlockedUserModal.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { Forms, Tooltip, useState } from "@webpack/common";
|
||||
|
||||
import { Auth } from "../auth";
|
||||
import { ReviewDBUser } from "../entities";
|
||||
import { fetchBlocks, unblockUser } from "../reviewDbApi";
|
||||
import { cl } from "../utils";
|
||||
|
||||
function UnblockButton(props: { onClick?(): void; }) {
|
||||
return (
|
||||
<Tooltip text="Unblock user">
|
||||
{tooltipProps => (
|
||||
<div
|
||||
{...tooltipProps}
|
||||
role="button"
|
||||
onClick={props.onClick}
|
||||
className={cl("block-modal-unblock")}
|
||||
>
|
||||
<svg height="20" viewBox="0 -960 960 960" width="20" fill="var(--status-danger)">
|
||||
<path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q54 0 104-17.5t92-50.5L228-676q-33 42-50.5 92T160-480q0 134 93 227t227 93Zm252-124q33-42 50.5-92T800-480q0-134-93-227t-227-93q-54 0-104 17.5T284-732l448 448Z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function BlockedUser({ user, isBusy, setIsBusy }: { user: ReviewDBUser; isBusy: boolean; setIsBusy(v: boolean): void; }) {
|
||||
const [gone, setGone] = useState(false);
|
||||
if (gone) return null;
|
||||
|
||||
return (
|
||||
<div className={cl("block-modal-row")}>
|
||||
<img src={user.profilePhoto} alt="" />
|
||||
<Forms.FormText className={cl("block-modal-username")}>{user.username}</Forms.FormText>
|
||||
<UnblockButton
|
||||
onClick={isBusy ? undefined : async () => {
|
||||
setIsBusy(true);
|
||||
try {
|
||||
await unblockUser(user.discordID);
|
||||
setGone(true);
|
||||
} finally {
|
||||
setIsBusy(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Modal() {
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
const [blocks, error, pending] = useAwaiter(fetchBlocks, {
|
||||
onError: e => new Logger("ReviewDB").error("Failed to fetch blocks", e),
|
||||
fallbackValue: [],
|
||||
});
|
||||
|
||||
if (pending)
|
||||
return null;
|
||||
if (error)
|
||||
return <Forms.FormText>Failed to fetch blocks: ${String(error)}</Forms.FormText>;
|
||||
if (!blocks.length)
|
||||
return <Forms.FormText>No blocked users.</Forms.FormText>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{blocks.map(b => (
|
||||
<BlockedUser
|
||||
key={b.discordID}
|
||||
user={b}
|
||||
isBusy={isBusy}
|
||||
setIsBusy={setIsBusy}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function openBlockModal() {
|
||||
openModal(modalProps => (
|
||||
<ModalRoot {...modalProps}>
|
||||
<ModalHeader className={cl("block-modal-header")}>
|
||||
<Forms.FormTitle style={{ margin: 0 }}>Blocked Users</Forms.FormTitle>
|
||||
<ModalCloseButton onClick={modalProps.onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent className={cl("block-modal")}>
|
||||
{Auth.token ? <Modal /> : <Forms.FormText>You are not logged into ReviewDB!</Forms.FormText>}
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
));
|
||||
}
|
85
src/plugins/reviewDB/components/MessageButton.tsx
Normal file
85
src/plugins/reviewDB/components/MessageButton.tsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { DeleteIcon } from "@components/Icons";
|
||||
import { classes } from "@utils/misc";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Tooltip } from "@webpack/common";
|
||||
|
||||
const iconClasses = findByPropsLazy("button", "wrapper", "disabled", "separator");
|
||||
|
||||
export function DeleteButton({ onClick }: { onClick(): void; }) {
|
||||
return (
|
||||
<Tooltip text="Delete Review">
|
||||
{props => (
|
||||
<div
|
||||
{...props}
|
||||
className={classes(iconClasses.button, iconClasses.dangerous)}
|
||||
onClick={onClick}
|
||||
role="button"
|
||||
>
|
||||
<DeleteIcon width="20" height="20" />
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function ReportButton({ onClick }: { onClick(): void; }) {
|
||||
return (
|
||||
<Tooltip text="Report Review">
|
||||
{props => (
|
||||
<div
|
||||
{...props}
|
||||
className={iconClasses.button}
|
||||
onClick={onClick}
|
||||
role="button"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export function BlockButton({ onClick, isBlocked }: { onClick(): void; isBlocked: boolean; }) {
|
||||
return (
|
||||
<Tooltip text={`${isBlocked ? "Unblock" : "Block"} user`}>
|
||||
{props => (
|
||||
<div
|
||||
{...props}
|
||||
className={iconClasses.button}
|
||||
onClick={onClick}
|
||||
role="button"
|
||||
>
|
||||
<svg height="20" viewBox="0 -960 960 960" width="20" fill="currentColor">
|
||||
{isBlocked
|
||||
? <path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z" />
|
||||
: <path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q54 0 104-17.5t92-50.5L228-676q-33 42-50.5 92T160-480q0 134 93 227t227 93Zm252-124q33-42 50.5-92T800-480q0-134-93-227t-227-93q-54 0-104 17.5T284-732l448 448Z" />
|
||||
}
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
50
src/plugins/reviewDB/components/ReviewBadge.tsx
Normal file
50
src/plugins/reviewDB/components/ReviewBadge.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { MaskedLink, React, Tooltip } from "@webpack/common";
|
||||
import { HTMLAttributes } from "react";
|
||||
|
||||
import { Badge } from "../entities";
|
||||
import { cl } from "../utils";
|
||||
|
||||
export default function ReviewBadge(badge: Badge & { onClick?(): void; }) {
|
||||
const Wrapper = badge.redirectURL
|
||||
? MaskedLink
|
||||
: (props: HTMLAttributes<HTMLDivElement>) => (
|
||||
<span {...props} role="button">{props.children}</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
text={badge.name}>
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<Wrapper className={cl("blocked-badge")} href={badge.redirectURL!} onClick={badge.onClick}>
|
||||
<img
|
||||
className={cl("badge")}
|
||||
width="22px"
|
||||
height="22px"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
src={badge.icon}
|
||||
alt={badge.description}
|
||||
/>
|
||||
</Wrapper>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
188
src/plugins/reviewDB/components/ReviewComponent.tsx
Normal file
188
src/plugins/reviewDB/components/ReviewComponent.tsx
Normal file
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { openUserProfile } from "@utils/discord";
|
||||
import { classes } from "@utils/misc";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { filters, findBulk } from "@webpack";
|
||||
import { Alerts, moment, Parser, showToast, Timestamp } from "@webpack/common";
|
||||
|
||||
import { Auth, getToken } from "../auth";
|
||||
import { Review, ReviewType } from "../entities";
|
||||
import { blockUser, deleteReview, reportReview, unblockUser } from "../reviewDbApi";
|
||||
import { settings } from "../settings";
|
||||
import { canBlockReviewAuthor, canDeleteReview, canReportReview, cl } from "../utils";
|
||||
import { openBlockModal } from "./BlockedUserModal";
|
||||
import { BlockButton, DeleteButton, ReportButton } from "./MessageButton";
|
||||
import ReviewBadge from "./ReviewBadge";
|
||||
|
||||
export default LazyComponent(() => {
|
||||
// this is terrible, blame mantika
|
||||
const p = filters.byProps;
|
||||
const [
|
||||
{ cozyMessage, buttons, message, buttonsInner, groupStart },
|
||||
{ container, isHeader },
|
||||
{ avatar, clickable, username, wrapper, cozy },
|
||||
buttonClasses,
|
||||
botTag
|
||||
] = findBulk(
|
||||
p("cozyMessage"),
|
||||
p("container", "isHeader"),
|
||||
p("avatar", "zalgo"),
|
||||
p("button", "wrapper", "selected"),
|
||||
p("botTag", "botTagRegular")
|
||||
);
|
||||
|
||||
const dateFormat = new Intl.DateTimeFormat();
|
||||
|
||||
return function ReviewComponent({ review, refetch, profileId }: { review: Review; refetch(): void; profileId: string; }) {
|
||||
function openModal() {
|
||||
openUserProfile(review.sender.discordID);
|
||||
}
|
||||
|
||||
function delReview() {
|
||||
Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: "Do you really want to delete this review?",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Nevermind",
|
||||
onConfirm: async () => {
|
||||
if (!(await getToken())) {
|
||||
return showToast("You must be logged in to delete reviews.");
|
||||
} else {
|
||||
deleteReview(review.id).then(res => {
|
||||
if (res.success) {
|
||||
refetch();
|
||||
}
|
||||
showToast(res.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reportRev() {
|
||||
Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: "Do you really you want to report this review?",
|
||||
confirmText: "Report",
|
||||
cancelText: "Nevermind",
|
||||
// confirmColor: "red", this just adds a class name and breaks the submit button guh
|
||||
onConfirm: async () => {
|
||||
if (!(await getToken())) {
|
||||
return showToast("You must be logged in to report reviews.");
|
||||
} else {
|
||||
reportReview(review.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const isAuthorBlocked = Auth?.user?.blockedUsers?.includes(review.sender.discordID) ?? false;
|
||||
|
||||
function blockReviewSender() {
|
||||
if (isAuthorBlocked)
|
||||
return unblockUser(review.sender.discordID);
|
||||
|
||||
Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: "Do you really you want to block this user? They will be unable to leave further reviews on your profile. You can unblock users in the plugin settings.",
|
||||
confirmText: "Block",
|
||||
cancelText: "Nevermind",
|
||||
// confirmColor: "red", this just adds a class name and breaks the submit button guh
|
||||
onConfirm: async () => {
|
||||
if (!(await getToken())) {
|
||||
return showToast("You must be logged in to block users.");
|
||||
} else {
|
||||
blockUser(review.sender.discordID);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes(cozyMessage, wrapper, message, groupStart, cozy, cl("review"))} style={
|
||||
{
|
||||
marginLeft: "0px",
|
||||
paddingLeft: "52px", // wth is this
|
||||
paddingRight: "16px"
|
||||
}
|
||||
}>
|
||||
|
||||
<img
|
||||
className={classes(avatar, clickable)}
|
||||
onClick={openModal}
|
||||
src={review.sender.profilePhoto || "/assets/1f0bfc0865d324c2587920a7d80c609b.png?size=128"}
|
||||
style={{ left: "0px", zIndex: 0 }}
|
||||
/>
|
||||
<div style={{ display: "inline-flex", justifyContent: "center", alignItems: "center" }}>
|
||||
<span
|
||||
className={classes(clickable, username)}
|
||||
style={{ color: "var(--channels-default)", fontSize: "14px" }}
|
||||
onClick={() => openModal()}
|
||||
>
|
||||
{review.sender.username}
|
||||
</span>
|
||||
|
||||
{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>
|
||||
{isAuthorBlocked && (
|
||||
<ReviewBadge
|
||||
name="You have blocked this user"
|
||||
description="You have blocked this user"
|
||||
icon="/assets/aaee57e0090991557b66.svg"
|
||||
type={0}
|
||||
onClick={() => openBlockModal()}
|
||||
/>
|
||||
)}
|
||||
{review.sender.badges.map(badge => <ReviewBadge {...badge} />)}
|
||||
|
||||
{
|
||||
!settings.store.hideTimestamps && review.type !== ReviewType.System && (
|
||||
<Timestamp timestamp={moment(review.timestamp * 1000)} >
|
||||
{dateFormat.format(review.timestamp * 1000)}
|
||||
</Timestamp>)
|
||||
}
|
||||
|
||||
<div className={cl("review-comment")}>
|
||||
{Parser.parseGuildEventDescription(review.comment)}
|
||||
</div>
|
||||
|
||||
{review.id !== 0 && (
|
||||
<div className={classes(container, isHeader, buttons)} style={{
|
||||
padding: "0px",
|
||||
}}>
|
||||
<div className={classes(buttonClasses.wrapper, buttonsInner)} >
|
||||
{canReportReview(review) && <ReportButton onClick={reportRev} />}
|
||||
{canBlockReviewAuthor(profileId, review) && <BlockButton isBlocked={isAuthorBlocked} onClick={blockReviewSender} />}
|
||||
{canDeleteReview(profileId, review) && <DeleteButton onClick={delReview} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
105
src/plugins/reviewDB/components/ReviewModal.tsx
Normal file
105
src/plugins/reviewDB/components/ReviewModal.tsx
Normal file
|
@ -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 <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 { Auth } from "../auth";
|
||||
import { Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
|
||||
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 === Auth.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}
|
||||
profileId={discordId}
|
||||
/>
|
||||
)}
|
||||
<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}
|
||||
/>
|
||||
));
|
||||
}
|
199
src/plugins/reviewDB/components/ReviewsView.tsx
Normal file
199
src/plugins/reviewDB/components/ReviewsView.tsx
Normal file
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react";
|
||||
import { find, findByPropsLazy } from "@webpack";
|
||||
import { Forms, React, RelationshipStore, showToast, useRef, UserStore } from "@webpack/common";
|
||||
|
||||
import { Auth, authorize } from "../auth";
|
||||
import { Review } from "../entities";
|
||||
import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
|
||||
import { settings } from "../settings";
|
||||
import { cl } from "../utils";
|
||||
import ReviewComponent from "./ReviewComponent";
|
||||
|
||||
|
||||
const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms");
|
||||
const { ChatInputTypes } = findByPropsLazy("ChatInputTypes");
|
||||
|
||||
const InputComponent = LazyComponent(() => find(m => m.default?.type?.render?.toString().includes("default.CHANNEL_TEXT_AREA")).default);
|
||||
|
||||
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 => {
|
||||
if (settings.store.hideBlockedUsers)
|
||||
data!.reviews = data!.reviews?.filter(r => !RelationshipStore.isBlocked(r.sender.discordID));
|
||||
|
||||
scrollToTop?.();
|
||||
onFetchReviews(data!);
|
||||
}
|
||||
});
|
||||
|
||||
if (!reviewData) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReviewList
|
||||
refetch={refetch}
|
||||
reviews={reviewData!.reviews}
|
||||
hideOwnReview={hideOwnReview}
|
||||
profileId={discordId}
|
||||
/>
|
||||
|
||||
{showInput && (
|
||||
<ReviewsInputComponent
|
||||
name={name}
|
||||
discordId={discordId}
|
||||
refetch={refetch}
|
||||
isAuthor={reviewData!.reviews?.some(r => r.sender.discordID === UserStore.getCurrentUser().id)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ReviewList({ refetch, reviews, hideOwnReview, profileId }: { refetch(): void; reviews: Review[]; hideOwnReview: boolean; profileId: string; }) {
|
||||
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}
|
||||
profileId={profileId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{reviews?.length === 0 && (
|
||||
<Forms.FormText className={cl("placeholder")}>
|
||||
Looks like nobody reviewed this user yet. You could be the first!
|
||||
</Forms.FormText>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) {
|
||||
const { token } = Auth;
|
||||
const editorRef = useRef<any>(null);
|
||||
const inputType = ChatInputTypes.FORM;
|
||||
inputType.disableAutoFocus = true;
|
||||
|
||||
const channel = {
|
||||
flags_: 256,
|
||||
guild_id_: null,
|
||||
id: "0",
|
||||
getGuildId: () => null,
|
||||
isPrivate: () => true,
|
||||
isActiveThread: () => false,
|
||||
isArchivedLockedThread: () => false,
|
||||
isDM: () => true,
|
||||
roles: { "0": { permissions: 0n } },
|
||||
getRecipientId: () => "0",
|
||||
hasFlag: () => false,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onClick={() => {
|
||||
if (!token) {
|
||||
showToast("Opening authorization window...");
|
||||
authorize();
|
||||
}
|
||||
}}>
|
||||
<InputComponent
|
||||
className={cl("input")}
|
||||
channel={channel}
|
||||
placeholder={
|
||||
!token
|
||||
? "You need to authorize to review users!"
|
||||
: isAuthor
|
||||
? `Update review for @${name}`
|
||||
: `Review @${name}`
|
||||
}
|
||||
type={inputType}
|
||||
disableThemedBackground={true}
|
||||
setEditorRef={ref => editorRef.current = ref}
|
||||
textValue=""
|
||||
onSubmit={
|
||||
async res => {
|
||||
const response = await addReview({
|
||||
userid: discordId,
|
||||
comment: res.value,
|
||||
});
|
||||
|
||||
if (response?.success) {
|
||||
refetch();
|
||||
|
||||
const slateEditor = editorRef.current.ref.current.getSlateEditor();
|
||||
|
||||
// clear editor
|
||||
Transforms.delete(slateEditor, {
|
||||
at: {
|
||||
anchor: Editor.start(slateEditor, []),
|
||||
focus: Editor.end(slateEditor, []),
|
||||
}
|
||||
});
|
||||
} else if (response?.message) {
|
||||
showToast(response.message);
|
||||
}
|
||||
|
||||
// even tho we need to return this, it doesnt do anything
|
||||
return {
|
||||
shouldClear: false,
|
||||
shouldRefocus: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
100
src/plugins/reviewDB/entities.ts
Normal file
100
src/plugins/reviewDB/entities.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
export const enum UserType {
|
||||
Banned = -1,
|
||||
Normal = 0,
|
||||
Admin = 1
|
||||
}
|
||||
|
||||
export const enum ReviewType {
|
||||
User = 0,
|
||||
Server = 1,
|
||||
Support = 2,
|
||||
System = 3
|
||||
}
|
||||
|
||||
export const enum NotificationType {
|
||||
Info = 0,
|
||||
Ban = 1,
|
||||
Unban = 2,
|
||||
Warning = 3
|
||||
}
|
||||
|
||||
export interface ReviewDBAuth {
|
||||
token?: string;
|
||||
user?: ReviewDBCurrentUser;
|
||||
}
|
||||
|
||||
export interface Badge {
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
redirectURL?: string;
|
||||
type: number;
|
||||
}
|
||||
|
||||
export interface BanInfo {
|
||||
id: string;
|
||||
discordID: string;
|
||||
reviewID: number;
|
||||
reviewContent: string;
|
||||
banEndDate: number;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
type: NotificationType;
|
||||
}
|
||||
|
||||
export interface ReviewDBUser {
|
||||
ID: number;
|
||||
discordID: string;
|
||||
username: string;
|
||||
type: UserType;
|
||||
profilePhoto: string;
|
||||
badges: any[];
|
||||
}
|
||||
|
||||
export interface ReviewDBCurrentUser extends ReviewDBUser {
|
||||
warningCount: number;
|
||||
clientMod: string;
|
||||
banInfo: BanInfo | null;
|
||||
notification: Notification | null;
|
||||
lastReviewID: number;
|
||||
blockedUsers?: string[];
|
||||
}
|
||||
|
||||
export interface ReviewAuthor {
|
||||
id: number,
|
||||
discordID: string,
|
||||
username: string,
|
||||
profilePhoto: string,
|
||||
badges: Badge[];
|
||||
}
|
||||
|
||||
export interface Review {
|
||||
comment: string,
|
||||
id: number,
|
||||
star: number,
|
||||
sender: ReviewAuthor,
|
||||
timestamp: number;
|
||||
type?: ReviewType;
|
||||
}
|
157
src/plugins/reviewDB/index.tsx
Normal file
157
src/plugins/reviewDB/index.tsx
Normal file
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./style.css";
|
||||
|
||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import ExpandableHeader from "@components/ExpandableHeader";
|
||||
import { OpenExternalIcon } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Alerts, Menu, Parser, showToast, useState } from "@webpack/common";
|
||||
import { Guild, User } from "discord-types/general";
|
||||
|
||||
import { Auth, initAuth, updateAuth } from "./auth";
|
||||
import { openReviewsModal } from "./components/ReviewModal";
|
||||
import ReviewsView from "./components/ReviewsView";
|
||||
import { NotificationType } from "./entities";
|
||||
import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
|
||||
import { settings } from "./settings";
|
||||
|
||||
const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => {
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
label="View Reviews"
|
||||
id="vc-rdb-server-reviews"
|
||||
icon={OpenExternalIcon}
|
||||
action={() => openReviewsModal(props.guild.id, props.guild.name)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "ReviewDB",
|
||||
description: "Review other users (Adds a new settings to profiles)",
|
||||
authors: [Devs.mantikafasi, Devs.Ven],
|
||||
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "showBorder:null",
|
||||
replacement: {
|
||||
match: /user:(\i),setNote:\i,canDM.+?\}\)/,
|
||||
replace: "$&,$self.getReviewsComponent($1)"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
flux: {
|
||||
CONNECTION_OPEN: initAuth,
|
||||
},
|
||||
|
||||
async start() {
|
||||
addContextMenuPatch("guild-header-popout", guildPopoutPatch);
|
||||
|
||||
const s = settings.store;
|
||||
const { lastReviewId, notifyReviews } = s;
|
||||
|
||||
const legacy = s as any as { token?: string; };
|
||||
if (legacy.token) {
|
||||
await updateAuth({ token: legacy.token });
|
||||
legacy.token = undefined;
|
||||
new Logger("ReviewDB").info("Migrated legacy settings");
|
||||
}
|
||||
|
||||
await initAuth();
|
||||
|
||||
setTimeout(async () => {
|
||||
if (!Auth.token) return;
|
||||
|
||||
const user = await getCurrentUserInfo(Auth.token);
|
||||
updateAuth({ user });
|
||||
|
||||
if (notifyReviews) {
|
||||
if (lastReviewId && lastReviewId < user.lastReviewID) {
|
||||
s.lastReviewId = user.lastReviewID;
|
||||
if (user.lastReviewID !== 0)
|
||||
showToast("You have new reviews on your profile!");
|
||||
}
|
||||
}
|
||||
|
||||
if (user.notification) {
|
||||
const props = user.notification.type === NotificationType.Ban ? {
|
||||
cancelText: "Appeal",
|
||||
confirmText: "Ok",
|
||||
onCancel: async () =>
|
||||
VencordNative.native.openExternal(
|
||||
"https://reviewdb.mantikafasi.dev/api/redirect?"
|
||||
+ new URLSearchParams({
|
||||
token: Auth.token!,
|
||||
page: "dashboard/appeal"
|
||||
})
|
||||
)
|
||||
} : {};
|
||||
|
||||
Alerts.show({
|
||||
title: user.notification.title,
|
||||
body: (
|
||||
Parser.parse(
|
||||
user.notification.content,
|
||||
false
|
||||
)
|
||||
),
|
||||
...props
|
||||
});
|
||||
|
||||
readNotification(user.notification.id);
|
||||
}
|
||||
}, 4000);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeContextMenuPatch("guild-header-popout", guildPopoutPatch);
|
||||
},
|
||||
|
||||
getReviewsComponent: ErrorBoundary.wrap((user: User) => {
|
||||
const [reviewCount, setReviewCount] = useState<number>();
|
||||
|
||||
return (
|
||||
<ExpandableHeader
|
||||
headerText="User Reviews"
|
||||
onMoreClick={() => openReviewsModal(user.id, user.username)}
|
||||
moreTooltipText={
|
||||
reviewCount && reviewCount > 50
|
||||
? `View all ${reviewCount} reviews`
|
||||
: "Open Review Modal"
|
||||
}
|
||||
onDropDownClick={state => settings.store.reviewsDropdownState = !state}
|
||||
defaultState={settings.store.reviewsDropdownState}
|
||||
>
|
||||
<ReviewsView
|
||||
discordId={user.id}
|
||||
name={user.username}
|
||||
onFetchReviews={r => setReviewCount(r.reviewCount)}
|
||||
showInput
|
||||
/>
|
||||
</ExpandableHeader>
|
||||
);
|
||||
}, { message: "Failed to render Reviews" })
|
||||
});
|
198
src/plugins/reviewDB/reviewDbApi.ts
Normal file
198
src/plugins/reviewDB/reviewDbApi.ts
Normal file
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { showToast, Toasts } from "@webpack/common";
|
||||
|
||||
import { Auth, authorize, getToken, updateAuth } from "./auth";
|
||||
import { Review, ReviewDBCurrentUser, ReviewDBUser, ReviewType } from "./entities";
|
||||
import { settings } from "./settings";
|
||||
|
||||
const API_URL = "https://manti.vendicated.dev/api/reviewdb";
|
||||
|
||||
export const REVIEWS_PER_PAGE = 50;
|
||||
|
||||
export interface Response {
|
||||
success: boolean,
|
||||
message: string;
|
||||
reviews: Review[];
|
||||
updated: boolean;
|
||||
hasNextPage: boolean;
|
||||
reviewCount: number;
|
||||
}
|
||||
|
||||
const WarningFlag = 0b00000010;
|
||||
|
||||
export async function getReviews(id: string, offset = 0): Promise<Response> {
|
||||
let flags = 0;
|
||||
if (!settings.store.showWarning) flags |= WarningFlag;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
flags: String(flags),
|
||||
offset: String(offset)
|
||||
});
|
||||
const req = await fetch(`${API_URL}/users/${id}/reviews?${params}`);
|
||||
|
||||
const res = (req.status === 200)
|
||||
? await req.json() as Response
|
||||
: {
|
||||
success: false,
|
||||
message: req.status === 429 ? "You are sending requests too fast. Wait a few seconds and try again." : "An Error occured while fetching reviews. Please try again later.",
|
||||
reviews: [],
|
||||
updated: false,
|
||||
hasNextPage: false,
|
||||
reviewCount: 0
|
||||
};
|
||||
|
||||
if (!res.success) {
|
||||
showToast(res.message, Toasts.Type.FAILURE);
|
||||
return {
|
||||
...res,
|
||||
reviews: [
|
||||
{
|
||||
id: 0,
|
||||
comment: res.message,
|
||||
star: 0,
|
||||
timestamp: 0,
|
||||
type: ReviewType.System,
|
||||
sender: {
|
||||
id: 0,
|
||||
username: "ReviewDB",
|
||||
profilePhoto: "https://cdn.discordapp.com/avatars/1134864775000629298/3f87ad315b32ee464d84f1270c8d1b37.png?size=256&format=webp&quality=lossless",
|
||||
discordID: "1134864775000629298",
|
||||
badges: []
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function addReview(review: any): Promise<Response | null> {
|
||||
review.token = await getToken();
|
||||
|
||||
if (!review.token) {
|
||||
showToast("Please authorize to add a review.");
|
||||
authorize();
|
||||
return null;
|
||||
}
|
||||
|
||||
return fetch(API_URL + `/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 async function deleteReview(id: number): Promise<Response> {
|
||||
return fetch(API_URL + `/users/${id}/reviews`, {
|
||||
method: "DELETE",
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
token: await getToken(),
|
||||
reviewid: id
|
||||
})
|
||||
}).then(r => r.json());
|
||||
}
|
||||
|
||||
export async function reportReview(id: number) {
|
||||
const res = await fetch(API_URL + "/reports", {
|
||||
method: "PUT",
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
reviewid: id,
|
||||
token: await getToken()
|
||||
})
|
||||
}).then(r => r.json()) as Response;
|
||||
|
||||
showToast(res.message);
|
||||
}
|
||||
|
||||
async function patchBlock(action: "block" | "unblock", userId: string) {
|
||||
const res = await fetch(API_URL + "/blocks", {
|
||||
method: "PATCH",
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: await getToken() || ""
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
action: action,
|
||||
discordId: userId
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
showToast(`Failed to ${action} user`, Toasts.Type.FAILURE);
|
||||
} else {
|
||||
showToast(`Successfully ${action}ed user`, Toasts.Type.SUCCESS);
|
||||
|
||||
if (Auth?.user?.blockedUsers) {
|
||||
const newBlockedUsers = action === "block"
|
||||
? [...Auth.user.blockedUsers, userId]
|
||||
: Auth.user.blockedUsers.filter(id => id !== userId);
|
||||
updateAuth({ user: { ...Auth.user, blockedUsers: newBlockedUsers } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const blockUser = (userId: string) => patchBlock("block", userId);
|
||||
export const unblockUser = (userId: string) => patchBlock("unblock", userId);
|
||||
|
||||
export async function fetchBlocks(): Promise<ReviewDBUser[]> {
|
||||
const res = await fetch(API_URL + "/blocks", {
|
||||
method: "GET",
|
||||
headers: new Headers({
|
||||
Accept: "application/json",
|
||||
Authorization: await getToken() || ""
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export function getCurrentUserInfo(token: string): Promise<ReviewDBCurrentUser> {
|
||||
return fetch(API_URL + "/users", {
|
||||
body: JSON.stringify({ token }),
|
||||
method: "POST",
|
||||
}).then(r => r.json());
|
||||
}
|
||||
|
||||
export async function readNotification(id: number) {
|
||||
return fetch(API_URL + `/notifications?id=${id}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Authorization": await getToken() || "",
|
||||
},
|
||||
});
|
||||
}
|
93
src/plugins/reviewDB/settings.tsx
Normal file
93
src/plugins/reviewDB/settings.tsx
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { OptionType } from "@utils/types";
|
||||
import { Button } from "@webpack/common";
|
||||
|
||||
import { authorize, getToken } from "./auth";
|
||||
import { openBlockModal } from "./components/BlockedUserModal";
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
authorize: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Authorize with ReviewDB",
|
||||
component: () => (
|
||||
<Button onClick={authorize}>
|
||||
Authorize with ReviewDB
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
notifyReviews: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Notify about new reviews on startup",
|
||||
default: true,
|
||||
},
|
||||
showWarning: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Display warning to be respectful at the top of the reviews list",
|
||||
default: true,
|
||||
},
|
||||
hideTimestamps: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Hide timestamps on reviews",
|
||||
default: false,
|
||||
},
|
||||
hideBlockedUsers: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Hide reviews from blocked users",
|
||||
default: true,
|
||||
},
|
||||
manageBlocks: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Manage Blocked Users",
|
||||
component: () => (
|
||||
<Button onClick={openBlockModal}>Manage Blocked Users</Button>
|
||||
)
|
||||
},
|
||||
website: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "ReviewDB website",
|
||||
component: () => (
|
||||
<Button onClick={async () => {
|
||||
let url = "https://reviewdb.mantikafasi.dev/";
|
||||
const token = await getToken();
|
||||
if (token)
|
||||
url += "/api/redirect?token=" + encodeURIComponent(token);
|
||||
|
||||
VencordNative.native.openExternal(url);
|
||||
}}>
|
||||
ReviewDB website
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
supportServer: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "ReviewDB Support Server",
|
||||
component: () => (
|
||||
<Button onClick={() => {
|
||||
VencordNative.native.openExternal("https://discord.gg/eWPBSbvznt");
|
||||
}}>
|
||||
ReviewDB Support Server
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}).withPrivateSettings<{
|
||||
lastReviewId?: number;
|
||||
reviewsDropdownState?: boolean;
|
||||
}>();
|
121
src/plugins/reviewDB/style.css
Normal file
121
src/plugins/reviewDB/style.css
Normal file
|
@ -0,0 +1,121 @@
|
|||
[class|="section"]:not([class|="lastSection"]) + .vc-rdb-view {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.vc-rdb-badge {
|
||||
vertical-align: middle;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.vc-rdb-input {
|
||||
margin-top: 6px;
|
||||
margin-bottom: 12px;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border: 1px solid var(--profile-message-input-border-color);
|
||||
}
|
||||
|
||||
.vc-rdb-modal-footer > div {
|
||||
width: 100%;
|
||||
margin: 6px 16px;
|
||||
}
|
||||
|
||||
/* When input becomes disabled(while sending review), input adds unneccesary padding to left, this prevents it */
|
||||
.vc-rdb-input > div > div {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.vc-rdb-placeholder {
|
||||
margin-bottom: 4px;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.vc-rdb-input * {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.vc-rdb-modal-footer {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.vc-rdb-modal-footer .vc-rdb-input {
|
||||
margin-bottom: 0;
|
||||
background: var(--input-background);
|
||||
}
|
||||
|
||||
.vc-rdb-modal-footer [class|="pageControlContainer"] {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.vc-rdb-modal-header {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.vc-rdb-modal-reviews {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.vc-rdb-review {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.vc-rdb-review-comment img {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.vc-rdb-review-comment {
|
||||
overflow-y: hidden;
|
||||
margin-top: 1px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-normal);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.vc-rdb-blocked-badge {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-rdb-block-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vc-rdb-block-modal {
|
||||
padding: 1em;
|
||||
display: grid;
|
||||
gap: 0.75em;
|
||||
}
|
||||
|
||||
.vc-rdb-block-modal-row {
|
||||
display: flex;
|
||||
height: 2em;
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vc-rdb-block-modal-row img {
|
||||
border-radius: 50%;
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.vc-rdb-block-modal img::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--background-modifier-accent);
|
||||
}
|
||||
|
||||
.vc-rdb-block-modal-username {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.vc-rdb-block-modal-unblock {
|
||||
cursor: pointer;
|
||||
}
|
43
src/plugins/reviewDB/utils.tsx
Normal file
43
src/plugins/reviewDB/utils.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { UserStore } from "@webpack/common";
|
||||
|
||||
import { Auth } from "./auth";
|
||||
import { Review, UserType } from "./entities";
|
||||
|
||||
export const cl = classNameFactory("vc-rdb-");
|
||||
|
||||
export function canDeleteReview(profileId: string, review: Review) {
|
||||
const myId = UserStore.getCurrentUser().id;
|
||||
return (
|
||||
myId === profileId
|
||||
|| review.sender.discordID === myId
|
||||
|| Auth.user?.type === UserType.Admin
|
||||
);
|
||||
}
|
||||
|
||||
export function canBlockReviewAuthor(profileId: string, review: Review) {
|
||||
const myId = UserStore.getCurrentUser().id;
|
||||
return profileId === myId && review.sender.discordID !== myId;
|
||||
}
|
||||
|
||||
export function canReportReview(review: Review) {
|
||||
return review.sender.discordID !== UserStore.getCurrentUser().id;
|
||||
}
|
|
@ -72,10 +72,6 @@ export default definePlugin({
|
|||
{
|
||||
find: 'tutorialId:"whos-online',
|
||||
replacement: [
|
||||
{
|
||||
match: /\i.roleIcon,\.\.\.\i/,
|
||||
replace: "$&,color:$self.roleGroupColor(arguments[0])"
|
||||
},
|
||||
{
|
||||
match: /null,\i," — ",\i\]/,
|
||||
replace: "null,$self.roleGroupColor(arguments[0])]"
|
||||
|
@ -83,6 +79,16 @@ export default definePlugin({
|
|||
],
|
||||
predicate: () => settings.store.memberList,
|
||||
},
|
||||
{
|
||||
find: ".Messages.THREAD_BROWSER_PRIVATE",
|
||||
replacement: [
|
||||
{
|
||||
match: /children:\[\i," — ",\i\]/,
|
||||
replace: "children:[$self.roleGroupColor(arguments[0])]"
|
||||
},
|
||||
],
|
||||
predicate: () => settings.store.memberList,
|
||||
},
|
||||
{
|
||||
find: "renderPrioritySpeaker",
|
||||
replacement: [
|
||||
|
|
|
@ -19,14 +19,23 @@
|
|||
import "./styles.css";
|
||||
|
||||
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getTheme, insertTextIntoChatInputBox, Theme } from "@utils/discord";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
|
||||
import definePlugin from "@utils/types";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Button, ButtonLooks, ButtonWrapperClasses, Forms, Parser, Select, Tooltip, useMemo, useState } from "@webpack/common";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
replaceMessageContents: {
|
||||
description: "Replace timestamps in message contents",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
function parseTime(time: string) {
|
||||
const cleanTime = time.slice(1, -1).replace(/(\d)(AM|PM)$/i, "$1 $2");
|
||||
|
||||
|
@ -116,9 +125,11 @@ function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): voi
|
|||
export default definePlugin({
|
||||
name: "SendTimestamps",
|
||||
description: "Send timestamps easily via chat box button & text shortcuts. Read the extended description!",
|
||||
authors: [Devs.Ven, Devs.Tyler],
|
||||
authors: [Devs.Ven, Devs.Tyler, Devs.Grzesiek11],
|
||||
dependencies: ["MessageEventsAPI"],
|
||||
|
||||
settings: settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "ChannelTextAreaButtons",
|
||||
|
@ -131,7 +142,9 @@ export default definePlugin({
|
|||
|
||||
start() {
|
||||
this.listener = addPreSendListener((_, msg) => {
|
||||
msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime);
|
||||
if (settings.store.replaceMessageContents) {
|
||||
msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -213,7 +213,7 @@ function applyRules(content: string): string {
|
|||
|
||||
if (stringRules) {
|
||||
for (const rule of stringRules) {
|
||||
if (!rule.find || !rule.replace) continue;
|
||||
if (!rule.find) continue;
|
||||
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
|
||||
|
||||
content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, "");
|
||||
|
@ -222,7 +222,7 @@ function applyRules(content: string): string {
|
|||
|
||||
if (regexRules) {
|
||||
for (const rule of regexRules) {
|
||||
if (!rule.find || !rule.replace) continue;
|
||||
if (!rule.find) continue;
|
||||
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
|
||||
|
||||
try {
|
||||
|
|
|
@ -33,7 +33,7 @@ function VencordPopout(onClose: () => void) {
|
|||
const pluginEntries = [] as ReactNode[];
|
||||
|
||||
for (const plugin of Object.values(Vencord.Plugins.plugins)) {
|
||||
if (plugin.toolboxActions) {
|
||||
if (plugin.toolboxActions && Vencord.Plugins.isPluginEnabled(plugin.name)) {
|
||||
pluginEntries.push(
|
||||
<Menu.MenuGroup
|
||||
label={plugin.name}
|
||||
|
|
|
@ -27,7 +27,7 @@ import { Margins } from "@utils/margins";
|
|||
import { copyWithToast } from "@utils/misc";
|
||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Button, ChannelStore, Forms, Menu, Text } from "@webpack/common";
|
||||
import { Button, ChannelStore, Forms, i18n, Menu, Text } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
|
||||
|
@ -117,22 +117,26 @@ const settings = definePluginSettings({
|
|||
}
|
||||
});
|
||||
|
||||
function MakeContextCallback(name: string) {
|
||||
function MakeContextCallback(name: "Guild" | "User" | "Channel") {
|
||||
const callback: NavContextMenuPatchCallback = (children, props) => () => {
|
||||
if ((name === "Guild" && !props.guild) || (name === "User" && !props.user)) return;
|
||||
const value = props[name.toLowerCase()];
|
||||
if (!value) return;
|
||||
if (props.label === i18n.Messages.CHANNEL_ACTIONS_MENU_LABEL) return; // random shit like notification settings
|
||||
|
||||
const lastChild = children.at(-1);
|
||||
if (lastChild?.key === "developer-actions") {
|
||||
const p = lastChild.props;
|
||||
if (!Array.isArray(p.children))
|
||||
p.children = [p.children];
|
||||
({ children } = p);
|
||||
|
||||
children = p.children;
|
||||
}
|
||||
|
||||
children.splice(-1, 0,
|
||||
<Menu.MenuItem
|
||||
id={`vc-view-${name.toLowerCase()}-raw`}
|
||||
label="View Raw"
|
||||
action={() => openViewRawModal(JSON.stringify(props[name.toLowerCase()], null, 4), name)}
|
||||
action={() => openViewRawModal(JSON.stringify(value, null, 4), name)}
|
||||
icon={CopyIcon}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -169,10 +169,25 @@ export default definePlugin({
|
|||
match: /let\{text:\i=""/,
|
||||
replace: "return [null,null];$&"
|
||||
}
|
||||
},
|
||||
|
||||
// Add back "Show My Camera" context menu
|
||||
{
|
||||
find: '.default("MediaEngineWebRTC");',
|
||||
replacement: {
|
||||
match: /supports\(\i\)\{switch\(\i\)\{case (\i).Features/,
|
||||
replace: "$&.DISABLE_VIDEO:return true;case $1.Features"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
async copyImage(url: string) {
|
||||
if (IS_VESKTOP && VesktopNative.clipboard) {
|
||||
const data = await fetch(url).then(r => r.arrayBuffer());
|
||||
VesktopNative.clipboard.copyImage(data, url);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clipboard only supports image/png, jpeg and similar won't work. Thus, we need to convert it to png
|
||||
// via canvas first
|
||||
const img = new Image();
|
||||
|
|
|
@ -403,6 +403,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "maisy",
|
||||
id: 257109471589957632n,
|
||||
},
|
||||
Grzesiek11: {
|
||||
name: "Grzesiek11",
|
||||
id: 368475654662127616n,
|
||||
},
|
||||
Samwich: {
|
||||
name: "Samwich",
|
||||
id: 976176454511509554n,
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
Loading…
Reference in a new issue