Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 1036 additions and 559 deletions
......@@ -11,15 +11,13 @@
}
}
ul {
.actions {
gap: 8px;
margin: auto;
display: block;
font-size: 18px;
text-align: center;
li {
list-style: lower-greek;
}
display: flex;
width: fit-content;
align-items: center;
flex-direction: column;
}
}
......
import styles from "./Home.module.scss";
import { Home as HomeIcon } from "@styled-icons/boxicons-solid";
import { Link } from "react-router-dom";
import styles from "./Home.module.scss";
import { Text } from "preact-i18n";
import wideSVG from "../../assets/wide.svg";
import Tooltip from "../../components/common/Tooltip";
import Button from "../../components/ui/Button";
import Header from "../../components/ui/Header";
import { Home as HomeIcon } from "@styled-icons/boxicons-solid";
import wideSVG from '../../assets/wide.svg';
export default function Home() {
return (
......@@ -14,26 +17,33 @@ export default function Home() {
<Text id="app.navigation.tabs.home" />
</Header>
<h3>
<Text id="app.special.modals.onboarding.welcome" /> <img src={wideSVG} />
<Text id="app.special.modals.onboarding.welcome" />
<br />
<img src={wideSVG} />
</h3>
<ul>
<li>
Go to your <Link to="/friends">friends list</Link>.
</li>
<li>
Give <Link to="/settings/feedback">feedback</Link>.
</li>
<li>
Join <Link to="/invite/Testers">testers server</Link>.
</li>
<li>
View{" "}
<a href="https://gitlab.insrt.uk/revolt" target="_blank">
source code
</a>
.
</li>
</ul>
<div className={styles.actions}>
<Link to="/invite/Testers">
<Button contrast error>
Join testers server
</Button>
</Link>
<a
href="https://insrt.uk/donate"
target="_blank"
rel="noreferrer">
<Button contrast gold>
Donate to Revolt
</Button>
</a>
<Link to="/settings/feedback">
<Button contrast>Give feedback</Button>
</Link>
<Link to="/settings">
<Tooltip content="You can also right-click the user icon in the top left, or left click it if you're already home.">
<Button contrast>Open settings</Button>
</Tooltip>
</Link>
</div>
</div>
);
}
......@@ -34,10 +34,10 @@
.details {
text-align: center;
border-radius: 3px;
align-self: center;
padding: 32px 16px 16px 16px;
background: rgba(0, 0, 0, 0.6);
border-radius: var(--border-radius);
h1 {
margin: 0;
......
import styles from './Invite.module.scss';
import Button from '../../components/ui/Button';
import { ArrowBack } from "@styled-icons/boxicons-regular";
import Overline from '../../components/ui/Overline';
import { Invites } from "revolt.js/dist/api/objects";
import Preloader from '../../components/ui/Preloader';
import { takeError } from "../../context/revoltjs/util";
import { autorun } from "mobx";
import { useHistory, useParams } from "react-router-dom";
import ServerIcon from '../../components/common/ServerIcon';
import UserIcon from '../../components/common/user/UserIcon';
import { RetrievedInvite } from "revolt-api/types/Invites";
import styles from "./Invite.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import RequiresOnline from '../../context/revoltjs/RequiresOnline';
import { AppContext, ClientStatus, StatusContext } from "../../context/revoltjs/RevoltClient";
import { defer } from "../../lib/defer";
import { TextReact } from "../../lib/i18n";
import RequiresOnline from "../../context/revoltjs/RequiresOnline";
import {
AppContext,
ClientStatus,
StatusContext,
} from "../../context/revoltjs/RevoltClient";
import { takeError } from "../../context/revoltjs/util";
import ServerIcon from "../../components/common/ServerIcon";
import UserIcon from "../../components/common/user/UserIcon";
import Button from "../../components/ui/Button";
import Overline from "../../components/ui/Overline";
import Preloader from "../../components/ui/Preloader";
export default function Invite() {
const history = useHistory();
const client = useContext(AppContext);
const status = useContext(StatusContext);
const { code } = useParams<{ code: string }>();
const [ processing, setProcessing ] = useState(false);
const [ error, setError ] = useState<string | undefined>(undefined);
const [ invite, setInvite ] = useState<Invites.RetrievedInvite | undefined>(undefined);
const [processing, setProcessing] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const [invite, setInvite] = useState<RetrievedInvite | undefined>(
undefined,
);
useEffect(() => {
if (typeof invite === 'undefined' && (status === ClientStatus.ONLINE || status === ClientStatus.READY)) {
client.fetchInvite(code)
.then(data => setInvite(data))
.catch(err => setError(takeError(err)))
if (
typeof invite === "undefined" &&
(status === ClientStatus.ONLINE || status === ClientStatus.READY)
) {
client
.fetchInvite(code)
.then((data) => setInvite(data))
.catch((err) => setError(takeError(err)));
}
}, [ status ]);
}, [client, code, invite, status]);
if (typeof invite === 'undefined') {
if (typeof invite === "undefined") {
return (
<div className={styles.preloader}>
<RequiresOnline>
{ error ? <Overline type="error" error={error} />
: <Preloader type="spinner" /> }
{error ? (
<Overline type="error" error={error} />
) : (
<Preloader type="spinner" />
)}
</RequiresOnline>
</div>
)
);
}
// ! FIXME: add i18n translations
return (
<div className={styles.invite} style={{ backgroundImage: invite.server_banner ? `url('${client.generateFileURL(invite.server_banner)}')` : undefined }}>
<div
className={styles.invite}
style={{
backgroundImage: invite.server_banner
? `url('${client.generateFileURL(invite.server_banner)}')`
: undefined,
}}>
<div className={styles.leave}>
<ArrowBack size={32} onClick={() => history.push('/')} />
<ArrowBack size={32} onClick={() => history.push("/")} />
</div>
{ !processing &&
{!processing && (
<div className={styles.icon}>
<ServerIcon attachment={invite.server_icon} server_name={invite.server_name} size={64} />
</div> }
<ServerIcon
attachment={invite.server_icon}
server_name={invite.server_name}
size={64}
/>
</div>
)}
<div className={styles.details}>
{ processing ? <Preloader type="ring" /> :
{processing ? (
<Preloader type="ring" />
) : (
<>
<h1>{ invite.server_name }</h1>
<h1>{invite.server_name}</h1>
<h2>#{invite.channel_name}</h2>
<h3>Invited by <UserIcon size={24} attachment={invite.user_avatar} /> { invite.user_name }</h3>
<h3>
<TextReact
id="app.special.invite.invited_by"
fields={{
user: (
<>
<UserIcon
size={24}
attachment={invite.user_avatar}
/>{" "}
{invite.user_name}
</>
),
}}
/>
</h3>
<Overline type="error" error={error} />
<Button contrast
onClick={
async () => {
if (status === ClientStatus.READY) {
return history.push('/');
}
<Button
contrast
onClick={async () => {
if (status === ClientStatus.READY) {
return history.push("/");
}
try {
setProcessing(true);
try {
setProcessing(true);
let result = await client.joinInvite(code);
if (result.type === 'Server') {
history.push(`/server/${result.server._id}/channel/${result.channel._id}`);
if (invite.type === "Server") {
if (
client.servers.get(invite.server_id)
) {
history.push(
`/server/${invite.server_id}/channel/${invite.channel_id}`,
);
}
} catch (err) {
setError(takeError(err));
setProcessing(false);
const dispose = autorun(() => {
const server = client.servers.get(
invite.server_id,
);
defer(() => {
if (server) {
history.push(
`/server/${server._id}/channel/${invite.channel_id}`,
);
}
});
dispose();
});
}
await client.joinInvite(code);
} catch (err) {
setError(takeError(err));
setProcessing(false);
}
}
>{ status === ClientStatus.READY ? 'Login to REVOLT' : 'Accept Invite' }</Button>
}}>
{status === ClientStatus.READY ? (
<Text id="app.special.invite.login" />
) : (
<Text id="app.special.invite.accept" />
)}
</Button>
</>
}
)}
</div>
</div>
);
......
import Overline from '../../components/ui/Overline';
import InputBox from '../../components/ui/InputBox';
import { Text, Localizer } from 'preact-i18n';
import { UseFormMethods } from "react-hook-form";
import { Text, Localizer } from "preact-i18n";
import InputBox from "../../components/ui/InputBox";
import Overline from "../../components/ui/Overline";
interface Props {
type: "email" | "username" | "password" | "invite" | "current_password";
showOverline?: boolean;
register: Function;
register: UseFormMethods["register"];
error?: string;
name?: string;
}
......@@ -15,7 +18,7 @@ export default function FormField({
register,
showOverline,
error,
name
name,
}: Props) {
return (
<>
......@@ -26,7 +29,11 @@ export default function FormField({
)}
<Localizer>
<InputBox
placeholder={(<Text id={`login.enter.${type}`} />) as any}
placeholder={
(
<Text id={`login.enter.${type}`} />
) as unknown as string
}
name={
type === "current_password" ? "password" : name ?? type
}
......@@ -37,6 +44,8 @@ export default function FormField({
? "password"
: type
}
// See https://github.com/mozilla/contain-facebook/issues/783
className="fbc-has-badge"
ref={register(
type === "password" || type === "current_password"
? {
......@@ -47,19 +56,19 @@ export default function FormField({
? "TooShort"
: value.length > 1024
? "TooLong"
: undefined
: undefined,
}
: type === "email"
? {
required: "RequiredField",
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "InvalidEmail"
}
message: "InvalidEmail",
},
}
: type === "username"
? { required: "RequiredField" }
: { required: "RequiredField" }
: { required: "RequiredField" },
)}
/>
</Localizer>
......
import { Text } from "preact-i18n";
import { Helmet } from "react-helmet";
import { Route, Switch } from "react-router-dom";
import { LIBRARY_VERSION } from "revolt.js";
import styles from "./Login.module.scss";
import { Text } from "preact-i18n";
import { useContext } from "preact/hooks";
import { APP_VERSION } from "../../version";
import { LIBRARY_VERSION } from "revolt.js";
import { Route, Switch } from "react-router-dom";
import { ThemeContext } from "../../context/Theme";
import { AppContext } from "../../context/revoltjs/RevoltClient";
import LocaleSelector from "../../components/common/LocaleSelector";
import { Titlebar } from "../../components/native/Titlebar";
import { APP_VERSION } from "../../version";
import background from "./background.jpg";
import { FormLogin } from "./forms/FormLogin";
import { FormCreate } from "./forms/FormCreate";
import { FormLogin } from "./forms/FormLogin";
import { FormResend } from "./forms/FormResend";
import { FormReset, FormSendReset } from "./forms/FormReset";
......@@ -21,52 +24,57 @@ export default function Login() {
const client = useContext(AppContext);
return (
<div className={styles.login}>
<Helmet>
<meta name="theme-color" content={theme.background} />
</Helmet>
<div className={styles.content}>
<div className={styles.attribution}>
<span>
API:{" "}
<code>{client.configuration?.revolt ?? "???"}</code>{" "}
&middot; revolt.js: <code>{LIBRARY_VERSION}</code>{" "}
&middot; App: <code>{APP_VERSION}</code>
</span>
<span>
<LocaleSelector />
</span>
</div>
<div className={styles.modal}>
<Switch>
<Route path="/login/create">
<FormCreate />
</Route>
<Route path="/login/resend">
<FormResend />
</Route>
<Route path="/login/reset/:token">
<FormReset />
</Route>
<Route path="/login/reset">
<FormSendReset />
</Route>
<Route path="/">
<FormLogin />
</Route>
</Switch>
</div>
<div className={styles.attribution}>
<span>
<Text id="general.image_by" /> &lrm;@lorenzoherrera
&rlm;· unsplash.com
</span>
<>
{window.isNative && !window.native.getConfig().frame && (
<Titlebar />
)}
<div className={styles.login}>
<Helmet>
<meta name="theme-color" content={theme.background} />
</Helmet>
<div className={styles.content}>
<div className={styles.attribution}>
<span>
API:{" "}
<code>{client.configuration?.revolt ?? "???"}</code>{" "}
&middot; revolt.js: <code>{LIBRARY_VERSION}</code>{" "}
&middot; App: <code>{APP_VERSION}</code>
</span>
<span>
<LocaleSelector />
</span>
</div>
<div className={styles.modal}>
<Switch>
<Route path="/login/create">
<FormCreate />
</Route>
<Route path="/login/resend">
<FormResend />
</Route>
<Route path="/login/reset/:token">
<FormReset />
</Route>
<Route path="/login/reset">
<FormSendReset />
</Route>
<Route path="/">
<FormLogin />
</Route>
</Switch>
</div>
<div className={styles.attribution}>
<span>
<Text id="general.image_by" /> &lrm;@lorenzoherrera
&rlm;· unsplash.com
</span>
</div>
</div>
<div
className={styles.bg}
style={{ background: `url('${background}')` }}
/>
</div>
<div
className={styles.bg}
style={{ background: `url('${background}')` }}
/>
</div>
</>
);
};
}
import { Text } from "preact-i18n";
import styles from "../Login.module.scss";
import HCaptcha from "@hcaptcha/react-hcaptcha";
import styles from "../Login.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect } from "preact/hooks";
import Preloader from "../../../components/ui/Preloader";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Preloader from "../../../components/ui/Preloader";
export interface CaptchaProps {
onSuccess: (token?: string) => void;
onCancel: () => void;
......@@ -17,7 +20,7 @@ export function CaptchaBlock(props: CaptchaProps) {
if (!client.configuration?.features.captcha.enabled) {
props.onSuccess();
}
}, []);
}, [client.configuration?.features.captcha.enabled, props]);
if (!client.configuration?.features.captcha.enabled)
return <Preloader type="spinner" />;
......@@ -26,7 +29,7 @@ export function CaptchaBlock(props: CaptchaProps) {
<div>
<HCaptcha
sitekey={client.configuration.features.captcha.key}
onVerify={token => props.onSuccess(token)}
onVerify={(token) => props.onSuccess(token)}
/>
<div className={styles.footer}>
<a onClick={props.onCancel}>
......
import { Legal } from "./Legal";
import { Text } from "preact-i18n";
import { CheckCircle, Envelope } from "@styled-icons/boxicons-regular";
import { useForm } from "react-hook-form";
import { Link } from "react-router-dom";
import styles from "../Login.module.scss";
import { useForm } from "react-hook-form";
import { MailProvider } from "./MailProvider";
import { Text } from "preact-i18n";
import { useContext, useState } from "preact/hooks";
import { CheckCircle, Envelope } from "@styled-icons/boxicons-regular";
import { takeError } from "../../../context/revoltjs/util";
import { CaptchaBlock, CaptchaProps } from "./CaptchaBlock";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { takeError } from "../../../context/revoltjs/util";
import FormField from "../FormField";
import wideSVG from "../../../assets/wide.svg";
import Button from "../../../components/ui/Button";
import Overline from "../../../components/ui/Overline";
import Preloader from "../../../components/ui/Preloader";
import wideSVG from '../../../assets/wide.svg';
import FormField from "../FormField";
import { CaptchaBlock, CaptchaProps } from "./CaptchaBlock";
import { Legal } from "./Legal";
import { MailProvider } from "./MailProvider";
interface Props {
page: "create" | "login" | "send_reset" | "reset" | "resend";
......@@ -28,11 +30,17 @@ interface Props {
}
function getInviteCode() {
if (typeof window === 'undefined') return '';
if (typeof window === "undefined") return "";
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
return code ?? '';
const code = urlParams.get("code");
return code ?? "";
}
interface FormInputs {
email: string;
password: string;
invite: string;
}
export function Form({ page, callback }: Props) {
......@@ -43,23 +51,19 @@ export function Form({ page, callback }: Props) {
const [error, setGlobalError] = useState<string | undefined>(undefined);
const [captcha, setCaptcha] = useState<CaptchaProps | undefined>(undefined);
const { handleSubmit, register, errors, setError } = useForm({
const { handleSubmit, register, errors, setError } = useForm<FormInputs>({
defaultValues: {
email: '',
password: '',
invite: getInviteCode()
}
email: "",
password: "",
invite: getInviteCode(),
},
});
async function onSubmit(data: {
email: string;
password: string;
invite: string;
}) {
async function onSubmit(data: FormInputs) {
setGlobalError(undefined);
setLoading(true);
function onError(err: any) {
function onError(err: unknown) {
setLoading(false);
const error = takeError(err);
......@@ -81,7 +85,7 @@ export function Form({ page, callback }: Props) {
page !== "reset"
) {
setCaptcha({
onSuccess: async captcha => {
onSuccess: async (captcha) => {
setCaptcha(undefined);
try {
await callback({ ...data, captcha });
......@@ -93,7 +97,7 @@ export function Form({ page, callback }: Props) {
onCancel: () => {
setCaptcha(undefined);
setLoading(false);
}
},
});
} else {
await callback(data);
......@@ -143,7 +147,13 @@ export function Form({ page, callback }: Props) {
return (
<div className={styles.form}>
<img src={wideSVG} />
<form onSubmit={handleSubmit(onSubmit) as any}>
{/* Preact / React typing incompatabilities */}
<form
onSubmit={
handleSubmit(
onSubmit,
) as JSX.GenericEventHandler<HTMLFormElement>
}>
{page !== "reset" && (
<FormField
type="email"
......
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { useContext } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { Form } from "./Form";
export function FormCreate() {
......@@ -8,7 +10,7 @@ export function FormCreate() {
return (
<Form
page="create"
callback={async data => {
callback={async (data) => {
await client.register(import.meta.env.VITE_API_URL, data);
}}
/>
......
import { Form } from "./Form";
import { detect } from "detect-browser";
import { useContext } from "preact/hooks";
import { useHistory } from "react-router-dom";
import { useContext } from "preact/hooks";
import { OperationsContext } from "../../../context/revoltjs/RevoltClient";
import { Form } from "./Form";
export function FormLogin() {
const { login } = useContext(OperationsContext);
const history = useHistory();
......@@ -11,12 +14,16 @@ export function FormLogin() {
return (
<Form
page="login"
callback={async data => {
callback={async (data) => {
const browser = detect();
let device_name;
if (browser) {
const { name, os } = browser;
device_name = `${name} on ${os}`;
if (window.isNative) {
device_name = `Revolt Desktop on ${os}`;
} else {
device_name = `${name} on ${os}`;
}
} else {
device_name = "Unknown Device";
}
......
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { useContext } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { Form } from "./Form";
export function FormResend() {
......@@ -8,7 +10,7 @@ export function FormResend() {
return (
<Form
page="resend"
callback={async data => {
callback={async (data) => {
await client.req("POST", "/auth/resend", data);
}}
/>
......
import { Form } from "./Form";
import { useContext } from "preact/hooks";
import { useHistory, useParams } from "react-router-dom";
import { useContext } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { Form } from "./Form";
export function FormSendReset() {
const client = useContext(AppContext);
return (
<Form
page="send_reset"
callback={async data => {
callback={async (data) => {
await client.req("POST", "/auth/send_reset", data);
}}
/>
......@@ -24,10 +27,10 @@ export function FormReset() {
return (
<Form
page="reset"
callback={async data => {
await client.req("POST", "/auth/reset" as any, {
callback={async (data) => {
await client.req("POST", "/auth/reset", {
token,
...(data as any)
...data,
});
history.push("/login");
}}
......
......@@ -7,21 +7,21 @@ export function Legal() {
<a
href="https://revolt.chat/about"
target="_blank"
>
rel="noreferrer">
<Text id="general.about" />
</a>
&middot;
<a
href="https://revolt.chat/terms"
target="_blank"
>
rel="noreferrer">
<Text id="general.tos" />
</a>
&middot;
<a
href="https://revolt.chat/privacy"
target="_blank"
>
rel="noreferrer">
<Text id="general.privacy" />
</a>
</span>
......
import { Text } from "preact-i18n";
import styles from "../Login.module.scss";
import { Text } from "preact-i18n";
import Button from "../../../components/ui/Button";
interface Props {
......@@ -42,7 +43,7 @@ export function MailProvider({ email }: Props) {
return (
<div className={styles.mailProvider}>
<a href={provider[1]} target="_blank">
<a href={provider[1]} target="_blank" rel="noreferrer">
<Button>
<Text
id="login.open_mail_provider"
......
import { ListCheck, ListUl } from "@styled-icons/boxicons-regular";
import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { Text } from "preact-i18n";
import Category from "../../components/ui/Category";
import { GenericSettings } from "./GenericSettings";
import { useClient } from "../../context/revoltjs/RevoltClient";
import { getChannelName } from "../../context/revoltjs/util";
import { Route, useHistory, useParams } from "react-router-dom";
import { ListCheck, ListUl } from "@styled-icons/boxicons-regular";
import { useChannel, useForceUpdate } from "../../context/revoltjs/hooks";
import Category from "../../components/ui/Category";
import { GenericSettings } from "./GenericSettings";
import Overview from "./channel/Overview";
import Permissions from "./channel/Permissions";
export default function ChannelSettings() {
const { channel: cid } = useParams<{ channel: string; }>();
const ctx = useForceUpdate();
const channel = useChannel(cid, ctx);
if (!channel) return null;
if (channel.channel_type === 'SavedMessages' || channel.channel_type === 'DirectMessage') return null;
const { channel: cid } = useParams<{ channel: string }>();
const client = useClient();
const history = useHistory();
const channel = client.channels.get(cid);
if (!channel) return null;
if (
channel.channel_type === "SavedMessages" ||
channel.channel_type === "DirectMessage"
)
return null;
function switchPage(to?: string) {
let base_url;
switch (channel?.channel_type) {
case 'TextChannel':
case 'VoiceChannel': base_url = `/server/${channel.server}/channel/${cid}/settings`; break;
default: base_url = `/channel/${cid}/settings`;
case "TextChannel":
case "VoiceChannel":
base_url = `/server/${channel.server_id}/channel/${cid}/settings`;
break;
default:
base_url = `/channel/${cid}/settings`;
}
if (to) {
......@@ -36,27 +48,44 @@ export default function ChannelSettings() {
<GenericSettings
pages={[
{
category: <Category variant="uniform" text={getChannelName(ctx.client, channel, true)} />,
id: 'overview',
category: (
<Category
variant="uniform"
text={getChannelName(channel, true)}
/>
),
id: "overview",
icon: <ListUl size={20} />,
title: <Text id="app.settings.channel_pages.overview.title" />
title: (
<Text id="app.settings.channel_pages.overview.title" />
),
},
{
id: 'permissions',
id: "permissions",
icon: <ListCheck size={20} />,
title: <Text id="app.settings.channel_pages.permissions.title" />
}
title: (
<Text id="app.settings.channel_pages.permissions.title" />
),
},
]}
children={[
<Route path="/server/:server/channel/:channel/settings/permissions"><Permissions channel={channel} /></Route>,
<Route path="/channel/:channel/settings/permissions"><Permissions channel={channel} /></Route>,
children={
<Switch>
<Route path="/server/:server/channel/:channel/settings/permissions">
<Permissions channel={channel} />
</Route>
<Route path="/channel/:channel/settings/permissions">
<Permissions channel={channel} />
</Route>
<Route path="/"><Overview channel={channel} /></Route>
]}
<Route>
<Overview channel={channel} />
</Route>
</Switch>
}
category="channel_pages"
switchPage={switchPage}
defaultPage="overview"
showExitButton
/>
)
);
}
import { Text } from "preact-i18n";
import { ArrowBack, X } from "@styled-icons/boxicons-regular";
import { Helmet } from "react-helmet";
import { useHistory, useParams } from "react-router-dom";
import styles from "./Settings.module.scss";
import { Children } from "../../types/Preact";
import Header from '../../components/ui/Header';
import classNames from "classnames";
import { Text } from "preact-i18n";
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { ThemeContext } from "../../context/Theme";
import Category from '../../components/ui/Category';
import { useContext, useEffect } from "preact/hooks";
import Category from "../../components/ui/Category";
import Header from "../../components/ui/Header";
import IconButton from "../../components/ui/IconButton";
import LineDivider from "../../components/ui/LineDivider";
import { Switch, useHistory, useParams } from "react-router-dom";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import ButtonItem from "../../components/navigation/items/ButtonItem";
import { ArrowBack, X, XCircle } from "@styled-icons/boxicons-regular";
import { Children } from "../../types/Preact";
interface Props {
pages: {
category?: Children
divider?: boolean
id: string
icon: Children
title: Children
hideTitle?: boolean
}[]
custom?: Children
children: Children
defaultPage: string
showExitButton?: boolean
switchPage: (to?: string) => void
category: 'pages' | 'channel_pages' | 'server_pages'
category?: Children;
divider?: boolean;
id: string;
icon: Children;
title: Children;
hidden?: boolean;
hideTitle?: boolean;
}[];
custom?: Children;
children: Children;
defaultPage: string;
showExitButton?: boolean;
switchPage: (to?: string) => void;
category: "pages" | "channel_pages" | "server_pages";
}
export function GenericSettings({ pages, switchPage, category, custom, children, defaultPage, showExitButton }: Props) {
export function GenericSettings({
pages,
switchPage,
category,
custom,
children,
defaultPage,
showExitButton,
}: Props) {
const history = useHistory();
const theme = useContext(ThemeContext);
const { page } = useParams<{ page: string; }>();
const { page } = useParams<{ page: string }>();
const [closing, setClosing] = useState(false);
const exitSettings = useCallback(() => {
if (history.length > 1) {
setClosing(true);
function exitSettings() {
if (history.length > 0) {
history.goBack();
setTimeout(() => {
history.goBack();
}, 100);
} else {
history.push('/');
history.push("/");
}
}
}, [history]);
useEffect(() => {
function keyDown(e: KeyboardEvent) {
......@@ -52,16 +72,21 @@ export function GenericSettings({ pages, switchPage, category, custom, children,
document.body.addEventListener("keydown", keyDown);
return () => document.body.removeEventListener("keydown", keyDown);
}, []);
}, [exitSettings]);
return (
<div className={styles.settings} data-mobile={isTouchscreenDevice}>
<div
className={classNames(styles.settings, {
[styles.closing]: closing,
[styles.native]: window.isNative,
})}
data-mobile={isTouchscreenDevice}>
<Helmet>
<meta
name="theme-color"
content={
isTouchscreenDevice
? theme["primary-header"]
? theme["background"]
: theme["secondary-background"]
}
/>
......@@ -70,16 +95,23 @@ export function GenericSettings({ pages, switchPage, category, custom, children,
<Header placement="primary">
{typeof page === "undefined" ? (
<>
{ showExitButton &&
{showExitButton && (
<IconButton onClick={exitSettings}>
<X size={24} />
</IconButton> }
<X
size={27}
style={{ marginInlineEnd: "8px" }}
/>
</IconButton>
)}
<Text id="app.settings.title" />
</>
) : (
<>
<IconButton onClick={() => switchPage()}>
<ArrowBack size={24} />
<ArrowBack
size={24}
style={{ marginInlineEnd: "10px" }}
/>
</IconButton>
<Text
id={`app.settings.${category}.${page}.title`}
......@@ -90,43 +122,65 @@ export function GenericSettings({ pages, switchPage, category, custom, children,
)}
{(!isTouchscreenDevice || typeof page === "undefined") && (
<div className={styles.sidebar}>
<div className={styles.container}>
{
pages.map((entry, i) =>
<>
{ entry.category && <Category variant="uniform" text={entry.category} /> }
<ButtonItem
active={page === entry.id || (i === 0 && !isTouchscreenDevice && typeof page === "undefined")}
onClick={() => switchPage(entry.id)}
compact
>{entry.icon} {entry.title}</ButtonItem>
{ entry.divider && <LineDivider /> }
</>
)
}
{ custom }
<div className={styles.scrollbox}>
<div className={styles.container}>
{pages.map((entry, i) =>
entry.hidden ? undefined : (
<>
{entry.category && (
<Category
variant="uniform"
text={entry.category}
/>
)}
<ButtonItem
active={
page === entry.id ||
(i === 0 &&
!isTouchscreenDevice &&
typeof page === "undefined")
}
onClick={() => switchPage(entry.id)}
compact>
{entry.icon} {entry.title}
</ButtonItem>
{entry.divider && <LineDivider />}
</>
),
)}
{custom}
</div>
</div>
</div>
)}
{(!isTouchscreenDevice || typeof page === "string") && (
<div className={styles.content}>
{!isTouchscreenDevice && !(pages.find(x => x.id === page && x.hideTitle)) && (
<h1>
<Text
id={`app.settings.${category}.${page ?? defaultPage}.title`}
/>
</h1>
)}
<Switch>
{ children }
</Switch>
</div>
)}
{!isTouchscreenDevice && (
<div className={styles.action}>
<IconButton onClick={exitSettings}>
<XCircle size={48} />
</IconButton>
<div className={styles.scrollbox}>
<div className={styles.contentcontainer}>
{!isTouchscreenDevice &&
!pages.find(
(x) => x.id === page && x.hideTitle,
) && (
<h1>
<Text
id={`app.settings.${category}.${
page ?? defaultPage
}.title`}
/>
</h1>
)}
{children}
</div>
{!isTouchscreenDevice && (
<div className={styles.action}>
<div
onClick={exitSettings}
className={styles.closeButton}>
<X size={28} />
</div>
</div>
)}
</div>
</div>
)}
</div>
......
import { ListUl, ListCheck, ListMinus } from "@styled-icons/boxicons-regular";
import { XSquare, Share, Group } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { Text } from "preact-i18n";
import Category from "../../components/ui/Category";
import { GenericSettings } from "./GenericSettings";
import { useServer } from "../../context/revoltjs/hooks";
import { Route, useHistory, useParams } from "react-router-dom";
import { ListUl, Share, Group, ListCheck } from "@styled-icons/boxicons-regular";
import { XSquare } from "@styled-icons/boxicons-solid";
import RequiresOnline from "../../context/revoltjs/RequiresOnline";
import { useClient } from "../../context/revoltjs/RevoltClient";
import { Overview } from "./server/Overview";
import { Members } from "./server/Members";
import { Invites } from "./server/Invites";
import Category from "../../components/ui/Category";
import { GenericSettings } from "./GenericSettings";
import { Bans } from "./server/Bans";
import { Categories } from "./server/Categories";
import { Invites } from "./server/Invites";
import { Members } from "./server/Members";
import { Overview } from "./server/Overview";
import { Roles } from "./server/Roles";
export default function ServerSettings() {
const { server: sid } = useParams<{ server: string; }>();
const server = useServer(sid);
export default observer(() => {
const { server: sid } = useParams<{ server: string }>();
const client = useClient();
const server = client.servers.get(sid);
if (!server) return null;
const history = useHistory();
......@@ -31,44 +37,80 @@ export default function ServerSettings() {
<GenericSettings
pages={[
{
category: <Category variant="uniform" text={server.name} />, //TOFIX: Just add the server.name as a string, otherwise it makes a duplicate category
id: 'overview',
category: <Category variant="uniform" text={server.name} />,
id: "overview",
icon: <ListUl size={20} />,
title: <Text id="app.settings.server_pages.overview.title" />
title: (
<Text id="app.settings.server_pages.overview.title" />
),
},
{
id: 'members',
id: "categories",
icon: <ListMinus size={20} />,
title: (
<Text id="app.settings.server_pages.categories.title" />
),
},
{
id: "members",
icon: <Group size={20} />,
title: <Text id="app.settings.server_pages.members.title" />
title: (
<Text id="app.settings.server_pages.members.title" />
),
},
{
id: 'invites',
id: "invites",
icon: <Share size={20} />,
title: <Text id="app.settings.server_pages.invites.title" />
title: (
<Text id="app.settings.server_pages.invites.title" />
),
},
{
id: 'bans',
id: "bans",
icon: <XSquare size={20} />,
title: <Text id="app.settings.server_pages.bans.title" />
title: <Text id="app.settings.server_pages.bans.title" />,
},
{
id: 'roles',
id: "roles",
icon: <ListCheck size={20} />,
title: <Text id="app.settings.server_pages.roles.title" />,
hideTitle: true
}
]}
children={[
<Route path="/server/:server/settings/members"><RequiresOnline><Members server={server} /></RequiresOnline></Route>,
<Route path="/server/:server/settings/invites"><RequiresOnline><Invites server={server} /></RequiresOnline></Route>,
<Route path="/server/:server/settings/bans"><RequiresOnline><Bans server={server} /></RequiresOnline></Route>,
<Route path="/server/:server/settings/roles"><RequiresOnline><Roles server={server} /></RequiresOnline></Route>,
<Route path="/"><Overview server={server} /></Route>
hideTitle: true,
},
]}
children={
<Switch>
<Route path="/server/:server/settings/categories">
<Categories server={server} />
</Route>
<Route path="/server/:server/settings/members">
<RequiresOnline>
<Members server={server} />
</RequiresOnline>
</Route>
<Route path="/server/:server/settings/invites">
<RequiresOnline>
<Invites server={server} />
</RequiresOnline>
</Route>
<Route path="/server/:server/settings/bans">
<RequiresOnline>
<Bans server={server} />
</RequiresOnline>
</Route>
<Route path="/server/:server/settings/roles">
<RequiresOnline>
<Roles server={server} />
</RequiresOnline>
</Route>
<Route>
<Overview server={server} />
</Route>
</Switch>
}
category="server_pages"
switchPage={switchPage}
defaultPage="overview"
showExitButton
/>
)
}
);
});
/* Settings animations */
@keyframes open {
0% {transform: scale(1.2);};
100% {transform: scale(1);};
0% {
transform: scale(1.2);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes opacity {
0% {opacity: 0;};
20% {opacity: .5;}
50% {opacity: 1;}
@keyframes close {
0% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(1.2);
opacity: 0;
}
}
@keyframes close {
0% {transform: scale(1); opacity: 1;};
100% {transform: scale(1.2); opacity: 0;};
@keyframes opacity {
0% {
opacity: 0;
}
20% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
/* Settings CSS */
.settings[data-mobile="true"] {
flex-direction: column;
background: var(--primary-header);
.sidebar, .content {
.sidebar,
.content {
background: var(--primary-background);
}
.scrollbox {
&::-webkit-scrollbar-thumb {
border-top: none;
}
}
/* Sidebar */
.sidebar {
justify-content: flex-start;
overflow-y: auto;
.container {
padding: 20px 8px;
padding: 20px 8px calc(var(--bottom-navigation-height) + 30px);
min-width: 218px;
}
> div {
.scrollbox {
width: 100%;
}
.version {
place-items: center;
}
}
/* Content */
.content {
padding: 10px 12px 50px;
padding: 0;
.scrollbox {
overflow: auto;
}
.contentcontainer {
max-width: unset !important;
padding: 16px 12px var(--bottom-navigation-height) !important;
}
}
}
......@@ -52,8 +88,11 @@
width: 100%;
height: 100%;
position: fixed;
animation: open .18s ease-out,
opacity .18s;
animation: open 0.18s ease-out, opacity 0.18s;
&.closing {
animation: close 0.18s ease-in;
}
}
.settings {
......@@ -61,20 +100,40 @@
display: flex;
user-select: none;
flex-direction: row;
justify-content: center;
background: var(--primary-background);
.scrollbox {
overflow-y: scroll;
visibility: hidden;
transition: visibility 0.1s;
}
.container,
.contentcontainer,
.scrollbox:hover,
.scrollbox:focus {
visibility: visible;
}
// All children receive custom scrollbar.
> * > ::-webkit-scrollbar-thumb {
width: 4px;
background-clip: content-box;
border-top: 80px solid transparent;
}
.sidebar {
flex: 2;
flex: 1 0 218px;
display: flex;
flex-shrink: 0;
overflow-y: scroll;
justify-content: flex-end;
background: var(--secondary-background);
.container {
width: 218px;
padding: 60px 8px;
min-width: 218px;
padding: 80px 8px;
display: flex;
gap: 2px;
flex-direction: column;
}
.divider {
......@@ -84,20 +143,17 @@
.donate {
color: goldenrod !important;
}
.logOut {
color: var(--error) !important;
}
.version {
margin: 1rem 12px 0;
font-size: 10px;
font-size: 0.625rem;
color: var(--secondary-foreground);
font-family: "Fira Mono", monospace;
font-family: var(--monospace-font), monospace;
user-select: text;
display: grid;
//place-items: center;
> div {
gap: 2px;
......@@ -105,87 +161,118 @@
flex-direction: column;
}
.revision a:hover {
a:hover {
text-decoration: underline;
}
}
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
.content {
flex: 3;
max-width: 740px;
padding: 60px 2em;
overflow-y: scroll;
overflow-x: hidden;
flex: 1 1 800px;
display: flex;
overflow-y: auto;
.scrollbox {
display: flex;
flex-grow: 1;
}
.contentcontainer {
display: flex;
gap: 13px;
height: fit-content;
max-width: 740px;
padding: 80px 32px;
width: 100%;
flex-direction: column;
}
details {
margin: 14px 0;
}
h1 {
margin-top: 0;
line-height: 1em;
font-size: 1.2em;
margin: 0;
line-height: 1rem;
font-size: 1.2rem;
font-weight: 600;
}
h3 {
font-size: 13px;
font-size: 0.8125rem;
text-transform: uppercase;
color: var(--secondary-foreground);
&:first-child {
margin-top: 0;
}
}
h4 {
margin: 4px 2px;
font-size: 13px;
font-size: 0.8125rem;
color: var(--tertiary-foreground);
text-transform: uppercase;
}
h5 {
margin-top: 0;
font-size: 0.75rem;
font-weight: 400;
}
.footer {
border-top: 1px solid;
margin: 0;
padding-top: 5px;
font-size: 14px;
font-size: 0.875rem;
color: var(--secondary-foreground);
}
}
.action {
flex: 1;
flex-shrink: 0;
padding: 60px 8px;
color: var(--tertiary-background);
flex-grow: 1;
padding: 80px 8px;
visibility: visible;
position: sticky;
top: 0;
&:after {
content: "ESC";
margin-top: 4px;
display: flex;
text-align: center;
align-content: center;
justify-content: center;
position: relative;
color: var(--foreground);
width: 48px;
opacity: .5;
font-size: .75em;
}
> div {
display: inline;
> svg {
&:active {
transform: translateY(2px);
}
width: 40px;
opacity: 0.5;
font-size: 0.75rem;
}
.closeButton {
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
height: 40px;
width: 40px;
border: 3px solid var(--tertiary-background);
cursor: pointer;
svg {
color: var(--secondary-foreground);
}
&:hover {
background: var(--secondary-header);
}
&:active {
transform: translateY(2px);
}
}
}
}
.loader {
> div {
margin: auto;
@media (pointer: coarse) {
.scrollbox {
visibility: visible !important;
overflow-y: auto;
}
}
import { Text } from "preact-i18n";
import { Sync } from "./panes/Sync";
import { useContext } from "preact/hooks";
import styles from "./Settings.module.scss";
import { LIBRARY_VERSION } from "revolt.js";
import { APP_VERSION } from "../../version";
import { GenericSettings } from "./GenericSettings";
import { Route, useHistory } from "react-router-dom";
import { Gitlab } from "@styled-icons/boxicons-logos";
import {
Sync as SyncIcon,
Globe,
LogOut,
Desktop,
} from "@styled-icons/boxicons-regular";
import {
Bell,
Palette,
Coffee,
Globe,
IdCard,
LogOut,
Sync as SyncIcon,
Shield,
Vial,
User
} from "@styled-icons/boxicons-regular";
import { Brush, Megaphone } from "@styled-icons/boxicons-solid";
import { Gitlab } from "@styled-icons/boxicons-logos";
import { GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../revision";
import LineDivider from "../../components/ui/LineDivider";
CheckShield,
Flask,
User,
Megaphone,
} from "@styled-icons/boxicons-solid";
import { Route, Switch, useHistory } from "react-router-dom";
import { LIBRARY_VERSION } from "revolt.js";
import styles from "./Settings.module.scss";
import { Text } from "preact-i18n";
import { useContext } from "preact/hooks";
import RequiresOnline from "../../context/revoltjs/RequiresOnline";
import ButtonItem from "../../components/navigation/items/ButtonItem";
import { AppContext, OperationsContext } from "../../context/revoltjs/RevoltClient";
import {
AppContext,
OperationsContext,
} from "../../context/revoltjs/RevoltClient";
import LineDivider from "../../components/ui/LineDivider";
import ButtonItem from "../../components/navigation/items/ButtonItem";
import { GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../revision";
import { APP_VERSION } from "../../version";
import { GenericSettings } from "./GenericSettings";
import { Account } from "./panes/Account";
import { Profile } from "./panes/Profile";
import { Sessions } from "./panes/Sessions";
import { Appearance } from "./panes/Appearance";
import { ExperimentsPage } from "./panes/Experiments";
import { Feedback } from "./panes/Feedback";
import { Languages } from "./panes/Languages";
import { Appearance } from "./panes/Appearance";
import { Native } from "./panes/Native";
import { Notifications } from "./panes/Notifications";
import { ExperimentsPage } from "./panes/Experiments";
import { Profile } from "./panes/Profile";
import { Sessions } from "./panes/Sessions";
import { Sync } from "./panes/Sync";
export default function Settings() {
const history = useHistory();
const client = useContext(AppContext);
const operations = useContext(OperationsContext);
function switchPage(to?: string) {
if (to) {
history.replace(`/settings/${to}`);
......@@ -52,112 +62,165 @@ export default function Settings() {
<GenericSettings
pages={[
{
category: <Text id="app.settings.categories.user_settings" />,
id: 'account',
category: (
<Text id="app.settings.categories.user_settings" />
),
id: "account",
icon: <User size={20} />,
title: <Text id="app.settings.pages.account.title" />
title: <Text id="app.settings.pages.account.title" />,
},
{
id: 'profile',
id: "profile",
icon: <IdCard size={20} />,
title: <Text id="app.settings.pages.profile.title" />
title: <Text id="app.settings.pages.profile.title" />,
},
{
id: 'sessions',
icon: <Shield size={20} />,
title: <Text id="app.settings.pages.sessions.title" />
id: "sessions",
icon: <CheckShield size={20} />,
title: <Text id="app.settings.pages.sessions.title" />,
},
{
category: <Text id="app.settings.categories.client_settings" />,
id: 'appearance',
category: (
<Text id="app.settings.categories.client_settings" />
),
id: "appearance",
icon: <Palette size={20} />,
title: <Text id="app.settings.pages.appearance.title" />
title: <Text id="app.settings.pages.appearance.title" />,
},
{
id: 'notifications',
id: "notifications",
icon: <Bell size={20} />,
title: <Text id="app.settings.pages.notifications.title" />
title: <Text id="app.settings.pages.notifications.title" />,
},
{
id: 'language',
id: "language",
icon: <Globe size={20} />,
title: <Text id="app.settings.pages.language.title" />
title: <Text id="app.settings.pages.language.title" />,
},
{
id: 'sync',
id: "sync",
icon: <SyncIcon size={20} />,
title: <Text id="app.settings.pages.sync.title" />
title: <Text id="app.settings.pages.sync.title" />,
},
{
id: "native",
hidden: !window.isNative,
icon: <Desktop size={20} />,
title: <Text id="app.settings.pages.native.title" />,
},
{
divider: true,
id: 'experiments',
icon: <Vial size={20} />,
title: <Text id="app.settings.pages.experiments.title" />
id: "experiments",
icon: <Flask size={20} />,
title: <Text id="app.settings.pages.experiments.title" />,
},
{
id: 'feedback',
id: "feedback",
icon: <Megaphone size={20} />,
title: <Text id="app.settings.pages.feedback.title" />
}
]}
children={[
<Route path="/settings/profile"><Profile /></Route>,
<Route path="/settings/sessions">
<RequiresOnline><Sessions /></RequiresOnline>
</Route>,
<Route path="/settings/appearance"><Appearance /></Route>,
<Route path="/settings/notifications"><Notifications /></Route>,
<Route path="/settings/language"><Languages /></Route>,
<Route path="/settings/sync"><Sync /></Route>,
<Route path="/settings/experiments"><ExperimentsPage /></Route>,
<Route path="/settings/feedback"><Feedback /></Route>,
<Route path="/"><Account /></Route>
title: <Text id="app.settings.pages.feedback.title" />,
},
]}
children={
<Switch>
<Route path="/settings/profile">
<Profile />
</Route>
<Route path="/settings/sessions">
<RequiresOnline>
<Sessions />
</RequiresOnline>
</Route>
<Route path="/settings/appearance">
<Appearance />
</Route>
<Route path="/settings/notifications">
<Notifications />
</Route>
<Route path="/settings/language">
<Languages />
</Route>
<Route path="/settings/sync">
<Sync />
</Route>
<Route path="/settings/native">
<Native />
</Route>
<Route path="/settings/experiments">
<ExperimentsPage />
</Route>
<Route path="/settings/feedback">
<Feedback />
</Route>
<Route path="/">
<Account />
</Route>
</Switch>
}
defaultPage="account"
switchPage={switchPage}
category="pages"
custom={[
<a
href="https://gitlab.insrt.uk/revolt"
target="_blank"
>
<ButtonItem compact>
<Gitlab size={20} />
<Text id="app.settings.pages.source_code" />
</ButtonItem>
</a>,
<a href="https://ko-fi.com/insertish" target="_blank">
<ButtonItem className={styles.donate} compact>
<Coffee size={20} />
<Text id="app.settings.pages.donate.title" />
custom={
<>
<a
href="https://gitlab.insrt.uk/revolt"
target="_blank"
rel="noreferrer">
<ButtonItem compact>
<Gitlab size={20} />
<Text id="app.settings.pages.source_code" />
</ButtonItem>
</a>
<a
href="https://insrt.uk/donate"
target="_blank"
rel="noreferrer">
<ButtonItem className={styles.donate} compact>
<Coffee size={20} />
<Text id="app.settings.pages.donate.title" />
</ButtonItem>
</a>
<LineDivider />
<ButtonItem
onClick={() => operations.logout()}
className={styles.logOut}
compact>
<LogOut size={20} />
<Text id="app.settings.pages.logOut" />
</ButtonItem>
</a>,
<LineDivider />,
<ButtonItem
onClick={() => operations.logout()}
className={styles.logOut}
compact
>
<LogOut size={20} />
<Text id="app.settings.pages.logOut" />
</ButtonItem>,
<div className={styles.version}>
<div>
<div className={styles.version}>
<span className={styles.revision}>
<a href={`${REPO_URL}/${GIT_REVISION}`} target="_blank">
{ GIT_REVISION.substr(0, 7) }
<a
href={`${REPO_URL}/${GIT_REVISION}`}
target="_blank"
rel="noreferrer">
{GIT_REVISION.substr(0, 7)}
</a>
{` `}
<a href={GIT_BRANCH !== 'DETACHED' ? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}` : undefined} target="_blank">
({ GIT_BRANCH })
<a
href={
GIT_BRANCH !== "DETACHED"
? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}`
: undefined
}
target="_blank"
rel="noreferrer">
({GIT_BRANCH})
</a>
</span>
<span>{ GIT_BRANCH === 'production' ? 'Stable' : 'Nightly' } {APP_VERSION}</span>
<span>API: {client.configuration?.revolt ?? "N/A"}</span>
<span>
{GIT_BRANCH === "production" ? "Stable" : "Nightly"}{" "}
{APP_VERSION}
</span>
{window.isNative && (
<span>Native: {window.nativeVersion}</span>
)}
<span>
API: {client.configuration?.revolt ?? "N/A"}
</span>
<span>revolt.js: {LIBRARY_VERSION}</span>
</div>
</div>
]}
</>
}
/>
)
);
}
<svg xmlns="http://www.w3.org/2000/svg" width="323" height="202" viewBox="0 0 323 202">
<g id="Dark" transform="translate(-225 -535)">
<g id="Group_188" data-name="Group 188">
<rect id="Rectangle_207" data-name="Rectangle 207" width="323" height="202" rx="6" transform="translate(225 535)" fill="#333234"/>
<path id="Rectangle_209" data-name="Rectangle 209" d="M6,0H95a0,0,0,0,1,0,0V202a0,0,0,0,1,0,0H6a6,6,0,0,1-6-6V6A6,6,0,0,1,6,0Z" transform="translate(225 535)" fill="#212121"/>
<g id="Group_195" data-name="Group 195" transform="translate(-345 1)">
<g id="Group_196" data-name="Group 196" transform="translate(0 -6)">
<rect id="Rectangle_221" data-name="Rectangle 221" width="81" height="15" rx="2" transform="translate(577 560)" fill="#373737"/>
<rect id="Rectangle_217" data-name="Rectangle 217" width="22" height="4" rx="1" transform="translate(577 602)" fill="#efefef"/>
<rect id="Rectangle_231" data-name="Rectangle 231" width="29" height="4" rx="1" transform="translate(591 566)" fill="#fff"/>
<g id="Group_198" data-name="Group 198">
<rect id="Rectangle_226" data-name="Rectangle 226" width="81" height="15" rx="2" transform="translate(577 580)" fill="#212121"/>
<rect id="Rectangle_232" data-name="Rectangle 232" width="30" height="4" rx="1" transform="translate(613 586)" fill="#fff"/>
<rect id="Rectangle_233" data-name="Rectangle 233" width="20" height="4" rx="1" transform="translate(591 586)" fill="#fff"/>
</g>
<g id="Voice_Channel" data-name="Voice Channel">
<rect id="Rectangle_227" data-name="Rectangle 227" width="81" height="15" rx="2" transform="translate(577 614)" fill="#36ad93"/>
<rect id="Rectangle_230" data-name="Rectangle 230" width="16" height="4" rx="1" transform="translate(591 620)" fill="#fff"/>
<g id="Connected_Users" data-name="Connected Users">
<circle id="Ellipse_50" data-name="Ellipse 50" cx="4.5" cy="4.5" r="4.5" transform="translate(630 617)" fill="#8b8b8b"/>
<circle id="Ellipse_51" data-name="Ellipse 51" cx="4.5" cy="4.5" r="4.5" transform="translate(635 617)" fill="#b7b7b7"/>
<circle id="Ellipse_52" data-name="Ellipse 52" cx="4.5" cy="4.5" r="4.5" transform="translate(641 617)" fill="#e8e8e8"/>
</g>
</g>
<g id="Group_199" data-name="Group 199" transform="translate(0 54)">
<rect id="Rectangle_226-2" data-name="Rectangle 226" width="81" height="15" rx="2" transform="translate(577 580)" fill="#212121"/>
<rect id="Rectangle_234" data-name="Rectangle 234" width="25" height="4" rx="1" transform="translate(591 586)" fill="#fff"/>
</g>
<g id="Group_200" data-name="Group 200" transform="translate(0 74)">
<rect id="Rectangle_226-3" data-name="Rectangle 226" width="81" height="15" rx="2" transform="translate(577 580)" fill="#212121"/>
<rect id="Rectangle_235" data-name="Rectangle 235" width="15" height="4" rx="1" transform="translate(629 586)" fill="#fff"/>
<rect id="Rectangle_236" data-name="Rectangle 236" width="36" height="4" rx="1" transform="translate(591 586)" fill="#fff"/>
</g>
<g id="Group_201" data-name="Group 201" transform="translate(0 94)">
<rect id="Rectangle_226-4" data-name="Rectangle 226" width="81" height="15" rx="2" transform="translate(577 580)" fill="#212121"/>
<rect id="Rectangle_237" data-name="Rectangle 237" width="20" height="4" rx="1" transform="translate(591 586)" fill="#fff"/>
<rect id="Rectangle_238" data-name="Rectangle 238" width="30" height="4" rx="1" transform="translate(613 586)" fill="#fff"/>
</g>
</g>
<path id="Rectangle_224" data-name="Rectangle 224" d="M0,0H95a0,0,0,0,1,0,0V51a0,0,0,0,1,0,0H6a6,6,0,0,1-6-6V0A0,0,0,0,1,0,0Z" transform="translate(570 685)" fill="#404040"/>
</g>
</g>
<g id="Group_187" data-name="Group 187" transform="translate(0 -9)">
<circle id="Ellipse_49" data-name="Ellipse 49" cx="10.5" cy="10.5" r="10.5" transform="translate(336 681)" fill="#686868"/>
<rect id="Rectangle_210" data-name="Rectangle 210" width="34" height="8" rx="2" transform="translate(365 681)" fill="#e8e8e8"/>
<rect id="Rectangle_211" data-name="Rectangle 211" width="28" height="8" rx="2" transform="translate(404 681)" fill="#676767"/>
<rect id="Rectangle_212" data-name="Rectangle 212" width="24" height="6" rx="2" transform="translate(365 696)" fill="#fff"/>
<rect id="Rectangle_213" data-name="Rectangle 213" width="47" height="6" rx="2" transform="translate(393 696)" fill="#fff"/>
<rect id="Rectangle_214" data-name="Rectangle 214" width="55" height="6" rx="2" transform="translate(444 696)" fill="#fff"/>
</g>
<g id="Group_189" data-name="Group 189">
<line id="Line_18" data-name="Line 18" x2="191" transform="translate(335.5 656.5)" fill="none" stroke="#707070" stroke-linecap="round" stroke-width="1" opacity="0.5"/>
</g>
<g id="Group_192" data-name="Group 192" transform="translate(0 -83)">
<rect id="Rectangle_210-2" data-name="Rectangle 210" width="34" height="8" rx="2" transform="translate(365 681)" fill="#e8e8e8"/>
<rect id="Rectangle_211-2" data-name="Rectangle 211" width="28" height="8" rx="2" transform="translate(404 681)" fill="#676767"/>
<rect id="Rectangle_212-2" data-name="Rectangle 212" width="81" height="6" rx="2" transform="translate(365 696)" fill="#68abee"/>
<g id="Group_194" data-name="Group 194" transform="translate(0 2)">
<rect id="Rectangle_219" data-name="Rectangle 219" width="23" height="6" rx="2" transform="translate(372 707)" fill="#68abee"/>
<rect id="Rectangle_220" data-name="Rectangle 220" width="103" height="6" rx="2" transform="translate(372 718)" fill="#888"/>
<line id="Line_35" data-name="Line 35" y2="17" transform="translate(365.5 707.5)" fill="none" stroke="#707070" stroke-linecap="round" stroke-width="1"/>
</g>
<circle id="Ellipse_49-2" data-name="Ellipse 49" cx="10.5" cy="10.5" r="10.5" transform="translate(336 681)" fill="#686868"/>
</g>
<g id="Group_193" data-name="Group 193" transform="translate(0 -133)">
<circle id="Ellipse_49-3" data-name="Ellipse 49" cx="10.5" cy="10.5" r="10.5" transform="translate(336 681)" fill="#686868"/>
<rect id="Rectangle_210-3" data-name="Rectangle 210" width="34" height="8" rx="2" transform="translate(365 681)" fill="#e8e8e8"/>
<rect id="Rectangle_211-3" data-name="Rectangle 211" width="28" height="8" rx="2" transform="translate(404 681)" fill="#676767"/>
<rect id="Rectangle_212-3" data-name="Rectangle 212" width="95" height="6" rx="2" transform="translate(365 696)" fill="#fff"/>
<rect id="Rectangle_213-2" data-name="Rectangle 213" width="29" height="6" rx="2" transform="translate(365 709)" fill="#fff"/>
<rect id="Rectangle_218" data-name="Rectangle 218" width="50" height="6" rx="2" transform="translate(398 709)" fill="#fff"/>
<rect id="Rectangle_214-2" data-name="Rectangle 214" width="68" height="6" rx="2" transform="translate(464 696)" fill="#fff"/>
</g>
<rect id="Rectangle_228" data-name="Rectangle 228" width="191" height="18" rx="4" transform="translate(336 709)" fill="#434343"/>
<g id="Rectangle_229" data-name="Rectangle 229" transform="translate(506 709)" fill="#707070" stroke="#707070" stroke-width="1">
<path d="M0,0H17a4,4,0,0,1,4,4V14a4,4,0,0,1-4,4H0a0,0,0,0,1,0,0V0A0,0,0,0,1,0,0Z" stroke="none"/>
<path d="M1,.5H17A3.5,3.5,0,0,1,20.5,4V14A3.5,3.5,0,0,1,17,17.5H1A.5.5,0,0,1,.5,17V1A.5.5,0,0,1,1,.5Z" fill="none"/>
</g>
</g>
</svg>
<svg width="323" height="202" viewBox="0 0 323 202" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="323" height="202" fill="#191919"/>
<path d="M27 14C27 11.7909 28.7909 10 31 10H90V202H31C28.7909 202 27 200.209 27 198V14Z" fill="#1E1E1E"/>
<rect x="90" y="10" width="233" height="192" fill="#242424"/>
<rect x="90" y="10" width="233" height="18" fill="#363636"/>
<rect x="97" y="16" width="30" height="6" rx="2" fill="#DEDEDE"/>
<path d="M106.517 163.445C111.534 163.445 115.601 159.378 115.601 154.361C115.601 149.344 111.534 145.277 106.517 145.277C101.5 145.277 97.4326 149.344 97.4326 154.361C97.4326 159.378 101.5 163.445 106.517 163.445Z" fill="#686868"/>
<path d="M150.206 145.277H124.252C123.296 145.277 122.522 146.052 122.522 147.008V150.468C122.522 151.424 123.296 152.198 124.252 152.198H150.206C151.162 152.198 151.936 151.424 151.936 150.468V147.008C151.936 146.052 151.162 145.277 150.206 145.277Z" fill="#E8E8E8"/>
<path d="M178.756 145.277H157.992C157.037 145.277 156.262 146.052 156.262 147.008V150.468C156.262 151.424 157.037 152.198 157.992 152.198H178.756C179.711 152.198 180.486 151.424 180.486 150.468V147.008C180.486 146.052 179.711 145.277 178.756 145.277Z" fill="#676767"/>
<path d="M141.555 158.255H124.252C123.296 158.255 122.522 159.029 122.522 159.985V161.715C122.522 162.671 123.296 163.445 124.252 163.445H141.555C142.51 163.445 143.285 162.671 143.285 161.715V159.985C143.285 159.029 142.51 158.255 141.555 158.255Z" fill="white"/>
<path d="M185.677 158.255H148.476C147.52 158.255 146.746 159.029 146.746 159.985V161.715C146.746 162.671 147.52 163.445 148.476 163.445H185.677C186.632 163.445 187.407 162.671 187.407 161.715V159.985C187.407 159.029 186.632 158.255 185.677 158.255Z" fill="white"/>
<path d="M236.72 158.255H192.598C191.642 158.255 190.868 159.029 190.868 159.985V161.715C190.868 162.671 191.642 163.445 192.598 163.445H236.72C237.676 163.445 238.45 162.671 238.45 161.715V159.985C238.45 159.029 237.676 158.255 236.72 158.255Z" fill="white"/>
<path opacity="0.5" d="M97 131.868H262.242" stroke="#707070" stroke-width="0.86514" stroke-linecap="round"/>
<path d="M150.206 81.257H124.252C123.296 81.257 122.522 82.0316 122.522 82.9872V86.4478C122.522 87.4034 123.296 88.1781 124.252 88.1781H150.206C151.162 88.1781 151.936 87.4034 151.936 86.4478V82.9872C151.936 82.0316 151.162 81.257 150.206 81.257Z" fill="#E8E8E8"/>
<path d="M178.756 81.257H157.992C157.037 81.257 156.262 82.0316 156.262 82.9872V86.4478C156.262 87.4034 157.037 88.1781 157.992 88.1781H178.756C179.711 88.1781 180.486 87.4034 180.486 86.4478V82.9872C180.486 82.0316 179.711 81.257 178.756 81.257Z" fill="#676767"/>
<path d="M190.868 94.2341H124.252C123.296 94.2341 122.522 95.0088 122.522 95.9644V97.6947C122.522 98.6503 123.296 99.425 124.252 99.425H190.868C191.823 99.425 192.598 98.6503 192.598 97.6947V95.9644C192.598 95.0088 191.823 94.2341 190.868 94.2341Z" fill="#68ABEE"/>
<path d="M146.746 106.708H130.308C129.352 106.708 128.578 107.483 128.578 108.439V110.169C128.578 111.124 129.352 111.899 130.308 111.899H146.746C147.701 111.899 148.476 111.124 148.476 110.169V108.439C148.476 107.483 147.701 106.708 146.746 106.708Z" fill="#68ABEE"/>
<path d="M215.957 114.997H130.308C129.352 114.997 128.578 115.772 128.578 116.728V118.458C128.578 119.413 129.352 120.188 130.308 120.188H215.957C216.912 120.188 217.687 119.413 217.687 118.458V116.728C217.687 115.772 216.912 114.997 215.957 114.997Z" fill="#888888"/>
<path d="M122.954 105.913V120.621" stroke="#707070" stroke-width="0.86514" stroke-linecap="round"/>
<path d="M106.517 99.4249C111.534 99.4249 115.601 95.3579 115.601 90.3409C115.601 85.324 111.534 81.257 106.517 81.257C101.5 81.257 97.4326 85.324 97.4326 90.3409C97.4326 95.3579 101.5 99.4249 106.517 99.4249Z" fill="#686868"/>
<path d="M106.517 56.1679C111.534 56.1679 115.601 52.1009 115.601 47.084C115.601 42.067 111.534 38 106.517 38C101.5 38 97.4326 42.067 97.4326 47.084C97.4326 52.1009 101.5 56.1679 106.517 56.1679Z" fill="#686868"/>
<path d="M150.206 38H124.252C123.296 38 122.522 38.7747 122.522 39.7303V43.1908C122.522 44.1464 123.296 44.9211 124.252 44.9211H150.206C151.162 44.9211 151.936 44.1464 151.936 43.1908V39.7303C151.936 38.7747 151.162 38 150.206 38Z" fill="#E8E8E8"/>
<path d="M178.756 38H157.992C157.037 38 156.262 38.7747 156.262 39.7303V43.1908C156.262 44.1464 157.037 44.9211 157.992 44.9211H178.756C179.711 44.9211 180.486 44.1464 180.486 43.1908V39.7303C180.486 38.7747 179.711 38 178.756 38Z" fill="#676767"/>
<path d="M202.98 50.9771H124.252C123.296 50.9771 122.522 51.7517 122.522 52.7073V54.4376C122.522 55.3932 123.296 56.1679 124.252 56.1679H202.98C203.935 56.1679 204.71 55.3932 204.71 54.4376V52.7073C204.71 51.7517 203.935 50.9771 202.98 50.9771Z" fill="white"/>
<path d="M145.88 62.2239H124.252C123.296 62.2239 122.522 62.9985 122.522 63.9542V65.6844C122.522 66.64 123.296 67.4147 124.252 67.4147H145.88C146.836 67.4147 147.611 66.64 147.611 65.6844V63.9542C147.611 62.9985 146.836 62.2239 145.88 62.2239Z" fill="white"/>
<path d="M192.598 62.2239H152.802C151.846 62.2239 151.071 62.9985 151.071 63.9542V65.6844C151.071 66.64 151.846 67.4147 152.802 67.4147H192.598C193.554 67.4147 194.328 66.64 194.328 65.6844V63.9542C194.328 62.9985 193.554 62.2239 192.598 62.2239Z" fill="white"/>
<path d="M265.27 50.9771H209.901C208.945 50.9771 208.17 51.7517 208.17 52.7073V54.4376C208.17 55.3932 208.945 56.1679 209.901 56.1679H265.27C266.225 56.1679 267 55.3932 267 54.4376V52.7073C267 51.7517 266.225 50.9771 265.27 50.9771Z" fill="white"/>
<rect x="90" y="184" width="233" height="18" fill="#363636"/>
<circle cx="317" cy="5" r="2" fill="#C4C4C4"/>
<circle cx="310" cy="5" r="2" fill="#C4C4C4"/>
<circle cx="303" cy="5" r="2" fill="#C4C4C4"/>
<line x1="4.5" y1="34.5" x2="21.5" y2="34.5" stroke="#414141" stroke-linecap="round"/>
<rect x="30" y="16" width="36" height="6" rx="2" fill="#F3F3F3"/>
<rect x="30" y="35" width="26" height="4" rx="2" fill="#F3F3F3"/>
<rect x="39" y="46" width="32" height="4" rx="2" fill="#BBBBBB"/>
<rect x="39" y="70" width="29" height="4" rx="2" fill="#BBBBBB"/>
<rect x="39" y="58" width="13" height="4" rx="2" fill="#BBBBBB"/>
<rect x="55" y="58" width="22" height="4" rx="2" fill="#BBBBBB"/>
<rect x="30" y="83" width="26" height="4" rx="2" fill="#F3F3F3"/>
<rect x="39" y="94" width="32" height="4" rx="2" fill="#BBBBBB"/>
<rect x="39" y="118" width="29" height="4" rx="2" fill="#BBBBBB"/>
<rect x="39" y="106" width="13" height="4" rx="2" fill="#BBBBBB"/>
<rect x="55" y="106" width="22" height="4" rx="2" fill="#BBBBBB"/>
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="4" y="10" width="18" height="18">
<circle cx="13" cy="19" r="9" fill="#C4C4C4"/>
</mask>
<g mask="url(#mask0)">
<circle cx="13" cy="19" r="9" fill="url(#paint0_linear)"/>
<circle cx="11.92" cy="22.24" r="3.6" fill="#F9FAFB"/>
<path d="M4 22.6H6.88L9.04 21.52L11.2 20.8L12.64 21.52L14.44 21.16L16.24 21.52L16.96 21.88L19.12 21.52L20.56 21.88L22 21.16V29.08H16.6H11.92H4V24.04V22.6Z" fill="#C42626"/>
<path d="M6.88 22.6H4V24.04L6.88 22.6Z" fill="#882C2F"/>
<path d="M14.44 21.16L12.64 21.52L11.2 24.04L11.92 29.08H16.6L15.88 27.64L16.24 22.96L16.96 21.88L16.24 21.52L14.44 21.16Z" fill="#AF373B"/>
</g>
<mask id="mask1" mask-type="alpha" maskUnits="userSpaceOnUse" x="4" y="42" width="18" height="18">
<circle cx="13" cy="51" r="9" fill="#C4C4C4"/>
</mask>
<g mask="url(#mask1)">
<circle cx="13" cy="51" r="9" fill="#D6D4D5"/>
<path d="M13.612 53.8162C19.048 49.6124 22.0252 53.8162 22.0252 53.8162V60.4402H5.89721L7.49201 53.988C7.99666 53.5705 8.17601 58.02 13.612 53.8162Z" fill="url(#paint1_linear)"/>
<path d="M4.53998 54.0601C4.53998 54.0601 8.10867 49.2615 12.388 54.9961C16.3011 60.2399 20.3145 56.741 20.668 55.8518V55.6801C20.7021 55.7083 20.7011 55.7686 20.668 55.8518V60.684H4.53998V54.0601Z" fill="url(#paint2_linear)"/>
<path d="M21.568 47.1119C21.568 48.2254 20.6654 49.1279 19.552 49.1279C18.4386 49.1279 17.536 48.2254 17.536 47.1119C17.536 45.9985 18.4386 45.0959 19.552 45.0959C20.6654 45.0959 21.568 45.9985 21.568 47.1119Z" fill="#E76563"/>
<path d="M19.12 49.0559H19.984V49.4879H19.12V49.0559Z" fill="#E76563"/>
<rect x="19.12" y="49.344" width="0.864" height="0.072" fill="white"/>
<path d="M19.264 49.488H19.336V49.776H19.264V49.488Z" fill="#4F65B6"/>
<path d="M19.48 49.488H19.624V49.776H19.48V49.488Z" fill="#4F65B6"/>
<path d="M19.768 49.488H19.84V49.776H19.768V49.488Z" fill="#4F65B6"/>
<path d="M19.048 49.776H20.056L19.984 50.28H19.12L19.048 49.776Z" fill="#4F65B6"/>
</g>
<circle cx="20" cy="45" r="3.5" fill="#EF3B3B" stroke="black"/>
<mask id="mask2" mask-type="alpha" maskUnits="userSpaceOnUse" x="4" y="65" width="18" height="18">
<circle cx="13" cy="74" r="9" fill="#C4C4C4"/>
</mask>
<g mask="url(#mask2)">
<circle cx="13" cy="74" r="9" fill="url(#paint3_linear)"/>
<path d="M11.056 79.184L13.936 75.764V80.516L11.992 80.336L11.056 79.184Z" fill="#2E2816"/>
<path d="M5.97998 76.2679L13.72 80.3719L13.792 85.0159L5.97998 82.3519L5.15198 79.1839L5.97998 76.2679Z" fill="url(#paint4_linear)"/>
<path d="M4.75598 78.0319L5.97998 76.2679L5.22398 79.4719L4.93598 78.5359L4.75598 78.0319Z" fill="#7EA6A6"/>
<path d="M19.12 68.708L21.64 70.544L24.484 76.124L22.468 79.94L19.12 68.708Z" fill="#EDEDED"/>
<path d="M12.964 79.976L13.864 80.444L13.936 84.008L13 83.972L12.964 79.976Z" fill="#878787" fill-opacity="0.5"/>
<path d="M13.468 75.584L19.12 68.708L23.008 79.112L13.72 85.736L13.468 75.584Z" fill="url(#paint5_linear)"/>
</g>
<mask id="mask3" mask-type="alpha" maskUnits="userSpaceOnUse" x="4" y="88" width="18" height="18">
<circle cx="13" cy="97" r="9" fill="#C4C4C4"/>
</mask>
<g mask="url(#mask3)">
<circle cx="13" cy="97" r="9" fill="url(#paint6_linear)"/>
<path d="M4.252 89.404C4.252 89.404 11.236 87.2439 16.024 92.284C20.812 97.324 19.732 106.396 19.732 106.396H4.252L3.604 97.936L4.252 89.404Z" fill="url(#paint7_linear)"/>
<path d="M14.404 106.396C12.208 111.508 19.732 106.396 19.732 106.396C19.732 106.396 20.488 100.348 18.508 95.956C16.528 91.564 13.72 90.448 13.72 90.448C13.72 90.448 16.6 101.284 14.404 106.396Z" fill="url(#paint8_linear)"/>
</g>
<mask id="mask4" mask-type="alpha" maskUnits="userSpaceOnUse" x="4" y="110" width="18" height="18">
<circle cx="13" cy="119" r="9" fill="#C4C4C4"/>
</mask>
<g mask="url(#mask4)">
<circle cx="13" cy="119" r="9" fill="url(#paint9_linear)"/>
<path d="M3.35199 122.708L22.216 122.708" stroke="#8181B1" stroke-width="0.144"/>
<path d="M3.28003 121.376L22.144 121.376" stroke="#A2A2BE" stroke-width="0.144"/>
<path d="M3.56799 119.936L22.432 119.936" stroke="#ADADBD" stroke-width="0.216"/>
<path d="M3.784 118.496L22.648 118.496" stroke="#BBBBCD" stroke-width="0.216"/>
<line x1="3.35199" y1="124.004" x2="22.216" y2="124.004" stroke="#8181B1" stroke-width="0.072"/>
<line x1="3.35199" y1="125.3" x2="22.216" y2="125.3" stroke="#8181B1" stroke-width="0.072"/>
<path d="M13.144 122.816L13.828 123.824C13.828 123.824 13.936 124.256 13.828 124.328C13.72 124.4 13.288 123.824 13.18 123.5C13.072 123.176 13.144 122.816 13.144 122.816Z" fill="#E6E7F4"/>
<path d="M13.828 124.328V123.824C13.828 123.824 15.304 123.608 16.708 122.816C18.112 122.024 18.364 120.944 18.364 120.944C18.364 120.944 18.292 121.952 16.924 123.032C15.556 124.112 13.828 124.328 13.828 124.328Z" fill="#E6E7F4"/>
<path d="M18.364 120.944C15.448 120.764 13.144 122.826 13.144 122.826L13.828 123.834C13.828 123.834 17.644 123.248 18.364 120.944Z" fill="white"/>
<path d="M18.256 121.016C15.6819 120.86 13.252 122.816 13.252 122.816L13.864 123.716C13.864 123.716 17.6204 123.017 18.256 121.016Z" fill="url(#paint10_linear)"/>
</g>
<defs>
<linearGradient id="paint0_linear" x1="13" y1="10" x2="13" y2="28" gradientUnits="userSpaceOnUse">
<stop stop-color="#AAB6BD"/>
<stop offset="1" stop-color="#D4DDE1"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="14.908" y1="54.744" x2="22.756" y2="61.08" gradientUnits="userSpaceOnUse">
<stop stop-color="#4F65B6"/>
<stop offset="1" stop-color="#C6D0F1"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="8.39198" y1="54.276" x2="21.496" y2="60.324" gradientUnits="userSpaceOnUse">
<stop stop-color="#4F65B6"/>
<stop offset="1" stop-color="#C6D0F1"/>
</linearGradient>
<linearGradient id="paint3_linear" x1="9.292" y1="65.432" x2="18.22" y2="82.388" gradientUnits="userSpaceOnUse">
<stop stop-color="#009092"/>
<stop offset="1" stop-color="#79C6C8"/>
</linearGradient>
<linearGradient id="paint4_linear" x1="9.39998" y1="76.2679" x2="9.39998" y2="85.0519" gradientUnits="userSpaceOnUse">
<stop stop-color="#CBCBCB"/>
<stop offset="1" stop-color="#FAFAFA"/>
</linearGradient>
<linearGradient id="paint5_linear" x1="18.238" y1="68.708" x2="18.238" y2="85.736" gradientUnits="userSpaceOnUse">
<stop stop-color="#95ABA9"/>
<stop offset="1" stop-color="#DCDCDC"/>
</linearGradient>
<linearGradient id="paint6_linear" x1="10.876" y1="87.604" x2="17.86" y2="105.136" gradientUnits="userSpaceOnUse">
<stop stop-color="#41486A"/>
<stop offset="1" stop-color="#3B3F5C"/>
</linearGradient>
<linearGradient id="paint7_linear" x1="7.312" y1="91.168" x2="19.84" y2="107.224" gradientUnits="userSpaceOnUse">
<stop stop-color="#4C7799"/>
<stop offset="0.9999" stop-color="#39AEBF"/>
<stop offset="1" stop-color="#4C7799" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint8_linear" x1="16.636" y1="96.568" x2="12.388" y2="87.316" gradientUnits="userSpaceOnUse">
<stop stop-color="#DD4878"/>
<stop offset="1" stop-color="#D7E1E8"/>
</linearGradient>
<linearGradient id="paint9_linear" x1="9.94" y1="109.244" x2="13.756" y2="125.912" gradientUnits="userSpaceOnUse">
<stop stop-color="#847DAF"/>
<stop offset="1" stop-color="#4547AE"/>
</linearGradient>
<linearGradient id="paint10_linear" x1="15.484" y1="122.024" x2="16.924" y2="123.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#DFDFE1"/>
<stop offset="1" stop-color="#F5F4FB"/>
</linearGradient>
</defs>
</svg>