for-legacy-web/src/controllers/modals/ModalController.tsx

280 lines
7.9 KiB
TypeScript

import {
action,
computed,
makeObservable,
observable,
runInAction,
} from "mobx";
import type { Client, API } from "revolt.js";
import { ulid } from "ulid";
import { determineLink } from "../../lib/links";
import { injectController } from "../../lib/window";
import { getApplicationState } from "../../mobx/State";
import { history } from "../../context/history";
import AddFriend from "./components/AddFriend";
import BanMember from "./components/BanMember";
import Changelog from "./components/Changelog";
import ChannelInfo from "./components/ChannelInfo";
import Clipboard from "./components/Clipboard";
import ConfirmLeave from "./components/ConfirmLeave";
import Confirmation from "./components/Confirmation";
import CreateBot from "./components/CreateBot";
import CreateCategory from "./components/CreateCategory";
import CreateChannel from "./components/CreateChannel";
import CreateGroup from "./components/CreateGroup";
import CreateInvite from "./components/CreateInvite";
import CreateRole from "./components/CreateRole";
import CreateServer from "./components/CreateServer";
import CustomStatus from "./components/CustomStatus";
import DeleteMessage from "./components/DeleteMessage";
import Error from "./components/Error";
import ImageViewer from "./components/ImageViewer";
import KickMember from "./components/KickMember";
import LinkWarning from "./components/LinkWarning";
import MFAEnableTOTP from "./components/MFAEnableTOTP";
import MFAFlow from "./components/MFAFlow";
import MFARecovery from "./components/MFARecovery";
import ModifyAccount from "./components/ModifyAccount";
import OutOfDate from "./components/OutOfDate";
import PendingFriendRequests from "./components/PendingFriendRequests";
import ServerIdentity from "./components/ServerIdentity";
import ServerInfo from "./components/ServerInfo";
import ShowToken from "./components/ShowToken";
import SignOutSessions from "./components/SignOutSessions";
import SignedOut from "./components/SignedOut";
import UserPicker from "./components/UserPicker";
import { OnboardingModal } from "./components/legacy/Onboarding";
import { UserProfile } from "./components/legacy/UserProfile";
import { Modal } from "./types";
type Components = Record<string, React.FC<any>>;
/**
* Handles layering and displaying modals to the user.
*/
class ModalController<T extends Modal> {
stack: T[] = [];
components: Components;
constructor(components: Components) {
this.components = components;
makeObservable(this, {
stack: observable,
push: action,
pop: action,
remove: action,
rendered: computed,
isVisible: computed,
});
this.close = this.close.bind(this);
// Inject globally
injectController("modal", this);
}
/**
* Display a new modal on the stack
* @param modal Modal data
*/
push(modal: T) {
this.stack = [
...this.stack,
{
...modal,
key: ulid(),
},
];
}
/**
* Remove the top modal from the screen
* @param signal What action to trigger
*/
pop(signal: "close" | "confirm" | "force") {
this.stack = this.stack.map((entry, index) =>
index === this.stack.length - 1 ? { ...entry, signal } : entry,
);
}
/**
* Close the top modal
*/
close() {
this.pop("close");
}
/**
* Remove the keyed modal from the stack
*/
remove(key: string) {
this.stack = this.stack.filter((x) => x.key !== key);
}
/**
* Render modals
*/
get rendered() {
return (
<>
{this.stack.map((modal) => {
const Component = this.components[modal.type];
return (
// ESLint does not understand spread operator
// eslint-disable-next-line
<Component
{...modal}
onClose={() => this.remove(modal.key!)}
/>
);
})}
</>
);
}
/**
* Whether a modal is currently visible
*/
get isVisible() {
return this.stack.length > 0;
}
}
/**
* Modal controller with additional helpers.
*/
class ModalControllerExtended extends ModalController<Modal> {
/**
* Perform MFA flow
* @param client Client
*/
mfaFlow(client: Client) {
return runInAction(
() =>
new Promise((callback: (ticket?: API.MFATicket) => void) =>
this.push({
type: "mfa_flow",
state: "known",
client,
callback,
}),
),
);
}
/**
* Open TOTP secret modal
* @param client Client
*/
mfaEnableTOTP(secret: string, identifier: string) {
return runInAction(
() =>
new Promise((callback: (value?: string) => void) =>
this.push({
type: "mfa_enable_totp",
identifier,
secret,
callback,
}),
),
);
}
/**
* Write text to the clipboard
* @param text Text to write
*/
writeText(text: string) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
} else {
this.push({
type: "clipboard",
text,
});
}
}
/**
* Safely open external or internal link
* @param href Raw URL
* @param trusted Whether we trust this link
* @returns Whether to cancel default event
*/
openLink(href?: string, trusted?: boolean) {
const link = determineLink(href);
const settings = getApplicationState().settings;
switch (link.type) {
case "navigate": {
history.push(link.path);
break;
}
case "external": {
if (
!trusted &&
!settings.security.isTrustedOrigin(link.url.hostname)
) {
modalController.push({
type: "link_warning",
link: link.href,
callback: () => this.openLink(href, true) as true,
});
} else {
window.open(link.href, "_blank", "noreferrer");
}
}
}
return true;
}
}
export const modalController = new ModalControllerExtended({
add_friend: AddFriend,
ban_member: BanMember,
changelog: Changelog,
channel_info: ChannelInfo,
clipboard: Clipboard,
leave_group: ConfirmLeave,
close_dm: Confirmation,
leave_server: ConfirmLeave,
delete_server: Confirmation,
delete_channel: Confirmation,
delete_bot: Confirmation,
block_user: Confirmation,
unfriend_user: Confirmation,
create_category: CreateCategory,
create_channel: CreateChannel,
create_group: CreateGroup,
create_invite: CreateInvite,
create_role: CreateRole,
create_server: CreateServer,
create_bot: CreateBot,
custom_status: CustomStatus,
delete_message: DeleteMessage,
error: Error,
image_viewer: ImageViewer,
kick_member: KickMember,
link_warning: LinkWarning,
mfa_flow: MFAFlow,
mfa_recovery: MFARecovery,
mfa_enable_totp: MFAEnableTOTP,
modify_account: ModifyAccount,
onboarding: OnboardingModal,
out_of_date: OutOfDate,
pending_friend_requests: PendingFriendRequests,
server_identity: ServerIdentity,
server_info: ServerInfo,
show_token: ShowToken,
signed_out: SignedOut,
sign_out_sessions: SignOutSessions,
user_picker: UserPicker,
user_profile: UserProfile,
});