Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
2c6cd72a53
39 changed files with 545 additions and 206 deletions
|
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { classes } from "@utils/misc";
|
||||
import { findCssClassesLazy } from "@webpack";
|
||||
import { Parser } from "@webpack/common";
|
||||
|
||||
|
|
@ -12,9 +13,9 @@ const CodeContainerClasses = findCssClassesLazy("markup", "codeContainer");
|
|||
/**
|
||||
* Renders code in a Discord codeblock
|
||||
*/
|
||||
export function CodeBlock(props: { content?: string, lang: string; }) {
|
||||
export function CodeBlock({ className, ...props }: { content?: string, lang: string; className?: string; }) {
|
||||
return (
|
||||
<div className={CodeContainerClasses.markup}>
|
||||
<div className={classes(CodeContainerClasses.markup, className)}>
|
||||
{Parser.defaultRules.codeBlock.react(props, null, {})}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -23,9 +24,9 @@ export function CodeBlock(props: { content?: string, lang: string; }) {
|
|||
/**
|
||||
* Renders inline code like `this`
|
||||
*/
|
||||
export function InlineCode({ children }: { children: React.ReactNode; }) {
|
||||
export function InlineCode({ children, className }: { children: React.ReactNode; className?: string; }) {
|
||||
return (
|
||||
<span className={CodeContainerClasses.markup}>
|
||||
<span className={classes(CodeContainerClasses.markup, className)}>
|
||||
<code className="inline">{children}</code>
|
||||
</span>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -81,6 +81,31 @@ export function CopyIcon(props: IconProps) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discord's ID icon, as seen in Developer Mode "Copy ID" context menu items
|
||||
*/
|
||||
export function CopyIdIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={classes(props.className, "vc-copy-id-icon")}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M15.3 14.48c-.46.45-1.08.67-1.86.67h-1.39V9.2h1.39c.78 0 1.4.22 1.86.67.46.45.68 1.22.68 2.31 0 1.1-.22 1.86-.68 2.31Z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm1 15h2.04V7.34H6V17Zm4-9.66V17h3.44c1.46 0 2.6-.42 3.38-1.25.8-.83 1.2-2.02 1.2-3.58s-.4-2.75-1.2-3.58c-.79-.83-1.92-1.25-3.38-1.25H10Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Discord's open external icon, as seen in the user profile connections
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export function BooleanSetting({ setting, pluginSettings, definedSettings, id, o
|
|||
}
|
||||
|
||||
return (
|
||||
<SettingsSection tag="label" name={id} description={setting.description} error={error} inlineSetting>
|
||||
<SettingsSection tag="label" name={setting.displayName} id={id} description={setting.description} error={error} inlineSetting>
|
||||
<Switch
|
||||
checked={state}
|
||||
onChange={handleChange}
|
||||
|
|
|
|||
|
|
@ -35,19 +35,20 @@ export function resolveError(isValidResult: boolean | string) {
|
|||
}
|
||||
|
||||
interface SettingsSectionProps extends PropsWithChildren {
|
||||
name: string;
|
||||
name?: string;
|
||||
id: string;
|
||||
description: string;
|
||||
error?: string | null;
|
||||
inlineSetting?: boolean;
|
||||
tag?: "label" | "div";
|
||||
}
|
||||
|
||||
export function SettingsSection({ tag: Tag = "div", name, description, error, inlineSetting, children }: SettingsSectionProps) {
|
||||
export function SettingsSection({ tag: Tag = "div", name, id, description, error, inlineSetting, children }: SettingsSectionProps) {
|
||||
return (
|
||||
<Tag className={cl("section")}>
|
||||
<div className={classes(cl("content"), inlineSetting && cl("inline"))}>
|
||||
<div className={cl("label")}>
|
||||
{name && <BaseText className={cl("title")} size="md" weight="medium">{wordsToTitle(wordsFromCamel(name))}</BaseText>}
|
||||
<BaseText className={cl("title")} size="md" weight="medium">{name ?? wordsToTitle(wordsFromCamel(id))}</BaseText>
|
||||
{description && <BaseText className={cl("description")} size="sm">{description}</BaseText>}
|
||||
</div>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export function NumberSetting({ setting, pluginSettings, definedSettings, id, on
|
|||
}
|
||||
|
||||
return (
|
||||
<SettingsSection name={id} description={setting.description} error={error}>
|
||||
<SettingsSection name={setting.displayName} id={id} description={setting.description} error={error}>
|
||||
<TextInput
|
||||
type="number"
|
||||
pattern="-?[0-9]+"
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export function SelectSetting({ setting, pluginSettings, definedSettings, onChan
|
|||
}
|
||||
|
||||
return (
|
||||
<SettingsSection name={id} description={setting.description} error={error}>
|
||||
<SettingsSection name={setting.displayName} id={id} description={setting.description} error={error}>
|
||||
<Select
|
||||
placeholder={setting.placeholder ?? "Select an option"}
|
||||
options={setting.options}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export function SliderSetting({ setting, pluginSettings, definedSettings, id, on
|
|||
}
|
||||
|
||||
return (
|
||||
<SettingsSection name={id} description={setting.description} error={error}>
|
||||
<SettingsSection name={setting.displayName} id={id} description={setting.description} error={error}>
|
||||
<Slider
|
||||
markers={setting.markers}
|
||||
minValue={setting.markers[0]}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export function TextSetting({ setting, pluginSettings, definedSettings, id, onCh
|
|||
}
|
||||
|
||||
return (
|
||||
<SettingsSection name={id} description={setting.description} error={error}>
|
||||
<SettingsSection name={setting.displayName} id={id} description={setting.description} error={error}>
|
||||
{setting.multiline
|
||||
? <TextArea
|
||||
placeholder={setting.placeholder ?? "Enter a value"}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,22 @@ export default definePlugin({
|
|||
replace: "$1!0"
|
||||
}
|
||||
},
|
||||
{
|
||||
// Gradient roles in chat
|
||||
find: "=!1,contentOnly:",
|
||||
replacement: {
|
||||
match: /animate:\i/,
|
||||
replace: "animate:!0"
|
||||
}
|
||||
},
|
||||
{
|
||||
// Gradient roles in member list
|
||||
find: '="left",className:',
|
||||
replacement: {
|
||||
match: /,animateGradient:/,
|
||||
replace: ",animateGradient:!0,_oldAnimateGradient:"
|
||||
}
|
||||
},
|
||||
{
|
||||
// Nameplates
|
||||
find: ".MINI_PREVIEW,[",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export interface TrackData {
|
|||
artist?: string;
|
||||
|
||||
appleMusicLink?: string;
|
||||
appleMusicArtistLink?: string;
|
||||
songLink?: string;
|
||||
|
||||
albumArtwork?: string;
|
||||
|
|
@ -35,6 +36,12 @@ const enum AssetImageType {
|
|||
Disabled = "Disabled"
|
||||
}
|
||||
|
||||
const enum LinkType {
|
||||
Album = "Album",
|
||||
Artist = "Artist",
|
||||
Disabled = "Disabled"
|
||||
}
|
||||
|
||||
const applicationId = "1239490006054207550";
|
||||
|
||||
let updateInterval: NodeJS.Timeout | undefined;
|
||||
|
|
@ -107,6 +114,24 @@ const settings = definePluginSettings({
|
|||
description: "Activity state format string",
|
||||
default: "{artist} · {album}"
|
||||
},
|
||||
detailsLink: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Activity details link",
|
||||
options: [
|
||||
{ label: "Album", value: LinkType.Album, default: true },
|
||||
{ label: "Artist", value: LinkType.Artist },
|
||||
{ label: "Disabled", value: LinkType.Disabled }
|
||||
],
|
||||
},
|
||||
stateLink: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Activity state link",
|
||||
options: [
|
||||
{ label: "Album", value: LinkType.Album },
|
||||
{ label: "Artist", value: LinkType.Artist, default: true },
|
||||
{ label: "Disabled", value: LinkType.Disabled }
|
||||
],
|
||||
},
|
||||
largeImageType: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Activity assets large image type",
|
||||
|
|
@ -121,6 +146,15 @@ const settings = definePluginSettings({
|
|||
description: "Activity assets large text format string",
|
||||
default: "{album}"
|
||||
},
|
||||
largeImageLink: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Activity assets large image link",
|
||||
options: [
|
||||
{ label: "Album", value: LinkType.Album, default: true },
|
||||
{ label: "Artist", value: LinkType.Artist },
|
||||
{ label: "Disabled", value: LinkType.Disabled }
|
||||
],
|
||||
},
|
||||
smallImageType: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Activity assets small image type",
|
||||
|
|
@ -135,6 +169,15 @@ const settings = definePluginSettings({
|
|||
description: "Activity assets small text format string",
|
||||
default: "{artist}"
|
||||
},
|
||||
smallImageLink: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Activity assets small image link",
|
||||
options: [
|
||||
{ label: "Album", value: LinkType.Album },
|
||||
{ label: "Artist", value: LinkType.Artist, default: true },
|
||||
{ label: "Disabled", value: LinkType.Disabled }
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
function customFormat(formatStr: string, data: TrackData) {
|
||||
|
|
@ -144,6 +187,14 @@ function customFormat(formatStr: string, data: TrackData) {
|
|||
.replaceAll("{artist}", data.artist ?? "");
|
||||
}
|
||||
|
||||
function getLink(type: LinkType, data: TrackData) {
|
||||
return type === LinkType.Album
|
||||
? data.appleMusicLink
|
||||
: type === LinkType.Artist
|
||||
? data.appleMusicArtistLink
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function getImageAsset(type: AssetImageType, data: TrackData) {
|
||||
const source = type === AssetImageType.Album
|
||||
? data.albumArtwork
|
||||
|
|
@ -204,11 +255,13 @@ export default definePlugin({
|
|||
if (settings.store.largeImageType !== AssetImageType.Disabled) {
|
||||
assets.large_image = largeImageAsset;
|
||||
if (!isRadio) assets.large_text = customFormat(settings.store.largeTextString, trackData);
|
||||
assets.large_url = getLink(settings.store.largeImageLink, trackData);
|
||||
}
|
||||
|
||||
if (settings.store.smallImageType !== AssetImageType.Disabled) {
|
||||
assets.small_image = smallImageAsset;
|
||||
if (!isRadio) assets.small_text = customFormat(settings.store.smallTextString, trackData);
|
||||
assets.small_url = getLink(settings.store.smallImageLink, trackData);
|
||||
}
|
||||
|
||||
const buttons: ActivityButton[] = [];
|
||||
|
|
@ -233,6 +286,8 @@ export default definePlugin({
|
|||
name: customFormat(settings.store.nameString, trackData),
|
||||
details: customFormat(settings.store.detailsString, trackData),
|
||||
state: isRadio ? undefined : customFormat(settings.store.stateString, trackData),
|
||||
details_url: getLink(settings.store.detailsLink, trackData),
|
||||
state_url: getLink(settings.store.stateLink, trackData),
|
||||
|
||||
timestamps: (trackData.playerPosition && trackData.duration && settings.store.enableTimestamps) ? {
|
||||
start: Date.now() - (trackData.playerPosition * 1000),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ async function applescript(cmds: string[]) {
|
|||
|
||||
interface RemoteData {
|
||||
appleMusicLink?: string,
|
||||
appleMusicArtistLink?: string;
|
||||
songLink?: string,
|
||||
albumArtwork?: string,
|
||||
artistArtwork?: string;
|
||||
|
|
@ -65,6 +66,7 @@ async function fetchRemoteData({ id, name, artist, album }: { id: string, name:
|
|||
id,
|
||||
data: {
|
||||
appleMusicLink: songData.trackViewUrl,
|
||||
appleMusicArtistLink: songData.artistViewUrl,
|
||||
songLink: `https://song.link/i/${new URL(songData.trackViewUrl).searchParams.get("i")}`,
|
||||
albumArtwork: (songData.artworkUrl100).replace("100x100", "512x512"),
|
||||
artistArtwork: artistArtworkURL
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# BetterRoleContext
|
||||
|
||||
Adds options to copy role color, edit role and view role icon when right clicking roles in the user profile
|
||||
Adds options to copy role color, edit role and view role icon when right clicking roles in the user profile or in the member list
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -6,16 +6,16 @@
|
|||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import { ImageIcon } from "@components/Icons";
|
||||
import { CopyIdIcon, ImageIcon } from "@components/Icons";
|
||||
import { copyToClipboard } from "@utils/clipboard";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getCurrentChannel, getCurrentGuild, openImageModal } from "@utils/discord";
|
||||
import { getCurrentChannel, getCurrentGuild, getIntlMessage, openImageModal } from "@utils/discord";
|
||||
import { isTruthy } from "@utils/guards";
|
||||
import { classes } from "@utils/misc";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Guild, PopoutProps, Role } from "@vencord/discord-types";
|
||||
import { findByCodeLazy, findByPropsLazy, findCssClassesLazy } from "@webpack";
|
||||
import { GuildRoleStore, Menu, PermissionStore, Popout, useRef } from "@webpack/common";
|
||||
import { ContextMenuApi, GuildRoleStore, Menu, PermissionStore, Popout, useRef } from "@webpack/common";
|
||||
import { ComponentType } from "react";
|
||||
|
||||
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
|
||||
|
|
@ -184,22 +184,63 @@ export function buildExtraRoleContextMenuItems(role: Role, guild: Guild, popoutR
|
|||
return { before, after };
|
||||
}
|
||||
|
||||
export function openRoleContextMenu(event: React.MouseEvent<HTMLElement>, { guildId, id: roleId }: { guildId: string; id: string; }) {
|
||||
const guild = getCurrentGuild();
|
||||
if (!guild || guild.id !== guildId) return;
|
||||
|
||||
const role = GuildRoleStore.getRole(guildId, roleId);
|
||||
if (!role) return;
|
||||
|
||||
ContextMenuApi.openContextMenu(event, () => {
|
||||
const popoutRef = useRef(null);
|
||||
const { before, after } = buildExtraRoleContextMenuItems(role, guild, popoutRef);
|
||||
|
||||
return (
|
||||
<Menu.Menu
|
||||
navId="vc-better-role-context-member-list"
|
||||
onClose={ContextMenuApi.closeContextMenu}
|
||||
aria-label="Role Actions"
|
||||
>
|
||||
{before}
|
||||
{after}
|
||||
<Menu.MenuItem
|
||||
key="vc-better-role-context-copy-role-id"
|
||||
id="vc-better-role-context-copy-role-id"
|
||||
label={getIntlMessage("COPY_ID_ROLE")}
|
||||
icon={CopyIdIcon}
|
||||
action={() => copyToClipboard(role.id)}
|
||||
/>
|
||||
</Menu.Menu>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterRoleContext",
|
||||
description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile",
|
||||
description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile or in the member list",
|
||||
tags: ["Roles", "Appearance"],
|
||||
authors: [Devs.Ven, Devs.goodbee],
|
||||
authors: [Devs.Ven, Devs.goodbee, Devs.nightmaresan],
|
||||
dependencies: ["UserSettingsAPI"],
|
||||
|
||||
settings,
|
||||
|
||||
patches: [{
|
||||
find: ".ROLE_MENTION)",
|
||||
replacement: {
|
||||
match: /function (\i)(?=.+?renderPopout:.{0,20}\1,\{guildId:\i,channelId:\i)/,
|
||||
replace: "$self.RoleMembers=$1;$&"
|
||||
openRoleContextMenu,
|
||||
patches: [
|
||||
{
|
||||
find: ".ROLE_MENTION)",
|
||||
replacement: {
|
||||
match: /function (\i)(?=.+?renderPopout:.{0,20}\1,\{guildId:\i,channelId:\i)/,
|
||||
replace: "$self.RoleMembers=$1;$&"
|
||||
}
|
||||
},
|
||||
// Conflicts with RoleColorEverywhere which changes the code at the end of our match. (and also uses same find & similar match)
|
||||
// However, BetterRoleContext applies first (alphabetic order), so it's not an issue
|
||||
{
|
||||
find: 'tutorialId:"whos-online',
|
||||
replacement: {
|
||||
match: /(?<=#{intl::CHANNEL_MEMBERS_A11Y_LABEL}.{0,200}?"aria-hidden":!0,)children:.{0,200}?(?:—|\\u2014) ",\i\]\}\)\]/,
|
||||
replace: "onContextMenu:e=>$self.openRoleContextMenu(e,arguments[0]),$&"
|
||||
}
|
||||
}
|
||||
}],
|
||||
],
|
||||
|
||||
start() {
|
||||
// DeveloperMode needs to be enabled for the context menu to be shown
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ function AllowLevelSetting({ settingKey }: AllowLevelSettingProps) {
|
|||
|
||||
const AllowLevelSettings = ErrorBoundary.wrap(() => {
|
||||
return (
|
||||
<SettingsSection name="Filter List" description="Always allow loggers of these types">
|
||||
<SettingsSection name="Filter List" id="filterList" description="Always allow loggers of these types">
|
||||
<div style={{ display: "flex", flexDirection: "row" }}>
|
||||
{Object.keys(settings.store.allowLevel).map(key => (
|
||||
<AllowLevelSetting key={key} settingKey={key as keyof AllowLevels} />
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ const settings = definePluginSettings({
|
|||
default: false
|
||||
},
|
||||
applyColorOnlyInDms: {
|
||||
displayName: "Apply Color Only In DMs",
|
||||
description: "Apply colors only in direct messages; do not apply colors in servers.",
|
||||
restartNeeded: false,
|
||||
type: OptionType.BOOLEAN,
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ function setActivity(activity: Activity | null) {
|
|||
|
||||
const settings = definePluginSettings({
|
||||
apiKey: {
|
||||
displayName: "API Key",
|
||||
description: "Custom Last.fm API key. Not required but highly recommended to avoid rate limiting with our shared key",
|
||||
type: OptionType.STRING,
|
||||
},
|
||||
|
|
@ -172,6 +173,7 @@ const settings = definePluginSettings({
|
|||
],
|
||||
},
|
||||
showLastFmLogo: {
|
||||
displayName: "Show Last.fm Logo",
|
||||
description: "Show the Last.fm logo by the album cover",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { JSX } from "react";
|
|||
const settings = definePluginSettings({
|
||||
showAtSymbol: {
|
||||
type: OptionType.BOOLEAN,
|
||||
displayName: "Show @ Symbol",
|
||||
description: "Whether the the @ symbol should be displayed on user mentions",
|
||||
default: true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ const settings = definePluginSettings({
|
|||
]
|
||||
},
|
||||
idList: {
|
||||
displayName: "ID List",
|
||||
description: "Guild/channel/user IDs to blacklist or whitelist (separate with comma)",
|
||||
type: OptionType.STRING,
|
||||
default: "",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import type { MouseEvent } from "react";
|
|||
interface URLReplacementRule {
|
||||
match: RegExp;
|
||||
replace: (...matches: string[]) => string;
|
||||
displayName?: string;
|
||||
description: string;
|
||||
shortlinkMatch?: RegExp;
|
||||
accountViewReplace?: (userId: string) => string;
|
||||
|
|
@ -59,6 +60,7 @@ const UrlReplacementRules: Record<string, URLReplacementRule> = {
|
|||
itunes: {
|
||||
match: /^https:\/\/(?:geo\.)?music\.apple\.com\/([a-z]{2}\/)?(album|artist|playlist|song|curator)\/([^/?#]+)\/?([^/?#]+)?(?:\?.*)?(?:#.*)?$/,
|
||||
replace: (_, lang, type, name, id) => id ? `itunes://music.apple.com/us/${type}/${name}/${id}` : `itunes://music.apple.com/us/${type}/${name}`,
|
||||
displayName: "iTunes",
|
||||
description: "Open Apple Music links in the iTunes app"
|
||||
},
|
||||
vrcx: {
|
||||
|
|
@ -77,6 +79,7 @@ const pluginSettings = definePluginSettings(
|
|||
Object.entries(UrlReplacementRules).reduce((acc, [key, rule]) => {
|
||||
acc[key] = {
|
||||
type: OptionType.BOOLEAN,
|
||||
displayName: rule.displayName,
|
||||
description: rule.description,
|
||||
default: true,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
|
||||
import { CopyIdIcon, InfoIcon, OwnerCrownIcon } from "@components/Icons";
|
||||
import { buildExtraRoleContextMenuItems } from "@plugins/betterRoleContext";
|
||||
import { cl, getGuildPermissionSpecMap, loadGetGuildPermissionSpecMap } from "@plugins/permissionsViewer/utils";
|
||||
import { copyToClipboard } from "@utils/clipboard";
|
||||
|
|
@ -198,15 +198,6 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
|
|||
);
|
||||
}
|
||||
|
||||
function IDIcon() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M15.3 14.48c-.46.45-1.08.67-1.86.67h-1.39V9.2h1.39c.78 0 1.4.22 1.86.67.46.45.68 1.22.68 2.31 0 1.1-.22 1.86-.68 2.31Z" />
|
||||
<path fill="currentColor" fillRule="evenodd" clipRule="evenodd" d="M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm1 15h2.04V7.34H6V17Zm4-9.66V17h3.44c1.46 0 2.6-.42 3.38-1.25.8-.83 1.2-2.02 1.2-3.58s-.4-2.75-1.2-3.58c-.79-.83-1.92-1.25-3.38-1.25H10Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewAsRoleIcon() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24">
|
||||
|
|
@ -231,7 +222,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
|
|||
<Menu.MenuItem
|
||||
id={cl("copy-role-id")}
|
||||
label={getIntlMessage("COPY_ID_ROLE")}
|
||||
icon={IDIcon}
|
||||
icon={CopyIdIcon}
|
||||
action={() => copyToClipboard(roleId)}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -48,11 +48,13 @@ export const settings = definePluginSettings({
|
|||
},
|
||||
canCollapseDmSection: {
|
||||
type: OptionType.BOOLEAN,
|
||||
displayName: "Can Collapse DM Section",
|
||||
description: "Allow uncategorised DMs section to be collapsable",
|
||||
default: false
|
||||
},
|
||||
dmSectionCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
displayName: "DM Section Collapsed",
|
||||
description: "Collapse DM section",
|
||||
default: false,
|
||||
hidden: true
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const settings = definePluginSettings({
|
|||
placeholder: "Google"
|
||||
},
|
||||
customEngineURL: {
|
||||
displayName: "Custom Engine URL",
|
||||
description: "The URL of your Engine",
|
||||
type: OptionType.STRING,
|
||||
placeholder: "https://google.com/search?q="
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export async function updateAuth(newAuth: ReviewDBAuth) {
|
|||
});
|
||||
}
|
||||
|
||||
export function authorize(callback?: any) {
|
||||
export function authorize(callback?: () => void) {
|
||||
openModal(props =>
|
||||
<OAuth2AuthorizeModal
|
||||
{...props}
|
||||
|
|
@ -50,7 +50,7 @@ export function authorize(callback?: any) {
|
|||
permissions={0n}
|
||||
clientId="915703782174752809"
|
||||
cancelCompletesFlow={false}
|
||||
callback={async (response: any) => {
|
||||
callback={async (response: { location: string }) => {
|
||||
try {
|
||||
const url = new URL(response.location);
|
||||
url.searchParams.append("clientMod", "vencord");
|
||||
|
|
@ -60,7 +60,7 @@ export function authorize(callback?: any) {
|
|||
|
||||
if (!res.ok) {
|
||||
const { message } = await res.json();
|
||||
showToast(message || "An error occured while authorizing", Toasts.Type.FAILURE);
|
||||
showToast(message ?? "An error occured while authorizing", Toasts.Type.FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@
|
|||
|
||||
import { Auth, getToken } from "@plugins/reviewDB/auth";
|
||||
import { Review, ReviewType } from "@plugins/reviewDB/entities";
|
||||
import { blockUser, deleteReview, reportReview, unblockUser } from "@plugins/reviewDB/reviewDbApi";
|
||||
import { blockUser, deleteReview, deleteReviewVote, reportReview, unblockUser, voteReview } from "@plugins/reviewDB/reviewDbApi";
|
||||
import { settings } from "@plugins/reviewDB/settings";
|
||||
import { canBlockReviewAuthor, canDeleteReview, canReportReview, cl, showToast } from "@plugins/reviewDB/utils";
|
||||
import { openUserProfile } from "@utils/discord";
|
||||
import { classes } from "@utils/misc";
|
||||
import { findCssClassesLazy } from "@webpack";
|
||||
import { ConfirmModal,IconUtils, openModal as openVencordModal, Parser, Timestamp, useState } from "@webpack/common";
|
||||
import { ConfirmModal, IconUtils, openModal as openVencordModal, Parser, Timestamp, useEffect, useState } from "@webpack/common";
|
||||
|
||||
import { openBlockModal } from "./BlockedUserModal";
|
||||
import { BlockButton, DeleteButton, ReportButton } from "./MessageButton";
|
||||
|
|
@ -40,6 +40,14 @@ const dateFormat = new Intl.DateTimeFormat();
|
|||
|
||||
export default function ReviewComponent({ review, refetch, profileId }: { review: Review; refetch(): void; profileId: string; }) {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const [localVote, setLocalVote] = useState<boolean | null>(review.userVote ?? null);
|
||||
const [score, setScore] = useState(review.score ?? 0);
|
||||
const [isVoting, setIsVoting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalVote(review.userVote ?? null);
|
||||
setScore(review.score ?? 0);
|
||||
}, [review.score, review.userVote]);
|
||||
|
||||
function openModal() {
|
||||
openUserProfile(review.sender.discordID);
|
||||
|
|
@ -56,13 +64,9 @@ export default function ReviewComponent({ review, refetch, profileId }: { review
|
|||
onConfirm={async () => {
|
||||
if (!(await getToken())) {
|
||||
return showToast("You must be logged in to delete reviews.");
|
||||
} else {
|
||||
deleteReview(review.id).then(res => {
|
||||
if (res) {
|
||||
refetch();
|
||||
}
|
||||
});
|
||||
}
|
||||
const res = await deleteReview(review.id);
|
||||
if (res) refetch();
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
|
@ -79,9 +83,8 @@ export default function ReviewComponent({ review, refetch, profileId }: { review
|
|||
onConfirm={async () => {
|
||||
if (!(await getToken())) {
|
||||
return showToast("You must be logged in to report reviews.");
|
||||
} else {
|
||||
reportReview(review.id);
|
||||
}
|
||||
await reportReview(review.id);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
|
@ -103,27 +106,87 @@ export default function ReviewComponent({ review, refetch, profileId }: { review
|
|||
onConfirm={async () => {
|
||||
if (!(await getToken())) {
|
||||
return showToast("You must be logged in to block users.");
|
||||
} else {
|
||||
blockUser(review.sender.discordID);
|
||||
}
|
||||
await blockUser(review.sender.discordID);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
async function submitVote(isUpvote: boolean) {
|
||||
if (isVoting) return;
|
||||
|
||||
if (review.sender.discordID === Auth.user?.discordID) {
|
||||
return showToast("You cannot vote on your own review.");
|
||||
}
|
||||
|
||||
setIsVoting(true);
|
||||
|
||||
try {
|
||||
if (localVote === isUpvote) {
|
||||
if (await deleteReviewVote(review.id)) {
|
||||
setLocalVote(null);
|
||||
setScore(currentScore => currentScore + (isUpvote ? -1 : 1));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (await voteReview(review.id, isUpvote)) {
|
||||
const delta = localVote == null
|
||||
? isUpvote ? 1 : -1
|
||||
: isUpvote ? 2 : -2;
|
||||
|
||||
setLocalVote(isUpvote);
|
||||
setScore(currentScore => currentScore + delta);
|
||||
}
|
||||
} finally {
|
||||
setIsVoting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes(cl("review"), MessageClasses.cozyMessage, AvatarClasses.wrapper, MessageClasses.message, MessageClasses.groupStart, AvatarClasses.cozy)} style={
|
||||
{
|
||||
marginLeft: "0px",
|
||||
paddingLeft: "52px", // wth is this
|
||||
paddingLeft: "52px",
|
||||
// nobody knows anymore
|
||||
}
|
||||
}>
|
||||
|
||||
{review.id !== 0 && (
|
||||
<div className={cl("vote-column")}>
|
||||
<span className={classes(cl("vote-column-score"), score > 0 && cl("vote-column-score-positive"), score < 0 && cl("vote-column-score-negative"))}>
|
||||
{score}
|
||||
</span>
|
||||
<div className={cl("vote-column-buttons")}>
|
||||
<button
|
||||
className={classes(cl("vote-column-button"), !!localVote && cl("vote-column-up-selected"))}
|
||||
disabled={isVoting}
|
||||
onClick={() => submitVote(true)}
|
||||
type="button"
|
||||
>
|
||||
<svg height="20" viewBox="0 0 12 12" width="20" fill="none">
|
||||
<path d="M3 7.5 6 4.5l3 3" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className={classes(cl("vote-column-button"), localVote === false && cl("vote-column-down-selected"))}
|
||||
disabled={isVoting}
|
||||
onClick={() => submitVote(false)}
|
||||
type="button"
|
||||
>
|
||||
<svg height="20" viewBox="0 0 12 12" width="20" fill="none">
|
||||
<path d="M3 4.5 6 7.5l3-3" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<img
|
||||
className={classes(AvatarClasses.avatar, AvatarClasses.clickable)}
|
||||
onClick={openModal}
|
||||
src={review.sender.profilePhoto || IconUtils.getDefaultAvatarURL(review.sender.discordID)}
|
||||
src={review.sender.profilePhoto ?? IconUtils.getDefaultAvatarURL(review.sender.discordID)}
|
||||
style={{ left: "0px", zIndex: 0 }}
|
||||
onError={e => e.currentTarget.src = IconUtils.getDefaultAvatarURL(review.sender.discordID)}
|
||||
/>
|
||||
|
|
@ -163,7 +226,6 @@ export default function ReviewComponent({ review, refetch, profileId }: { review
|
|||
{dateFormat.format(review.timestamp * 1000)}
|
||||
</Timestamp>)
|
||||
}
|
||||
|
||||
<div className={cl("review-comment")}>
|
||||
{(review.comment.length > 200 && !showAll)
|
||||
? (
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export default function ReviewsView({
|
|||
}: Props) {
|
||||
const [signal, refetch] = useForceUpdater(true);
|
||||
|
||||
const [reviewData] = useAwaiter(() => getReviews(discordId, { offset: (page - 1) * REVIEWS_PER_PAGE }), {
|
||||
const [reviewData] = useAwaiter(() => getReviews(discordId, { offset: (page - 1) * REVIEWS_PER_PAGE, fetchVotes: true }), {
|
||||
fallbackValue: null,
|
||||
deps: [refetchSignal, signal, page],
|
||||
onSuccess: data => {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export interface ReviewDBUser {
|
|||
username: string;
|
||||
type: UserType;
|
||||
profilePhoto: string;
|
||||
badges: any[];
|
||||
badges: Badge[];
|
||||
}
|
||||
|
||||
export interface ReviewDBCurrentUser extends ReviewDBUser {
|
||||
|
|
@ -93,9 +93,11 @@ export interface ReviewAuthor {
|
|||
export interface Review {
|
||||
comment: string,
|
||||
id: number,
|
||||
score?: number,
|
||||
star: number,
|
||||
sender: ReviewAuthor,
|
||||
timestamp: number;
|
||||
type?: ReviewType;
|
||||
replies?: Review[];
|
||||
userVote?: boolean | null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,49 +96,51 @@ export default definePlugin({
|
|||
setTimeout(async () => {
|
||||
if (!Auth.token) return;
|
||||
|
||||
const user = await getCurrentUserInfo(Auth.token);
|
||||
updateAuth({ user });
|
||||
const user = await getCurrentUserInfo();
|
||||
if (user) {
|
||||
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 (notifyReviews) {
|
||||
if (lastReviewId && lastReviewId < user.lastReviewID) {
|
||||
s.lastReviewId = user.lastReviewID;
|
||||
if (user.lastReviewID !== 0)
|
||||
showToast("You have new reviews on your profile!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { notification } = user;
|
||||
if (notification) {
|
||||
const props = 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"
|
||||
})
|
||||
)
|
||||
} : {};
|
||||
const { notification } = user;
|
||||
if (notification) {
|
||||
const props = 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"
|
||||
})
|
||||
)
|
||||
} : {};
|
||||
|
||||
openModal(modalProps => (
|
||||
<ConfirmModal
|
||||
{...modalProps}
|
||||
title={notification.title}
|
||||
confirmText={props.confirmText ?? "OK"}
|
||||
cancelText={props.cancelText}
|
||||
variant="primary"
|
||||
onCancel={props.onCancel}
|
||||
>
|
||||
{Parser.parse(
|
||||
notification.content,
|
||||
false
|
||||
)}
|
||||
</ConfirmModal>
|
||||
));
|
||||
openModal(modalProps => (
|
||||
<ConfirmModal
|
||||
{...modalProps}
|
||||
title={notification.title}
|
||||
confirmText={props.confirmText ?? "OK"}
|
||||
cancelText={props.cancelText}
|
||||
variant="primary"
|
||||
onCancel={props.onCancel}
|
||||
>
|
||||
{Parser.parse(
|
||||
notification.content,
|
||||
false
|
||||
)}
|
||||
</ConfirmModal>
|
||||
));
|
||||
|
||||
readNotification(notification.id);
|
||||
readNotification(notification.id);
|
||||
}
|
||||
}
|
||||
}, 4000);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,20 +1,8 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Toasts } from "@webpack/common";
|
||||
|
||||
|
|
@ -36,19 +24,51 @@ export interface UserReviewsData {
|
|||
hasOptedOut: boolean;
|
||||
}
|
||||
|
||||
const WarningFlag = 0b00000010;
|
||||
|
||||
async function rdbRequest(path: string, options: RequestInit = {}) {
|
||||
return fetch(API_URL + path, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
Authorization: await getToken() || "",
|
||||
}
|
||||
});
|
||||
export interface ReviewVote {
|
||||
reviewID: number;
|
||||
isUpvote: boolean;
|
||||
}
|
||||
|
||||
export async function getReviews(id: string, { limit, offset = 0 }: { limit?: number; offset?: number; } = {}): Promise<UserReviewsData> {
|
||||
interface ReviewVotesData {
|
||||
message?: string;
|
||||
votes: ReviewVote[];
|
||||
}
|
||||
|
||||
const WarningFlag = 0b00000010;
|
||||
|
||||
async function rdbRequest<T = unknown>(path: string, options: RequestInit = {}): Promise<T | null> {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "application/json",
|
||||
Authorization: await getToken() ?? "",
|
||||
...options.headers as Record<string, string>,
|
||||
};
|
||||
|
||||
if (options.body) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
const res = await fetch(API_URL + path, {
|
||||
...options,
|
||||
headers,
|
||||
}).catch(err => {
|
||||
showToast("Network error: Failed to connect to ReviewDB.", Toasts.Type.FAILURE);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!res) return null;
|
||||
|
||||
const data = await res.json().catch(() => null);
|
||||
|
||||
if (!res.ok) {
|
||||
const message = data?.message ?? `ReviewDB: Request failed with status ${res.status}`;
|
||||
showToast(message, Toasts.Type.FAILURE);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data as T;
|
||||
}
|
||||
|
||||
export async function getReviews(id: string, { limit, offset = 0, fetchVotes = false }: { limit?: number; offset?: number; fetchVotes?: boolean } = {}): Promise<UserReviewsData> {
|
||||
let flags = 0;
|
||||
if (!settings.store.showWarning) flags |= WarningFlag;
|
||||
|
||||
|
|
@ -57,6 +77,7 @@ export async function getReviews(id: string, { limit, offset = 0 }: { limit?: nu
|
|||
if (offset) params.append("offset", String(offset));
|
||||
if (limit) params.append("limit", String(limit));
|
||||
|
||||
const votesPromise = fetchVotes ? getReviewVotes(id).catch(() => []) : Promise.resolve([]);
|
||||
const req = await fetch(`${API_URL}/users/${id}/reviews?${params}`);
|
||||
|
||||
const res = (req.ok)
|
||||
|
|
@ -93,10 +114,33 @@ export async function getReviews(id: string, { limit, offset = 0 }: { limit?: nu
|
|||
};
|
||||
}
|
||||
|
||||
if (!fetchVotes || res.reviews.length === 0) return res;
|
||||
|
||||
const votes = await votesPromise;
|
||||
if (votes.length === 0) return res;
|
||||
|
||||
const voteByReviewId = new Map<number, boolean>();
|
||||
for (const vote of votes) {
|
||||
voteByReviewId.set(vote.reviewID, vote.isUpvote);
|
||||
}
|
||||
|
||||
res.reviews = res.reviews.map(review => ({
|
||||
...review,
|
||||
userVote: voteByReviewId.get(review.id) ?? null,
|
||||
}));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function addReview(review: any): Promise<UserReviewsData | null> {
|
||||
export async function getReviewVotes(id: string): Promise<ReviewVote[]> {
|
||||
const token = await getToken();
|
||||
if (!token) return [];
|
||||
|
||||
const res = await rdbRequest<ReviewVotesData>(`/users/${id}/reviews/votes`);
|
||||
return res?.votes ?? [];
|
||||
}
|
||||
|
||||
export async function addReview(review: { userid: string; comment: string }): Promise<UserReviewsData | null> {
|
||||
|
||||
const token = await getToken();
|
||||
if (!token) {
|
||||
|
|
@ -105,75 +149,88 @@ export async function addReview(review: any): Promise<UserReviewsData | null> {
|
|||
return null;
|
||||
}
|
||||
|
||||
return await rdbRequest(`/users/${review.userid}/reviews`, {
|
||||
const data = await rdbRequest<UserReviewsData>(`/users/${review.userid}/reviews`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(review),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
}).then(async r => {
|
||||
const data = await r.json() as UserReviewsData;
|
||||
showToast(data.message);
|
||||
return r.ok ? data : null;
|
||||
});
|
||||
if (data?.message) showToast(data.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteReview(id: number): Promise<UserReviewsData | null> {
|
||||
return await rdbRequest(`/users/${id}/reviews`, {
|
||||
const data = await rdbRequest<UserReviewsData>(`/users/${id}/reviews`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reviewid: id
|
||||
})
|
||||
}).then(async r => {
|
||||
const data = await r.json() as UserReviewsData;
|
||||
showToast(data.message);
|
||||
return r.ok ? data : null;
|
||||
});
|
||||
if (data?.message) showToast(data.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function reportReview(id: number) {
|
||||
const res = await rdbRequest("/reports", {
|
||||
const data = await rdbRequest<UserReviewsData>("/reports", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reviewid: id,
|
||||
})
|
||||
}).then(r => r.json()) as UserReviewsData;
|
||||
});
|
||||
if (data?.message) showToast(data.message);
|
||||
}
|
||||
|
||||
showToast(res.message);
|
||||
export async function voteReview(id: number, isUpvote: boolean) {
|
||||
const token = await getToken();
|
||||
if (!token) {
|
||||
showToast("Please authorize to vote on reviews.");
|
||||
authorize();
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await rdbRequest<{ message?: string }>(`/reviews/${id}/vote`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ isUpvote })
|
||||
});
|
||||
|
||||
if (!data) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function deleteReviewVote(id: number) {
|
||||
const token = await getToken();
|
||||
if (!token) {
|
||||
showToast("Please authorize to vote on reviews.");
|
||||
authorize();
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await rdbRequest<{ message?: string }>(`/reviews/${id}/vote`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (!data) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function patchBlock(action: "block" | "unblock", userId: string) {
|
||||
const res = await rdbRequest("/blocks", {
|
||||
const data = await rdbRequest("/blocks", {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
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 (!data) return;
|
||||
|
||||
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 } });
|
||||
}
|
||||
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 } });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,25 +238,17 @@ 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 rdbRequest("/blocks", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`);
|
||||
return res.json();
|
||||
return await rdbRequest<ReviewDBUser[]>("/blocks") ?? [];
|
||||
}
|
||||
|
||||
export function getCurrentUserInfo(token: string): Promise<ReviewDBCurrentUser> {
|
||||
return rdbRequest("/users", {
|
||||
export function getCurrentUserInfo(): Promise<ReviewDBCurrentUser | null> {
|
||||
return rdbRequest<ReviewDBCurrentUser>("/users", {
|
||||
method: "POST",
|
||||
}).then(r => r.json());
|
||||
});
|
||||
}
|
||||
|
||||
export async function readNotification(id: number) {
|
||||
return rdbRequest(`/notifications?id=${id}`, {
|
||||
return await rdbRequest(`/notifications?id=${id}`, {
|
||||
method: "PATCH"
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
.vc-rdb-review {
|
||||
padding-top: 8px !important;
|
||||
padding-bottom: 8px !important;
|
||||
padding-right: 32px !important;
|
||||
padding-right: 64px !important;
|
||||
}
|
||||
|
||||
.vc-rdb-review:hover {
|
||||
|
|
@ -57,6 +57,7 @@
|
|||
.vc-rdb-review-comment {
|
||||
overflow-y: hidden;
|
||||
margin-top: 1px;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-default);
|
||||
font-size: 15px;
|
||||
|
|
@ -116,4 +117,76 @@
|
|||
.vc-rdb-profile-popout-disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.vc-rdb-vote-column {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.vc-rdb-vote-column-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.vc-rdb-vote-column-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--interactive-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-rdb-vote-column-button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.vc-rdb-vote-column-button:hover:not(:disabled) {
|
||||
background: var(--background-modifier-hover);
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.vc-rdb-vote-column-score {
|
||||
min-width: 15px;
|
||||
color: var(--text-muted);
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vc-rdb-vote-column-score-positive,
|
||||
.vc-rdb-vote-column-up-selected {
|
||||
color: var(--status-positive);
|
||||
}
|
||||
|
||||
.vc-rdb-vote-column-score-negative,
|
||||
.vc-rdb-vote-column-down-selected {
|
||||
color: var(--status-danger);
|
||||
}
|
||||
|
||||
.vc-rdb-vote-column-up-selected:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--status-positive) 14%, transparent);
|
||||
color: var(--status-positive);
|
||||
}
|
||||
|
||||
.vc-rdb-vote-column-down-selected:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--status-danger) 14%, transparent);
|
||||
color: var(--status-danger);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export const settings = definePluginSettings({
|
|||
},
|
||||
tryHljs: {
|
||||
type: OptionType.SELECT,
|
||||
displayName: "Try Highlight.js",
|
||||
description: "Use the more lightweight default Discord highlighter and theme.",
|
||||
options: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@
|
|||
padding-left: 5px;
|
||||
padding-right: 8px;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vc-shiki-root .vc-shiki-table-cell:last-child {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export const settings = definePluginSettings({
|
|||
},
|
||||
deeplApiKey: {
|
||||
type: OptionType.STRING,
|
||||
displayName: "DeepL API Key",
|
||||
description: "Your DeepL API key (from deepl.com/your-account)",
|
||||
default: ""
|
||||
},
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ interface VoiceStateChangeEvent {
|
|||
// not say the second mute, which would lead you to believe they're unmuted
|
||||
|
||||
function speak(text: string) {
|
||||
if (!text) return;
|
||||
// Don't narrate in the overlay window, otherwise everything is said twice
|
||||
if (!text || window.__OVERLAY__) return;
|
||||
|
||||
const { volume, rate } = settings.store;
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ const settings = definePluginSettings({
|
|||
},
|
||||
imgSize: {
|
||||
type: OptionType.SELECT,
|
||||
displayName: "Image Size",
|
||||
description: "The image size to use",
|
||||
options: ["128", "256", "512", "1024", "2048", "4096"].map(n => ({ label: n, value: n, default: n === "1024" }))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,19 +21,19 @@ import "./style.css";
|
|||
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
|
||||
import { CodeBlock } from "@components/CodeBlock";
|
||||
import { Divider } from "@components/Divider";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Heading } from "@components/Heading";
|
||||
import { HeadingSecondary } from "@components/Heading";
|
||||
import { Margins } from "@components/margins";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { copyWithToast, getCurrentGuild, getIntlMessage } from "@utils/discord";
|
||||
import { isTruthy } from "@utils/guards";
|
||||
import { Margins } from "@utils/margins";
|
||||
import definePlugin, { IconComponent, OptionType } from "@utils/types";
|
||||
import { Message } from "@vencord/discord-types";
|
||||
import { ChannelStore, GuildRoleStore, Menu, Modal, openModal, UserProfileStore } from "@webpack/common";
|
||||
import { MouseEventHandler } from "react";
|
||||
|
||||
const CopyIcon: IconComponent = ({ height = 20, width = 20, className }) => {
|
||||
|
||||
const CopyRawIcon: IconComponent = ({ height = 20, width = 20, className }) => {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
|
|
@ -77,12 +77,12 @@ function openViewRawModal(json: string, type: string, msgContent?: string) {
|
|||
<ErrorBoundary>
|
||||
<Modal
|
||||
{...props}
|
||||
title="View Raw"
|
||||
title={`Raw ${type} Data`}
|
||||
size="xl"
|
||||
actions={[
|
||||
{
|
||||
text: `Copy ${type} JSON`,
|
||||
variant: "primary",
|
||||
text: `Copy ${type} Data`,
|
||||
variant: "secondary",
|
||||
onClick: () => copyWithToast(json, `${type} data copied to clipboard!`)
|
||||
},
|
||||
msgContent && {
|
||||
|
|
@ -94,18 +94,12 @@ function openViewRawModal(json: string, type: string, msgContent?: string) {
|
|||
>
|
||||
{!!msgContent && (
|
||||
<>
|
||||
<Heading tag="h5">Content</Heading>
|
||||
<div className="vc-viewraw-codeblock">
|
||||
<CodeBlock content={msgContent} lang="" />
|
||||
</div>
|
||||
<Divider className={Margins.bottom20} />
|
||||
<HeadingSecondary>Message Content</HeadingSecondary>
|
||||
<CodeBlock className="vc-viewRaw-codeBlock" content={msgContent} lang="" />
|
||||
<HeadingSecondary className={Margins.top16}>Message Data</HeadingSecondary>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Heading tag="h5">{type} Data</Heading>
|
||||
<div className="vc-viewraw-codeblock">
|
||||
<CodeBlock content={json} lang="json" />
|
||||
</div>
|
||||
<CodeBlock className="vc-viewRaw-codeBlock" content={json} lang="json" />
|
||||
</Modal>
|
||||
</ErrorBoundary >
|
||||
));
|
||||
|
|
@ -155,7 +149,7 @@ function MakeContextCallback(name: "Guild" | "Role" | "User" | "Channel" | "Mess
|
|||
id={id}
|
||||
label="View Raw"
|
||||
action={action}
|
||||
icon={CopyIcon}
|
||||
icon={CopyRawIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -173,7 +167,7 @@ const devContextCallback: NavContextMenuPatchCallback = (children, { id }: { id:
|
|||
id={"vc-view-role-raw"}
|
||||
label="View Raw"
|
||||
action={() => openViewRawModal(JSON.stringify(role, null, 4), "Role")}
|
||||
icon={CopyIcon}
|
||||
icon={CopyRawIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -200,7 +194,7 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
messagePopoverButton: {
|
||||
icon: CopyIcon,
|
||||
icon: CopyRawIcon,
|
||||
render(msg) {
|
||||
const handleClick = () => {
|
||||
if (settings.store.clickMethod === "Right") {
|
||||
|
|
@ -228,7 +222,7 @@ export default definePlugin({
|
|||
|
||||
return {
|
||||
label,
|
||||
icon: CopyIcon,
|
||||
icon: CopyRawIcon,
|
||||
message: msg,
|
||||
channel: ChannelStore.getChannel(msg.channel_id),
|
||||
onClick: handleClick,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
.vc-viewraw-codeblock {
|
||||
white-space: nowrap !important;
|
||||
word-break: normal !important;
|
||||
min-width: 40px !important;
|
||||
}
|
||||
.vc-viewRaw-codeBlock,
|
||||
.vc-viewRaw-codeBlock :is(pre, code, [class*="codeContainer"]) {
|
||||
max-width: unset;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ const settings = definePluginSettings({
|
|||
},
|
||||
preferUDP: {
|
||||
type: OptionType.BOOLEAN,
|
||||
displayName: "Prefer UDP",
|
||||
description: "Enable if you use an older build of XSOverlay unable to connect through websockets. This setting is ignored on web.",
|
||||
default: false,
|
||||
disabled: () => IS_WEB
|
||||
|
|
@ -119,11 +120,13 @@ const settings = definePluginSettings({
|
|||
},
|
||||
dmNotifications: {
|
||||
type: OptionType.BOOLEAN,
|
||||
displayName: "DM Notifications",
|
||||
description: "Allow Direct Message notifications",
|
||||
default: true
|
||||
},
|
||||
groupDmNotifications: {
|
||||
type: OptionType.BOOLEAN,
|
||||
displayName: "Group DM Notifications",
|
||||
description: "Allow Group DM notifications",
|
||||
default: true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -658,6 +658,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "prism",
|
||||
id: 390884143749136386n,
|
||||
},
|
||||
nightmaresan: {
|
||||
name: "NightmareSan",
|
||||
id: 304239816466235392n
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
export const EquicordDevs = Object.freeze({
|
||||
|
|
|
|||
|
|
@ -292,6 +292,7 @@ export type PluginSettingDef =
|
|||
|
||||
export interface PluginSettingDefCommon extends IsDisabledOrHidden, IsValid<unknown> {
|
||||
description: string;
|
||||
displayName?: string;
|
||||
placeholder?: string;
|
||||
onChange?(newValue: any): void;
|
||||
/**
|
||||
|
|
@ -375,7 +376,7 @@ export interface PluginSettingSliderDef extends PluginSettingDefCommon {
|
|||
stickToMarkers?: boolean;
|
||||
}
|
||||
|
||||
export interface PluginSettingComponentDef extends Omit<PluginSettingDefCommon, "description" | "placeholder"> {
|
||||
export interface PluginSettingComponentDef extends Omit<PluginSettingDefCommon, "description" | "placeholder" | "displayName"> {
|
||||
type: OptionType.COMPONENT;
|
||||
component: (props: PluginSettingComponentProps) => ReactNode | Promise<ReactNode>;
|
||||
default?: any;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue