1
0
mirror of https://github.com/spacebarchat/client.git synced 2024-11-25 03:32:54 +01:00
This commit is contained in:
Puyodead1 2023-12-10 22:20:09 -05:00
parent cdea3bea19
commit 457ed452fb
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
29 changed files with 841 additions and 1344 deletions

View File

@ -1,94 +1,66 @@
import styled from "styled-components";
// Adapted from https://github.com/revoltchat/components/blob/master/src/components/design/atoms/inputs/Button.tsx
interface Props {
variant?: "primary" | "secondary" | "danger" | "success" | "warning";
outlined?: boolean;
import styled, { css } from "styled-components";
export interface Props {
readonly compact?: boolean | "icon";
palette?: "primary" | "secondary" | "success" | "warning" | "danger" | "accent" | "link";
readonly disabled?: boolean;
}
export default styled.button<Props>`
background: ${(props) => {
if (props.outlined) return "transparent";
switch (props.variant) {
case "primary":
return "var(--primary)";
case "secondary":
return "var(--secondary)";
case "danger":
return "var(--danger)";
case "success":
return "var(--success)";
case "warning":
return "var(--warning)";
default:
return "var(--primary)";
}
}};
border: ${(props) => {
if (!props.outlined) return "none";
switch (props.variant) {
case "primary":
return "1px solid var(--primary)";
case "secondary":
return "1px solid var(--secondary)";
case "danger":
return "1px solid var(--danger)";
case "success":
return "1px solid var(--success)";
case "warning":
return "1px solid var(--warning)";
default:
return "1px solid var(--primary)";
}
}};
color: var(--text);
padding: 8px 16px;
border-radius: 8px;
font-size: 13px;
font-size: 12px;
font-weight: var(--font-weight-medium);
cursor: pointer;
outline: none;
border: none;
transition: background 0.2s ease-in-out;
pointer-events: ${(props) => (props.disabled ? "none" : null)};
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
opacity: ${(props) => (props.disabled ? 0.5 : 1)};
font-weight: var(--font-weight-bold);
&:hover {
background: ${(props) => {
switch (props.variant) {
case "primary":
return "var(--primary-light)";
case "secondary":
return "var(--secondary-light)";
case "danger":
return "var(--danger-light)";
case "success":
return "var(--success-light)";
case "warning":
return "var(--warning-light)";
default:
return "var(--primary-light)";
}
}};
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
}
${(props) => {
if (!props.palette) props.palette = "primary";
switch (props.palette) {
case "primary":
case "secondary":
case "success":
case "warning":
case "danger":
case "accent":
return css`
background: var(--${props.palette});
&:active {
background: ${(props) => {
switch (props.variant) {
case "primary":
return "var(--primary-dark)";
case "secondary":
return "var(--secondary-dark)";
case "danger":
return "var(--danger-dark)";
case "success":
return "var(--success-dark)";
case "warning":
return "var(--warning-dark)";
default:
return "var(--primary-dark)";
}
}};
}
&:hover {
filter: brightness(1.2);
}
&:active {
filter: brightness(0.8);
}
&:disabled {
filter: brightness(0.7);
}
`;
case "link":
return css`
background: transparent;
&:hover {
text-decoration: underline;
}
&:active {
filter: brightness(0.8);
}
&:disabled {
filter: brightness(0.7);
}
`;
}
}}
`;

View File

@ -3,11 +3,11 @@ import React from "react";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
import { modalController } from "../../controllers/modals/ModalController";
import Channel from "../../stores/objects/Channel";
import { IContextMenuItem } from "../ContextMenuItem";
import Icon from "../Icon";
import Tooltip from "../Tooltip";
import CreateInviteModal from "../modals/CreateInviteModal";
const ListItem = styled.div<{ isCategory?: boolean }>`
padding: ${(props) => (props.isCategory ? "16px 8px 0 0" : "1px 8px 0 0")};
@ -64,7 +64,10 @@ function ChannelListItem({ channel, isCategory, active }: Props) {
index: 0,
label: "Create Channel Invite",
onClick: () => {
openModal(CreateInviteModal, { guild_id: channel.guildId!, channel_id: channel.id });
modalController.push({
type: "create_invite",
target: channel,
});
},
iconProps: {
icon: "mdiAccountPlus",

View File

@ -1,10 +1,11 @@
import { useModals } from "@mattjennings/react-modal-stack";
import { CDNRoutes, ChannelType, ImageFormat } from "@spacebarchat/spacebar-api-types/v9";
import { observer } from "mobx-react-lite";
import React from "react";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
import { ContextMenuContext } from "../contexts/ContextMenuContext";
import { modalController } from "../controllers/modals/ModalController";
import useLogger from "../hooks/useLogger";
import { useAppStore } from "../stores/AppStore";
import Guild from "../stores/objects/Guild";
import { Permissions } from "../utils/Permissions";
@ -13,7 +14,6 @@ import Container from "./Container";
import { IContextMenuItem } from "./ContextMenuItem";
import SidebarPill, { PillType } from "./SidebarPill";
import Tooltip from "./Tooltip";
import CreateInviteModal from "./modals/CreateInviteModal";
export const GuildSidebarListItem = styled.div`
position: relative;
@ -48,9 +48,9 @@ interface Props {
* List item for use in the guild sidebar
*/
function GuildItem({ guild, active }: Props) {
const logger = useLogger("GuildItem");
const app = useAppStore();
const navigate = useNavigate();
const { openModal } = useModals();
const [pillType, setPillType] = React.useState<PillType>("none");
const [isHovered, setHovered] = React.useState(false);
@ -71,7 +71,17 @@ function GuildItem({ guild, active }: Props) {
index: 0,
label: "Create Invite",
onClick: () => {
openModal(CreateInviteModal, { guild_id: guild.id });
// get first channel with view permissions in guild
const channel = guild.channels.find((x) => {
const permission = Permissions.getPermission(app.account!.id, guild, x);
return permission.has("VIEW_CHANNEL") && x.type !== ChannelType.GuildCategory;
});
if (!channel) return logger.error("No suitable channel found for invite creation");
modalController.push({
type: "create_invite",
target: channel,
});
},
iconProps: {
icon: "mdiAccountPlus",

View File

@ -4,10 +4,10 @@ import React from "react";
import { useNavigate } from "react-router-dom";
import { AutoSizer, List, ListRowProps } from "react-virtualized";
import styled from "styled-components";
import { modalController } from "../controllers/modals/ModalController";
import { useAppStore } from "../stores/AppStore";
import GuildItem, { GuildSidebarListItem } from "./GuildItem";
import SidebarAction from "./SidebarAction";
import AddServerModal from "./modals/AddServerModal";
const Container = styled.div`
display: flex;
@ -80,7 +80,9 @@ function GuildSidebar() {
color: "var(--success)",
}}
action={() => {
openModal(AddServerModal);
modalController.push({
type: "add_server",
});
}}
margin={false}
disablePill

View File

@ -1,14 +1,13 @@
import { useModals } from "@mattjennings/react-modal-stack";
import React from "react";
import styled from "styled-components";
import { PopoutContext } from "../contexts/PopoutContext";
import { modalController } from "../controllers/modals/ModalController";
import { useAppStore } from "../stores/AppStore";
import Avatar from "./Avatar";
import Icon from "./Icon";
import IconButton from "./IconButton";
import Tooltip from "./Tooltip";
import UserProfilePopout from "./UserProfilePopout";
import SettingsModal from "./modals/SettingsModal";
const Section = styled.section`
flex: 0 0 auto;
@ -73,11 +72,14 @@ const ActionsWrapper = styled.div`
function UserPanel() {
const app = useAppStore();
const popoutContext = React.useContext(PopoutContext);
const { openModal } = useModals();
const ref = React.useRef<HTMLDivElement>(null);
const openSettingsModal = () => {
openModal(SettingsModal);
modalController.push({
type: "error",
title: "File Too Large",
error: "Max file size is 25MB.",
});
};
const openPopout = (e: React.MouseEvent) => {

View File

@ -0,0 +1,26 @@
// https://github.com/revoltchat/components/blob/master/src/components/common/animations.ts
import { keyframes } from "styled-components";
export const animationFadeIn = keyframes`
0% {opacity: 0;}
70% {opacity: 0;}
100% {opacity: 1;}
`;
export const animationFadeOut = keyframes`
0% {opacity: 1;}
70% {opacity: 0;}
100% {opacity: 0;}
`;
export const animationZoomIn = keyframes`
0% {transform: scale(0.5);}
98% {transform: scale(1.01);}
100% {transform: scale(1);}
`;
export const animationZoomOut = keyframes`
0% {transform: scale(1);}
100% {transform: scale(0.5);}
`;

View File

@ -5,6 +5,7 @@ import { ChannelType, MessageType, RESTPostAPIChannelMessageJSONBody } from "@sp
import { observer } from "mobx-react-lite";
import React from "react";
import styled from "styled-components";
import { modalController } from "../../controllers/modals/ModalController";
import useLogger from "../../hooks/useLogger";
import { useAppStore } from "../../stores/AppStore";
import Guild from "../../stores/objects/Guild";
@ -12,7 +13,6 @@ import Snowflake from "../../utils/Snowflake";
import { MAX_ATTACHMENTS } from "../../utils/constants";
import { debounce } from "../../utils/debounce";
import { isTouchscreenDevice } from "../../utils/isTouchscreenDevice";
import ErrorModal from "../modals/ErrorModal";
import MessageTextArea from "./MessageTextArea";
import AttachmentUpload from "./attachments/AttachmentUpload";
import AttachmentUploadList from "./attachments/AttachmentUploadPreview";
@ -161,13 +161,10 @@ function MessageInput({ channel }: Props) {
const appendAttachment = (files: File[]) => {
if (files.length === 0) return;
if (files.length > MAX_ATTACHMENTS || attachments.length + files.length > MAX_ATTACHMENTS) {
openModal(ErrorModal, {
modalController.push({
type: "error",
title: "Too many attachments",
message: (
<div style={{ justifyContent: "center", display: "flex" }}>
You can only attach {MAX_ATTACHMENTS} files at once.
</div>
),
error: `You can only attach ${MAX_ATTACHMENTS} files at once.`,
});
return;
}

View File

@ -1,12 +1,11 @@
import { useModals } from "@mattjennings/react-modal-stack";
import { observer } from "mobx-react-lite";
import React from "react";
import styled from "styled-components";
import { modalController } from "../../../controllers/modals/ModalController";
import useLogger from "../../../hooks/useLogger";
import { bytesToSize } from "../../../utils/Utils";
import { MAX_UPLOAD_SIZE } from "../../../utils/constants";
import Icon from "../../Icon";
import ErrorModal from "../../modals/ErrorModal";
const Container = styled.button`
height: 45px;
@ -64,16 +63,12 @@ interface Props {
function AttachmentUpload({ append }: Props) {
const logger = useLogger("AttachmentUpload");
const { openModal } = useModals();
const fileTooLarge = () => {
openModal(ErrorModal, {
modalController.push({
type: "error",
title: "File Too Large",
message: (
<div style={{ justifyContent: "center", display: "flex" }}>
Max file size is {bytesToSize(MAX_UPLOAD_SIZE)}.
</div>
),
error: `Max file size is ${bytesToSize(MAX_UPLOAD_SIZE)}.`,
});
return;
};

View File

@ -1,95 +1,40 @@
import { useModals } from "@mattjennings/react-modal-stack";
import styled from "styled-components";
import Icon from "../Icon";
import CreateServerModal from "./CreateServerModal";
import JoinServerModal from "./JoinServerModal";
import {
Modal,
ModalActionItem,
ModalCloseWrapper,
ModalHeaderText,
ModalSubHeaderText,
ModelContentContainer,
} from "./ModalComponents";
import { AnimatedModalProps } from "./ModalRenderer";
import { modalController } from "../../controllers/modals/ModalController";
import { ModalProps } from "../../controllers/modals/types";
import Button from "../Button";
import { Modal } from "./ModalComponents";
export const ModalHeader = styled.div`
padding: 16px;
export const ActionWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`;
const CreateButton = styled(ModalActionItem)`
transition: background-color 0.2s ease-in-out;
margin-bottom: 8px;
font-size: 16px;
font-weight: var(--font-weight-medium);
&:hover {
background-color: var(--primary-light);
}
`;
const JoinButton = styled(ModalActionItem)`
transition: background-color 0.2s ease-in-out;
margin-bottom: 8px;
font-size: 16px;
font-weight: var(--font-weight-medium);
&:hover {
background-color: var(--background-secondary-highlight);
}
`;
function AddServerModal(props: AnimatedModalProps) {
const { openModal, closeModal } = useModals();
function AddServerModal({ ...props }: ModalProps<"add_server">) {
return (
<Modal {...props}>
<ModalCloseWrapper>
<button
onClick={closeModal}
style={{
background: "none",
border: "none",
outline: "none",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
cursor: "pointer",
color: "var(--text)",
}}
/>
</button>
</ModalCloseWrapper>
<ModalHeader>
<ModalHeaderText>Add a Guild</ModalHeaderText>
<ModalSubHeaderText>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</ModalSubHeaderText>
</ModalHeader>
<ModelContentContainer>
<CreateButton
variant="filled"
size="med"
<Modal {...props} title="Add a Guild" description="Lorem ipsum dolor sit amet, consectetur adipiscing elit.">
<ActionWrapper>
<Button
palette="primary"
onClick={() => {
openModal(CreateServerModal);
modalController.push({
type: "create_server",
});
}}
>
Create a Guild
</CreateButton>
</Button>
<JoinButton
variant="outlined"
size="med"
<Button
palette="secondary"
onClick={() => {
openModal(JoinServerModal);
modalController.push({
type: "join_server",
});
}}
>
Join a Guild
</JoinButton>
</ModelContentContainer>
</Button>
</ActionWrapper>
</Modal>
);
}

View File

@ -1,30 +1,5 @@
import { APIAttachment } from "@spacebarchat/spacebar-api-types/v9";
import { Modal } from "./ModalComponents";
import { AnimatedModalProps } from "./ModalRenderer";
interface Props extends AnimatedModalProps {
attachment: APIAttachment;
width?: number;
height?: number;
}
function AttachmentPreviewModal(props: Props) {
const width = props.width ?? props.attachment.width ?? 0;
const height = props.height ?? props.attachment.height ?? 0;
return (
<Modal
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "transparent",
}}
{...props}
>
<img src={props.attachment.url} width={width} height={height} loading="eager" />
</Modal>
);
function AttachmentPreviewModal() {
return null;
}
export default AttachmentPreviewModal;

View File

@ -4,6 +4,7 @@ import dayjs from "dayjs";
import React from "react";
import { useForm } from "react-hook-form";
import styled from "styled-components";
import { ModalProps } from "../../controllers/modals/types";
import useLogger from "../../hooks/useLogger";
import { useAppStore } from "../../stores/AppStore";
import { messageFromFieldError } from "../../utils/messageFromFieldError";
@ -13,15 +14,7 @@ import { TextDivider } from "../Divider";
import { InputSelect, InputSelectOption } from "../FormComponents";
import Icon from "../Icon";
import IconButton from "../IconButton";
import { InputContainer } from "./CreateServerModal";
import {
Modal,
ModalCloseWrapper,
ModalHeaderText,
ModalSubHeaderText,
ModelContentContainer,
} from "./ModalComponents";
import { AnimatedModalProps } from "./ModalRenderer";
import { InputContainer, Modal } from "./ModalComponents";
const EXPIRE_OPTIONS = [
{
@ -89,25 +82,12 @@ const MAX_USES_OPTIONS = [
},
];
const Mention = styled.span`
padding: 0 2px;
`;
const ModalHeader = styled.div`
padding: 24px 24px 0;
`;
const InputWrapper = styled.div`
width: 100%;
display: flex;
align-items: center;
`;
interface InviteModalProps extends AnimatedModalProps {
channel_id?: string;
guild_id: string;
}
interface APICreateInvite {
flags: 0;
target_type: null;
@ -121,7 +101,7 @@ interface FormValues extends APICreateInvite {
code: string;
}
function CreateInviteModal(props: InviteModalProps) {
function CreateInviteModal({ target, ...props }: ModalProps<"create_invite">) {
const logger = useLogger("CreateInviteModal");
const app = useAppStore();
const { openModal, closeModal } = useModals();
@ -130,14 +110,6 @@ function CreateInviteModal(props: InviteModalProps) {
const [isEdited, setIsEdited] = React.useState(false);
const [inviteExpiresAt, setInviteExpiresAt] = React.useState<Date | null>(null);
const guild = app.guilds.get(props.guild_id);
const channel = props.channel_id ? guild?.channels.find((x) => x.id === props.channel_id) : guild?.channels[0];
if (!guild || !channel) {
closeModal();
return null;
}
const {
register,
handleSubmit,
@ -154,7 +126,7 @@ function CreateInviteModal(props: InviteModalProps) {
clearErrors();
app.rest
.post<APICreateInvite, APIInvite>(
Routes.channelInvites(channel.id),
Routes.channelInvites(target.id),
Object.assign(
{
flags: 0,
@ -223,144 +195,115 @@ function CreateInviteModal(props: InviteModalProps) {
React.useEffect(() => createCode(), []);
return (
<Modal {...props}>
<ModalCloseWrapper>
<button
onClick={closeModal}
<Modal {...props} title="Invite People" description={`to #${target.name} in ${target.guild?.name}`}>
<form
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onSubmit();
}
}}
>
<InputContainer>
<LabelWrapper error={false}>
<InputLabel>Expire after</InputLabel>
</LabelWrapper>
<InputWrapper>
<InputSelect
{...register("max_age", { value: EXPIRE_OPTIONS[5].value })}
onChange={handleAgeChange}
value={maxAge.value}
>
{EXPIRE_OPTIONS.map((option) => (
<InputSelectOption value={option.value}>{option.label}</InputSelectOption>
))}
</InputSelect>
</InputWrapper>
</InputContainer>
<InputContainer>
<LabelWrapper error={false}>
<InputLabel>Maximum Uses</InputLabel>
</LabelWrapper>
<InputWrapper>
<InputSelect
{...register("max_uses", { value: 0 })}
onChange={handleMaxUsesChange}
value={maxUses.value}
>
{MAX_USES_OPTIONS.map((option) => (
<InputSelectOption value={option.value}>{option.label}</InputSelectOption>
))}
</InputSelect>
</InputWrapper>
</InputContainer>
<div style={{ display: "flex", justifyContent: "flex-end", margin: "24px 0 12px 0" }}>
<Button disabled={!isEdited} onClick={onSubmit}>
Generate new Link
</Button>
</div>
<InputContainer
style={{
background: "none",
border: "none",
outline: "none",
marginTop: "0",
}}
>
<Icon
icon="mdiClose"
size={1}
<LabelWrapper error={!!errors.code}>
<InputLabel>Invite Code</InputLabel>
{errors.code && (
<InputErrorText>
<>
<TextDivider>-</TextDivider>
{errors.code.message}
</>
</InputErrorText>
)}
</LabelWrapper>
<InputWrapper
style={{
cursor: "pointer",
color: "var(--text)",
}}
/>
</button>
</ModalCloseWrapper>
<ModalHeader>
<ModalHeaderText>Invite People</ModalHeaderText>
<ModalSubHeaderText>
to <Mention>#{channel.name}</Mention> in <Mention>{guild.name}</Mention>
</ModalSubHeaderText>
</ModalHeader>
<ModelContentContainer>
<form
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onSubmit();
}
}}
>
<InputContainer>
<LabelWrapper error={false}>
<InputLabel>Expire after</InputLabel>
</LabelWrapper>
<InputWrapper>
<InputSelect
{...register("max_age", { value: EXPIRE_OPTIONS[5].value })}
onChange={handleAgeChange}
value={maxAge.value}
>
{EXPIRE_OPTIONS.map((option) => (
<InputSelectOption value={option.value}>{option.label}</InputSelectOption>
))}
</InputSelect>
</InputWrapper>
</InputContainer>
<InputContainer>
<LabelWrapper error={false}>
<InputLabel>Maximum Uses</InputLabel>
</LabelWrapper>
<InputWrapper>
<InputSelect
{...register("max_uses", { value: 0 })}
onChange={handleMaxUsesChange}
value={maxUses.value}
>
{MAX_USES_OPTIONS.map((option) => (
<InputSelectOption value={option.value}>{option.label}</InputSelectOption>
))}
</InputSelect>
</InputWrapper>
</InputContainer>
<div style={{ display: "flex", justifyContent: "flex-end", margin: "24px 0 12px 0" }}>
<Button disabled={!isEdited} onClick={onSubmit}>
Generate new Link
</Button>
</div>
<InputContainer
style={{
marginTop: "0",
background: "var(--background-secondary-alt)",
borderRadius: "12px",
}}
>
<LabelWrapper error={!!errors.code}>
<InputLabel>Invite Code</InputLabel>
{errors.code && (
<InputErrorText>
<>
<TextDivider>-</TextDivider>
{errors.code.message}
</>
</InputErrorText>
)}
</LabelWrapper>
<Input
autoFocus
{...register("code")}
readOnly={true}
placeholder={`${window.location.origin}/invite/`}
/>
<InputWrapper
<IconButton
style={{
background: "var(--background-secondary-alt)",
borderRadius: "12px",
marginRight: "8px",
}}
onClick={(e) => {
e.preventDefault();
navigator.clipboard.writeText(getValues("code"));
}}
>
<Input
autoFocus
{...register("code")}
readOnly={true}
placeholder={`${window.location.origin}/invite/`}
/>
<Icon icon="mdiContentCopy" size="20px" color="white" />
</IconButton>
</InputWrapper>
<IconButton
style={{
marginRight: "8px",
}}
onClick={(e) => {
e.preventDefault();
navigator.clipboard.writeText(getValues("code"));
}}
>
<Icon icon="mdiContentCopy" size="20px" color="white" />
</IconButton>
</InputWrapper>
<span
style={{
color: "var(--text-secondary)",
marginTop: "8px",
fontSize: "12px",
fontWeight: "var(--font-weight-regular)",
padding: "0 8px",
}}
>
{inviteExpiresAt ? (
<>This invite will expire {dayjs(inviteExpiresAt).calendar()}</>
) : (
"Invite will never expire."
)}
</span>
</InputContainer>
</form>
</ModelContentContainer>
<span
style={{
color: "var(--text-secondary)",
marginTop: "8px",
fontSize: "12px",
fontWeight: "var(--font-weight-regular)",
padding: "0 8px",
}}
>
{inviteExpiresAt ? (
<>This invite will expire {dayjs(inviteExpiresAt).fromNow()}</>
) : (
"Invite will never expire."
)}
</span>
</InputContainer>
</form>
</Modal>
);
}

View File

@ -1,25 +1,16 @@
import { useModals } from "@mattjennings/react-modal-stack";
import { APIGuild, Routes } from "@spacebarchat/spacebar-api-types/v9";
import React from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
import { modalController } from "../../controllers/modals/ModalController";
import { ModalProps } from "../../controllers/modals/types";
import useLogger from "../../hooks/useLogger";
import { useAppStore } from "../../stores/AppStore";
import { messageFromFieldError } from "../../utils/messageFromFieldError";
import { Input, InputErrorText, InputLabel, InputWrapper, LabelWrapper } from "../AuthComponents";
import { TextDivider } from "../Divider";
import Icon from "../Icon";
import {
Modal,
ModalActionItem,
ModalCloseWrapper,
ModalFooter,
ModalHeaderText,
ModalSubHeaderText,
ModelContentContainer,
} from "./ModalComponents";
import { AnimatedModalProps } from "./ModalRenderer";
import { InputContainer, Modal } from "./ModalComponents";
export const ModalHeader = styled.div`
margin-bottom: 30px;
@ -56,20 +47,13 @@ const FileInput = styled.div`
font-size: 0px;
`;
export const InputContainer = styled.div`
margin-top: 24px;
display: flex;
flex-direction: column;
`;
type FormValues = {
name: string;
};
function CreateServerModal(props: AnimatedModalProps) {
function CreateServerModal({ ...props }: ModalProps<"create_server">) {
const app = useAppStore();
const logger = useLogger("CreateServerModal");
const { openModal, closeModal, closeAllModals } = useModals();
const [selectedFile, setSelectedFile] = React.useState<File>();
const fileInputRef = React.useRef<HTMLInputElement>(null);
const navigate = useNavigate();
@ -98,7 +82,7 @@ function CreateServerModal(props: AnimatedModalProps) {
})
.then((r) => {
navigate(`/channels/${r.id}`);
closeAllModals();
modalController.closeAll();
})
.catch((r) => {
if ("message" in r) {
@ -135,122 +119,86 @@ function CreateServerModal(props: AnimatedModalProps) {
return (
<Modal
{...props}
// used for clicking outside of the modal to close it
onClose={closeAllModals}
title="Customize your guild"
description="Give your new guild a personality with a name and an icon. You can always change it later."
actions={[
{
onClick: onSubmit,
children: <span>Create</span>,
palette: "primary",
confirmation: true,
disabled: isLoading,
},
{
onClick: () => modalController.pop("close"),
children: <span>Back</span>,
palette: "link",
disabled: isLoading,
},
]}
>
<ModalCloseWrapper>
<button
onClick={closeAllModals}
style={{
background: "none",
border: "none",
outline: "none",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
cursor: "pointer",
color: "var(--text)",
}}
/>
</button>
</ModalCloseWrapper>
<UploadIcon>
<IconContainer>
<svg width="80" height="80" viewBox="0 0 80 80" fill="none">
<path
fillRule="evenodd"
clipRule="evenodd"
d="m 39.88 78.32 c 4.0666 0 8.0467 -0.648 11.8282 -1.9066 l -0.9101 -2.7331 c -3.4906 1.1606 -7.1626 1.7597 -10.921 1.7597 v 2.88 m 17.4528 -4.3056 c 3.5539 -1.8749 6.7824 -4.3142 9.5645 -7.2115 l -2.0765 -1.993 c -2.569 2.6755 -5.5526 4.9277 -8.833 6.6586 l 1.345 2.5459 m 13.4208 -11.9434 c 2.2752 -3.3091 4.0061 -6.9638 5.1178 -10.8346 l -2.7677 -0.7949 c -1.0253 3.5712 -2.6237 6.9437 -4.7232 9.9965 l 2.3731 1.633 m 6.2899 -16.5917 c 0.1814 -1.4918 0.2765 -2.9981 0.2794 -4.5158 c 0 -2.5805 -0.2448 -5.063 -0.7344 -7.4966 l -2.8224 0.5674 c 0.4522 2.2464 0.6797 4.5389 0.6797 6.9264 c -0.0029 1.3997 -0.0893 2.7936 -0.2592 4.1702 l 2.8598 0.3514 m -2.1254 -17.8243 c -1.4198 -3.7642 -3.4416 -7.2662 -5.976 -10.3824 l -2.2349 1.8173 c 2.3386 2.8771 4.2048 6.1114 5.5181 9.5818 l 2.6928 -1.0166 m -10.1923 -14.783 c -3.0038 -2.6669 -6.4195 -4.8384 -10.1146 -6.4195 l -1.1347 2.6467 c 3.4099 1.4602 6.5635 3.4646 9.337 5.927 l 1.9123 -2.1542 m -15.7334 -8.3117 c -2.9146 -0.7286 -5.927 -1.1059 -8.9827 -1.1174 c -1.0886 0 -2.065 0.0374 -3.0499 0.1123 l 0.2218 2.8714 c 0.9101 -0.0691 1.8115 -0.1037 2.8224 -0.1037 c 2.8195 0.0086 5.5987 0.3571 8.2915 1.031 l 0.6998 -2.7936 m -17.9654 -0.0634 c -3.9197 0.9504 -7.6406 2.5286 -11.0362 4.6627 l 1.5322 2.4394 c 3.1363 -1.9699 6.5693 -3.4243 10.1837 -4.3027 l -0.6797 -2.7994 m -15.889 8.2886 c -3.0154 2.6554 -5.5872 5.7888 -7.6118 9.2506 l 2.4883 1.4515 c 1.8691 -3.2026 4.2451 -6.0883 7.0301 -8.5421 l -1.9037 -2.16 m -10.1952 14.6189 c -1.4371 3.7238 -2.2723 7.6723 -2.4595 11.7274 l 2.8771 0.1325 c 0.1728 -3.744 0.9446 -7.3843 2.2694 -10.823 l -2.687 -1.0368 m -2.2464 17.8618 c 0.4694 4.0205 1.5811 7.9027 3.2832 11.52 l 2.6064 -1.224 c -1.5696 -3.3408 -2.5978 -6.9235 -3.0298 -10.633 l -2.8598 0.3341 m 6.2698 16.7443 c 2.2694 3.3149 5.0573 6.2467 8.2541 8.6688 l 1.7453 -2.2925 c -2.952 -2.2464 -5.5267 -4.9565 -7.6205 -8.015 l -2.376 1.6272 m 13.4352 11.9923 c 3.5424 1.872 7.3699 3.168 11.3558 3.8246 l 0.4666 -2.8426 c -3.6806 -0.6048 -7.2086 -1.8 -10.4774 -3.528 l -1.3478 2.5459 m 17.3376 4.3229 c 0.0691 0 0.0691 0 0.1382 0 v -2.88 c -0.0634 0 -0.0634 0 -0.1267 0 l -0.0115 2.88"
fill="currentColor"
></path>
<path
d="M40 29C37.794 29 36 30.794 36 33C36 35.207 37.794 37 40 37C42.206 37 44 35.207 44 33C44 30.795 42.206 29 40 29Z"
fill="currentColor"
></path>
<path
d="M48 26.001H46.07C45.402 26.001 44.777 25.667 44.406 25.111L43.594 23.891C43.223 23.335 42.598 23 41.93 23H38.07C37.402 23 36.777 23.335 36.406 23.89L35.594 25.11C35.223 25.667 34.598 26 33.93 26H32C30.895 26 30 26.896 30 28V39C30 40.104 30.895 41 32 41H48C49.104 41 50 40.104 50 39V28C50 26.897 49.104 26.001 48 26.001ZM40 39C36.691 39 34 36.309 34 33C34 29.692 36.691 27 40 27C43.309 27 46 29.692 46 33C46 36.31 43.309 39 40 39Z"
fill="currentColor"
></path>
<path
d="M24.6097 52.712V47.72H22.5457V52.736C22.5457 53.792 22.0777 54.404 21.1417 54.404C20.2177 54.404 19.7377 53.78 19.7377 52.712V47.72H17.6737V52.724C17.6737 55.04 19.0897 56.132 21.1177 56.132C23.1217 56.132 24.6097 55.016 24.6097 52.712ZM26.0314 56H28.0834V53.252H28.6114C30.6154 53.252 31.9474 52.292 31.9474 50.42C31.9474 48.62 30.7114 47.72 28.6954 47.72H26.0314V56ZM29.9554 50.456C29.9554 51.308 29.4514 51.704 28.5394 51.704H28.0594V49.268H28.5754C29.4874 49.268 29.9554 49.664 29.9554 50.456ZM37.8292 56L37.5532 54.224H35.0092V47.72H32.9572V56H37.8292ZM45.9558 51.848C45.9558 49.292 44.4078 47.564 42.0078 47.564C39.6078 47.564 38.0478 49.304 38.0478 51.872C38.0478 54.428 39.6078 56.156 41.9838 56.156C44.3958 56.156 45.9558 54.404 45.9558 51.848ZM43.8918 51.86C43.8918 53.504 43.1958 54.548 41.9958 54.548C40.8078 54.548 40.0998 53.504 40.0998 51.86C40.0998 50.216 40.8078 49.172 41.9958 49.172C43.1958 49.172 43.8918 50.216 43.8918 51.86ZM52.2916 56.084L54.3676 55.748L51.4876 47.684H49.2316L46.2556 56H48.2716L48.8236 54.284H51.6916L52.2916 56.084ZM50.2516 49.796L51.1756 52.676H49.3156L50.2516 49.796ZM62.5174 51.848C62.5174 49.388 61.0174 47.72 58.1374 47.72H55.2814V56H58.1854C60.9814 56 62.5174 54.308 62.5174 51.848ZM60.4534 51.86C60.4534 53.636 59.5414 54.404 58.0774 54.404H57.3334V49.316H58.0774C59.4814 49.316 60.4534 50.12 60.4534 51.86Z"
fill="currentColor"
></path>
</svg>
<IconInput ref={fileInputRef} type="file" name="icon" accept="image/*" onChange={onIconChange} />
<FileInput
role="button"
// disabled until I get the motiviation to not make it shit, I don't really want to use an invisible input
onClick={() => fileInputRef.current?.click()}
></FileInput>
</IconContainer>
</UploadIcon>
<ModalHeader>
<ModalHeaderText>Customize your guild</ModalHeaderText>
<ModalSubHeaderText>
Give your new guild a personality with a name and an icon. You can always change it later.
</ModalSubHeaderText>
</ModalHeader>
<ModelContentContainer>
<UploadIcon>
<IconContainer>
<svg width="80" height="80" viewBox="0 0 80 80" fill="none">
<path
fillRule="evenodd"
clipRule="evenodd"
d="m 39.88 78.32 c 4.0666 0 8.0467 -0.648 11.8282 -1.9066 l -0.9101 -2.7331 c -3.4906 1.1606 -7.1626 1.7597 -10.921 1.7597 v 2.88 m 17.4528 -4.3056 c 3.5539 -1.8749 6.7824 -4.3142 9.5645 -7.2115 l -2.0765 -1.993 c -2.569 2.6755 -5.5526 4.9277 -8.833 6.6586 l 1.345 2.5459 m 13.4208 -11.9434 c 2.2752 -3.3091 4.0061 -6.9638 5.1178 -10.8346 l -2.7677 -0.7949 c -1.0253 3.5712 -2.6237 6.9437 -4.7232 9.9965 l 2.3731 1.633 m 6.2899 -16.5917 c 0.1814 -1.4918 0.2765 -2.9981 0.2794 -4.5158 c 0 -2.5805 -0.2448 -5.063 -0.7344 -7.4966 l -2.8224 0.5674 c 0.4522 2.2464 0.6797 4.5389 0.6797 6.9264 c -0.0029 1.3997 -0.0893 2.7936 -0.2592 4.1702 l 2.8598 0.3514 m -2.1254 -17.8243 c -1.4198 -3.7642 -3.4416 -7.2662 -5.976 -10.3824 l -2.2349 1.8173 c 2.3386 2.8771 4.2048 6.1114 5.5181 9.5818 l 2.6928 -1.0166 m -10.1923 -14.783 c -3.0038 -2.6669 -6.4195 -4.8384 -10.1146 -6.4195 l -1.1347 2.6467 c 3.4099 1.4602 6.5635 3.4646 9.337 5.927 l 1.9123 -2.1542 m -15.7334 -8.3117 c -2.9146 -0.7286 -5.927 -1.1059 -8.9827 -1.1174 c -1.0886 0 -2.065 0.0374 -3.0499 0.1123 l 0.2218 2.8714 c 0.9101 -0.0691 1.8115 -0.1037 2.8224 -0.1037 c 2.8195 0.0086 5.5987 0.3571 8.2915 1.031 l 0.6998 -2.7936 m -17.9654 -0.0634 c -3.9197 0.9504 -7.6406 2.5286 -11.0362 4.6627 l 1.5322 2.4394 c 3.1363 -1.9699 6.5693 -3.4243 10.1837 -4.3027 l -0.6797 -2.7994 m -15.889 8.2886 c -3.0154 2.6554 -5.5872 5.7888 -7.6118 9.2506 l 2.4883 1.4515 c 1.8691 -3.2026 4.2451 -6.0883 7.0301 -8.5421 l -1.9037 -2.16 m -10.1952 14.6189 c -1.4371 3.7238 -2.2723 7.6723 -2.4595 11.7274 l 2.8771 0.1325 c 0.1728 -3.744 0.9446 -7.3843 2.2694 -10.823 l -2.687 -1.0368 m -2.2464 17.8618 c 0.4694 4.0205 1.5811 7.9027 3.2832 11.52 l 2.6064 -1.224 c -1.5696 -3.3408 -2.5978 -6.9235 -3.0298 -10.633 l -2.8598 0.3341 m 6.2698 16.7443 c 2.2694 3.3149 5.0573 6.2467 8.2541 8.6688 l 1.7453 -2.2925 c -2.952 -2.2464 -5.5267 -4.9565 -7.6205 -8.015 l -2.376 1.6272 m 13.4352 11.9923 c 3.5424 1.872 7.3699 3.168 11.3558 3.8246 l 0.4666 -2.8426 c -3.6806 -0.6048 -7.2086 -1.8 -10.4774 -3.528 l -1.3478 2.5459 m 17.3376 4.3229 c 0.0691 0 0.0691 0 0.1382 0 v -2.88 c -0.0634 0 -0.0634 0 -0.1267 0 l -0.0115 2.88"
fill="currentColor"
></path>
<path
d="M40 29C37.794 29 36 30.794 36 33C36 35.207 37.794 37 40 37C42.206 37 44 35.207 44 33C44 30.795 42.206 29 40 29Z"
fill="currentColor"
></path>
<path
d="M48 26.001H46.07C45.402 26.001 44.777 25.667 44.406 25.111L43.594 23.891C43.223 23.335 42.598 23 41.93 23H38.07C37.402 23 36.777 23.335 36.406 23.89L35.594 25.11C35.223 25.667 34.598 26 33.93 26H32C30.895 26 30 26.896 30 28V39C30 40.104 30.895 41 32 41H48C49.104 41 50 40.104 50 39V28C50 26.897 49.104 26.001 48 26.001ZM40 39C36.691 39 34 36.309 34 33C34 29.692 36.691 27 40 27C43.309 27 46 29.692 46 33C46 36.31 43.309 39 40 39Z"
fill="currentColor"
></path>
<path
d="M24.6097 52.712V47.72H22.5457V52.736C22.5457 53.792 22.0777 54.404 21.1417 54.404C20.2177 54.404 19.7377 53.78 19.7377 52.712V47.72H17.6737V52.724C17.6737 55.04 19.0897 56.132 21.1177 56.132C23.1217 56.132 24.6097 55.016 24.6097 52.712ZM26.0314 56H28.0834V53.252H28.6114C30.6154 53.252 31.9474 52.292 31.9474 50.42C31.9474 48.62 30.7114 47.72 28.6954 47.72H26.0314V56ZM29.9554 50.456C29.9554 51.308 29.4514 51.704 28.5394 51.704H28.0594V49.268H28.5754C29.4874 49.268 29.9554 49.664 29.9554 50.456ZM37.8292 56L37.5532 54.224H35.0092V47.72H32.9572V56H37.8292ZM45.9558 51.848C45.9558 49.292 44.4078 47.564 42.0078 47.564C39.6078 47.564 38.0478 49.304 38.0478 51.872C38.0478 54.428 39.6078 56.156 41.9838 56.156C44.3958 56.156 45.9558 54.404 45.9558 51.848ZM43.8918 51.86C43.8918 53.504 43.1958 54.548 41.9958 54.548C40.8078 54.548 40.0998 53.504 40.0998 51.86C40.0998 50.216 40.8078 49.172 41.9958 49.172C43.1958 49.172 43.8918 50.216 43.8918 51.86ZM52.2916 56.084L54.3676 55.748L51.4876 47.684H49.2316L46.2556 56H48.2716L48.8236 54.284H51.6916L52.2916 56.084ZM50.2516 49.796L51.1756 52.676H49.3156L50.2516 49.796ZM62.5174 51.848C62.5174 49.388 61.0174 47.72 58.1374 47.72H55.2814V56H58.1854C60.9814 56 62.5174 54.308 62.5174 51.848ZM60.4534 51.86C60.4534 53.636 59.5414 54.404 58.0774 54.404H57.3334V49.316H58.0774C59.4814 49.316 60.4534 50.12 60.4534 51.86Z"
fill="currentColor"
></path>
</svg>
<IconInput
ref={fileInputRef}
type="file"
name="icon"
accept="image/*"
onChange={onIconChange}
<form
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onSubmit();
}
}}
>
<InputContainer>
<LabelWrapper error={!!errors.name}>
<InputLabel>Guild Name</InputLabel>
{errors.name && (
<InputErrorText>
<>
<TextDivider>-</TextDivider>
{errors.name.message}
</>
</InputErrorText>
)}
</LabelWrapper>
<InputWrapper>
<Input
autoFocus
{...register("name", { required: true })}
placeholder="Guild Name"
error={!!errors.name}
disabled={isLoading}
/>
<FileInput
role="button"
// disabled until I get the motiviation to not make it shit, I don't really want to use an invisible input
onClick={() => fileInputRef.current?.click()}
></FileInput>
</IconContainer>
</UploadIcon>
<form
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onSubmit();
}
}}
>
<InputContainer>
<LabelWrapper error={!!errors.name}>
<InputLabel>Guild Name</InputLabel>
{errors.name && (
<InputErrorText>
<>
<TextDivider>-</TextDivider>
{errors.name.message}
</>
</InputErrorText>
)}
</LabelWrapper>
<InputWrapper>
<Input
autoFocus
{...register("name", { required: true })}
placeholder="Guild Name"
error={!!errors.name}
disabled={isLoading}
/>
</InputWrapper>
</InputContainer>
</form>
</ModelContentContainer>
<ModalFooter>
<ModalActionItem variant="filled" size="med" onClick={onSubmit} disabled={isLoading}>
Create
</ModalActionItem>
<ModalActionItem
variant="link"
size="min"
onClick={() => {
closeModal();
}}
>
Back
</ModalActionItem>
</ModalFooter>
</InputWrapper>
</InputContainer>
</form>
</Modal>
);
}

View File

@ -1,71 +1,22 @@
import { useModals } from "@mattjennings/react-modal-stack";
import styled from "styled-components";
import Icon from "../Icon";
import {
Modal,
ModalActionItem,
ModalCloseWrapper,
ModalFooter,
ModalHeaderText,
ModalSubHeaderText,
ModelContentContainer,
} from "./ModalComponents";
import { AnimatedModalProps } from "./ModalRenderer";
export const ModalHeader = styled.div`
padding: 16px;
`;
const CloseButton = styled(ModalActionItem)`
transition: background-color 0.2s ease-in-out;
margin-bottom: 8px;
font-size: 16px;
font-weight: var(--font-weight-medium);
`;
interface Props extends AnimatedModalProps {
title: string;
subtitle?: string;
message: React.ReactNode;
}
function ErrorModal(props: Props) {
const { closeModal } = useModals();
import { ModalProps } from "../../controllers/modals/types";
import { Modal } from "./ModalComponents";
function ErrorModal({ error, ...props }: ModalProps<"error">) {
return (
<Modal {...props}>
<ModalCloseWrapper>
<button
onClick={closeModal}
style={{
background: "none",
border: "none",
outline: "none",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
cursor: "pointer",
color: "var(--text)",
}}
/>
</button>
</ModalCloseWrapper>
<ModalHeader>
<ModalHeaderText>{props.title}</ModalHeaderText>
{props.subtitle && <ModalSubHeaderText>{props.subtitle}</ModalSubHeaderText>}
</ModalHeader>
<ModelContentContainer>{props.message}</ModelContentContainer>
<ModalFooter>
<CloseButton variant="filled" size="med" onClick={() => closeModal()}>
Dismiss
</CloseButton>
</ModalFooter>
<Modal
{...props}
actions={[
{
onClick: () => true,
confirmation: true,
children: <span>Dismiss</span>,
palette: "primary",
disabled: !(props.recoverable ?? true),
},
]}
nonDismissable={!(props.recoverable ?? true)}
>
{error}
</Modal>
);
}

View File

@ -1,71 +1,5 @@
import { useModals } from "@mattjennings/react-modal-stack";
import styled from "styled-components";
import Icon from "../Icon";
import {
Modal,
ModalActionItem,
ModalCloseWrapper,
ModalFooter,
ModalHeaderText,
ModelContentContainer,
} from "./ModalComponents";
import { AnimatedModalProps } from "./ModalRenderer";
export const ModalHeader = styled.div`
padding: 16px;
`;
const SubmitButton = styled(ModalActionItem)`
transition: background-color 0.2s ease-in-out;
font-size: 16px;
font-weight: var(--font-weight-medium);
&:hover {
background-color: var(--background-secondary-highlight);
}
`;
function ForgotPasswordModal(props: AnimatedModalProps) {
const { closeModal } = useModals();
return (
<Modal {...props}>
<ModalCloseWrapper>
<button
onClick={closeModal}
style={{
background: "none",
border: "none",
outline: "none",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
cursor: "pointer",
color: "var(--text)",
}}
/>
</button>
</ModalCloseWrapper>
<ModalHeader>
<ModalHeaderText>Instructions Sent</ModalHeaderText>
</ModalHeader>
<ModelContentContainer>
We sent instructions to change your password to user@example.com, please check both your inbox and spam
folder.
</ModelContentContainer>
<ModalFooter>
<SubmitButton variant="filled" size="med" onClick={closeModal}>
Okay
</SubmitButton>
</ModalFooter>
</Modal>
);
function ForgotPasswordModal() {
return null;
}
export default ForgotPasswordModal;

View File

@ -3,23 +3,14 @@ import { Routes } from "@spacebarchat/spacebar-api-types/v9";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
import { modalController } from "../../controllers/modals/ModalController";
import { ModalProps } from "../../controllers/modals/types";
import useLogger from "../../hooks/useLogger";
import { useAppStore } from "../../stores/AppStore";
import { messageFromFieldError } from "../../utils/messageFromFieldError";
import { Input, InputErrorText, InputLabel, LabelWrapper } from "../AuthComponents";
import { TextDivider } from "../Divider";
import Icon from "../Icon";
import AddServerModal from "./AddServerModal";
import {
Modal,
ModalActionItem,
ModalCloseWrapper,
ModalFooter,
ModalHeaderText,
ModalSubHeaderText,
ModelContentContainer,
} from "./ModalComponents";
import { AnimatedModalProps } from "./ModalRenderer";
import { Modal } from "./ModalComponents";
export const ModalHeader = styled.div`
padding: 16px;
@ -34,7 +25,7 @@ type FormValues = {
code: string;
};
function JoinServerModal(props: AnimatedModalProps) {
function JoinServerModal({ ...props }: ModalProps<"join_server">) {
const logger = useLogger("JoinServerModal");
const { openModal, closeAllModals } = useModals();
const app = useAppStore();
@ -92,86 +83,58 @@ function JoinServerModal(props: AnimatedModalProps) {
return (
<Modal
{...props}
// used for clicking outside of the modal to close it
onClose={closeAllModals}
title="Join a Guild"
description="Enter an invite below to join an existing guild."
actions={[
{
onClick: onSubmit,
children: <span>Join</span>,
palette: "primary",
confirmation: true,
disabled: isLoading,
},
{
onClick: () => modalController.pop("close"),
children: <span>Back</span>,
palette: "link",
disabled: isLoading,
},
]}
>
<ModalCloseWrapper>
<button
onClick={closeAllModals}
style={{
background: "none",
border: "none",
outline: "none",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
cursor: "pointer",
color: "var(--text)",
}}
<form
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onSubmit();
}
}}
>
<InviteInputContainer>
<LabelWrapper error={!!errors.code}>
<InputLabel>Invite Link</InputLabel>
{errors.code && (
<InputErrorText>
<>
<TextDivider>-</TextDivider>
{errors.code.message}
</>
</InputErrorText>
)}
</LabelWrapper>
<Input
{...register("code", { required: true })}
placeholder={`${window.location.origin}/invite/`}
type="text"
maxLength={9999}
required
error={!!errors.code}
disabled={isLoading}
autoFocus
minLength={6}
/>
</button>
</ModalCloseWrapper>
<ModalHeader>
<ModalHeaderText>Join a Guild</ModalHeaderText>
<ModalSubHeaderText>Enter an invite below to join an existing guild.</ModalSubHeaderText>
</ModalHeader>
<ModelContentContainer>
<form
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onSubmit();
}
}}
>
<InviteInputContainer>
<LabelWrapper error={!!errors.code}>
<InputLabel>Invite Link</InputLabel>
{errors.code && (
<InputErrorText>
<>
<TextDivider>-</TextDivider>
{errors.code.message}
</>
</InputErrorText>
)}
</LabelWrapper>
<Input
{...register("code", { required: true })}
placeholder={`${window.location.origin}/invite/`}
type="text"
maxLength={9999}
required
error={!!errors.code}
disabled={isLoading}
autoFocus
minLength={6}
/>
</InviteInputContainer>
</form>
</ModelContentContainer>
<ModalFooter>
<ModalActionItem variant="filled" size="med" onClick={onSubmit}>
Join Guild
</ModalActionItem>
<ModalActionItem
variant="link"
size="min"
onClick={() => {
openModal(AddServerModal);
}}
>
Back
</ModalActionItem>
</ModalFooter>
</InviteInputContainer>
</form>
</Modal>
);
}

View File

@ -1,74 +1,5 @@
import { useModals } from "@mattjennings/react-modal-stack";
import styled from "styled-components";
import GuildMember from "../../stores/objects/GuildMember";
import Icon from "../Icon";
import {
Modal,
ModalActionItem,
ModalCloseWrapper,
ModalFooter,
ModalHeaderText,
ModalSubHeaderText,
} from "./ModalComponents";
import { AnimatedModalProps } from "./ModalRenderer";
export const ModalHeader = styled.div`
padding: 16px;
`;
const CloseButton = styled(ModalActionItem)`
transition: background-color 0.2s ease-in-out;
margin-bottom: 8px;
font-size: 16px;
font-weight: var(--font-weight-medium);
`;
interface Props extends AnimatedModalProps {
member: GuildMember;
}
function KickModal(props: Props) {
const { closeModal } = useModals();
return (
<Modal {...props}>
<ModalCloseWrapper>
<button
onClick={closeModal}
style={{
background: "none",
border: "none",
outline: "none",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
cursor: "pointer",
color: "var(--text)",
}}
/>
</button>
</ModalCloseWrapper>
<ModalHeader>
<ModalHeaderText>Kick {props.member.user!.username}</ModalHeaderText>
<ModalSubHeaderText>
Are you sure you want to kick @{props.member.user!.username} from the server? They will be able to
rejoin again with a new invite.
</ModalSubHeaderText>
</ModalHeader>
{/* <ModelContentContainer>{props.message}</ModelContentContainer> */}
<ModalFooter>
<CloseButton variant="filled" size="med" onClick={() => closeModal()}>
Dismiss
</CloseButton>
</ModalFooter>
</Modal>
);
function KickModal() {
return null;
}
export default KickModal;

View File

@ -1,131 +1,5 @@
import { useModals } from "@mattjennings/react-modal-stack";
import { Routes } from "@spacebarchat/spacebar-api-types/v9";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
import { useAppStore } from "../../stores/AppStore";
import Guild from "../../stores/objects/Guild";
import Icon from "../Icon";
import {
Modal,
ModalActionItem,
ModalCloseWrapper,
ModalFooter,
ModalHeaderText,
ModelContentContainer,
} from "./ModalComponents";
import { AnimatedModalProps } from "./ModalRenderer";
export const ModalHeader = styled.div`
padding: 16px;
`;
const CancelButton = styled(ModalActionItem)`
transition: background-color 0.2s ease-in-out;
margin-bottom: 8px;
font-size: 14px;
font-weight: var(--font-weight-medium);
&:hover {
text-decoration: underline;
}
`;
const LeaveButton = styled(ModalActionItem)`
transition: background-color 0.2s ease-in-out;
margin-bottom: 8px;
font-size: 14px;
font-weight: var(--font-weight-medium);
border-radius: 4px;
background-color: var(--danger);
&:hover {
background-color: var(--background-secondary-highlight);
}
`;
interface Props {
guild: Guild;
}
function LeaveServerModal(props: Props & AnimatedModalProps) {
const app = useAppStore();
const { closeModal } = useModals();
const navigate = useNavigate();
if (!open) {
return null;
}
const handleLeaveServer = () => {
app.rest.delete(Routes.userGuild(props.guild.id)).finally(() => {
closeModal();
// navigate to @me
navigate("/channels/@me");
});
};
return (
<Modal {...props}>
<ModalCloseWrapper>
<button
onClick={closeModal}
style={{
background: "none",
border: "none",
outline: "none",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
cursor: "pointer",
color: "var(--text)",
}}
/>
</button>
</ModalCloseWrapper>
<ModalHeader>
<ModalHeaderText>Leave {props.guild.name}</ModalHeaderText>
</ModalHeader>
<ModelContentContainer>
<span>
Are you sure you want to leave <b>{props.guild.name}</b>? You won't be able to rejoin this server
unless you are re-invited.
</span>
</ModelContentContainer>
<ModalFooter
style={{
flexDirection: "row",
justifyContent: "flex-end",
}}
>
<CancelButton
variant="link"
size="med"
onClick={() => {
closeModal();
}}
>
Cancel
</CancelButton>
<LeaveButton
variant="outlined"
size="med"
onClick={handleLeaveServer}
style={{
backgroundColor: "var(--danger)",
}}
>
Leave
</LeaveButton>
</ModalFooter>
</Modal>
);
function LeaveServerModal() {
return null;
}
export default LeaveServerModal;

View File

@ -1,253 +1,217 @@
import { useModals, type StackedModalProps } from "@mattjennings/react-modal-stack";
import { AnimatePresence, motion } from "framer-motion";
import React from "react";
import styled from "styled-components";
import { type StackedModalProps } from "@mattjennings/react-modal-stack";
import React, { useCallback, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import styled, { css } from "styled-components";
import Button, { Props as ButtonProps } from "../Button";
import { animationFadeIn, animationFadeOut, animationZoomIn, animationZoomOut } from "../common/animations";
export type ModalAction = Omit<React.HTMLAttributes<HTMLButtonElement>, "as"> &
Omit<ButtonProps, "onClick"> & {
confirmation?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onClick: () => any | Promise<any>;
};
interface ModalProps extends StackedModalProps {
children: React.ReactNode;
onClose?: (force: boolean) => void;
signal?: "close" | "confirm" | "cancel";
title?: string;
description?: string;
transparent?: boolean;
nonDismissable?: boolean;
maxWidth?: string;
maxHeight?: string;
padding?: string;
actions?: ModalAction[];
disabled?: boolean;
withEmptyActionBar?: boolean;
}
/**
* Main container for all modals, handles the background overlay and positioning
*/
export const ModalBase = styled(motion.div)`
z-index: 100;
position: fixed;
export const ModalBase = styled.div<{ closing?: boolean }>`
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
z-index: 9999;
position: fixed;
max-height: 100%;
user-select: none;
animation-duration: 0.2s;
animation-fill-mode: forwards;
display: grid;
overflow-y: auto;
place-items: center;
color: var(--text);
background: rgba(0, 0, 0, 0.8);
${(props) =>
props.closing
? css`
animation-name: ${animationFadeOut};
> div {
animation-name: ${animationZoomOut};
}
`
: css`
animation-name: ${animationFadeIn};
`}
`;
/**
* Wrapper for modal content, handles the sizing and positioning
*/
export const ModalWrapper = styled(motion.div)<{ full?: boolean }>`
width: ${(props) => (props.full ? "100%" : "440px")};
height: ${(props) => (props.full ? "100%" : "auto")};
border-radius: 4px;
background-color: var(--background-secondary);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05), 0 1px 3px 1px rgba(0, 0, 0, 0.05);
position: relative;
export const ModalWrapper = styled.div<
Pick<ModalProps, "transparent" | "maxWidth" | "maxHeight"> & { actions: boolean }
>`
min-height: 200px;
max-width: min(calc(100vw - 20px), ${(props) => props.maxWidth ?? "450px"});
max-height: min(calc(100vh - 20px), ${(props) => props.maxHeight ?? "650px"});
margin: 20px;
display: flex;
justify-content: ${(props) => (props.full ? undefined : "center")};
flex-direction: ${(props) => (props.full ? "row" : "column")};
flex-direction: column;
animation-name: ${animationZoomIn};
animation-duration: 0.25s;
animation-timing-function: cubic-bezier(0.3, 0.3, 0.18, 1.1);
${(props) =>
!props.maxWidth &&
css`
width: 100%;
`}
${(props) =>
!props.transparent &&
css`
overflow: hidden;
background: var(--background-secondary);
border-radius: 8px;
`}
`;
/**
* Wrapper for modal close button
*/
export const ModalCloseWrapper = styled.div`
position: absolute;
top: 10px;
right: 10px;
export const ModalHeader = styled.div`
padding: 16px;
flex-shrink: 0;
word-break: break-word;
gap: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;
export const ModalHeaderText = styled.h1`
font-size: 24px;
font-weight: var(--font-weight-bold);
color: var(--text-header);
text-align: center;
margin: 0;
padding: 0;
export const ModalContentContainer = styled.div<Pick<ModalProps, "transparent" | "padding">>`
display: flex;
flex-direction: column;
flex-grow: 1;
padding-top: 0;
padding: ${(props) => props.padding ?? "0 16px 16px"};
overflow-y: auto;
font-size: 0.9375rem;
${(props) =>
!props.transparent &&
css`
background: var(--background-secondary);
`}
`;
const Actions = styled.div`
gap: 8px;
display: flex;
padding: 16px;
flex-direction: row-reverse;
background: var(--background-primary);
border-radius: 0 0 4px 4px;
`;
export const ModalSubHeaderText = styled.div`
font-size: 16px;
font-weight: var(--font-weight-regular);
color: var(--text-header-secondary);
text-align: center;
margin-top: 8px;
`;
export const ModelContentContainer = styled.div`
display: flex;
flex-direction: column;
padding: 0 16px;
margin: 16px 0;
border-radius: 5px 5px 0 0;
`;
export const ModalActionItem = styled.button<{
variant?: "filled" | "blank" | "outlined" | "link";
size?: "med" | "min";
}>`
export const ModalHeaderText = styled.div`
font-size: 24px;
font-weight: var(--font-weight-bold);
color: var(--text);
display: flex;
position: relative;
justify-content: center;
align-items: center;
background: none;
border: none;
outline: none;
border-radius: 3px;
font-size: 14px;
font-weight: var(--font-weight-medium);
padding: 2px 16px;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
${(props) => {
if (props.variant === "filled") {
return `
background-color: var(--primary);
&:hover {
background-color: var(--primary-light);
}
`;
} else if (props.variant === "blank") {
return `
background: transparent;
`;
} else if (props.variant === "link") {
return `
background: transparent;
&:hover {
text-decoration: underline;
}
`;
} else if (props.variant === "outlined") {
return `
background: transparent;
border: 1px solid var(--background-secondary-highlight);
`;
}
}}
${(props) => {
if (props.size === "med") {
return `
width: auto;
height: 38px;
min-width: 96px;
min-height: 38px;
`;
} else if (props.size === "min") {
return `
width: auto;
display: inline;
height: auto;
padding: 2px 4px;
`;
}
}}
// disabled styling
${(props) => {
if (props.disabled) {
return `
opacity: 0.5;
cursor: not-allowed;
`;
}
}}
margin: 0;
padding: 0;
`;
export const ModalFooter = styled.div`
border-radius: 0 0 5px 5px;
background-color: var(--background-primary-alt);
position: relative;
padding: 16px;
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
`;
export const ModalFullSidebar = styled.div`
display: flex;
justify-content: flex-end;
flex: 1 0 15%;
z-index: 1;
padding: 10px;
`;
export const ModalFullSidebarContainer = styled.div`
overflow: hidden scroll;
flex: 1 0 auto;
flex-direction: row;
display: flex;
justify-content: flex-end;
background-color: var(--background-secondary);
`;
export const ModalFullSidebarContent = styled.div`
width: 200px;
padding: 40px 0;
export const InputContainer = styled.div`
margin-top: 24px;
display: flex;
flex-direction: column;
`;
export const ModalFullContentContainer = styled.div`
display: flex;
flex: 1 1 700px;
align-items: flex-start;
`;
export const ModalFullContentWrapper = styled.div`
flex: 1;
height: 100%;
`;
export const ModalFullContentContainerContentWrapper = styled.div`
height: 100%;
display: flex;
flex-direction: row;
background-color: var(--background-primary);
`;
export const ModalFullContent = styled.div`
padding: 50px;
flex: 1 1 auto;
max-width: 80%;
min-width: 50%;
min-height: 100%;
`;
interface ModalProps extends StackedModalProps {
children: React.ReactNode;
full?: boolean;
onClick?: () => void;
/**
* Custom callback for when a modal is closed by clicking the background overlay
*/
onClose?: () => void;
style?: React.CSSProperties;
}
export function Modal(props: ModalProps) {
const { closeModal } = useModals();
const [closing, setClosing] = useState(false);
return (
<AnimatePresence>
{props.open && (
<ModalBase
variants={{
show: {
opacity: 1,
scale: 1,
},
hide: {
opacity: 0,
scale: 0,
},
}}
initial="hide"
animate="show"
exit="hide"
onClick={(e) => {
if (e.target !== e.currentTarget) return;
if (props.onClose) props.onClose();
else closeModal();
}}
{...props}
>
<ModalWrapper full={props.full} style={props.style}>
{props.children}
</ModalWrapper>
</ModalBase>
)}
</AnimatePresence>
const closeModal = useCallback(() => {
setClosing(true);
if (!closing) setTimeout(() => props.onClose?.(true), 2e2);
}, [closing, props]);
const confirm = useCallback(async () => {
if (await props.actions?.find((x) => x.confirmation)?.onClick?.()) {
closeModal();
}
}, [props.actions]);
useEffect(() => {
if (props.signal === "confirm") {
confirm();
} else if (props.signal) {
if (props.signal === "close" && props.nonDismissable) {
return;
}
closeModal();
}
}, [props.signal]);
return createPortal(
<ModalBase closing={closing} onClick={() => !props.nonDismissable && closeModal()}>
<ModalWrapper {...props} onClick={(e) => e.stopPropagation()} actions={false}>
{(props.title || props.description) && (
<ModalHeader>
{props.title && <ModalHeaderText>{props.title}</ModalHeaderText>}
{props.description && <ModalSubHeaderText>{props.description}</ModalSubHeaderText>}
</ModalHeader>
)}
<ModalContentContainer {...props}>{props.children}</ModalContentContainer>
{props.actions && props.actions.length > 0 && (
<Actions>
{props.actions.map((x, index) => (
<Button
disabled={props.disabled}
key={index}
{...x}
onClick={async () => {
if (await x.onClick()) {
closeModal();
}
}}
/>
))}
</Actions>
)}
</ModalWrapper>
</ModalBase>,
document.body,
);
}

View File

@ -1,75 +0,0 @@
import { ModalStackValue } from "@mattjennings/react-modal-stack";
import { AnimatePresence, motion } from "framer-motion";
import React from "react";
export type AnimatedModalProps = {
open: boolean;
};
function ModalRenderer({ stack }: ModalStackValue) {
const [displayedStack, setDisplayedStack] = React.useState(stack);
const [isOpen, setOpen] = React.useState(false);
React.useEffect(() => {
// we're opening the first modal, so update the stack right away
if (stack.length === 1 && displayedStack.length === 0) {
setOpen(true);
setDisplayedStack(stack);
}
// stack updated, trigger a dismissal of the current modal
else {
setOpen(false);
}
}, [stack]);
return (
<>
<AnimatePresence>
{stack.length > 0 && (
<motion.div
style={{
zIndex: 90,
position: `fixed`,
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0,0,0,0.8)",
}}
variants={{
show: { opacity: 1 },
hide: { opacity: 0 },
}}
initial="hide"
animate="show"
exit="hide"
/>
)}
</AnimatePresence>
{displayedStack.map((modal, index) => (
<modal.component
key={index}
open={index === displayedStack.length - 1 && isOpen}
onAnimationComplete={() => {
// set open state for next modal
if (stack.length > 0) {
setOpen(true);
} else {
setOpen(false);
}
// update displayed stack
// setTimeout is a hack to prevent a warning about updating state
// in an unmounted component (I can't figure out why it happens, or why this fixes it)
setTimeout(() => setDisplayedStack(stack));
modal.props?.onAnimationComplete?.();
}}
{...modal.props}
/>
))}
</>
);
}
export default ModalRenderer;

View File

@ -1,102 +1,7 @@
import { useModals } from "@mattjennings/react-modal-stack";
import { FormControlLabel, FormGroup, Switch } from "@mui/material";
import { observer } from "mobx-react-lite";
import { useAppStore } from "../../stores/AppStore";
import { GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../utils/revison";
import Button from "../Button";
import Icon from "../Icon";
import Link from "../Link";
import {
Modal,
ModalCloseWrapper,
ModalFullContent,
ModalFullContentContainer,
ModalFullContentContainerContentWrapper,
ModalFullContentWrapper,
ModalFullSidebar,
ModalFullSidebarContainer,
ModalFullSidebarContent,
} from "./ModalComponents";
import { AnimatedModalProps } from "./ModalRenderer";
function SettingsModal(props: AnimatedModalProps) {
const app = useAppStore();
const { closeModal, closeAllModals } = useModals();
const logout = () => {
app.logout();
closeAllModals();
};
return (
<Modal full {...props}>
<ModalFullSidebar>
<ModalFullSidebarContainer>
<ModalFullSidebarContent>
SIDEBAR
<Button variant="danger" onClick={logout}>
Logout
</Button>
</ModalFullSidebarContent>
</ModalFullSidebarContainer>
</ModalFullSidebar>
<ModalFullContentContainer>
<ModalFullContentWrapper>
<ModalFullContentContainerContentWrapper>
<ModalFullContent>
<ModalCloseWrapper>
<button
onClick={closeModal}
style={{
background: "none",
border: "none",
outline: "none",
}}
>
<Icon
icon="mdiClose"
size={1}
style={{
cursor: "pointer",
color: "var(--text)",
}}
/>
</button>
</ModalCloseWrapper>
<div>
<span>Client Version: </span>
<Link href={`${REPO_URL}/commit/${GIT_REVISION}`} target="_blank" rel="noreferrer">
{GIT_REVISION.substring(0, 7)}
</Link>
{` `}
<Link
href={GIT_BRANCH !== "DETACHED" ? `${REPO_URL}/tree/${GIT_BRANCH}` : undefined}
target="_blank"
rel="noreferrer"
>
({GIT_BRANCH})
</Link>
</div>
<FormGroup>
<FormControlLabel
control={
<Switch
checked={app.fpsShown}
onChange={(e) => app.setFpsShown(e.target.checked)}
/>
}
label="Show FPS Graph"
/>
</FormGroup>
</ModalFullContent>
</ModalFullContentContainerContentWrapper>
</ModalFullContentWrapper>
</ModalFullContentContainer>
</Modal>
);
function SettingsModal() {
return null;
}
export default observer(SettingsModal);

View File

@ -58,7 +58,8 @@ export type ThemeVariables =
| "scrollbarTrack"
| "scrollbarThumb"
| "statusIdle"
| "statusOffline";
| "statusOffline"
| "accent";
export type Overrides = {
[variable in ThemeVariables]: string;
@ -108,9 +109,10 @@ export const ThemePresets: Record<string, Theme> = {
primaryLight: "#339dff",
primaryDark: "#005db2",
primaryContrastText: "#ffffff",
secondary: "#000115",
secondaryLight: "#000677",
secondaryDark: "#000111",
accent: "#000115",
secondary: "#4e4e4e",
secondaryLight: "#ff9633",
secondaryDark: "#b25e00",
secondaryContrastText: "",
danger: "",
dangerLight: "",
@ -153,11 +155,10 @@ export const ThemePresets: Record<string, Theme> = {
primaryLight: "#339dff",
primaryDark: "#005db2",
primaryContrastText: "#ffffff",
secondary: "#000115",
secondaryLight: "#000677",
secondaryDark: "#000111",
// secondary: "#ff7c01",
// secondaryLight: "#ff9633",
accent: "#000115",
secondary: "#4e4e4e",
secondaryLight: "#ff9633",
secondaryDark: "#b25e00",
secondaryContrastText: "#040404",
danger: "#ff3a3b",
dangerLight: "#ff6162",

View File

@ -0,0 +1,178 @@
// adapted from https://github.com/revoltchat/revite/blob/master/src/controllers/modals/ModalController.tsx
import { action, computed, makeObservable, observable } from "mobx";
import AddServerModal from "../../components/modals/AddServerModal";
import CreateInviteModal from "../../components/modals/CreateInviteModal";
import CreateServerModal from "../../components/modals/CreateServerModal";
import ErrorModal from "../../components/modals/ErrorModal";
import JoinServerModal from "../../components/modals/JoinServerModal";
import { Modal } from "./types";
function randomUUID() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
// eslint-disable-next-line no-bitwise
const r = (Math.random() * 16) | 0;
// eslint-disable-next-line no-bitwise, no-mixed-operators
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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);
}
/**
* Display a new modal on the stack
* @param modal Modal data
*/
push(modal: T) {
this.stack = [
...this.stack,
{
...modal,
key: randomUUID(), // TODO:
},
];
}
/**
* 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");
}
/**
* Close all modals on the stack
*/
closeAll() {
this.stack = [];
}
/**
* 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 <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> {
/**
* 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,
});
}
}
}
export const modalController = new ModalControllerExtended({
add_server: AddServerModal,
// 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: CreateInviteModal,
// create_role: CreateRole,
create_server: CreateServerModal,
join_server: JoinServerModal,
// create_bot: CreateBot,
// custom_status: CustomStatus,
// delete_message: DeleteMessage,
error: ErrorModal,
// 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,
// report: ReportContent,
// report_success: ReportSuccess,
// modify_displayname: ModifyDisplayname,
// changelog_usernames: ChangelogUsernames,
});

View File

@ -0,0 +1,24 @@
// adapted from https://github.com/revoltchat/revite/blob/master/src/controllers/modals/ModalRenderer.tsx
// Removed usage of `Prompt`
import { observer } from "mobx-react-lite";
import { useEffect } from "react";
import { modalController } from "./ModalController";
export default observer(() => {
useEffect(() => {
function keyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
modalController.pop("close");
} else if (event.key === "Enter") {
if (event.target instanceof HTMLSelectElement) return;
modalController.pop("confirm");
}
}
document.addEventListener("keydown", keyDown);
return () => document.removeEventListener("keydown", keyDown);
}, []);
return <>{modalController.rendered}</>;
});

View File

@ -0,0 +1,31 @@
// adapted from https://github.com/revoltchat/revite/blob/master/src/controllers/modals/types.ts
import Channel from "../../stores/objects/Channel";
export type Modal = {
key?: string;
} & (
| {
type: "add_server" | "create_server" | "join_server";
}
| {
type: "error";
title: string;
description?: string;
error: string;
recoverable?: boolean;
}
| {
type: "clipboard";
text: string;
}
| {
type: "create_invite";
target: Channel;
}
);
export type ModalProps<T extends Modal["type"]> = Modal & { type: T } & {
onClose: () => void;
signal?: "close" | "confirm";
};

View File

@ -11,7 +11,6 @@ import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css";
import "@fontsource/roboto/900.css";
import { ModalStack } from "@mattjennings/react-modal-stack";
import dayjs from "dayjs";
import calendar from "dayjs/plugin/calendar";
import relativeTime from "dayjs/plugin/relativeTime";
@ -19,11 +18,11 @@ import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { ErrorBoundaryContext } from "react-use-error-boundary";
import App from "./App";
import ModalRenderer from "./components/modals/ModalRenderer";
import { BannerContextProvider } from "./contexts/BannerContext";
import { ContextMenuContextProvider } from "./contexts/ContextMenuContext";
import { PopoutContextProvider } from "./contexts/PopoutContext";
import Theme from "./contexts/Theme";
import ModalRenderer from "./controllers/modals/ModalRenderer";
import "./index.css";
import { calendarStrings } from "./utils/i18n";
@ -33,16 +32,15 @@ dayjs.extend(calendar, calendarStrings);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<ErrorBoundaryContext>
<BrowserRouter>
<ModalStack renderModals={ModalRenderer}>
<PopoutContextProvider>
<ContextMenuContextProvider>
<BannerContextProvider>
<App />
</BannerContextProvider>
</ContextMenuContextProvider>
</PopoutContextProvider>
<Theme />
</ModalStack>
<PopoutContextProvider>
<ContextMenuContextProvider>
<BannerContextProvider>
<App />
<ModalRenderer />
</BannerContextProvider>
</ContextMenuContextProvider>
</PopoutContextProvider>
<Theme />
</BrowserRouter>
</ErrorBoundaryContext>,
);

View File

@ -40,7 +40,7 @@ function LoadingPage() {
bottom: "30vh",
}}
>
<Button variant="danger" onClick={() => app.logout()}>
<Button palette="danger" onClick={() => app.logout()}>
Logout
</Button>
</div>

View File

@ -331,7 +331,7 @@ function LoginPage() {
Forgot your password?
</PasswordResetLink> */}
<SubmitButton variant="primary" type="submit" disabled={loading}>
<SubmitButton palette="primary" type="submit" disabled={loading}>
Login
</SubmitButton>

View File

@ -275,7 +275,7 @@ function RegistrationPage() {
</InputWrapper>
</InputContainer>
<SubmitButton variant="primary" type="submit" disabled={loading}>
<SubmitButton palette="primary" type="submit" disabled={loading}>
Create Account
</SubmitButton>

View File

@ -130,7 +130,7 @@ function MFA(props: Props) {
</InputWrapper>
</InputContainer>
<SubmitButton variant="primary" type="submit" disabled={loading}>
<SubmitButton palette="primary" type="submit" disabled={loading}>
Log In
</SubmitButton>