Migrate to Discord's new Modals (#4206)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
V 2026-05-10 13:58:48 +02:00 • committed by GitHub
parent 909df8b8c2
commit 3b9292d3bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
70 changed files with 1453 additions and 1521 deletions

View file

@ -192,6 +192,7 @@ export type TextInput = ComponentType<PropsWithChildren<{
export type TextArea = ComponentType<Omit<HTMLProps<HTMLTextAreaElement>, "onChange"> & {
onChange(v: string): void;
inputRef?: Ref<HTMLTextAreaElement>;
autosize?: boolean;
}>;
export interface SelectOption {

View file

@ -0,0 +1,136 @@
import { ComponentType, ReactNode, Ref, RefObject } from "react";
import { LiteralUnion } from "type-fest";
export type ModalActionVariant = LiteralUnion<"primary" | "secondary" | "critical-primary", string>;
export type ModalSize = LiteralUnion<"sm" | "md" | "lg" | "xl" | "xxl", string>;
export interface ModalAction {
text: string;
variant: ModalActionVariant;
onClick(): void;
loading?: boolean;
disabled?: boolean;
}
/**
* Modal with all options: https://files.catbox.moe/c8qxt0.png
*/
export interface ModalProps extends RenderModalProps {
size?: ModalSize;
role?: "alertdialog" | "dialog";
title: ReactNode;
/** Optional subtitle, will render below title (duh) */
subtitle?: ReactNode;
/** Optional Modal content */
children?: ReactNode;
input?: ReactNode;
preview?: ReactNode;
listProps?: any;
onScroll?(): void;
scrollerRef?: Ref<HTMLDivElement>;
/** Action buttons at the bottom of the Modal */
actions?: ModalAction[];
/** Custom component to show before actions, useful for e.g. "Don't show again" checkbox */
actionBarInput?: ReactNode;
actionBarInputLayout?: "default" | "chat-input";
/** Shows a Notice (card) between (sub-)title and content (children) */
notice?: {
message: string;
type: LiteralUnion<"critical", string>;
};
}
export interface RenderModalProps {
transitionState: number;
onClose(): void;
}
/** Wrapper around Modal */
export interface ConfirmModalProps extends ModalProps {
/** Variant for the confirm button, defaults to critical */
variant?: ModalActionVariant;
confirmText: ModalAction["text"];
/** Defaults to "Cancel" */
cancelText?: ModalAction["text"];
/**
* If onConfirm runs without error, the modal will close.
* To indicate an error, throw an Error. This will show the user a notice inside the Modal and keep it open.
* This notice will either be a generic message if you just throw. If you call setError, it will show that message.
* Even if you call setError, you still have to throw to keep the Modal open
*/
onConfirm?(setError: (error: string) => void): void;
/** Optional callback that runs when the user cancels the action. Whether provided or not, the Modal will close when user clicks Cancel. */
onCancel?(): void;
onCloseCallback?(): void;
/** Checkbox that shows before the action buttons */
checkboxProps?: {
/** Defaults to "Don't show again" */
label?: string;
checked: boolean;
onChange(checked: boolean): void;
};
/** Custom Modal content */
children?: ReactNode;
}
export type Modal = ComponentType<ModalProps>;
export type ConfirmModal = ComponentType<ConfirmModalProps>;
export type RenderModal = (props: RenderModalProps) => ReactNode;
export interface ModalOptions {
modalKey?: string;
onCloseRequest?(): void;
onCloseCallback?(): void;
}
export interface ModalAPI {
/**
* Wait for the render promise to resolve, then open a modal with it.
* This is usually preferable to render().then(openModal), because it shows a Loading modal
*/
openModalLazy: (renderModal: () => Promise<RenderModal>, options?: ModalOptions & { contextKey?: string; }) => Promise<string>;
/**
* Open a Modal with the given render function.
*/
openModal: (renderModal: RenderModal, options?: ModalOptions, contextKey?: string) => string;
/**
* Close a modal by its key
*/
closeModal: (modalKey: string, contextKey?: string) => void;
/**
* Close all open modals
*/
closeAllModals: () => void;
}
export interface MediaModalItem {
url: string;
type: "IMAGE" | "VIDEO" | "CLIP";
original?: string;
alt?: string;
width?: number;
height?: number;
animated?: boolean;
maxWidth?: number;
maxHeight?: number;
}
export interface MediaModalProps {
location?: string;
contextKey?: string;
onCloseCallback?: () => void;
className?: string;
items: MediaModalItem[];
startingIndex?: number;
onIndexChange?: (...args: any[]) => void;
fit?: string;
shouldRedactExplicitContent?: boolean;
shouldHideMediaOptions?: boolean;
}

View file

@ -1,5 +1,7 @@
export * from "./common";
// TODO: Splitting up components into files in the folder would be good
export * from "./components";
export * from "./components/Modal";
export * from "./flux";
export * from "./fluxEvents";
export * from "./menu";

View file

@ -18,12 +18,11 @@
import * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings";
import { Flex } from "@components/Flex";
import { openNotificationSettingsModal } from "@components/settings/tabs/vencord/NotificationSettings";
import { classNameFactory } from "@utils/css";
import { closeModal, ModalCloseButton, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useAwaiter } from "@utils/react";
import { Alerts, Button, Forms, ListScrollerThin, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
import { RenderModalProps } from "@vencord/discord-types";
import { ConfirmModal, Forms, ListScrollerThin, Modal,openModal, React, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
import { nanoid } from "nanoid";
import type { DispatchWithoutAction } from "react";
@ -130,7 +129,7 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
export function NotificationLog({ log, pending }: { log: PersistentNotificationData[], pending: boolean; }) {
if (!log.length && !pending)
return (
<div className={cl("container")}>
<div>
<div className={cl("empty")} />
<Forms.FormText style={{ textAlign: "center" }}>
No notifications yet
@ -150,56 +149,46 @@ export function NotificationLog({ log, pending }: { log: PersistentNotificationD
);
}
function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void; }) {
function LogModal(props: RenderModalProps) {
const [log, pending] = useLogs();
return (
<ModalRoot {...modalProps} size={ModalSize.LARGE} className={cl("modal")}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Log</Text>
<ModalCloseButton onClick={close} />
</ModalHeader>
<div style={{ width: "100%" }}>
<NotificationLog log={log} pending={pending} />
</div>
<ModalFooter>
<Flex>
<Button onClick={openNotificationSettingsModal}>
Notification Settings
</Button>
<Button
disabled={log.length === 0}
color={Button.Colors.RED}
onClick={() => {
Alerts.show({
title: "Are you sure?",
body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`,
async onConfirm() {
<Modal
{...props}
size="xl"
title="Notification Log"
actions={[
{
text: "Notification Settings",
variant: "primary",
onClick: openNotificationSettingsModal
},
{
text: "Clear Notification Log",
variant: "critical-primary",
disabled: !log.length,
onClick() {
openModal(props =>
<ConfirmModal
{...props}
title="Are you sure?"
subtitle={`This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`}
confirmText="Do it!"
onConfirm={async () => {
await DataStore.set(KEY, []);
signals.forEach(x => x());
},
confirmText: "Do it!",
confirmColor: "vc-notification-log-danger-btn",
cancelText: "Nevermind"
});
}}
>
Clear Notification Log
</Button>
</Flex>
</ModalFooter>
</ModalRoot>
}}
/>
);
}
}
]}
>
<NotificationLog log={log} pending={pending} />
</Modal>
);
}
export function openNotificationLogModal() {
const key = openModal(modalProps => (
<LogModal
modalProps={modalProps}
close={() => closeModal(key)}
/>
));
openModal(props => <LogModal {...props} />);
}

View file

@ -89,12 +89,6 @@
margin-bottom: 40px;
}
.vc-notification-log-container {
padding: 1em;
max-height: min(750px, 75vh);
width: 100%;
}
.vc-notification-log-wrapper {
height: 120px;
width: 100%;

View file

@ -8,9 +8,8 @@ import * as DataStore from "@api/DataStore";
import { showNotification } from "@api/Notifications";
import { Settings } from "@api/Settings";
import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal";
import { relaunch } from "@utils/native";
import { Alerts, OAuth2AuthorizeModal, UserStore } from "@webpack/common";
import { ConfirmModal,OAuth2AuthorizeModal, openModal, UserStore } from "@webpack/common";
export const logger = new Logger("SettingsSync:CloudSetup", "#39b7e0");
@ -29,13 +28,17 @@ export async function checkCloudUrlCsp() {
const res = await VencordNative.csp.requestAddOverride(Settings.cloud.url, ["connect-src"], "Cloud Sync");
if (res === "ok") {
Alerts.show({
title: "Cloud Integration enabled",
body: `${host} has been added to the whitelist. Please restart the app for the changes to take effect.`,
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: relaunch
});
openModal(props => (
<ConfirmModal
{...props}
title="Cloud Integration enabled"
subtitle={`${host} has been added to the whitelist. Please restart the app for the changes to take effect.`}
confirmText="Restart now"
cancelText="Later!"
variant="primary"
onConfirm={relaunch}
/>
));
}
return false;
}

View file

@ -4,7 +4,7 @@
}
.vc-card-normal {
border: 1px var(--border-subtle);
border: 1px solid var(--border-subtle);
}
.vc-card-warning {

View file

@ -16,11 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { BaseText } from "@components/BaseText";
import ErrorBoundary from "@components/ErrorBoundary";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { onlyOnce } from "@utils/onlyOnce";
import { Modal,openModal } from "@webpack/common";
import type { ComponentType, PropsWithChildren } from "react";
export function SettingsTab({ children }: PropsWithChildren) {
@ -42,19 +41,18 @@ export function wrapTab(component: ComponentType<any>, tab: string) {
}
export function openSettingsTabModal(Tab: ComponentType<any>) {
try {
openModal(wrapTab((modalProps: ModalProps) => (
<ModalRoot {...modalProps} size={ModalSize.MEDIUM} className="vc-settings-modal-root">
<ModalHeader>
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>{Tab.displayName?.replace("SettingsTab", "") || "Settings"}</BaseText>
<ModalCloseButton onClick={modalProps.onClose} />
</ModalHeader>
Tab = wrapTab(Tab, Tab.displayName || "SettingsTab");
<ModalContent className="vc-settings-modal">
<Tab />
</ModalContent>
</ModalRoot>
), Tab.displayName || "SettingsTab"));
try {
openModal(props => (
<Modal
{...props}
size="lg"
title={Tab.displayName?.replace("SettingsTab", "") || "Settings"}
>
<Tab />
</Modal>
));
} catch {
handleSettingsTabError();
}

View file

@ -1,13 +1,3 @@
.vc-author-modal-root {
padding: 1em;
}
.vc-author-modal-header {
display: flex;
align-items: center;
margin-bottom: 1em;
}
.vc-author-modal-name {
text-transform: none;
flex-grow: 0;

View file

@ -7,15 +7,13 @@
import "./ContributorModal.css";
import { useSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { DevsById } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import { fetchUserProfile } from "@utils/discord";
import { classes, pluralise } from "@utils/misc";
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
import { User } from "@vencord/discord-types";
import { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
import { RenderModalProps, User } from "@vencord/discord-types";
import { Forms, Modal,openModal, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
import Plugins from "~plugins";
@ -25,18 +23,10 @@ import { PluginCard } from "./PluginCard";
const cl = classNameFactory("vc-author-modal-");
export function openContributorModal(user: User) {
openModal(modalProps =>
<ModalRoot {...modalProps}>
<ErrorBoundary>
<ModalContent className={cl("root")}>
<ContributorModal user={user} />
</ModalContent>
</ErrorBoundary>
</ModalRoot>
);
openModal(modalProps => <ContributorModal user={user} modalProps={modalProps} />);
}
function ContributorModal({ user }: { user: User; }) {
function ContributorModal({ user, modalProps }: { user: User; modalProps: RenderModalProps; }) {
useSettings();
const profile = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(user.id));
@ -63,41 +53,47 @@ function ContributorModal({ user }: { user: User; }) {
const ContributedHyperLink = <Link href="https://vencord.dev/source">contributed</Link>;
return (
<>
<div className={cl("header")}>
<img
className={cl("avatar")}
src={user.getAvatarURL(void 0, 512, true)}
alt=""
/>
<Forms.FormTitle tag="h2" className={cl("name")}>{user.username}</Forms.FormTitle>
<Modal
{...modalProps}
title={
<div className="vc-plugin-modal-header">
<img
className={cl("avatar")}
src={user.getAvatarURL(void 0, 512, true)}
alt=""
/>
<Forms.FormTitle tag="h2" className={cl("name")}>{user.username}</Forms.FormTitle>
<div className={classes("vc-settings-modal-links", cl("links"))}>
{website && (
<WebsiteButton
text={website}
href={`https://${website}`}
/>
)}
{githubName && (
<GithubButton
text={githubName}
href={`https://github.com/${githubName}`}
/>
)}
<div className={classes("vc-settings-modal-links", cl("links"))}>
{website && (
<WebsiteButton
text={website}
href={`https://${website}`}
/>
)}
{githubName && (
<GithubButton
text={githubName}
href={`https://github.com/${githubName}`}
/>
)}
</div>
</div>
</div>
{plugins.length ? (
<Forms.FormText>
This person has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}!
</Forms.FormText>
) : (
<Forms.FormText>
This person has not made any plugins. They likely {ContributedHyperLink} to Vencord in other ways!
</Forms.FormText>
)}
}
subtitle={
plugins.length
? (
<Forms.FormText>
This person has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}!
</Forms.FormText>
)
: (
<Forms.FormText>
This person has not made any plugins. They likely {ContributedHyperLink} to Vencord in other ways!
</Forms.FormText>
)
}
>
{!!plugins.length && (
<div className={cl("plugins")}>
{plugins.map(p =>
@ -110,6 +106,6 @@ function ContributorModal({ user }: { user: User; }) {
)}
</div>
)}
</>
</Modal>
);
}

View file

@ -9,4 +9,5 @@
.vc-settings-modal-links {
display: flex;
gap: 0.2em;
margin-left: auto;
}

View file

@ -1,5 +1,5 @@
.vc-settings-modal-content {
padding-bottom: 24px;
.vc-plugin-modal-header {
display: flex;
}
.vc-plugin-modal-info {
@ -13,7 +13,7 @@
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-block: 8px 16px;
margin-top: 8px;
}
.vc-plugin-modal-tag {

View file

@ -21,6 +21,7 @@ import "./PluginModal.css";
import { generateId } from "@api/Commands";
import { hasAnyVisibleSettings, isSettingHidden } from "@api/PluginManager";
import { useSettings } from "@api/Settings";
import { BaseText } from "@components/BaseText";
import ErrorBoundary from "@components/ErrorBoundary";
import { debounce } from "@shared/debounce";
import { gitRemote } from "@shared/vencordUserAgent";
@ -28,11 +29,10 @@ import { classNameFactory } from "@utils/css";
import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { OptionType, Plugin, PluginTag } from "@utils/types";
import { User } from "@vencord/discord-types";
import { RenderModalProps, User } from "@vencord/discord-types";
import { findCssClassesLazy } from "@webpack";
import { Clickable, FluxDispatcher, Forms, React, Text, Tooltip, useEffect, useMemo, UserStore, UserSummaryItem, UserUtils, useState } from "@webpack/common";
import { Clickable, FluxDispatcher, Forms, Modal,openModal, React, Text, Tooltip, useEffect, useMemo, UserStore, UserSummaryItem, UserUtils, useState } from "@webpack/common";
import { Constructor } from "type-fest";
import { PluginMeta } from "~plugins";
@ -46,7 +46,7 @@ const cl = classNameFactory("vc-plugin-modal-");
const AvatarStyles = findCssClassesLazy("moreUsers", "avatar", "clickableAvatar");
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
interface PluginModalProps extends ModalProps {
interface PluginModalProps extends RenderModalProps {
plugin: Plugin;
onRestartNeeded(key: string): void;
}
@ -131,6 +131,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
onChange={debounce(onChange)}
pluginSettings={pluginSettings}
definedSettings={settings}
closePluginSettings={onClose}
/>
</ErrorBoundary>
);
@ -166,32 +167,38 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
const pluginMeta = PluginMeta[plugin.name];
return (
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
<ModalHeader separator={false} className={Margins.bottom8}>
<Text variant="heading-xl/bold" style={{ flexGrow: 1 }}>{plugin.name}</Text>
<ModalCloseButton onClick={onClose} />
</ModalHeader>
<ModalContent className={"vc-settings-modal-content"}>
<section>
<div className={cl("info")}>
<div>
<Forms.FormText>{plugin.description}</Forms.FormText>
{!!plugin.tags?.length && <PluginTags tags={plugin.tags} />}
<Modal
transitionState={transitionState}
onClose={onClose}
size="lg"
title={
<div className={cl("header")}>
<BaseText tag="h1" weight="semibold" size="lg">{plugin.name}</BaseText>
{!pluginMeta.userPlugin && (
<div className="vc-settings-modal-links">
<WebsiteButton
text="View more info"
href={`https://vencord.dev/plugins/${plugin.name}`}
/>
<GithubButton
text="View source code"
href={`https://github.com/${gitRemote}/tree/main/src/plugins/${pluginMeta.folderName}`}
/>
</div>
{!pluginMeta.userPlugin && (
<div className="vc-settings-modal-links">
<WebsiteButton
text="View more info"
href={`https://vencord.dev/plugins/${plugin.name}`}
/>
<GithubButton
text="View source code"
href={`https://github.com/${gitRemote}/tree/main/src/plugins/${pluginMeta.folderName}`}
/>
</div>
)}
)}
</div>
}
subtitle={
<div className={cl("info")}>
<div>
<Forms.FormText>{plugin.description}</Forms.FormText>
{!!plugin.tags?.length && <PluginTags tags={plugin.tags} />}
</div>
</div>
}
>
<div className={"vc-settings-modal-content"}>
<section>
<Text variant="heading-lg/semibold" className={classes(Margins.top8, Margins.bottom8)}>Authors</Text>
<div style={{ width: "fit-content" }}>
<ErrorBoundary noop>
@ -234,8 +241,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
<Text variant="heading-lg/semibold" className={classes(Margins.top16, Margins.bottom8)}>Settings</Text>
{renderSettings()}
</section>
</ModalContent>
</ModalRoot>
</div>
</Modal>
);
}

View file

@ -18,10 +18,9 @@
}
.vc-plugin-ui-elements-modal-content {
padding: 1em;
display: flex;
flex-direction: column;
gap: 1.5em;
gap: 2em;
}
.vc-plugin-ui-elements-switches {

View file

@ -17,9 +17,9 @@ import { Switch } from "@components/Switch";
import { classNameFactory } from "@utils/css";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { IconComponent } from "@utils/types";
import { Clickable } from "@webpack/common";
import { RenderModalProps } from "@vencord/discord-types";
import { Clickable, Modal,openModal } from "@webpack/common";
const cl = classNameFactory("vc-plugin-ui-elements-");
@ -75,7 +75,7 @@ function Section(props: {
return (
<section>
<BaseText tag="h3" size="xl" weight="bold">{title}</BaseText>
<BaseText tag="h3" size="lg" weight="semibold">{title}</BaseText>
<Paragraph size="sm" className={classes(Margins.top8, Margins.bottom20)}>{description}</Paragraph>
<div className={cl("switches")}>
@ -90,12 +90,12 @@ function Section(props: {
);
}
function UIElementsModal(props: ModalProps) {
function UIElementsModal(props: RenderModalProps) {
const { uiElements } = useSettings(["uiElements.*"]);
return (
<ModalRoot {...props} size={ModalSize.MEDIUM}>
<ModalContent className={cl("modal-content")}>
<Modal {...props} size="md" title="Manage plugin UI elements">
<div className={cl("modal-content")}>
<Section
title="Chatbar Buttons"
description="These are the buttons on the right side of the chat input bar"
@ -108,7 +108,7 @@ function UIElementsModal(props: ModalProps) {
buttonMap={MessagePopoverButtonMap}
settings={uiElements.messagePopoverButtons}
/>
</ModalContent>
</ModalRoot>
</div>
</Modal>
);
}

View file

@ -22,6 +22,7 @@ interface SettingBaseProps<T> {
};
id: string;
definedSettings: DefinedSettings;
closePluginSettings(): void;
}
export type SettingProps<T extends PluginSettingDefCommon> = SettingBaseProps<T>;

View file

@ -20,6 +20,6 @@ import { PluginSettingComponentDef } from "@utils/types";
import { ComponentSettingProps } from "./Common";
export function ComponentSetting({ setting, onChange }: ComponentSettingProps<PluginSettingComponentDef>) {
return setting.component({ setValue: onChange, option: setting });
export function ComponentSetting({ setting, onChange, closePluginSettings }: ComponentSettingProps<PluginSettingComponentDef>) {
return setting.component({ setValue: onChange, option: setting, closePluginSettings });
}

View file

@ -35,7 +35,7 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { useAwaiter, useCleanupEffect } from "@utils/react";
import { PluginTag, PluginTags } from "@utils/types";
import { Alerts, Button, lodash, Parser, React, SearchableSelect, Select, TextInput, Tooltip, useMemo, useRef, useState } from "@webpack/common";
import { Button, ConfirmModal,lodash, openModal, Parser, React, SearchableSelect, Select, TextInput, Tooltip, useMemo, useRef, useState } from "@webpack/common";
import { JSX } from "react";
import Plugins, { ExcludedPlugins, PluginMeta } from "~plugins";
@ -121,9 +121,15 @@ function PluginSettings() {
useCleanupEffect(() => {
if (changes.hasChanges)
Alerts.show({
title: "Restart required",
body: (
openModal(props => (
<ConfirmModal
{...props}
title="Restart required"
confirmText="Restart now"
cancelText="Later!"
variant="primary"
onConfirm={() => location.reload()}
>
<>
<p>The following plugins require a restart:</p>
<div>{changes.map((s, i) => (
@ -133,11 +139,8 @@ function PluginSettings() {
</>
))}</div>
</>
),
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: () => location.reload()
});
</ConfirmModal>
));
}, []);
const depMap = useMemo(() => {

View file

@ -59,12 +59,4 @@
.vc-cloud-button-icon {
height: 1.25em;
}
.vc-settings-modal-root {
background: var(--background-base-low);
}
.vc-settings-modal {
padding-bottom: 1em;
}

View file

@ -34,7 +34,7 @@ import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { IconComponent } from "@utils/types";
import { Alerts, Select, Tooltip, useState } from "@webpack/common";
import { ConfirmModal,openModal, Select, Tooltip, useState } from "@webpack/common";
function validateUrl(url: string) {
try {
@ -228,14 +228,16 @@ function ResetSection() {
<ButtonWithIcon
variant="dangerPrimary"
disabled={!authenticated}
onClick={() => Alerts.show({
title: "Are you sure?",
body: "Once your data is erased, we cannot recover it. There's no going back!",
onConfirm: eraseAllCloudData,
confirmText: "Erase it!",
confirmColor: "vc-cloud-erase-data-danger-btn",
cancelText: "Nevermind"
})}
onClick={() => openModal(props => (
<ConfirmModal
{...props}
title="Are you sure?"
subtitle="Once your data is erased, we cannot recover it. There's no going back!"
onConfirm={eraseAllCloudData}
confirmText="Erase it!"
cancelText="Nevermind"
/>
))}
Icon={DeleteIcon}
>
Delete your Cloud Account

View file

@ -12,7 +12,7 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { relaunch } from "@utils/native";
import { useForceUpdater } from "@utils/react";
import { Alerts, Button, Forms } from "@webpack/common";
import { Button, ConfirmModal, Forms, openModal } from "@webpack/common";
export function CspErrorCard() {
if (IS_WEB) return null;
@ -38,19 +38,23 @@ export function CspErrorCard() {
forceUpdate();
Alerts.show({
title: "Restart Required",
body: "A restart is required to apply this change",
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: relaunch
});
openModal(props => (
<ConfirmModal
{...props}
title="Restart Required"
subtitle="A restart is required to apply this change"
confirmText="Restart now"
cancelText="Later!"
variant="primary"
onConfirm={relaunch}
/>
));
};
const hasImgurHtmlDomain = errors.some(isImgurHtmlDomain);
return (
<ErrorCard className={Margins.bottom16}>
<ErrorCard>
<Forms.FormTitle tag="h5">Blocked Resources</Forms.FormTitle>
<Forms.FormText>Some images, styles, or fonts were blocked because they come from disallowed domains.</Forms.FormText>
<Forms.FormText>It is highly recommended to move them to GitHub or Imgur. But you may also allow domains if you fully trust them.</Forms.FormText>

View file

@ -18,8 +18,12 @@
import "./styles.css";
import { BaseText } from "@components/BaseText";
import { Card } from "@components/Card";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
import { Margins } from "@components/margins";
import { Paragraph } from "@components/Paragraph";
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
import { getStylusWebStoreUrl } from "@utils/web";
import { Forms, React, TabBar, useState } from "@webpack/common";
@ -59,10 +63,20 @@ function ThemesTab() {
</TabBar.Item>
</TabBar>
<CspErrorCard />
<Flex flexDirection="column" gap="1em">
<CspErrorCard />
{currentTab === ThemeTab.LOCAL && <LocalThemesTab />}
{currentTab === ThemeTab.ONLINE && <OnlineThemesTab />}
<Card variant="warning">
<BaseText tag="h3" size="md" weight="medium" className={Margins.bottom8}>Theme Performance</BaseText>
<Paragraph>
Themes and custom CSS have the potential to cause major lag! If you experience performance issues, try
disabling your themes and CSS to see if they're the cause. The most common cause of lag is the <code>:has()</code> operator.
</Paragraph>
</Card>
{currentTab === ThemeTab.LOCAL && <LocalThemesTab />}
{currentTab === ThemeTab.ONLINE && <OnlineThemesTab />}
</Flex>
</SettingsTab>
);
}

View file

@ -12,7 +12,7 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { relaunch } from "@utils/native";
import { changes, checkForUpdates, update, updateError } from "@utils/updater";
import { Alerts, Button, Forms, React, Toasts, useState } from "@webpack/common";
import { Button, ConfirmModal,Forms, openModal, React, Toasts, useState } from "@webpack/common";
import { runWithDispatch } from "./runWithDispatch";
@ -100,17 +100,21 @@ export function Updatable(props: CommonProps) {
setUpdates([]);
await new Promise<void>(r => {
Alerts.show({
title: "Update Success!",
body: "Successfully updated. Restart now to apply the changes?",
confirmText: "Restart",
cancelText: "Not now!",
onConfirm() {
relaunch();
r();
},
onCancel: r
});
openModal(props => (
<ConfirmModal
{...props}
title="Update Success!"
subtitle="Successfully updated. Restart now to apply the changes?"
confirmText="Restart"
cancelText="Not now!"
variant="primary"
onConfirm={() => {
relaunch();
r();
}}
onCancel={r}
/>
));
});
}
})}

View file

@ -6,7 +6,7 @@
import { ErrorCard } from "@components/ErrorCard";
import { UpdateLogger } from "@utils/updater";
import { Alerts, Parser } from "@webpack/common";
import { ConfirmModal,openModal, Parser } from "@webpack/common";
function getErrorMessage(e: any) {
if (!e?.code || !e.cmd)
@ -33,16 +33,20 @@ export function runWithDispatch(dispatch: React.Dispatch<React.SetStateAction<bo
const err = getErrorMessage(e);
Alerts.show({
title: "Oops!",
body: (
openModal(props => (
<ConfirmModal
{...props}
title="Oops!"
confirmText="OK"
variant="primary"
>
<ErrorCard>
{err.split("\n").map((line, idx) =>
<div key={idx}>{Parser.parse(line)}</div>
)}
</ErrorCard>
)
});
</ConfirmModal>
));
} finally {
dispatch(false);
}

View file

@ -10,8 +10,7 @@ import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { Margins } from "@utils/margins";
import { identity } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Button, Forms, Select, Slider, Text } from "@webpack/common";
import { Button, Forms, Modal,openModal, Select, Slider } from "@webpack/common";
export function NotificationSection() {
return (
@ -35,16 +34,13 @@ export function NotificationSection() {
export function openNotificationSettingsModal() {
openModal(props => (
<ModalRoot {...props} size={ModalSize.MEDIUM}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Settings</Text>
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent>
<NotificationSettings />
</ModalContent>
</ModalRoot>
<Modal
{...props}
size="lg"
title="Notification Settings"
>
<NotificationSettings />
</Modal>
));
}
@ -52,7 +48,7 @@ function NotificationSettings() {
const settings = useSettings(["notifications.*"]).notifications;
return (
<div style={{ padding: "1em 0" }}>
<>
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
{settings.useNative !== "never" && Notification?.permission === "denied" && (
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
@ -122,6 +118,6 @@ function NotificationSettings() {
onValueRender={v => v === 200 ? "∞" : v}
onMarkerRender={v => v === 200 ? "∞" : v}
/>
</div>
</>
);
}

View file

@ -32,7 +32,7 @@ import { IS_MAC, IS_WINDOWS } from "@utils/constants";
import { Margins } from "@utils/margins";
import { isPluginDev } from "@utils/misc";
import { relaunch } from "@utils/native";
import { Alerts, Forms, React, useMemo, UserStore } from "@webpack/common";
import { ConfirmModal,Forms, openModal, React, useMemo, UserStore } from "@webpack/common";
import { DonateButtonComponent, isDonor } from "./DonateButton";
import { VibrancySettings } from "./MacVibrancySettings";
@ -111,13 +111,17 @@ function Switches() {
settings[key] = v;
if (restartRequired) {
Alerts.show({
title: "Restart Required",
body: "A restart is required to apply this change",
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: relaunch
});
openModal(props => (
<ConfirmModal
{...props}
title="Restart Required"
subtitle="A restart is required to apply this change"
confirmText="Restart now"
cancelText="Later!"
variant="primary"
onConfirm={relaunch}
/>
));
}
}}
/>

View file

@ -29,9 +29,8 @@ import { copyWithToast } from "@utils/discord";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { shouldShowContributorBadge } from "@utils/misc";
import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { ContextMenuApi, Forms, Menu, Toasts, UserStore } from "@webpack/common";
import { ContextMenuApi, Forms, Menu, Modal,openModal, Toasts, UserStore } from "@webpack/common";
const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/emojis/1092089799109775453.png?size=64";
@ -190,13 +189,14 @@ export default definePlugin({
ContextMenuApi.openContextMenu(event, () => <BadgeContextMenu badge={badge} />);
},
onClick() {
const modalKey = openModal(props => (
openModal(props => (
<ErrorBoundary noop onError={() => {
closeModal(modalKey);
props.onClose();
VencordNative.native.openExternal("https://github.com/sponsors/Vendicated");
}}>
<ModalRoot {...props}>
<ModalHeader>
<Modal
{...props}
title={
<Forms.FormTitle
tag="h2"
style={{
@ -210,8 +210,9 @@ export default definePlugin({
Vencord Donor
</Flex>
</Forms.FormTitle>
</ModalHeader>
<ModalContent>
}
>
<div>
<Flex>
<img
role="presentation"
@ -234,13 +235,13 @@ export default definePlugin({
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
</Forms.FormText>
</div>
</ModalContent>
<ModalFooter>
</div>
<div>
<Flex justifyContent="center" style={{ width: "100%" }}>
<DonateButton />
</Flex>
</ModalFooter>
</ModalRoot>
</div>
</Modal>
</ErrorBoundary>
));
},

View file

@ -34,8 +34,8 @@ import { onlyOnce } from "@utils/onlyOnce";
import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types";
import { checkForUpdates, isOutdated, update } from "@utils/updater";
import { Channel } from "@vencord/discord-types";
import { Alerts, Button, ChannelStore, Forms, GuildMemberStore, Parser, PermissionsBits, PermissionStore, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
import { Channel, RenderModalProps } from "@vencord/discord-types";
import { Button, ChannelStore, ConfirmModal, Forms, GuildMemberStore, openModal, Parser, PermissionsBits, PermissionStore, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
import { JSX } from "react";
import gitHash from "~git-hash";
@ -136,6 +136,34 @@ const settings = definePluginSettings({}).withPrivateSettings<{
dismissedDevBuildWarning?: boolean;
}>();
function DevBuildConfirmModal(props: RenderModalProps) {
const s = settings.use(["dismissedDevBuildWarning"]);
return (
<ConfirmModal
{...props}
title="Hold on!"
confirmText="Understood"
variant="primary"
checkboxProps={{
checked: s.dismissedDevBuildWarning === true,
onChange: checked => s.dismissedDevBuildWarning = checked
}}
>
<div>
<Forms.FormText>You are using a custom build of Vencord, which we do not provide support for!</Forms.FormText>
<Forms.FormText className={Margins.top8}>
We only provide support for <Link href="https://vencord.dev/download">official builds</Link>.
Either <Link href="https://vencord.dev/download">switch to an official build</Link> or figure your issue out yourself.
</Forms.FormText>
<Text variant="text-md/bold" className={Margins.top8}>You will be banned from receiving support if you ignore this rule.</Text>
</div>
</ConfirmModal>
);
}
export default definePlugin({
name: "SupportHelper",
required: true,
@ -180,20 +208,28 @@ export default definePlugin({
await checkForUpdatesOnce().catch(() => { });
if (isOutdated) {
return Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
<Forms.FormText className={Margins.top8}>
Please first update before asking for support!
</Forms.FormText>
</div>,
onCancel: () => openSettingsTabModal(UpdaterTab!),
cancelText: "View Updates",
confirmText: "Update & Restart Now",
onConfirm: forceUpdate,
secondaryConfirmText: "I know what I'm doing or I can't update"
});
openModal(props => (
<ConfirmModal
{...props}
variant="primary"
title="Hold on!"
confirmText="Update & Restart Now"
cancelText="View Updates"
onConfirm={forceUpdate}
onCancel={() => openSettingsTabModal(UpdaterTab!)}
>
<div>
<Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
<Forms.FormText className={Margins.top8}>
Please first update before asking for support!
</Forms.FormText>
<Forms.FormText className={Margins.top8}>
If you know what you're doing or cannot update, you can dismiss this prompt.
</Forms.FormText>
</div>
</ConfirmModal>
));
return;
}
}
@ -201,35 +237,28 @@ export default definePlugin({
if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return;
if (!IS_WEB && IS_UPDATER_DISABLED) {
return Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>You are using an externally updated Vencord version, which we do not provide support for!</Forms.FormText>
<Forms.FormText className={Margins.top8}>
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
contact your package maintainer for support instead.
</Forms.FormText>
</div>
});
openModal(props => (
<ConfirmModal
{...props}
title="Hold on!"
confirmText="OK"
variant="primary"
>
<div>
<Forms.FormText>You are using an externally updated Vencord version, which we do not provide support for!</Forms.FormText>
<Forms.FormText className={Margins.top8}>
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
contact your package maintainer for support instead.
</Forms.FormText>
</div>
</ConfirmModal>
));
return;
}
if (!IS_STANDALONE && !settings.store.dismissedDevBuildWarning) {
return Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>You are using a custom build of Vencord, which we do not provide support for!</Forms.FormText>
<Forms.FormText className={Margins.top8}>
We only provide support for <Link href="https://vencord.dev/download">official builds</Link>.
Either <Link href="https://vencord.dev/download">switch to an official build</Link> or figure your issue out yourself.
</Forms.FormText>
<Text variant="text-md/bold" className={Margins.top8}>You will be banned from receiving support if you ignore this rule.</Text>
</div>,
confirmText: "Understood",
secondaryConfirmText: "Don't show again",
onConfirmSecondary: () => settings.store.dismissedDevBuildWarning = true
});
openModal(props => <DevBuildConfirmModal {...props} />);
return;
}
}
},

View file

@ -6,7 +6,7 @@
import { Button } from "@components/Button";
import { SessionInfo } from "@plugins/betterSessions/types";
import { openModal } from "@utils/modal";
import { openModal } from "@webpack/common";
import { RenameModal } from "./RenameModal";

View file

@ -16,13 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { TextButton } from "@components/Button";
import { SessionInfo } from "@plugins/betterSessions/types";
import { getDefaultName, savedSessionsCache, saveSessionsToDataStore } from "@plugins/betterSessions/utils";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
import { Button, Forms, React, TextInput } from "@webpack/common";
import { RenderModalProps } from "@vencord/discord-types";
import { Forms, Modal,React, TextInput } from "@webpack/common";
import { KeyboardEvent } from "react";
export function RenameModal({ props, session, state }: { props: ModalProps, session: SessionInfo["session"], state: [string, React.Dispatch<React.SetStateAction<string>>]; }) {
export function RenameModal({ props, session, state }: { props: RenderModalProps, session: SessionInfo["session"], state: [string, React.Dispatch<React.SetStateAction<string>>]; }) {
const [title, setTitle] = state;
const [value, setValue] = React.useState(savedSessionsCache.get(session.id_hash)?.name ?? "");
@ -39,13 +40,24 @@ export function RenameModal({ props, session, state }: { props: ModalProps, sess
}
return (
<ModalRoot {...props}>
<ModalHeader>
<Forms.FormTitle tag="h4">Rename</Forms.FormTitle>
</ModalHeader>
<ModalContent>
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>New device name</Forms.FormTitle>
<Modal
{...props}
title="Rename"
actions={[
{
text: "Cancel",
variant: "secondary",
onClick: () => props.onClose()
},
{
text: "Save",
variant: "primary",
onClick: onSaveClick
}
]}
>
<div>
<Forms.FormTitle tag="h5">New device name</Forms.FormTitle>
<TextInput
style={{ marginBottom: "10px" }}
placeholder={getDefaultName(session.client_info)}
@ -57,38 +69,16 @@ export function RenameModal({ props, session, state }: { props: ModalProps, sess
}
}}
/>
<Button
<TextButton
style={{
marginBottom: "20px",
paddingLeft: "1px",
paddingRight: "1px",
opacity: 0.6
}}
look={Button.Looks.LINK}
color={Button.Colors.LINK}
size={Button.Sizes.NONE}
onClick={() => setValue("")}
>
Reset Name
</Button>
</ModalContent>
<ModalFooter>
<div className="vc-betterSessions-footer-buttons">
<Button
color={Button.Colors.PRIMARY}
onClick={() => props.onClose()}
>
Cancel
</Button>
<Button
color={Button.Colors.BRAND}
onClick={onSaveClick}
>
Save
</Button>
</div>
</ModalFooter>
</ModalRoot >
</TextButton>
</div>
</Modal>
);
}

View file

@ -22,7 +22,6 @@ import { getCurrentChannel, getCurrentGuild } from "@utils/discord";
import { runtimeHashMessageKey } from "@utils/intlHash";
import { SYM_LAZY_CACHED, SYM_LAZY_GET } from "@utils/lazy";
import { sleep } from "@utils/misc";
import { ModalAPI } from "@utils/modal";
import { relaunch } from "@utils/native";
import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches";
import definePlugin, { PluginNative, StartAt } from "@utils/types";
@ -176,8 +175,8 @@ function makeShortcuts() {
me: { getter: () => Common.UserStore.getCurrentUser(), preload: false },
meId: { getter: () => Common.UserStore.getCurrentUser().id, preload: false },
messages: { getter: () => Common.MessageStore.getMessages(Common.SelectedChannelStore.getChannelId()), preload: false },
openModal: { getter: () => ModalAPI.openModal },
openModalLazy: { getter: () => ModalAPI.openModalLazy },
openModal: { getter: () => Common.openModal },
openModalLazy: { getter: () => Common.openModalLazy },
Stores: { getter: () => Object.fromEntries(Webpack.fluxStores) },

View file

@ -21,11 +21,10 @@ import { showNotification } from "@api/Notifications";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { closeAllModals } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { maybePromptToUpdate } from "@utils/updater";
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
import { DraftType, ExpressionPickerStore, FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
import { closeAllModals, DraftType, ExpressionPickerStore, FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
const CrashHandlerLogger = new Logger("CrashHandler");

View file

@ -4,130 +4,116 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BaseText } from "@components/BaseText";
import { Button } from "@components/Button";
import { Card } from "@components/Card";
import { InlineCode } from "@components/CodeBlock";
import { ExpandableSection } from "@components/ExpandableCard";
import { Flex } from "@components/Flex";
import { HeadingSecondary } from "@components/Heading";
import { InfoIcon } from "@components/Icons";
import { Margins } from "@components/margins";
import { Paragraph } from "@components/Paragraph";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { TextArea, TextInput, useState } from "@webpack/common";
import { RenderModalProps } from "@vencord/discord-types";
import { Modal,openModal, TextArea, TextInput, useState } from "@webpack/common";
import { parseTagArguments } from ".";
import { addTag, getTag, Tag } from "./settings";
export function openCreateTagModal(initialValue: Tag = { name: "", message: "" }) {
openModal(modalProps => (
<Modal initialValue={initialValue} modalProps={modalProps} />
<CreateTagDialog initialValue={initialValue} modalProps={modalProps} />
));
}
const EXAMPLE_RESPONSE = "Hello {{user}}! I am feeling {{mood = great}}.";
function Modal({ initialValue, modalProps }: { initialValue: Tag; modalProps: ModalProps; }) {
function CreateTagDialog({ initialValue, modalProps }: { initialValue: Tag; modalProps: RenderModalProps; }) {
const [name, setName] = useState(initialValue.name);
const [message, setMessage] = useState(initialValue.message.replaceAll("\\n", "\n"));
const isEdit = Boolean(initialValue.name);
const detectedArguments = parseTagArguments(message);
const hasReservedEphemeral = detectedArguments.some(arg => arg.name === "ephemeral");
const nameAlreadyExists = name !== initialValue.name && getTag(name);
const notice = hasReservedEphemeral
? 'The argument name "ephemeral" is reserved and cannot be used.'
: nameAlreadyExists
? `A tag with the name "${name}" already exists and will be overwritten.`
: undefined;
return (
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<ModalHeader>
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>Create Tag</BaseText>
<ModalCloseButton onClick={modalProps.onClose} />
</ModalHeader>
<Modal
{...modalProps}
title={isEdit ? "Edit Tag" : "Create New Tag"}
subtitle={isEdit ? "Edit your custom command." : "Create a new tag which will be registered as a slash command."}
actions={[
{
text: "Cancel",
variant: "secondary",
onClick: modalProps.onClose
},
{
text: isEdit ? "Save" : "Create",
variant: "primary",
onClick: () => {
const tag = { name, message };
addTag(tag);
modalProps.onClose();
},
disabled: !name || !message || hasReservedEphemeral
}
]}
notice={notice ? { message: notice, type: "critical" } : undefined}
>
<Flex flexDirection="column" gap={12}>
<section>
<HeadingSecondary>Name</HeadingSecondary>
<TextInput value={name} onChange={setName} placeholder="greet" />
</section>
<ModalContent>
<Flex flexDirection="column" gap={12}>
<Paragraph>Create a new tag which will be registered as a slash command.</Paragraph>
<section className={Margins.top8}>
<HeadingSecondary>Name</HeadingSecondary>
<TextInput value={name} onChange={setName} placeholder="greet" />
</section>
<section>
<HeadingSecondary>Response</HeadingSecondary>
<TextArea value={message} onChange={setMessage} placeholder={EXAMPLE_RESPONSE} autosize />
</section>
{detectedArguments.length > 0 && (
<section>
<HeadingSecondary>Response</HeadingSecondary>
<TextArea value={message} onChange={setMessage} placeholder={EXAMPLE_RESPONSE} />
<HeadingSecondary>Detected Arguments</HeadingSecondary>
<Paragraph>
<ul>
{detectedArguments.map(arg => (
<li key={arg.name}>
&mdash; <b>{arg.name}</b>{arg.defaultValue ? ` (default: ${arg.defaultValue})` : ""}
</li>
))}
</ul>
</Paragraph>
</section>
)}
{detectedArguments.length > 0 && (
<section>
<HeadingSecondary>Detected Arguments</HeadingSecondary>
<ExpandableSection
renderContent={() => (
<Flex flexDirection="column" gap={12}>
<Paragraph>
<ul>
{detectedArguments.map(arg => (
<li key={arg.name}>
&mdash; <b>{arg.name}</b>{arg.defaultValue ? ` (default: ${arg.defaultValue})` : ""}
</li>
))}
</ul>
Your response can include variables wrapped in double curly braces which will become command arguments, for example <InlineCode>{"Hello {{user}}"}</InlineCode>.
</Paragraph>
<Paragraph>
You can specify arguments with default values by using an equals sign, for example <InlineCode>{"Hello {{user = pal}}"}</InlineCode>.
</Paragraph>
</section>
)}
<ExpandableSection
renderContent={() => (
<Flex flexDirection="column" gap={12}>
<Paragraph>
Your response can include variables wrapped in double curly braces which will become command arguments, for example <InlineCode>{"Hello {{user}}"}</InlineCode>.
</Paragraph>
<Paragraph>
You can specify arguments with default values by using an equals sign, for example <InlineCode>{"Hello {{user = pal}}"}</InlineCode>.
</Paragraph>
<section>
<Paragraph><b>Example Command response:</b> <InlineCode>{EXAMPLE_RESPONSE}</InlineCode></Paragraph>
<Paragraph><b>Example usage:</b> <InlineCode>{"/greet user:@Clyde"}</InlineCode></Paragraph>
<Paragraph><b>Example output:</b> <InlineCode>{"Hello @Clyde! I am feeling great."}</InlineCode></Paragraph>
</section>
</Flex>
)}
>
<Flex alignItems="center" gap={8}>
<InfoIcon color="var(--text-muted)" height={16} width={16} />
View Arguments guide
<section>
<Paragraph><b>Example Command response:</b> <InlineCode>{EXAMPLE_RESPONSE}</InlineCode></Paragraph>
<Paragraph><b>Example usage:</b> <InlineCode>{"/greet user:@Clyde"}</InlineCode></Paragraph>
<Paragraph><b>Example output:</b> <InlineCode>{"Hello @Clyde! I am feeling great."}</InlineCode></Paragraph>
</section>
</Flex>
</ExpandableSection>
{hasReservedEphemeral &&
<Card variant="danger" className={Margins.top8} defaultPadding>
<Paragraph>The argument name "ephemeral" is reserved and cannot be used.</Paragraph>
</Card>
}
{nameAlreadyExists &&
<Card variant="warning" className={Margins.top8} defaultPadding>
<Paragraph>A tag with the name <InlineCode>{name}</InlineCode> already exists and will be overwritten.</Paragraph>
</Card>
}
</Flex>
</ModalContent>
<ModalFooter>
<Flex>
<Button
variant="secondary"
onClick={modalProps.onClose}
>
Cancel
</Button>
<Button
onClick={() => {
const tag = { name, message };
addTag(tag);
modalProps.onClose();
}}
disabled={!name || !message || hasReservedEphemeral}
>
Create
</Button>
</Flex>
</ModalFooter>
</ModalRoot>
)}
>
<Flex alignItems="center" gap={8}>
<InfoIcon color="var(--text-muted)" height={16} width={16} />
View Arguments guide
</Flex>
</ExpandableSection>
</Flex>
</Modal>
);
}

View file

@ -8,8 +8,7 @@ import * as DataStore from "@api/DataStore";
import { AUTHORIZE_URL, CLIENT_ID } from "@plugins/decor/lib/constants";
import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal";
import { OAuth2AuthorizeModal, showToast, Toasts, UserStore, zustandCreate, zustandPersist } from "@webpack/common";
import { OAuth2AuthorizeModal, openModal, showToast, Toasts, UserStore, zustandCreate, zustandPersist } from "@webpack/common";
interface AuthorizationState {
token: string | null;

View file

@ -5,12 +5,11 @@
*/
import { definePluginSettings } from "@api/Settings";
import { Link } from "@components/Link";
import { TextButton } from "@components/Button";
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 { Forms, SettingsRouter } from "@webpack/common";
import DecorPlugin from ".";
import DecorSection from "./ui/components/DecorSection";
@ -18,7 +17,7 @@ import DecorSection from "./ui/components/DecorSection";
export const settings = definePluginSettings({
changeDecoration: {
type: OptionType.COMPONENT,
component() {
component({ closePluginSettings }) {
if (!DecorPlugin.started) return <Forms.FormText>
Enable Decor and restart your client to change your avatar decoration.
</Forms.FormText>;
@ -26,14 +25,13 @@ export const settings = definePluginSettings({
return <div>
<DecorSection hideTitle hideDivider noMargin />
<Forms.FormText 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" });
You can also access Decor decorations from the <TextButton
variant="link"
onClick={async () => {
closePluginSettings();
SettingsRouter.openUserSettings("profile_panel");
}}
>Profiles</Link> page.
>Profiles</TextButton> page.
</Forms.FormText>
</div>;
}

View file

@ -9,7 +9,7 @@ import { Decoration } from "@plugins/decor/lib/api";
import { useCurrentUserDecorationsStore } from "@plugins/decor/lib/stores/CurrentUserDecorationsStore";
import { cl } from "@plugins/decor/ui";
import { copyToClipboard } from "@utils/clipboard";
import { Alerts, ContextMenuApi, Menu, UserStore } from "@webpack/common";
import { ConfirmModal,ContextMenuApi, Menu, openModal, UserStore } from "@webpack/common";
export default function DecorationContextMenu({ decoration }: { decoration: Decoration; }) {
const { delete: deleteDecoration } = useCurrentUserDecorationsStore();
@ -31,16 +31,18 @@ export default function DecorationContextMenu({ decoration }: { decoration: Deco
label="Delete Decoration"
color="danger"
icon={DeleteIcon}
action={() => Alerts.show({
title: "Delete Decoration",
body: `Are you sure you want to delete ${decoration.alt}?`,
confirmText: "Delete",
confirmColor: cl("danger-btn"),
cancelText: "Cancel",
onConfirm() {
deleteDecoration(decoration);
}
})}
action={() => openModal(props => (
<ConfirmModal
{...props}
title="Delete Decoration"
subtitle={`Are you sure you want to delete ${decoration.alt}?`}
confirmText="Delete"
cancelText="Cancel"
onConfirm={() => {
deleteDecoration(decoration);
}}
/>
))}
/>
}
</Menu.Menu>;

View file

@ -5,7 +5,6 @@
*/
import { Button as NewButton } from "@components/Button";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Decoration, getPresets, Preset } from "@plugins/decor/lib/api";
import { GUILD_ID, INVITE_KEY } from "@plugins/decor/lib/constants";
@ -21,10 +20,9 @@ import DecorDecorationGridDecoration from "@plugins/decor/ui/components/DecorDec
import SectionedGridList from "@plugins/decor/ui/components/SectionedGridList";
import { copyWithToast, openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Queue } from "@utils/Queue";
import { User } from "@vencord/discord-types";
import { Alerts, Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Parser, Text, Tooltip, useEffect, UserStore, UserSummaryItem, UserUtils, useState } from "@webpack/common";
import { RenderModalProps, User } from "@vencord/discord-types";
import { Button, closeAllModals, ConfirmModal, FluxDispatcher, Forms, GuildStore, Modal, NavigationRouter, openModal, Parser, Text, Tooltip, useEffect, UserStore, UserSummaryItem, UserUtils, useState } from "@webpack/common";
import { openCreateDecorationModal } from "./CreateDecorationModal";
import { openGuidelinesModal } from "./GuidelinesModal";
@ -90,7 +88,7 @@ function SectionHeader({ section }: SectionHeaderProps) {
</div>;
}
function ChangeDecorationModal(props: ModalProps) {
function ChangeDecorationModal(props: RenderModalProps) {
// undefined = not trying, null = none, Decoration = selected
const [tryingDecoration, setTryingDecoration] = useState<Decoration | null | undefined>(undefined);
const isTryingDecoration = typeof tryingDecoration !== "undefined";
@ -136,110 +134,27 @@ function ChangeDecorationModal(props: ModalProps) {
}))
] as Section[];
return <ModalRoot
return <Modal
{...props}
size={ModalSize.DYNAMIC}
className={DecorationModalClasses.modal}
>
<ModalHeader separator={false} className={cl("modal-header")}>
<Text
color="text-strong"
variant="heading-lg/semibold"
tag="h1"
style={{ flexGrow: 1 }}
>
Change Decoration
</Text>
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent
className={cl("change-decoration-modal-content")}
scrollbarType="none"
>
<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}
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
avatarDecoration={avatarDecoration}
user={UserStore.getCurrentUser()}
/>
{isActiveDecorationPreset && <Forms.FormTitle className="">Part of the {activeDecorationPreset.name} Preset</Forms.FormTitle>}
{typeof activeSelectedDecoration === "object" &&
<Text
variant="text-sm/semibold"
color="text-strong"
>
{activeSelectedDecoration?.alt}
</Text>
}
{activeDecorationHasAuthor && (
<Text key={`createdBy-${activeSelectedDecoration.authorId}`}>
Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}
</Text>
)}
{isActiveDecorationPreset && (
<Button onClick={() => copyWithToast(activeDecorationPreset.id)}>
Copy Preset ID
</Button>
)}
</div>
</ErrorBoundary>
</ModalContent>
<ModalFooter className={cl("change-decoration-modal-footer", "modal-footer")}>
<div className={cl("modal-footer-btn-container")}>
<Button
onClick={props.onClose}
color={Button.Colors.PRIMARY}
>
Cancel
</Button>
<Button
onClick={() => {
selectDecoration(tryingDecoration!).then(props.onClose);
}}
disabled={!isTryingDecoration}
>
Apply
</Button>
</div>
<div className={cl("modal-footer-btn-container")}>
title="Change Decoration"
size="lg"
actions={[
{
text: "Cancel",
variant: "secondary",
onClick: props.onClose
},
{
text: "Apply",
variant: "primary",
onClick: () => {
selectDecoration(tryingDecoration!).then(props.onClose);
},
disabled: !isTryingDecoration
}
]}
preview={
<div className={cl("modal-footer-btn-container", Margins.top8)}>
<Tooltip text="Join Decor's Discord Server for notifications on your decoration's review, and when new presets are released">
{tooltipProps => <NewButton
{...tooltipProps}
@ -262,24 +177,94 @@ function ChangeDecorationModal(props: ModalProps) {
</NewButton>}
</Tooltip>
<NewButton
onClick={() => Alerts.show({
title: "Log Out",
body: "Are you sure you want to log out of Decor?",
confirmText: "Log Out",
confirmColor: cl("danger-btn"),
cancelText: "Cancel",
onConfirm() {
useAuthorizationStore.getState().remove(UserStore.getCurrentUser().id);
props.onClose();
}
})}
onClick={() => openModal(modalProps => (
<ConfirmModal
{...modalProps}
title="Log Out"
subtitle="Are you sure you want to log out of Decor?"
confirmText="Log Out"
cancelText="Cancel"
onConfirm={() => {
useAuthorizationStore.getState().remove(UserStore.getCurrentUser().id);
props.onClose();
}}
/>
))}
variant="dangerSecondary"
>
Log Out
</NewButton>
</div>
</ModalFooter>
</ModalRoot>;
}
>
<div className={cl("change-decoration-modal-content", DecorationModalClasses.modal)}>
<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}
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
avatarDecoration={avatarDecoration}
user={UserStore.getCurrentUser()}
/>
{isActiveDecorationPreset && <Forms.FormTitle className="">Part of the {activeDecorationPreset.name} Preset</Forms.FormTitle>}
{typeof activeSelectedDecoration === "object" &&
<Text
variant="text-sm/semibold"
color="text-strong"
>
{activeSelectedDecoration?.alt}
</Text>
}
{activeDecorationHasAuthor && (
<Text key={`createdBy-${activeSelectedDecoration.authorId}`}>
Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}
</Text>
)}
{isActiveDecorationPreset && (
<Button onClick={() => copyWithToast(activeDecorationPreset.id)}>
Copy Preset ID
</Button>
)}
</div>
</div>
</Modal>;
}
export const openChangeDecorationModal = () =>

View file

@ -12,9 +12,9 @@ import { cl, DecorationModalClasses, requireAvatarDecorationModal, requireCreate
import { AvatarDecorationModalPreview } from "@plugins/decor/ui/components";
import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { RenderModalProps } from "@vencord/discord-types";
import { filters, findComponentByCodeLazy, mapMangledModuleLazy } from "@webpack";
import { Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Text, TextInput, useEffect, useMemo, UserStore, useState } from "@webpack/common";
import { closeAllModals, FluxDispatcher, Forms, GuildStore, Modal, NavigationRouter, openModal, Text, TextInput, useEffect, useMemo, UserStore, useState } from "@webpack/common";
const FileUpload = findComponentByCodeLazy(".currentTarget.files", "lineClamp:1");
@ -41,7 +41,7 @@ function useObjectURL(object: Blob | MediaSource | null) {
return url;
}
function CreateDecorationModal(props: ModalProps) {
function CreateDecorationModal(props: RenderModalProps) {
const [name, setName] = useState("");
const [file, setFile] = useState<File | null>(null);
const [submitting, setSubmitting] = useState(false);
@ -57,26 +57,29 @@ function CreateDecorationModal(props: ModalProps) {
const decoration = useMemo(() => fileUrl ? { asset: fileUrl, skuId: RAW_SKU_ID } : null, [fileUrl]);
return <ModalRoot
return <Modal
{...props}
size={ModalSize.MEDIUM}
className={DecorationModalClasses.modal}
size="lg"
title="Create Decoration"
actions={[
{
text: "Cancel",
variant: "secondary",
onClick: props.onClose
},
{
text: "Submit for Review",
variant: "primary",
onClick: () => {
setSubmitting(true);
createDecoration({ alt: name, file: file! })
.then(props.onClose).catch(e => { setSubmitting(false); setError(e); });
},
disabled: !file || !name || submitting
}
]}
>
<ModalHeader separator={false} className={cl("modal-header")}>
<Text
color="text-strong"
variant="heading-lg/semibold"
tag="h1"
style={{ flexGrow: 1 }}
>
Create Decoration
</Text>
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent
className={cl("create-decoration-modal-content")}
scrollbarType="none"
>
<div className={cl("create-decoration-modal-content", DecorationModalClasses.modal)}>
<ErrorBoundary>
<HelpMessage messageType={HelpMessageTypes.WARNING}>
Make sure your decoration does not violate <Link
@ -142,28 +145,8 @@ function CreateDecorationModal(props: ModalProps) {
</Link> and allow direct messages.
</HelpMessage>
</ErrorBoundary>
</ModalContent>
<ModalFooter className={cl("modal-footer")}>
<div className={cl("modal-footer-btn-container")}>
<Button
onClick={props.onClose}
color={Button.Colors.PRIMARY}
>
Cancel
</Button>
<Button
onClick={() => {
setSubmitting(true);
createDecoration({ alt: name, file: file! })
.then(props.onClose).catch(e => { setSubmitting(false); setError(e); });
}}
disabled={!file || !name || submitting}
>
Submit for Review
</Button>
</div>
</ModalFooter>
</ModalRoot>;
</div>
</Modal>;
}
export const openCreateDecorationModal = () =>

View file

@ -4,64 +4,38 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
import { settings } from "@plugins/decor/settings";
import { cl, DecorationModalClasses, requireAvatarDecorationModal } from "@plugins/decor/ui";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Button, Forms, Text } from "@webpack/common";
import { DecorationModalClasses, requireAvatarDecorationModal } from "@plugins/decor/ui";
import { RenderModalProps } from "@vencord/discord-types";
import { ConfirmModal, Forms, openModal } from "@webpack/common";
import { openCreateDecorationModal } from "./CreateDecorationModal";
function GuidelinesModal(props: ModalProps) {
return <ModalRoot
{...props}
size={ModalSize.SMALL}
className={DecorationModalClasses.modal}
>
<ModalHeader separator={false} className={cl("modal-header")}>
<Text
color="text-strong"
variant="heading-lg/semibold"
tag="h1"
style={{ flexGrow: 1 }}
>
Hold on
</Text>
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent
scrollbarType="none"
function GuidelinesModal(props: RenderModalProps) {
return (
<ConfirmModal
{...props}
title="Hold on"
confirmText="Continue"
variant="primary"
onConfirm={() => {
settings.store.agreedToGuidelines = true;
props.onClose();
openCreateDecorationModal();
}}
>
<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")}>
<Flex gap="4px">
<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>
</Flex>
</ModalFooter>
</ModalRoot>;
<div className={DecorationModalClasses.modal}>
<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>
</div>
</ConfirmModal>
);
}
export const openGuidelinesModal = () =>

View file

@ -7,7 +7,6 @@
position: relative;
display: flex;
border-radius: 5px 5px 0 0;
padding: 0 16px;
gap: 4px;
}
@ -32,7 +31,6 @@
display: flex;
flex-direction: column;
gap: 20px;
padding: 0 16px;
}
.vc-decor-create-decoration-modal-form-preview-container {

View file

@ -18,17 +18,17 @@
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { migratePluginSettings } from "@api/Settings";
import { BaseText } from "@components/BaseText";
import { CheckedTextInput } from "@components/CheckedTextInput";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import { getGuildAcronym } from "@utils/discord";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
import definePlugin from "@utils/types";
import { Guild, GuildSticker } from "@vencord/discord-types";
import { StickerFormatType } from "@vencord/discord-types/enums";
import { findByCodeLazy } from "@webpack";
import { Constants, EmojiStore, FluxDispatcher, Forms, GuildStore, IconUtils, Menu, PermissionsBits, PermissionStore, React, RestAPI, StickersStore, Toasts, Tooltip, UserStore } from "@webpack/common";
import { Constants, EmojiStore, FluxDispatcher, Forms, GuildStore, IconUtils, Menu, Modal, openModalLazy, PermissionsBits, PermissionStore, React, RestAPI, StickersStore, Toasts, Tooltip, UserStore } from "@webpack/common";
import { Promisable } from "type-fest";
const uploadEmoji = findByCodeLazy(".GUILD_EMOJIS(", "EMOJI_UPLOAD_START");
@ -228,7 +228,7 @@ function CloneModal({ data }: { data: Sticker | Emoji; }) {
return (
<>
<Forms.FormTitle className={Margins.top20}>Custom Name</Forms.FormTitle>
<Forms.FormTitle>Custom Name</Forms.FormTitle>
<CheckedTextInput
initialValue={name}
onChange={v => {
@ -329,23 +329,24 @@ function buildMenuItem(type: "Emoji" | "Sticker", fetchData: () => Promisable<Om
const url = getUrl(data, 128);
return modalProps => (
<ModalRoot {...modalProps}>
<ModalHeader>
<img
role="presentation"
aria-hidden
src={url}
alt=""
height={24}
width={24}
style={{ marginRight: "0.5em" }}
/>
<Forms.FormText>Clone {data.name}</Forms.FormText>
</ModalHeader>
<ModalContent>
<CloneModal data={data} />
</ModalContent>
</ModalRoot>
<Modal
{...modalProps}
title={
<Flex gap="0.5em" alignItems="center">
<img
role="presentation"
aria-hidden
src={url}
alt=""
height={24}
width={24}
/>
<BaseText tag="h3" size="md" weight="medium">Clone {data.name}</BaseText>
</Flex>
}
>
<CloneModal data={data} />
</Modal>
);
})
}

View file

@ -23,10 +23,10 @@ import { Devs } from "@utils/constants";
import { getCurrentGuild } from "@utils/discord";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import type { Emoji, Message, Sticker } from "@vencord/discord-types";
import type { Emoji, Message, RenderModalProps, Sticker } from "@vencord/discord-types";
import { StickerFormatType } from "@vencord/discord-types/enums";
import { findByCodeLazy, findByPropsLazy, proxyLazyWebpack } from "@webpack";
import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, GuildMemberStore, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, StickersStore, UploadHandler, UserSettingsActionCreators, UserSettingsProtoStore, UserStore } from "@webpack/common";
import { ChannelStore, ConfirmModal,DraftType, EmojiStore, FluxDispatcher, Forms, GuildMemberStore, IconUtils, lodash, openModal, Parser, PermissionsBits, PermissionStore, StickersStore, UploadHandler, UserSettingsActionCreators, UserSettingsProtoStore, UserStore } from "@webpack/common";
import { applyPalette, GIFEncoder, quantize } from "gifenc";
import type { ReactElement, ReactNode } from "react";
@ -151,6 +151,35 @@ const hasExternalStickerPerms = (channelId: string) => hasPermission(channelId,
const hasEmbedPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.EMBED_LINKS);
const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.ATTACH_FILES);
function getWordBoundary(origStr: string, offset: number) {
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
}
function CannotEmbedNoticeModal({ modalProps, resolve }: { modalProps: RenderModalProps; resolve: (value: boolean) => void; }) {
const s = settings.use(["disableEmbedPermissionCheck"]);
return (
<ConfirmModal
{...modalProps}
title="Hold on!"
subtitle="You are trying to send/edit a message that contains a FakeNitro emoji or sticker, however you do not have permissions to embed links in the current channel. Are you sure you want to send this message? Your FakeNitro items will appear as a link only."
confirmText="Send Anyway"
cancelText="Cancel"
onConfirm={() => resolve(true)}
onCloseCallback={() => setImmediate(() => resolve(false))}
checkboxProps={{
checked: s.disableEmbedPermissionCheck === true,
onChange: checked => s.disableEmbedPermissionCheck = checked
}}
/>
);
}
function showCannotEmbedNotice() {
return new Promise<boolean>(resolve => {
openModal(props => <CannotEmbedNoticeModal modalProps={props} resolve={resolve} />);
});
}
export default definePlugin({
name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN, Devs.sadan],
@ -795,37 +824,6 @@ export default definePlugin({
return;
}
function getWordBoundary(origStr: string, offset: number) {
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
}
function cannotEmbedNotice() {
return new Promise<boolean>(resolve => {
Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>
You are trying to send/edit a message that contains a FakeNitro emoji or sticker,
however you do not have permissions to embed links in the current channel.
Are you sure you want to send this message? Your FakeNitro items will appear as a link only.
</Forms.FormText>
<Forms.FormText>
You can disable this notice in the plugin settings.
</Forms.FormText>
</div>,
confirmText: "Send Anyway",
cancelText: "Cancel",
secondaryConfirmText: "Do not show again",
onConfirm: () => resolve(true),
onCloseCallback: () => setImmediate(() => resolve(false)),
onConfirmSecondary() {
settings.store.disableEmbedPermissionCheck = true;
resolve(true);
}
});
});
}
this.preSend = addMessagePreSendListener(async (channelId, messageObj, extra) => {
const { guildId } = this;
@ -851,15 +849,21 @@ export default definePlugin({
if (sticker.format_type === StickerFormatType.APNG) {
if (!hasAttachmentPerms(channelId)) {
Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>
You cannot send this message because it contains an animated FakeNitro sticker,
and you do not have permissions to attach files in the current channel. Please remove the sticker to proceed.
</Forms.FormText>
</div>
});
openModal(props => (
<ConfirmModal
{...props}
title="Hold on!"
confirmText="OK"
variant="primary"
>
<div>
<Forms.FormText>
You cannot send this message because it contains an animated FakeNitro sticker,
and you do not have permissions to attach files in the current channel. Please remove the sticker to proceed.
</Forms.FormText>
</div>
</ConfirmModal>
));
} else {
this.sendAnimatedSticker(link, sticker.id, channelId);
}
@ -901,7 +905,7 @@ export default definePlugin({
}
if (hasBypass && !s.disableEmbedPermissionCheck && !hasEmbedPerms(channelId)) {
if (!await cannotEmbedNotice()) {
if (!await showCannotEmbedNotice()) {
return { cancel: true };
}
}
@ -932,7 +936,7 @@ export default definePlugin({
});
if (hasBypass && !s.disableEmbedPermissionCheck && !hasEmbedPerms(channelId)) {
if (!await cannotEmbedNotice()) {
if (!await showCannotEmbedNotice()) {
return { cancel: true };
}
}

View file

@ -9,9 +9,9 @@ import { TooltipContainer } from "@components/TooltipContainer";
import { classNameFactory } from "@utils/css";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { RenderModalProps } from "@vencord/discord-types";
import { findCssClassesLazy } from "@webpack";
import { TabBar, Text, Timestamp, useState } from "@webpack/common";
import { Modal,openModal, TabBar, Timestamp, useState } from "@webpack/common";
import { parseEditContent } from ".";
@ -31,63 +31,60 @@ export function openHistoryModal(message: any) {
);
}
export function HistoryModal({ modalProps, message }: { modalProps: ModalProps; message: any; }) {
export function HistoryModal({ modalProps, message }: { modalProps: RenderModalProps; message: any; }) {
const [currentTab, setCurrentTab] = useState(message.editHistory.length);
const timestamps = [message.firstEditTimestamp, ...message.editHistory.map(m => m.timestamp)];
const contents = [...message.editHistory.map(m => m.content), message.content];
return (
<ModalRoot {...modalProps} size={ModalSize.LARGE}>
<ModalHeader className={cl("head")}>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Message Edit History</Text>
<ModalCloseButton onClick={modalProps.onClose} />
</ModalHeader>
<ModalContent className={cl("contents")}>
<TabBar
type="top"
look="brand"
className={classes("vc-settings-tab-bar", cl("tab-bar"))}
selectedItem={currentTab}
onItemSelect={setCurrentTab}
>
{message.firstEditTimestamp.getTime() !== message.timestamp.getTime() && (
<TooltipContainer text="This edit state was not logged so it can't be displayed.">
<TabBar.Item
className="vc-settings-tab-bar-item"
id={-1}
disabled
>
<Timestamp
className={cl("timestamp")}
timestamp={message.timestamp}
isEdited={true}
isInline={false}
/>
</TabBar.Item>
</TooltipContainer>
)}
{timestamps.map((timestamp, index) => (
<Modal
{...modalProps}
size="lg"
title="Message Edit History"
>
<TabBar
type="top"
look="brand"
className={classes("vc-settings-tab-bar", cl("tab-bar"))}
selectedItem={currentTab}
onItemSelect={setCurrentTab}
>
{message.firstEditTimestamp.getTime() !== message.timestamp.getTime() && (
<TooltipContainer text="This edit state was not logged so it can't be displayed.">
<TabBar.Item
key={index}
className="vc-settings-tab-bar-item"
id={index}
id={-1}
disabled
>
<Timestamp
className={cl("timestamp")}
timestamp={timestamp}
timestamp={message.timestamp}
isEdited={true}
isInline={false}
/>
</TabBar.Item>
))}
</TabBar>
</TooltipContainer>
)}
<div className={classes(CodeContainerClasses.markup, MiscClasses.messageContent, Margins.top20)}>
{parseEditContent(contents[currentTab], message)}
</div>
</ModalContent>
</ModalRoot>
{timestamps.map((timestamp, index) => (
<TabBar.Item
key={index}
className="vc-settings-tab-bar-item"
id={index}
>
<Timestamp
className={cl("timestamp")}
timestamp={timestamp}
isEdited={true}
isInline={false}
/>
</TabBar.Item>
))}
</TabBar>
<div className={classes(CodeContainerClasses.markup, MiscClasses.messageContent, Margins.top20)}>
{parseEditContent(contents[currentTab], message)}
</div>
</Modal>
);
}

View file

@ -23,11 +23,10 @@ import { buildExtraRoleContextMenuItems } from "@plugins/betterRoleContext";
import { cl, getGuildPermissionSpecMap, loadGetGuildPermissionSpecMap } from "@plugins/permissionsViewer/utils";
import { copyToClipboard } from "@utils/clipboard";
import { getIntlMessage, getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModalLazy } from "@utils/modal";
import { Guild, Role, RoleOrUserPermission, UnicodeEmoji, User } from "@vencord/discord-types";
import { Guild, RenderModalProps, Role, RoleOrUserPermission, UnicodeEmoji, User } from "@vencord/discord-types";
import { PermissionOverwriteType } from "@vencord/discord-types/enums";
import { findByCodeLazy } from "@webpack";
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildRoleStore, i18n, Menu, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, useMemo, useRef, UserStore, useState, useStateFromStores } from "@webpack/common";
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildRoleStore, i18n, Menu, Modal,openModalLazy, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, useMemo, useRef, UserStore, useState, useStateFromStores } from "@webpack/common";
import { settings } from "..";
import { PermissionAllowedIcon, PermissionDefaultIcon, PermissionDeniedIcon } from "./icons";
@ -43,7 +42,7 @@ function getRoleIconSrc(role: Role) {
return customIconSrc ?? unicodeEmoji?.url;
}
function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, header }: { permissions: Array<RoleOrUserPermission>; guild: Guild; modalProps: ModalProps; header: string; }) {
function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, header }: { permissions: Array<RoleOrUserPermission>; guild: Guild; modalProps: RenderModalProps; header: string; }) {
const guildPermissionSpecMap = useMemo(() => getGuildPermissionSpecMap(guild), [guild.id]);
useStateFromStores(
@ -75,133 +74,127 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
const roles = GuildRoleStore.getRolesSnapshot(guild.id);
return (
<ModalRoot
<Modal
{...modalProps}
size={ModalSize.LARGE}
size="xl"
title={`${header} Permissions`}
>
<ModalHeader>
<Text className={cl("modal-title")} variant="heading-lg/semibold">{header} Permissions</Text>
<ModalCloseButton onClick={modalProps.onClose} />
</ModalHeader>
{!selectedItem && (
<div className={cl("modal-no-perms")}>
<Text variant="heading-lg/normal">No permissions to display!</Text>
</div>
)}
<ModalContent className={cl("modal-content")}>
{!selectedItem && (
<div className={cl("modal-no-perms")}>
<Text variant="heading-lg/normal">No permissions to display!</Text>
</div>
)}
{selectedItem && (
<div className={cl("modal-container")}>
<ScrollerThin className={cl("modal-list")} orientation="auto">
{permissions.map((permission, index) => {
const user: User | undefined = UserStore.getUser(permission.id ?? "");
const role: Role | undefined = roles[permission.id ?? ""];
const roleIconSrc = role != null ? getRoleIconSrc(role) : undefined;
{selectedItem && (
<div className={cl("modal-container")}>
<ScrollerThin className={cl("modal-list")} orientation="auto">
{permissions.map((permission, index) => {
const user: User | undefined = UserStore.getUser(permission.id ?? "");
const role: Role | undefined = roles[permission.id ?? ""];
const roleIconSrc = role != null ? getRoleIconSrc(role) : undefined;
return (
return (
<div
key={index}
className={cl("modal-list-item-btn")}
onClick={() => selectItem(index)}
role="button"
tabIndex={0}
>
<div
key={index}
className={cl("modal-list-item-btn")}
onClick={() => selectItem(index)}
role="button"
tabIndex={0}
className={cl("modal-list-item", { "modal-list-item-active": selectedItemIndex === index })}
onContextMenu={e => {
if (permission.type === PermissionOverwriteType.ROLE)
ContextMenuApi.openContextMenu(e, () => (
<RoleContextMenu
guild={guild}
roleId={permission.id!}
onClose={modalProps.onClose}
/>
));
else if (permission.type === PermissionOverwriteType.MEMBER) {
ContextMenuApi.openContextMenu(e, () => (
<UserContextMenu
userId={permission.id!}
/>
));
}
}}
>
<div
className={cl("modal-list-item", { "modal-list-item-active": selectedItemIndex === index })}
onContextMenu={e => {
if (permission.type === PermissionOverwriteType.ROLE)
ContextMenuApi.openContextMenu(e, () => (
<RoleContextMenu
guild={guild}
roleId={permission.id!}
onClose={modalProps.onClose}
/>
));
else if (permission.type === PermissionOverwriteType.MEMBER) {
ContextMenuApi.openContextMenu(e, () => (
<UserContextMenu
userId={permission.id!}
/>
));
}
}}
>
{(permission.type === PermissionOverwriteType.ROLE || permission.type === PermissionOverwriteType.OWNER) && (
<span
className={cl("modal-role-circle")}
style={{ backgroundColor: role?.colorString ?? "var(--primary-300)" }}
/>
)}
{permission.type === PermissionOverwriteType.ROLE && roleIconSrc != null && (
<img
className={cl("modal-role-image")}
src={roleIconSrc}
/>
)}
{permission.type === PermissionOverwriteType.MEMBER && user != null && (
<img
className={cl("modal-user-img")}
src={user.getAvatarURL(void 0, void 0, false)}
/>
)}
<Text variant="text-md/normal" className={cl("modal-list-item-text")}>
{
permission.type === PermissionOverwriteType.ROLE
? role?.name ?? "Unknown Role"
: permission.type === PermissionOverwriteType.MEMBER
? (user != null && getUniqueUsername(user)) ?? "Unknown User"
: (
<Flex gap="0.2em">
@owner
<OwnerCrownIcon height={18} width={18} aria-hidden="true" />
</Flex>
)
}
</Text>
</div>
{(permission.type === PermissionOverwriteType.ROLE || permission.type === PermissionOverwriteType.OWNER) && (
<span
className={cl("modal-role-circle")}
style={{ backgroundColor: role?.colorString ?? "var(--primary-300)" }}
/>
)}
{permission.type === PermissionOverwriteType.ROLE && roleIconSrc != null && (
<img
className={cl("modal-role-image")}
src={roleIconSrc}
/>
)}
{permission.type === PermissionOverwriteType.MEMBER && user != null && (
<img
className={cl("modal-user-img")}
src={user.getAvatarURL(void 0, void 0, false)}
/>
)}
<Text variant="text-md/normal" className={cl("modal-list-item-text")}>
{
permission.type === PermissionOverwriteType.ROLE
? role?.name ?? "Unknown Role"
: permission.type === PermissionOverwriteType.MEMBER
? (user != null && getUniqueUsername(user)) ?? "Unknown User"
: (
<Flex gap="0.2em">
@owner
<OwnerCrownIcon height={18} width={18} aria-hidden="true" />
</Flex>
)
}
</Text>
</div>
);
})}
</ScrollerThin>
<div className={cl("modal-divider")} />
<ScrollerThin className={cl("modal-perms")} orientation="auto">
{Object.values(PermissionsBits).map(bit => (
<div key={bit} className={cl("modal-perms-item")}>
<div className={cl("modal-perms-item-icon")}>
{(() => {
const { permissions, overwriteAllow, overwriteDeny } = selectedItem;
if (permissions)
return (permissions & bit) === bit
? PermissionAllowedIcon()
: PermissionDeniedIcon();
if (overwriteAllow && (overwriteAllow & bit) === bit)
return PermissionAllowedIcon();
if (overwriteDeny && (overwriteDeny & bit) === bit)
return PermissionDeniedIcon();
return PermissionDefaultIcon();
})()}
</div>
<Text variant="text-md/normal">{guildPermissionSpecMap[String(bit)].title}</Text>
<Tooltip text={
(() => {
const { description } = guildPermissionSpecMap[String(bit)];
return typeof description === "function" ? i18n.intl.format(description, {}) : description;
})()
}>
{props => <InfoIcon {...props} />}
</Tooltip>
</div>
))}
</ScrollerThin>
</div>
)}
</ModalContent>
</ModalRoot>
);
})}
</ScrollerThin>
<div className={cl("modal-divider")} />
<ScrollerThin className={cl("modal-perms")} orientation="auto">
{Object.values(PermissionsBits).map(bit => (
<div key={bit} className={cl("modal-perms-item")}>
<div className={cl("modal-perms-item-icon")}>
{(() => {
const { permissions, overwriteAllow, overwriteDeny } = selectedItem;
if (permissions)
return (permissions & bit) === bit
? PermissionAllowedIcon()
: PermissionDeniedIcon();
if (overwriteAllow && (overwriteAllow & bit) === bit)
return PermissionAllowedIcon();
if (overwriteDeny && (overwriteDeny & bit) === bit)
return PermissionDeniedIcon();
return PermissionDefaultIcon();
})()}
</div>
<Text variant="text-md/normal">{guildPermissionSpecMap[String(bit)].title}</Text>
<Tooltip text={
(() => {
const { description } = guildPermissionSpecMap[String(bit)];
return typeof description === "function" ? i18n.intl.format(description, {}) : description;
})()
}>
{props => <InfoIcon {...props} />}
</Tooltip>
</div>
))}
</ScrollerThin>
</div>
)}
</Modal>
);
}

View file

@ -21,14 +21,6 @@
/* RolesAndUsersPermissions Component */
.vc-permviewer-modal-content {
padding: 16px 4px 16px 16px;
}
.vc-permviewer-modal-title {
flex-grow: 1;
}
.vc-permviewer-modal-no-perms {
width: 100%;
height: 100%;

View file

@ -4,15 +4,15 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Divider } from "@components/Divider";
import { DEFAULT_COLOR, SWATCHES } from "@plugins/pinDms/constants";
import { categoryLen, createCategory, getCategory } from "@plugins/pinDms/data";
import { classNameFactory } from "@utils/css";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal";
import { RenderModalProps } from "@vencord/discord-types";
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
import { Button, ColorPicker, Forms, Text, TextInput, Toasts, useMemo, useState } from "@webpack/common";
import { ColorPicker, Forms, Modal,openModalLazy, TextInput, Toasts, useMemo, useState } from "@webpack/common";
interface ColorPickerWithSwatchesProps {
className?: string;
defaultColor: number;
colors: number[];
value: number;
@ -31,7 +31,7 @@ const cl = classNameFactory("vc-pindms-modal-");
interface Props {
categoryId: string | null;
initialChannelId: string | null;
modalProps: ModalProps;
modalProps: RenderModalProps;
}
function useCategory(categoryId: string | null, initalChannelId: string | null) {
@ -59,9 +59,7 @@ export function NewCategoryModal({ categoryId, modalProps, initialChannelId }: P
const [name, setName] = useState(category.name);
const [color, setColor] = useState(category.color);
const onSave = (e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
const onSave = () => {
category.name = name;
category.color = color;
@ -73,47 +71,52 @@ export function NewCategoryModal({ categoryId, modalProps, initialChannelId }: P
};
return (
<ModalRoot {...modalProps}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{categoryId ? "Edit" : "New"} Category</Text>
</ModalHeader>
{/* form is here so when you press enter while in the text input it submits */}
<form onSubmit={onSave}>
<ModalContent className={cl("content")}>
<section>
<Forms.FormTitle>Name</Forms.FormTitle>
<TextInput
value={name}
onChange={e => setName(e)}
/>
</section>
<Divider />
<section>
<Forms.FormTitle>Color</Forms.FormTitle>
<ColorPickerWithSwatches
key={category.id}
defaultColor={DEFAULT_COLOR}
colors={SWATCHES}
onChange={c => setColor(c!)}
value={color}
renderDefaultButton={() => null}
renderCustomButton={() => (
<ColorPicker
color={color}
onChange={c => setColor(c!)}
key={category.id}
showEyeDropper={false}
/>
)}
/>
</section>
</ModalContent>
<ModalFooter>
<Button type="submit" onClick={onSave} disabled={!name}>{categoryId ? "Save" : "Create"}</Button>
</ModalFooter>
<Modal
{...modalProps}
title={`${categoryId ? "Edit" : "New"} Category`}
actions={[{
text: categoryId ? "Save" : "Create",
variant: "primary",
onClick: onSave,
disabled: !name
}]}
>
<form
className={cl("content")}
onSubmit={e => {
e.preventDefault();
onSave();
}}
>
<section>
<Forms.FormTitle>Name</Forms.FormTitle>
<TextInput
value={name}
onChange={e => setName(e)}
/>
</section>
<section>
<Forms.FormTitle>Color</Forms.FormTitle>
<ColorPickerWithSwatches
className={cl("color-picker")}
key={category.id}
defaultColor={DEFAULT_COLOR}
colors={SWATCHES}
onChange={c => setColor(c!)}
value={color}
renderDefaultButton={() => null}
renderCustomButton={() => (
<ColorPicker
color={color}
onChange={c => setColor(c!)}
key={category.id}
showEyeDropper={false}
/>
)}
/>
</section>
</form>
</ModalRoot>
</Modal>
);
}
@ -122,4 +125,3 @@ export const openCategoryModal = (categoryId: string | null, channelId: string |
await requireSettingsModal();
return modalProps => <NewCategoryModal categoryId={categoryId} modalProps={modalProps} initialChannelId={channelId} />;
});

View file

@ -14,6 +14,8 @@ import { openCategoryModal } from "./CreateCategoryModal";
function createPinMenuItem(channelId: string) {
const pinned = isPinned(channelId);
const showSeparator = pinned || currentUserCategories.length > 0;
return (
<Menu.MenuItem
id="pin-dm"
@ -28,7 +30,7 @@ function createPinMenuItem(channelId: string) {
color="brand"
action={() => openCategoryModal(null, channelId)}
/>
<Menu.MenuSeparator />
{showSeparator && <Menu.MenuSeparator />}
{
currentUserCategories.map(category => (

View file

@ -14,15 +14,17 @@
cursor: pointer;
}
.vc-pindms-modal-content {
display: grid;
justify-content: center;
padding: 1rem;
gap: 1.5rem;
.vc-pindms-modal-color-picker {
margin-inline-end: 0;
[class*="defaultContainer"] {
display: none;
}
}
.vc-pindms-modal-content [class*="defaultContainer"] {
display: none;
.vc-pindms-modal-content {
display: grid;
gap: 1.5em;
}
.vc-pindms-collapse-icon {

View file

@ -6,8 +6,7 @@
import * as DataStore from "@api/DataStore";
import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal";
import { OAuth2AuthorizeModal, showToast, Toasts, UserStore } from "@webpack/common";
import { OAuth2AuthorizeModal, openModal, showToast, Toasts, UserStore } from "@webpack/common";
import { ReviewDBAuth } from "./entities";

View file

@ -9,9 +9,8 @@ import { ReviewDBUser } from "@plugins/reviewDB/entities";
import { fetchBlocks, unblockUser } from "@plugins/reviewDB/reviewDbApi";
import { cl } from "@plugins/reviewDB/utils";
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 { Forms, Modal,openModal, Tooltip, useState } from "@webpack/common";
function UnblockButton(props: { onClick?(): void; }) {
return (
@ -55,7 +54,7 @@ function BlockedUser({ user, isBusy, setIsBusy }: { user: ReviewDBUser; isBusy:
);
}
function Modal() {
function BlockedUsersList() {
const [isBusy, setIsBusy] = useState(false);
const [blocks, error, pending] = useAwaiter(fetchBlocks, {
onError: e => new Logger("ReviewDB").error("Failed to fetch blocks", e),
@ -85,14 +84,13 @@ function Modal() {
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>
<Modal
{...modalProps}
title="Blocked Users"
>
<div className={cl("block-modal")}>
{Auth.token ? <BlockedUsersList /> : <Forms.FormText>You are not logged into ReviewDB!</Forms.FormText>}
</div>
</Modal>
));
}

View file

@ -24,7 +24,7 @@ import { canBlockReviewAuthor, canDeleteReview, canReportReview, cl, showToast }
import { openUserProfile } from "@utils/discord";
import { classes } from "@utils/misc";
import { findCssClassesLazy } from "@webpack";
import { Alerts, IconUtils, Parser, Timestamp, useState } from "@webpack/common";
import { ConfirmModal,IconUtils, openModal as openVencordModal, Parser, Timestamp, useState } from "@webpack/common";
import { openBlockModal } from "./BlockedUserModal";
import { BlockButton, DeleteButton, ReportButton } from "./MessageButton";
@ -46,40 +46,45 @@ export default function ReviewComponent({ review, refetch, profileId }: { review
}
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) {
refetch();
}
});
}
}
});
openVencordModal(props => (
<ConfirmModal
{...props}
title="Are you sure?"
subtitle="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) {
refetch();
}
});
}
}}
/>
));
}
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);
}
}
});
openVencordModal(props => (
<ConfirmModal
{...props}
title="Are you sure?"
subtitle="Do you really want to report this review?"
confirmText="Report"
cancelText="Nevermind"
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;
@ -88,20 +93,22 @@ export default function ReviewComponent({ review, refetch, profileId }: { review
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);
}
}
});
openVencordModal(props => (
<ConfirmModal
{...props}
title="Are you sure?"
subtitle="Do you really 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"
onConfirm={async () => {
if (!(await getToken())) {
return showToast("You must be logged in to block users.");
} else {
blockUser(review.sender.discordID);
}
}}
/>
));
}
return (

View file

@ -16,16 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Auth } from "@plugins/reviewDB/auth";
import { ReviewType } from "@plugins/reviewDB/entities";
import { REVIEWS_PER_PAGE, UserReviewsData } from "@plugins/reviewDB/reviewDbApi";
import { cl } from "@plugins/reviewDB/utils";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModalLazy } from "@utils/modal";
import { useForceUpdater } from "@utils/react";
import * as t from "@vencord/discord-types";
import { DefaultExtractAndLoadChunksRegex, extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
import { Text, useRef, useState } from "@webpack/common";
import { Modal,openModalLazy, Text, useRef, useState } from "@webpack/common";
import { ComponentProps } from "react";
import ReviewComponent from "./ReviewComponent";
@ -35,7 +33,7 @@ const Paginator = findComponentByCodeLazy<ComponentProps<t.Paginator>>('rel:"pre
// This matches a massive module with ~230k chars so we need an anchor before to prevent REDOS
const requirePaginator = extractAndLoadChunksLazy(['name:"SearchResults"'], new RegExp(`name:"StageChannelCall",renderLoader:.+?(?:${DefaultExtractAndLoadChunksRegex.source}).{0,30}?name:"SearchResults"`));
function Modal({ modalProps, modalKey, discordId, name, type }: { modalProps: any; modalKey: string, discordId: string; name: string; type: ReviewType; }) {
function ReviewsModal({ modalProps, modalKey, discordId, name, type }: { modalProps: t.RenderModalProps; modalKey: string, discordId: string; name: string; type: ReviewType; }) {
const [data, setData] = useState<UserReviewsData>();
const [signal, refetch] = useForceUpdater(true);
const [page, setPage] = useState(1);
@ -46,32 +44,17 @@ function Modal({ modalProps, modalKey, discordId, name, type }: { modalProps: an
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
type={type}
/>
</div>
</ModalContent>
<ModalFooter className={cl("modal-footer")}>
<Modal
{...modalProps}
size="lg"
title={
<Text variant="heading-lg/semibold" className={cl("modal-header")}>
{name}'s Reviews
{!!reviewCount && <span> ({reviewCount} Reviews)</span>}
</Text>
}
preview={
<div className={cl("modal-footer")}>
<div className={cl("modal-footer-wrapper")}>
{ownReview && (
<ReviewComponent
@ -98,9 +81,23 @@ function Modal({ modalProps, modalKey, discordId, name, type }: { modalProps: an
/>
)}
</div>
</ModalFooter>
</ModalRoot>
</ErrorBoundary>
</div>
}
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
type={type}
/>
</div>
</Modal>
);
}
@ -110,12 +107,13 @@ export function openReviewsModal(discordId: string, name: string, type: ReviewTy
openModalLazy(async () => {
await requirePaginator();
return props => (
<Modal
<ReviewsModal
modalKey={modalKey}
modalProps={props}
discordId={discordId}
name={name}
type={type} />
type={type}
/>
);
}, { modalKey });
}

View file

@ -29,7 +29,7 @@ import { useAwaiter } from "@utils/react";
import definePlugin from "@utils/types";
import { Guild, User } from "@vencord/discord-types";
import { findCssClassesLazy } from "@webpack";
import { Alerts, Clickable, IconUtils, Menu, Parser } from "@webpack/common";
import { Clickable, ConfirmModal,IconUtils, Menu, openModal, Parser } from "@webpack/common";
import { Auth, initAuth, updateAuth } from "./auth";
import { openReviewsModal } from "./components/ReviewModal";
@ -126,8 +126,9 @@ export default definePlugin({
}
}
if (user.notification) {
const props = user.notification.type === NotificationType.Ban ? {
const { notification } = user;
if (notification) {
const props = notification.type === NotificationType.Ban ? {
cancelText: "Appeal",
confirmText: "Ok",
onCancel: async () =>
@ -140,18 +141,23 @@ export default definePlugin({
)
} : {};
Alerts.show({
title: user.notification.title,
body: (
Parser.parse(
user.notification.content,
openModal(modalProps => (
<ConfirmModal
{...modalProps}
title={notification.title}
confirmText={props.confirmText ?? "OK"}
cancelText={props.cancelText}
variant="primary"
onCancel={props.onCancel}
>
{Parser.parse(
notification.content,
false
)
),
...props
});
)}
</ConfirmModal>
));
readNotification(user.notification.id);
readNotification(notification.id);
}
}, 4000);
},

View file

@ -14,11 +14,6 @@
box-sizing: border-box;
}
.vc-rdb-modal-footer-wrapper {
width: 100%;
margin: 6px 16px;
}
.vc-rdb-placeholder {
margin-bottom: 4px;
font-weight: bold;
@ -41,14 +36,7 @@
.vc-rdb-modal-footer [class*="pageControlContainer"] {
margin-top: 0;
}
.vc-rdb-modal-header {
flex-grow: 1;
}
.vc-rdb-modal-reviews {
margin-top: 16px;
white-space: nowrap;
}
.vc-rdb-review {
@ -78,14 +66,7 @@
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;
}

View file

@ -24,9 +24,9 @@ import { Devs } from "@utils/constants";
import { classNameFactory } from "@utils/css";
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, { IconComponent, OptionType } from "@utils/types";
import { Button, Forms, Parser, Select, useMemo, useState } from "@webpack/common";
import { RenderModalProps } from "@vencord/discord-types";
import { Forms, Modal,openModal, Parser, Select, useMemo, useState } from "@webpack/common";
const settings = definePluginSettings({
replaceMessageContents: {
@ -53,7 +53,7 @@ type Format = typeof Formats[number];
const cl = classNameFactory("vc-st-");
function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): void; }) {
function PickerModal(props: RenderModalProps) {
const [value, setValue] = useState<string>();
const [format, setFormat] = useState<Format>("");
const time = Math.round((new Date(value!).getTime() || Date.now()) / 1000);
@ -66,62 +66,54 @@ function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): voi
}, [time, format]);
return (
<ModalRoot {...rootProps}>
<ModalHeader className={cl("modal-header")}>
<Forms.FormTitle tag="h2" className={cl("modal-title")}>
Timestamp Picker
</Forms.FormTitle>
<Modal
{...props}
title="Timestamp Picker"
actions={[{
text: "Insert",
variant: "primary",
onClick() {
insertTextIntoChatInputBox(formatted + " ");
props.onClose();
}
}]}
>
<input
className={cl("date-picker")}
type="datetime-local"
value={value}
onChange={e => setValue(e.currentTarget.value)}
style={{
colorScheme: getTheme() === Theme.Light ? "light" : "dark",
}}
/>
<ModalCloseButton onClick={close} className={cl("modal-close-button")} />
</ModalHeader>
<ModalContent className={cl("modal-content")}>
<input
className={cl("date-picker")}
type="datetime-local"
value={value}
onChange={e => setValue(e.currentTarget.value)}
style={{
colorScheme: getTheme() === Theme.Light ? "light" : "dark",
}}
<Forms.FormTitle>Timestamp Format</Forms.FormTitle>
<div className={cl("format-select")}>
<Select
options={
Formats.map(m => ({
label: m,
value: m
}))
}
isSelected={v => v === format}
select={v => setFormat(v)}
serialize={v => v}
renderOptionLabel={o => (
<div className={cl("format-label")}>
{Parser.parse(formatTimestamp(time, o.value))}
</div>
)}
renderOptionValue={() => rendered}
/>
</div>
<Forms.FormTitle>Timestamp Format</Forms.FormTitle>
<div className={cl("format-select")}>
<Select
options={
Formats.map(m => ({
label: m,
value: m
}))
}
isSelected={v => v === format}
select={v => setFormat(v)}
serialize={v => v}
renderOptionLabel={o => (
<div className={cl("format-label")}>
{Parser.parse(formatTimestamp(time, o.value))}
</div>
)}
renderOptionValue={() => rendered}
/>
</div>
<Forms.FormTitle className={Margins.bottom8}>Preview</Forms.FormTitle>
<Forms.FormText className={cl("preview-text")}>
{rendered} ({formatted})
</Forms.FormText>
</ModalContent>
<ModalFooter>
<Button
onClick={() => {
insertTextIntoChatInputBox(formatted + " ");
close();
}}
>Insert</Button>
</ModalFooter>
</ModalRoot>
<Forms.FormTitle className={Margins.bottom8}>Preview</Forms.FormTitle>
<Forms.FormText className={cl("preview-text")}>
{rendered} ({formatted})
</Forms.FormText>
</Modal>
);
}
@ -150,14 +142,7 @@ const SendTimestampButton: ChatBarButtonFactory = ({ isAnyChat }) => {
return (
<ChatBarButton
tooltip="Insert Timestamp"
onClick={() => {
const key = openModal(props => (
<PickerModal
rootProps={props}
close={() => closeModal(key)}
/>
));
}}
onClick={() => openModal(props => <PickerModal {...props} />)}
buttonProps={{ "aria-haspopup": "dialog" }}
>
<SendTimestampIcon />

View file

@ -3,7 +3,7 @@
color: var(--text-default);
width: 95%;
padding: 8px 8px 8px 12px;
margin: 1em 0;
margin-bottom: 1em;
outline: none;
border: 1px solid var(--input-background-default);
border-radius: 4px;

View file

@ -9,11 +9,10 @@ import "./styles.css";
import { classNameFactory } from "@utils/css";
import { getGuildAcronym, openImageModal, openUserProfile } from "@utils/discord";
import { classes } from "@utils/misc";
import { ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useAwaiter } from "@utils/react";
import { Guild, User } from "@vencord/discord-types";
import { Guild, RenderModalProps, User } from "@vencord/discord-types";
import { findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, GuildRoleStore, IconUtils, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, GuildRoleStore, IconUtils, Modal,openModal, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
const IconClasses = findCssClassesLazy("icon", "acronym", "childWrapper");
const FriendRow = findComponentByCodeLazy("discriminatorClass:", ".isMobileOnline", "avatarSrc:");
@ -21,11 +20,7 @@ const FriendRow = findComponentByCodeLazy("discriminatorClass:", ".isMobileOnlin
const cl = classNameFactory("vc-gp-");
export function openGuildInfoModal(guild: Guild) {
openModal(props =>
<ModalRoot {...props} size={ModalSize.MEDIUM}>
<GuildInfoModal guild={guild} />
</ModalRoot>
);
openModal(props => <GuildInfoModal guild={guild} modalProps={props} />);
}
const enum Tabs {
@ -55,7 +50,7 @@ function renderTimestamp(timestamp: number) {
);
}
function GuildInfoModal({ guild }: GuildProps) {
function GuildInfoModal({ guild, modalProps }: GuildProps & { modalProps: RenderModalProps; }) {
const [friendCount, setFriendCount] = useState<number>();
const [blockedCount, setBlockedCount] = useState<number>();
const [ignoredCount, setIgnoredCount] = useState<number>();
@ -78,7 +73,32 @@ function GuildInfoModal({ guild }: GuildProps) {
});
return (
<div className={cl("root")}>
<Modal
{...modalProps}
size="lg"
title={
<div className={cl("header")}>
{iconUrl
? <img
className={cl("icon")}
src={iconUrl}
alt=""
onClick={() => openImageModal({
url: iconUrl,
height: 512,
width: 512,
})}
/>
: <div aria-hidden className={classes(IconClasses.childWrapper, IconClasses.acronym)}>{getGuildAcronym(guild)}</div>
}
<div className={cl("name-and-description")}>
<Forms.FormTitle tag="h5" className={cl("name")}>{guild.name}</Forms.FormTitle>
{guild.description && <Forms.FormText>{guild.description}</Forms.FormText>}
</div>
</div>
}
>
{bannerUrl && currentTab === Tabs.ServerInfo && (
<img
className={cl("banner")}
@ -91,27 +111,6 @@ function GuildInfoModal({ guild }: GuildProps) {
/>
)}
<div className={cl("header")}>
{iconUrl
? <img
className={cl("icon")}
src={iconUrl}
alt=""
onClick={() => openImageModal({
url: iconUrl,
height: 512,
width: 512,
})}
/>
: <div aria-hidden className={classes(IconClasses.childWrapper, IconClasses.acronym)}>{getGuildAcronym(guild)}</div>
}
<div className={cl("name-and-description")}>
<Forms.FormTitle tag="h5" className={cl("name")}>{guild.name}</Forms.FormTitle>
{guild.description && <Forms.FormText>{guild.description}</Forms.FormText>}
</div>
</div>
<TabBar
type="top"
look="brand"
@ -151,7 +150,7 @@ function GuildInfoModal({ guild }: GuildProps) {
{currentTab === Tabs.BlockedUsers && <BlockedUsersTab guild={guild} setCount={setBlockedCount} />}
{currentTab === Tabs.IgnoredUsers && <IgnoredUserTab guild={guild} setCount={setIgnoredCount} />}
</div>
</div>
</Modal>
);
}

View file

@ -1,13 +1,10 @@
.vc-gp-root {
height: 100%;
user-select: text;
}
.vc-gp-banner {
cursor: pointer;
aspect-ratio: auto 240 / 135;
height: 334px;
width: 100%;
padding-inline: 0.5em;
box-sizing: border-box;
object-fit: cover;
overflow: clip;
overflow-clip-margin: content-box;
@ -88,14 +85,9 @@
}
.vc-gp-scroller {
width: 100%;
max-height: 500px;
}
.vc-gp-scroller [class*="listRow"] {
.vc-gp-scroller [class*="row"] {
margin: 1px 0;
}
.vc-gp-scroller [class*="listRow"]:hover {
background-color: var(--background-mod-subtle);
}

View file

@ -20,7 +20,8 @@ import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { TooltipContainer } from "@components/TooltipContainer";
import { classes } from "@utils/misc";
import { IconComponent } from "@utils/types";
import { Alerts, Forms, useEffect, useState } from "@webpack/common";
import { RenderModalProps } from "@vencord/discord-types";
import { ConfirmModal,openModal, useEffect, useState } from "@webpack/common";
import { settings } from "./settings";
import { openTranslateModal } from "./TranslateModal";
@ -41,6 +42,26 @@ export const TranslateIcon: IconComponent = ({ height = 20, width = 20, classNam
export let setShouldShowTranslateEnabledTooltip: undefined | ((show: boolean) => void);
function AutoTranslateConfirmModal(props: RenderModalProps) {
const s = settings.use(["dismissedAutoTranslateAlert"]);
return (
<ConfirmModal
{...props}
title="Vencord Auto-Translate Enabled"
subtitle="You just enabled Auto Translate! Any message will automatically be translated before being sent."
confirmText="Disable Auto-Translate"
onConfirm={() => settings.store.autoTranslate = false}
cancelText="Got it"
variant="primary"
checkboxProps={{
checked: s.dismissedAutoTranslateAlert === true,
onChange: checked => s.dismissedAutoTranslateAlert = checked,
}}
/>
);
}
export const TranslateChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
const { autoTranslate } = settings.use(["autoTranslate"]);
@ -55,22 +76,8 @@ export const TranslateChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
const toggle = () => {
const newState = !autoTranslate;
settings.store.autoTranslate = newState;
if (newState && settings.store.showAutoTranslateAlert !== false)
Alerts.show({
title: "Vencord Auto-Translate Enabled",
body: <>
<Forms.FormText>
You just enabled Auto Translate! Any message <b>will automatically be translated</b> before being sent.
</Forms.FormText>
</>,
confirmText: "Disable Auto-Translate",
cancelText: "Got it",
secondaryConfirmText: "Don't show again",
onConfirmSecondary: () => settings.store.showAutoTranslateAlert = false,
onConfirm: () => settings.store.autoTranslate = false,
// troll
confirmColor: "vc-notification-log-danger-btn",
});
if (newState && !settings.store.dismissedAutoTranslateAlert)
openModal(props => <AutoTranslateConfirmModal {...props} />);
};
const button = (

View file

@ -16,15 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { BaseText } from "@components/BaseText";
import { Divider } from "@components/Divider";
import { FormSwitch } from "@components/FormSwitch";
import { Margins } from "@utils/margins";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
import { Forms, SearchableSelect, useMemo } from "@webpack/common";
import { RenderModalProps } from "@vencord/discord-types";
import { Forms, Modal,openModal, SearchableSelect, useMemo } from "@webpack/common";
import { settings } from "./settings";
import { cl, getLanguages } from "./utils";
import { getLanguages } from "./utils";
const LanguageSettingKeys = ["receivedInput", "receivedOutput", "sentInput", "sentOutput"] as const;
@ -74,30 +73,24 @@ function AutoTranslateToggle() {
}
function TranslateModal({ rootProps }: { rootProps: ModalProps; }) {
function TranslateModal({ rootProps }: { rootProps: RenderModalProps; }) {
return (
<ModalRoot {...rootProps}>
<ModalHeader className={cl("modal-header")}>
<BaseText tag="h2" size="lg" weight="semibold" className={cl("modal-title")}>
Translate
</BaseText>
<ModalCloseButton onClick={rootProps.onClose} />
</ModalHeader>
<Modal
{...rootProps}
title="Translate"
>
{LanguageSettingKeys.map(s => (
<LanguageSelect
key={s}
settingsKey={s}
includeAuto={s.endsWith("Input")}
/>
))}
<ModalContent className={cl("modal-content")}>
{LanguageSettingKeys.map(s => (
<LanguageSelect
key={s}
settingsKey={s}
includeAuto={s.endsWith("Input")}
/>
))}
<Divider className={Margins.bottom16} />
<Divider className={Margins.bottom16} />
<AutoTranslateToggle />
</ModalContent>
</ModalRoot>
<AutoTranslateToggle />
</Modal>
);
}

View file

@ -95,7 +95,7 @@ export const settings = definePluginSettings({
hidden() { return this.store.service !== "kagi"; }
}
}).withPrivateSettings<{
showAutoTranslateAlert: boolean;
dismissedAutoTranslateAlert?: boolean;
}>();
export function resetLanguageDefaults() {

View file

@ -1,15 +1,3 @@
.vc-trans-modal-content {
padding: 1em;
}
.vc-trans-modal-header {
place-content: center space-between;
}
.vc-trans-modal-title {
margin: 0;
}
.vc-trans-accessory {
color: var(--text-muted);
margin-top: 0.5em;
@ -39,4 +27,4 @@
.vc-trans-chat-button {
scale: 1.085;
}
}

View file

@ -21,14 +21,13 @@ import { definePluginSettings } from "@api/Settings";
import { CodeBlock } from "@components/CodeBlock";
import { Divider } from "@components/Divider";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import { copyWithToast, getCurrentGuild, getIntlMessage } from "@utils/discord";
import { isTruthy } from "@utils/guards";
import { Margins } from "@utils/margins";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { IconComponent, OptionType } from "@utils/types";
import { Message } from "@vencord/discord-types";
import { Button, ChannelStore, Forms, GuildRoleStore, Menu, Text } from "@webpack/common";
import { ChannelStore, Forms, GuildRoleStore, Menu, Modal,openModal } from "@webpack/common";
const CopyIcon: IconComponent = ({ height = 20, width = 20, className }) => {
@ -71,40 +70,36 @@ function cleanMessage(msg: Message) {
}
function openViewRawModal(json: string, type: string, msgContent?: string) {
const key = openModal(props => (
openModal(props => (
<ErrorBoundary>
<ModalRoot {...props} size={ModalSize.LARGE}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>View Raw</Text>
<ModalCloseButton onClick={() => closeModal(key)} />
</ModalHeader>
<ModalContent>
<div style={{ padding: "16px 0" }}>
{!!msgContent && (
<>
<Forms.FormTitle tag="h5">Content</Forms.FormTitle>
<CodeBlock content={msgContent} lang="" />
<Divider className={Margins.bottom20} />
</>
)}
<Modal
{...props}
title="View Raw"
size="xl"
actions={[
{
text: `Copy ${type} JSON`,
variant: "primary",
onClick: () => copyWithToast(json, `${type} data copied to clipboard!`)
},
msgContent && {
text: "Copy Raw Content",
variant: "secondary",
onClick: () => copyWithToast(msgContent, "Content copied to clipboard!")
}
].filter(isTruthy)}
>
{!!msgContent && (
<>
<Forms.FormTitle tag="h5">Content</Forms.FormTitle>
<CodeBlock content={msgContent} lang="" />
<Divider className={Margins.bottom20} />
</>
)}
<Forms.FormTitle tag="h5">{type} Data</Forms.FormTitle>
<CodeBlock content={json} lang="json" />
</div>
</ModalContent >
<ModalFooter>
<Flex>
<Button onClick={() => copyWithToast(json, `${type} data copied to clipboard!`)}>
Copy {type} JSON
</Button>
{!!msgContent && (
<Button onClick={() => copyWithToast(msgContent, "Content copied to clipboard!")}>
Copy Raw Content
</Button>
)}
</Flex>
</ModalFooter>
</ModalRoot >
<Forms.FormTitle tag="h5">{type} Data</Forms.FormTitle>
<CodeBlock content={json} lang="json" />
</Modal>
</ErrorBoundary >
));
}

View file

@ -26,14 +26,13 @@ import { Paragraph } from "@components/Paragraph";
import { Devs } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import { Margins } from "@utils/margins";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
import { useAwaiter } from "@utils/react";
import definePlugin from "@utils/types";
import { chooseFile } from "@utils/web";
import { CloudUpload as TCloudUpload } from "@vencord/discord-types";
import { CloudUpload as TCloudUpload, RenderModalProps } from "@vencord/discord-types";
import { CloudUploadPlatform } from "@vencord/discord-types/enums";
import { findLazy } from "@webpack";
import { Button, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PendingReplyStore, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
import { Button, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, Modal,openModal, PendingReplyStore, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
import { ComponentType } from "react";
import { VoiceRecorderDesktop } from "./DesktopRecorder";
@ -69,7 +68,7 @@ const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => {
icon: Microphone
}}
label="Send Voice Message"
action={() => openModal(modalProps => <Modal modalProps={modalProps} />)}
action={() => openModal(modalProps => <VoiceMessageModal modalProps={modalProps} />)}
/>
);
};
@ -157,7 +156,7 @@ function useObjectUrl() {
return [url, setWithFree] as const;
}
function Modal({ modalProps }: { modalProps: ModalProps; }) {
function VoiceMessageModal({ modalProps }: { modalProps: RenderModalProps; }) {
const [isRecording, setRecording] = useState(false);
const [blob, setBlob] = useState<Blob>();
const [blobUrl, setBlobUrl] = useObjectUrl();
@ -208,69 +207,62 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) {
);
return (
<ModalRoot {...modalProps}>
<ModalHeader>
<Forms.FormTitle>Record Voice Message</Forms.FormTitle>
</ModalHeader>
<Modal
{...modalProps}
title="Record Voice Message"
actions={[{
text: "Send",
variant: "primary",
onClick: () => {
sendAudio(blob!, meta ?? EMPTY_META);
modalProps.onClose();
showToast("Now sending voice message... Please be patient", Toasts.Type.MESSAGE);
},
disabled: !blob
}]}
>
<div className={cl("buttons")}>
<VoiceRecorder
setAudioBlob={blob => {
setBlob(blob);
setBlobUrl(blob);
}}
onRecordingChange={setRecording}
/>
<ModalContent className={cl("modal")}>
<div className={cl("buttons")}>
<VoiceRecorder
setAudioBlob={blob => {
setBlob(blob);
setBlobUrl(blob);
}}
onRecordingChange={setRecording}
/>
<Button
onClick={async () => {
const file = await chooseFile("audio/*");
if (file) {
setBlob(file);
setBlobUrl(file);
}
}}
>
Upload File
</Button>
</div>
<Forms.FormTitle>Preview</Forms.FormTitle>
{metaError
? <Paragraph className={cl("error")}>Failed to parse selected audio file: {metaError.message}</Paragraph>
: (
<VoicePreview
src={blobUrl}
waveform={meta.waveform}
recording={isRecording}
/>
)}
{isUnsupportedFormat && (
<Card variant="warning" className={Margins.top16} defaultPadding>
<Forms.FormText>Voice Messages have to be OggOpus to be playable on iOS. This file is <code>{blob.type}</code> so it will not be playable on iOS.</Forms.FormText>
<Forms.FormText className={Margins.top8}>
To fix it, first convert it to OggOpus, for example using the <Link href="https://convertio.co/mp3-opus/">convertio web converter</Link>
</Forms.FormText>
</Card>
)}
</ModalContent>
<ModalFooter>
<Button
disabled={!blob}
onClick={() => {
sendAudio(blob!, meta ?? EMPTY_META);
modalProps.onClose();
showToast("Now sending voice message... Please be patient", Toasts.Type.MESSAGE);
onClick={async () => {
const file = await chooseFile("audio/*");
if (file) {
setBlob(file);
setBlobUrl(file);
}
}}
>
Send
Upload File
</Button>
</ModalFooter>
</ModalRoot>
</div>
<Forms.FormTitle>Preview</Forms.FormTitle>
{metaError
? <Paragraph className={cl("error")}>Failed to parse selected audio file: {metaError.message}</Paragraph>
: (
<VoicePreview
src={blobUrl}
waveform={meta.waveform}
recording={isRecording}
/>
)}
{isUnsupportedFormat && (
<Card variant="warning" className={Margins.top16} defaultPadding>
<Forms.FormText>Voice Messages have to be OggOpus to be playable on iOS. This file is <code>{blob.type}</code> so it will not be playable on iOS.</Forms.FormText>
<Forms.FormText className={Margins.top8}>
To fix it, first convert it to OggOpus, for example using the <Link href="https://convertio.co/mp3-opus/">convertio web converter</Link>
</Forms.FormText>
</Card>
)}
</Modal>
);
}

View file

@ -1,7 +1,3 @@
.vc-vmsg-modal {
padding: 1em;
}
.vc-vmsg-buttons {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));

View file

@ -17,14 +17,13 @@
*/
import type { MessageObject } from "@api/MessageEvents";
import type { Channel, CloudUpload, Guild, GuildFeatures, Message, User } from "@vencord/discord-types";
import { ChannelActionCreators, ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, i18n, InviteActions, MessageActions, RestAPI, SelectedChannelStore, SelectedGuildStore, Toasts, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
import type { Channel, CloudUpload, Guild, GuildFeatures, MediaModalItem, MediaModalProps, Message, User } from "@vencord/discord-types";
import { ChannelActionCreators, ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, i18n, InviteActions, MessageActions, openMediaModal, RestAPI, SelectedChannelStore, SelectedGuildStore, Toasts, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
import { Except } from "type-fest";
import { copyToClipboard } from "./clipboard";
import { runtimeHashMessageKey } from "./intlHash";
import { Logger } from "./Logger";
import { MediaModalItem, MediaModalProps, openMediaModal } from "./modal";
const IntlManagerLogger = new Logger("IntlManager");

View file

@ -16,11 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { filters, findByCodeLazy, mapMangledModuleLazy } from "@webpack";
import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react";
import { filters, mapMangledModuleLazy } from "@webpack";
import { closeAllModals, closeModal, openMediaModal, openModal, openModalLazy } from "@webpack/common";
import { LazyComponent } from "./react";
/** @deprecated Migrate to new Modals */
export const enum ModalSize {
SMALL = "small",
MEDIUM = "medium",
@ -28,149 +29,43 @@ export const enum ModalSize {
DYNAMIC = "dynamic",
}
const enum ModalTransitionState {
ENTERING,
ENTERED,
EXITING,
EXITED,
HIDDEN,
}
export interface ModalProps {
transitionState: ModalTransitionState;
onClose(): void;
}
export interface ModalOptions {
modalKey?: string;
onCloseRequest?: (() => void);
onCloseCallback?: (() => void);
}
type RenderFunction = (props: ModalProps) => ReactNode | Promise<ReactNode>;
interface Modals {
ModalRoot: ComponentType<PropsWithChildren<{
transitionState: ModalTransitionState;
size?: ModalSize;
role?: "alertdialog" | "dialog";
className?: string;
fullscreenOnMobile?: boolean;
"aria-label"?: string;
"aria-labelledby"?: string;
onAnimationEnd?(): string;
}>>;
ModalHeader: ComponentType<PropsWithChildren<{
/** Flex.Justify.START */
justify?: string;
/** Flex.Direction.HORIZONTAL */
direction?: string;
/** Flex.Align.CENTER */
align?: string;
/** Flex.Wrap.NO_WRAP */
wrap?: string;
separator?: boolean;
className?: string;
}>>;
/** This also accepts Scroller props but good luck with that */
ModalContent: ComponentType<PropsWithChildren<{
className?: string;
scrollerRef?: Ref<HTMLElement>;
[prop: string]: any;
}>>;
ModalFooter: ComponentType<PropsWithChildren<{
/** Flex.Justify.START */
justify?: string;
/** Flex.Direction.HORIZONTAL_REVERSE */
direction?: string;
/** Flex.Align.STRETCH */
align?: string;
/** Flex.Wrap.NO_WRAP */
wrap?: string;
separator?: boolean;
className?: string;
}>>;
ModalCloseButton: ComponentType<{
focusProps?: any;
onClick(): void;
withCircleBackground?: boolean;
hideOnFullscreen?: boolean;
className?: string;
}>;
}
// TODO: move to new modal api
export const Modals: Modals = mapMangledModuleLazy(".MODAL_ROOT_LEGACY,", {
/** @deprecated Migrate to new Modals */
export const Modals = mapMangledModuleLazy(".MODAL_ROOT_LEGACY,", {
ModalRoot: filters.componentByCode('.MODAL,"aria-labelledby":'),
ModalHeader: filters.componentByCode(",id:"),
ModalContent: filters.componentByCode("scrollbarType:"),
ModalFooter: filters.componentByCode(".HORIZONTAL_REVERSE,"),
ModalCloseButton: filters.componentByCode(".withCircleBackground")
});
}) as never;
export const ModalRoot = LazyComponent(() => Modals.ModalRoot);
export const ModalHeader = LazyComponent(() => Modals.ModalHeader);
export const ModalContent = LazyComponent(() => Modals.ModalContent);
export const ModalFooter = LazyComponent(() => Modals.ModalFooter);
export const ModalCloseButton = LazyComponent(() => Modals.ModalCloseButton);
/** @deprecated Migrate to new Modals */
export const ModalRoot = LazyComponent(() => (Modals as any).ModalRoot) as never;
/** @deprecated Migrate to new Modals */
export const ModalHeader = LazyComponent(() => (Modals as any).ModalHeader) as never;
/** @deprecated Migrate to new Modals */
export const ModalContent = LazyComponent(() => (Modals as any).ModalContent) as never;
/** @deprecated Migrate to new Modals */
export const ModalFooter = LazyComponent(() => (Modals as any).ModalFooter) as never;
/** @deprecated Migrate to new Modals */
export const ModalCloseButton = LazyComponent(() => (Modals as any).ModalCloseButton) as never;
export type MediaModalItem = {
url: string;
type: "IMAGE" | "VIDEO";
original?: string;
alt?: string;
width?: number;
height?: number;
animated?: boolean;
maxWidth?: number;
maxHeight?: number;
} & Record<PropertyKey, any>;
/** @deprecated Migrate to new Modals */
export const ModalAPI = {
openModal,
openModalLazy,
closeModal,
closeAllModals
} as never;
export type MediaModalProps = {
location?: string;
contextKey?: string;
onCloseCallback?: () => void;
className?: string;
items: MediaModalItem[];
startingIndex?: number;
onIndexChange?: (...args: any[]) => void;
fit?: string;
shouldRedactExplicitContent?: boolean;
shouldHideMediaOptions?: boolean;
export {
/** @deprecated Migrate to new Modals */
closeAllModals,
/** @deprecated Migrate to new Modals */
closeModal,
/** @deprecated Migrate to new Modals */
openMediaModal,
/** @deprecated Migrate to new Modals */
openModal,
/** @deprecated Migrate to new Modals */
openModalLazy
};
// Modal key: "Media Viewer Modal"
export const openMediaModal: (props: MediaModalProps) => void = findByCodeLazy("hasMediaOptions", "shouldHideMediaOptions");
interface ModalAPI {
/**
* Wait for the render promise to resolve, then open a modal with it.
* This is equivalent to render().then(openModal)
* You should use the Modal components exported by this file
*/
openModalLazy: (render: () => Promise<RenderFunction>, options?: ModalOptions & { contextKey?: string; }) => Promise<string>;
/**
* Open a Modal with the given render function.
* You should use the Modal components exported by this file
*/
openModal: (render: RenderFunction, options?: ModalOptions, contextKey?: string) => string;
/**
* Close a modal by its key
*/
closeModal: (modalKey: string, contextKey?: string) => void;
/**
* Close all open modals
*/
closeAllModals: () => void;
}
export const ModalAPI: ModalAPI = mapMangledModuleLazy(".modalKey?", {
openModalLazy: filters.byCode(".modalKey?"),
openModal: filters.byCode(",instant:"),
closeModal: filters.byCode(".onCloseCallback()"),
closeAllModals: filters.byCode(".getState();for")
});
export const { openModalLazy, openModal, closeModal, closeAllModals } = ModalAPI;

View file

@ -366,6 +366,8 @@ export interface PluginSettingComponentProps {
* The options object
*/
option: PluginSettingComponentDef;
/** Close the plugin settings modal */
closePluginSettings(): void;
}
/** Maps a `PluginSettingDef` to its value type */

View file

@ -18,6 +18,7 @@
export * from "./components";
export * from "./menu";
export * from "./modals";
export * from "./react";
export * from "./stores";
export * from "./userSettings";

View file

@ -0,0 +1,23 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import * as t from "@vencord/discord-types";
import { filters, findByCodeLazy, findExportedComponentLazy, mapMangledModuleLazy } from "@webpack";
export const Modal: t.Modal = findExportedComponentLazy("Modal");
export const ConfirmModal: t.ConfirmModal = findExportedComponentLazy("ConfirmModal");
// Modal key: "Media Viewer Modal"
export const openMediaModal: (props: t.MediaModalProps) => void = findByCodeLazy("hasMediaOptions", "shouldHideMediaOptions");
const ModalAPI: t.ModalAPI = mapMangledModuleLazy(".modalKey?", {
openModalLazy: filters.byCode(".modalKey?"),
openModal: filters.byCode(",instant:"),
closeModal: filters.byCode(".onCloseCallback()"),
closeAllModals: filters.byCode(".getState();for")
});
export const { openModalLazy, openModal, closeModal, closeAllModals } = ModalAPI;