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:
parent
909df8b8c2
commit
3b9292d3bc
70 changed files with 1453 additions and 1521 deletions
1
packages/discord-types/src/components.d.ts
vendored
1
packages/discord-types/src/components.d.ts
vendored
|
|
@ -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 {
|
||||
|
|
|
|||
136
packages/discord-types/src/components/Modal.d.ts
vendored
Normal file
136
packages/discord-types/src/components/Modal.d.ts
vendored
Normal 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;
|
||||
}
|
||||
2
packages/discord-types/src/index.d.ts
vendored
2
packages/discord-types/src/index.d.ts
vendored
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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} />);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
}
|
||||
|
||||
.vc-card-normal {
|
||||
border: 1px var(--border-subtle);
|
||||
border: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.vc-card-warning {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@
|
|||
.vc-settings-modal-links {
|
||||
display: flex;
|
||||
gap: 0.2em;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ interface SettingBaseProps<T> {
|
|||
};
|
||||
id: string;
|
||||
definedSettings: DefinedSettings;
|
||||
closePluginSettings(): void;
|
||||
}
|
||||
|
||||
export type SettingProps<T extends PluginSettingDefCommon> = SettingBaseProps<T>;
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
));
|
||||
});
|
||||
}
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
));
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
— <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}>
|
||||
— <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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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 = () =>
|
||||
|
|
|
|||
|
|
@ -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 = () =>
|
||||
|
|
|
|||
|
|
@ -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 = () =>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 => (
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export const settings = definePluginSettings({
|
|||
hidden() { return this.store.service !== "kagi"; }
|
||||
}
|
||||
}).withPrivateSettings<{
|
||||
showAutoTranslateAlert: boolean;
|
||||
dismissedAutoTranslateAlert?: boolean;
|
||||
}>();
|
||||
|
||||
export function resetLanguageDefaults() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 >
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
.vc-vmsg-modal {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.vc-vmsg-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
export * from "./components";
|
||||
export * from "./menu";
|
||||
export * from "./modals";
|
||||
export * from "./react";
|
||||
export * from "./stores";
|
||||
export * from "./userSettings";
|
||||
|
|
|
|||
23
src/webpack/common/modals.ts
Normal file
23
src/webpack/common/modals.ts
Normal 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;
|
||||
Loading…
Add table
Reference in a new issue