diff --git a/package.json b/package.json
index 8862bde6..c639394e 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"@preact/preset-vite": "^2.0.0",
"@styled-icons/bootstrap": "^10.34.0",
"@styled-icons/feather": "^10.34.0",
+ "@styled-icons/simple-icons": "^10.33.0",
"@traptitech/markdown-it-katex": "^3.4.3",
"@traptitech/markdown-it-spoiler": "^1.1.6",
"@types/markdown-it": "^12.0.2",
@@ -70,6 +71,7 @@
"revolt.js": "4.3.0",
"rimraf": "^3.0.2",
"sass": "^1.35.1",
+ "shade-blend-color": "^1.0.0",
"styled-components": "^5.3.0",
"twemoji": "^13.1.0",
"typescript": "^4.3.2",
diff --git a/src/components/navigation/items/ButtonItem.tsx b/src/components/navigation/items/ButtonItem.tsx
index 0acfbf02..bcbd59aa 100644
--- a/src/components/navigation/items/ButtonItem.tsx
+++ b/src/components/navigation/items/ButtonItem.tsx
@@ -1,13 +1,17 @@
import classNames from 'classnames';
import styles from "./Item.module.scss";
+import Tooltip from '../../common/Tooltip';
+import IconButton from '../../ui/IconButton';
import UserIcon from '../../common/UserIcon';
import { Localizer, Text } from "preact-i18n";
import { X, Zap } from "@styled-icons/feather";
import UserStatus from '../../common/UserStatus';
import { Children } from "../../../types/Preact";
import ChannelIcon from '../../common/ChannelIcon';
+import { attachContextMenu } from 'preact-context-menu';
import { Channels, Users } from "revolt.js/dist/api/objects";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
+import { useIntermediate } from '../../../context/intermediate/Intermediate';
interface CommonProps {
active?: boolean
@@ -22,7 +26,7 @@ type UserProps = CommonProps & {
}
export function UserButton({ active, alert, alertCount, user, context, channel }: UserProps) {
- // const { openScreen } = useContext(IntermediateContext);
+ const { openScreen } = useIntermediate();
return (
+ })}>
@@ -56,24 +59,22 @@ export function UserButton({ active, alert, alertCount, user, context, channel }
{ context?.channel_type === "Group" &&
context.owner === user._id && (
- {/*
}
- >*/}
+ >
- {/**/}
+
)}
{alert &&
{ alertCount }
}
{ !isTouchscreenDevice && channel &&
- /*
openScreen({ id: 'special_prompt', type: 'close_dm', target: channel })}
- >*/
+ onClick={() => openScreen({ id: 'special_prompt', type: 'close_dm', target: channel })}>
- /**/
+
}
@@ -93,15 +94,14 @@ export function ChannelButton({ active, alert, alertCount, channel, user, compac
return
}
- //const { openScreen } = useContext(IntermediateContext);
+ const { openScreen } = useIntermediate();
return (
- >
+ onContextMenu={attachContextMenu('Menu', { channel: channel._id })}>
@@ -124,13 +124,11 @@ export function ChannelButton({ active, alert, alertCount, channel, user, compac
{alert &&
{ alertCount }
}
{!isTouchscreenDevice && channel.channel_type === "Group" && (
- /*
openScreen({ id: 'special_prompt', type: 'leave_group', target: channel })}
- >*/
+ onClick={() => openScreen({ id: 'special_prompt', type: 'leave_group', target: channel })}>
- /**/
+
)}
diff --git a/src/components/navigation/left/HomeSidebar.tsx b/src/components/navigation/left/HomeSidebar.tsx
index bf1b99ce..d014de10 100644
--- a/src/components/navigation/left/HomeSidebar.tsx
+++ b/src/components/navigation/left/HomeSidebar.tsx
@@ -1,24 +1,20 @@
import { Localizer, Text } from "preact-i18n";
-import { useContext, useLayoutEffect } from "preact/hooks";
-import { Home, Users, Tool, Settings, Save } from "@styled-icons/feather";
+import { useContext } from "preact/hooks";
+import { Home, Users, Tool, Save } from "@styled-icons/feather";
-import { Link, Redirect, useHistory, useLocation, useParams } from "react-router-dom";
+import { Link, Redirect, useLocation, useParams } from "react-router-dom";
import { WithDispatcher } from "../../../redux/reducers";
import { Unreads } from "../../../redux/reducers/unreads";
import { connectState } from "../../../redux/connector";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { useChannels, useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
-import { User } from "revolt.js";
import { Users as UsersNS } from 'revolt.js/dist/api/objects';
import { mapChannelWithUnread, useUnreads } from "./common";
import { Channels } from "revolt.js/dist/api/objects";
-import UserIcon from '../../common/UserIcon';
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import ConnectionStatus from '../items/ConnectionStatus';
-import UserStatus from '../../common/UserStatus';
import ButtonItem, { ChannelButton } from '../items/ButtonItem';
import styled from "styled-components";
-import Header from '../../ui/Header';
import UserHeader from "../../common/UserHeader";
import Category from '../../ui/Category';
import PaintCounter from "../../../lib/PaintCounter";
diff --git a/src/components/navigation/left/ServerListSidebar.tsx b/src/components/navigation/left/ServerListSidebar.tsx
index 7ff3e74d..f64e4fef 100644
--- a/src/components/navigation/left/ServerListSidebar.tsx
+++ b/src/components/navigation/left/ServerListSidebar.tsx
@@ -1,5 +1,3 @@
-import { useContext } from "preact/hooks";
-import { PlusCircle } from "@styled-icons/feather";
import { Channel, Servers } from "revolt.js/dist/api/objects";
import { Link, useLocation, useParams } from "react-router-dom";
import { useChannels, useForceUpdate, useServers } from "../../../context/revoltjs/hooks";
@@ -11,6 +9,7 @@ import { Children } from "../../../types/Preact";
import LineDivider from "../../ui/LineDivider";
import ServerIcon from "../../common/ServerIcon";
import PaintCounter from "../../../lib/PaintCounter";
+import { attachContextMenu } from 'preact-context-menu';
function Icon({ children, unread, size }: { children: Children, unread?: 'mention' | 'unread', size: number }) {
return (
@@ -157,8 +156,7 @@ export function ServerListSidebar({ unreads }: Props) {
- >
+ onContextMenu={attachContextMenu('Menu', { server: entry!._id })}>
@@ -169,6 +167,7 @@ export function ServerListSidebar({ unreads }: Props) {
+ // ! FIXME: add overlay back
/*
diff --git a/src/components/navigation/left/ServerSidebar.tsx b/src/components/navigation/left/ServerSidebar.tsx
index e5008550..f4cae5fd 100644
--- a/src/components/navigation/left/ServerSidebar.tsx
+++ b/src/components/navigation/left/ServerSidebar.tsx
@@ -12,11 +12,32 @@ import Header from '../../ui/Header';
import ConnectionStatus from '../items/ConnectionStatus';
import { connectState } from "../../../redux/connector";
import PaintCounter from "../../../lib/PaintCounter";
+import styled from "styled-components";
+import { attachContextMenu } from 'preact-context-menu';
interface Props {
unreads: Unreads;
}
+const ServerBase = styled.div`
+ height: 100%;
+ width: 240px;
+ display: flex;
+ flex-shrink: 0;
+ flex-direction: column;
+ background: var(--secondary-background);
+`;
+
+const ServerList = styled.div`
+ padding: 6px;
+ flex-grow: 1;
+ overflow-y: scroll;
+
+ > svg {
+ width: 100%;
+ }
+`;
+
function ServerSidebar(props: Props & WithDispatcher) {
const { server: server_id, channel: channel_id } = useParams<{ server?: string, channel?: string }>();
const ctx = useForceUpdate();
@@ -33,7 +54,7 @@ function ServerSidebar(props: Props & WithDispatcher) {
if (channel) useUnreads({ ...props, channel }, ctx);
return (
-
+
-
- >
+
{channels.map(entry => {
return (
@@ -61,9 +80,9 @@ function ServerSidebar(props: Props & WithDispatcher) {
);
})}
-
+
-
+
)
};
diff --git a/src/components/ui/Checkbox.tsx b/src/components/ui/Checkbox.tsx
index fb9adf31..e7cd62b9 100644
--- a/src/components/ui/Checkbox.tsx
+++ b/src/components/ui/Checkbox.tsx
@@ -23,6 +23,14 @@ const CheckboxBase = styled.label`
input {
display: none;
}
+
+ &:hover {
+ background: var(--secondary-background);
+
+ .check {
+ background: var(--background);
+ }
+ }
`;
const CheckboxContent = styled.span`
@@ -46,6 +54,7 @@ const Checkmark = styled.div<{ checked: boolean }>`
display: grid;
border-radius: 4px;
place-items: center;
+ transition: 0.2s ease all;
background: var(--secondary-background);
svg {
@@ -56,7 +65,7 @@ const Checkmark = styled.div<{ checked: boolean }>`
${(props) =>
props.checked &&
css`
- background: var(--accent);
+ background: var(--accent) !important;
`}
`;
@@ -71,7 +80,7 @@ export interface CheckboxProps {
export default function Checkbox(props: CheckboxProps) {
return (
-
+
{props.children}
{props.description && (
@@ -87,7 +96,7 @@ export default function Checkbox(props: CheckboxProps) {
!props.disabled && props.onChange(!props.checked)
}
/>
-
+
diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx
index 7d80cb81..26197b2b 100644
--- a/src/components/ui/Modal.tsx
+++ b/src/components/ui/Modal.tsx
@@ -47,7 +47,7 @@ const ModalContainer = styled.div`
animation-timing-function: cubic-bezier(.3,.3,.18,1.1);
`;
-const ModalContent = styled.div<{ [key in 'attachment' | 'noBackground' | 'border']?: boolean }>`
+const ModalContent = styled.div<{ [key in 'attachment' | 'noBackground' | 'border' | 'padding']?: boolean }>`
border-radius: 8px;
text-overflow: ellipsis;
@@ -56,10 +56,13 @@ const ModalContent = styled.div<{ [key in 'attachment' | 'noBackground' | 'borde
}
${ props => !props.noBackground && css`
- padding: 1.5em;
background: var(--secondary-header);
` }
+ ${ props => props.padding && css`
+ padding: 1.5em;
+ ` }
+
${ props => props.attachment && css`
border-radius: 8px 8px 0 0;
` }
@@ -110,7 +113,8 @@ export default function Modal(props: Props) {
+ border={props.border}
+ padding={!props.dontModal}>
{props.title && {props.title}
}
{props.children}
diff --git a/src/components/ui/TextArea.module.scss b/src/components/ui/TextArea.module.scss
new file mode 100644
index 00000000..7e009fd5
--- /dev/null
+++ b/src/components/ui/TextArea.module.scss
@@ -0,0 +1,31 @@
+.container {
+ font-size: 0.875rem;
+ line-height: 20px;
+ position: relative;
+}
+
+.textarea {
+ width: 100%;
+ white-space: pre-wrap;
+
+ textarea::placeholder {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+}
+
+.hide {
+ width: 100%;
+ overflow: hidden;
+ position: relative;
+}
+
+.ghost {
+ width: 100%;
+ white-space: pre-wrap;
+
+ top: 0;
+ position: absolute;
+ visibility: hidden;
+}
diff --git a/src/components/ui/TextArea.tsx b/src/components/ui/TextArea.tsx
new file mode 100644
index 00000000..d88dcb7b
--- /dev/null
+++ b/src/components/ui/TextArea.tsx
@@ -0,0 +1,146 @@
+// ! FIXME: temporarily here until re-written
+// ! DO NOT IMRPOVE, JUST RE-WRITE
+
+import classNames from "classnames";
+import { memo } from "preact/compat";
+import styles from "./TextArea.module.scss";
+import { useState, useEffect, useRef, useLayoutEffect } from "preact/hooks";
+
+export interface TextAreaProps {
+ id?: string;
+ value: string;
+ maxRows?: number;
+ padding?: number;
+ minHeight?: number;
+ disabled?: boolean;
+ maxLength?: number;
+ className?: string;
+ autoFocus?: boolean;
+ forceFocus?: boolean;
+ placeholder?: string;
+ onKeyDown?: (ev: KeyboardEvent) => void;
+ onKeyUp?: (ev: KeyboardEvent) => void;
+ onChange: (
+ value: string,
+ ev: JSX.TargetedEvent
+ ) => void;
+ onFocus?: (current: HTMLTextAreaElement) => void;
+ onBlur?: () => void;
+}
+
+const lineHeight = 20;
+
+export const TextArea = memo((props: TextAreaProps) => {
+ const padding = props.padding ? props.padding * 2 : 0;
+
+ const [height, setHeightState] = useState(
+ props.minHeight ?? lineHeight + padding
+ );
+ const ghost = useRef();
+ const ref = useRef();
+
+ function setHeight(h: number = lineHeight) {
+ let newHeight = Math.min(
+ Math.max(
+ lineHeight,
+ props.maxRows ? Math.min(h, props.maxRows * lineHeight) : h
+ ),
+ props.minHeight ?? Infinity
+ );
+
+ if (props.padding) newHeight += padding;
+ if (height !== newHeight) {
+ setHeightState(newHeight);
+ }
+ }
+
+ function onChange(ev: JSX.TargetedEvent) {
+ props.onChange(ev.currentTarget.value, ev);
+ }
+
+ useLayoutEffect(() => {
+ setHeight(ghost.current.clientHeight);
+ }, [ghost, props.value]);
+
+ useEffect(() => {
+ if (props.autoFocus) ref.current.focus();
+ }, [props.value]);
+
+ const inputSelected = () =>
+ ["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
+
+ useEffect(() => {
+ if (props.forceFocus) {
+ ref.current.focus();
+ }
+
+ if (props.autoFocus && !inputSelected()) {
+ ref.current.focus();
+ }
+
+ // ? if you are wondering what this is
+ // ? it is a quick and dirty hack to fix
+ // ? value not setting correctly
+ // ? I have no clue what's going on
+ ref.current.value = props.value;
+
+ if (!props.autoFocus) return;
+ function keyDown(e: KeyboardEvent) {
+ if ((e.ctrlKey && e.key !== "v") || e.altKey || e.metaKey) return;
+ if (e.key.length !== 1) return;
+ if (ref && !inputSelected()) {
+ ref.current.focus();
+ }
+ }
+
+ document.body.addEventListener("keydown", keyDown);
+ return () => document.body.removeEventListener("keydown", keyDown);
+ }, [ref]);
+
+ useEffect(() => {
+ function focus(textarea_id: string) {
+ if (props.id === textarea_id) {
+ ref.current.focus();
+ }
+ }
+
+ // InternalEventEmitter.addListener("focus_textarea", focus);
+ // return () =>
+ // InternalEventEmitter.removeListener("focus_textarea", focus);
+ }, [ref]);
+
+ return (
+
+ );
+});
diff --git a/src/context/Theme.tsx b/src/context/Theme.tsx
index 42365cb9..4c8a8dc9 100644
--- a/src/context/Theme.tsx
+++ b/src/context/Theme.tsx
@@ -1,5 +1,6 @@
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
import { createGlobalStyle } from "styled-components";
+import { connectState } from "../redux/connector";
import { Children } from "../types/Preact";
import { createContext } from "preact";
import { Helmet } from "react-helmet";
@@ -116,10 +117,15 @@ export const ThemeContext = createContext({} as any);
interface Props {
children: Children;
+ options?: ThemeOptions;
}
-export default function Theme(props: Props) {
- const theme = PRESETS.dark;
+function Theme(props: Props) {
+ const theme: Theme = {
+ ...PRESETS["dark"],
+ ...(PRESETS as any)[props.options?.preset as any],
+ ...props.options?.custom
+ };
return (
@@ -134,7 +140,16 @@ export default function Theme(props: Props) {
/>
+ {theme.css && (
+
+ )}
{props.children}
);
}
+
+export default connectState(Theme, state => {
+ return {
+ options: state.settings.theme
+ };
+});
diff --git a/src/context/intermediate/modals/Onboarding.tsx b/src/context/intermediate/modals/Onboarding.tsx
index b1741e2a..17fc0fd6 100644
--- a/src/context/intermediate/modals/Onboarding.tsx
+++ b/src/context/intermediate/modals/Onboarding.tsx
@@ -7,8 +7,6 @@ import Button from "../../../components/ui/Button";
import FormField from "../../../pages/login/FormField";
import Preloader from "../../../components/ui/Preloader";
-// import WideSvg from "../../../assets/wide.svg";
-
interface Props {
onClose: () => void;
callback: (username: string, loginAfterSuccess?: true) => Promise;
@@ -34,6 +32,7 @@ export function OnboardingModal({ onClose, callback }: Props) {
+
diff --git a/src/context/intermediate/popovers/UserProfile.module.scss b/src/context/intermediate/popovers/UserProfile.module.scss
index 448595ab..38094de1 100644
--- a/src/context/intermediate/popovers/UserProfile.module.scss
+++ b/src/context/intermediate/popovers/UserProfile.module.scss
@@ -9,6 +9,7 @@
background-size: cover;
border-radius: 8px 8px 0 0;
background-position: center;
+ background-color: var(--secondary-background);
&[data-force="light"] {
color: white;
diff --git a/src/context/revoltjs/FileUploads.module.scss b/src/context/revoltjs/FileUploads.module.scss
new file mode 100644
index 00000000..2d5a3b39
--- /dev/null
+++ b/src/context/revoltjs/FileUploads.module.scss
@@ -0,0 +1,82 @@
+.uploader {
+ display: flex;
+ flex-direction: column;
+
+ &.icon {
+ .image {
+ border-radius: 50%;
+ }
+ }
+
+ &.banner {
+ .image {
+ border-radius: 4px;
+ }
+
+ .modify {
+ gap: 4px;
+ flex-direction: row;
+ }
+ }
+
+ .image {
+ cursor: pointer;
+ overflow: hidden;
+ background-size: cover;
+ background-position: center;
+ background-color: var(--secondary-background);
+
+ .uploading {
+ width: 100%;
+ height: 100%;
+ display: grid;
+ place-items: center;
+ background: rgba(0, 0, 0, 0.5);
+ }
+
+ &:hover .edit {
+ opacity: 1;
+ }
+
+ &:active .edit {
+ filter: brightness(0.8);
+ }
+
+ .edit {
+ opacity: 0;
+ width: 100%;
+ height: 100%;
+ display: grid;
+ color: white;
+ place-items: center;
+ background: rgba(95, 95, 95, 0.5);
+ transition: .2s ease-in-out opacity;
+ }
+ }
+
+ .modify {
+ display: flex;
+ margin-top: 5px;
+ font-size: 12px;
+ align-items: center;
+ flex-direction: column;
+ justify-content: center;
+
+ :first-child {
+ cursor: pointer;
+ }
+
+ .small {
+ display: flex;
+ font-size: 10px;
+ flex-direction: column;
+ color: var(--tertiary-foreground);
+ }
+ }
+
+ &[data-uploading="true"] {
+ .image, .modify:first-child {
+ cursor: not-allowed !important;
+ }
+ }
+}
diff --git a/src/context/revoltjs/FileUploads.tsx b/src/context/revoltjs/FileUploads.tsx
new file mode 100644
index 00000000..61df4630
--- /dev/null
+++ b/src/context/revoltjs/FileUploads.tsx
@@ -0,0 +1,148 @@
+// ! FIXME: also TEMP CODE
+// ! RE-WRITE WITH STYLED-COMPONENTS
+
+import { Text } from "preact-i18n";
+import { takeError } from "./util";
+import classNames from "classnames";
+import styles from './FileUploads.module.scss';
+import Axios, { AxiosRequestConfig } from "axios";
+import { useContext, useState } from "preact/hooks";
+import { Edit, Plus, X } from "@styled-icons/feather";
+import Preloader from "../../components/ui/Preloader";
+import { determineFileSize } from "../../lib/fileSize";
+import IconButton from '../../components/ui/IconButton';
+import { useIntermediate } from "../intermediate/Intermediate";
+import { AppContext } from "./RevoltClient";
+
+type Props = {
+ maxFileSize: number
+ remove: () => Promise
+ fileType: 'backgrounds' | 'icons' | 'avatars' | 'attachments' | 'banners'
+} & (
+ { behaviour: 'ask', onChange: (file: File) => void } |
+ { behaviour: 'upload', onUpload: (id: string) => Promise }
+) & (
+ { style: 'icon' | 'banner', defaultPreview?: string, previewURL?: string, width?: number, height?: number } |
+ { style: 'attachment', attached: boolean, uploading: boolean, cancel: () => void, size?: number }
+)
+
+export async function uploadFile(autumnURL: string, tag: string, file: File, config?: AxiosRequestConfig) {
+ const formData = new FormData();
+ formData.append("file", file);
+
+ const res = await Axios.post(autumnURL + "/" + tag, formData, {
+ headers: {
+ "Content-Type": "multipart/form-data"
+ },
+ ...config
+ });
+
+ return res.data.id;
+}
+
+export function FileUploader(props: Props) {
+ const { fileType, maxFileSize, remove } = props;
+ const { openScreen } = useIntermediate();
+ const client = useContext(AppContext);
+
+ const [ uploading, setUploading ] = useState(false);
+
+ function onClick() {
+ if (uploading) return;
+
+ const input = document.createElement("input");
+ input.type = "file";
+
+ input.onchange = async e => {
+ setUploading(true);
+
+ try {
+ const files = (e.target as any)?.files;
+ if (files && files[0]) {
+ let file = files[0];
+
+ if (file.size > maxFileSize) {
+ return openScreen({ id: "error", error: "FileTooLarge" });
+ }
+
+ if (props.behaviour === 'ask') {
+ await props.onChange(file);
+ } else {
+ await props.onUpload(await uploadFile(client.configuration!.features.autumn.url, fileType, file));
+ }
+ }
+ } catch (err) {
+ return openScreen({ id: "error", error: takeError(err) });
+ } finally {
+ setUploading(false);
+ }
+ };
+
+ input.click();
+ }
+
+ function removeOrUpload() {
+ if (uploading) return;
+
+ if (props.style === 'attachment') {
+ if (props.attached) {
+ props.remove();
+ } else {
+ onClick();
+ }
+ } else {
+ if (props.previewURL) {
+ props.remove();
+ } else {
+ onClick();
+ }
+ }
+ }
+
+ if (props.style === 'icon' || props.style === 'banner') {
+ const { style, previewURL, defaultPreview, width, height } = props;
+ return (
+
+
+ { uploading ?
+
:
+
+
+
}
+
+
+ {
+ uploading ? :
+ props.previewURL ? :
+ }
+
+
+
+ )
+ } else if (props.style === 'attachment') {
+ const { attached, uploading, cancel, size } = props;
+ return (
+ {
+ if (uploading) return cancel();
+ if (attached) return remove();
+ onClick();
+ }}>
+ { attached ? : }
+
+ )
+ }
+
+ return null;
+}
diff --git a/src/context/revoltjs/RequiresOnline.tsx b/src/context/revoltjs/RequiresOnline.tsx
new file mode 100644
index 00000000..73b7f721
--- /dev/null
+++ b/src/context/revoltjs/RequiresOnline.tsx
@@ -0,0 +1,44 @@
+import { Text } from "preact-i18n";
+import styled from "styled-components";
+import { useContext } from "preact/hooks";
+import { Children } from "../../types/Preact";
+import { WifiOff } from "@styled-icons/feather";
+import Preloader from "../../components/ui/Preloader";
+import { ClientStatus, StatusContext } from "./RevoltClient";
+
+interface Props {
+ children: Children;
+}
+
+const Base = styled.div`
+ gap: 16px;
+ padding: 1em;
+ display: flex;
+ user-select: none;
+ align-items: center;
+ flex-direction: row;
+ justify-content: center;
+ color: var(--tertiary-foreground);
+ background: var(--secondary-header);
+
+ > div {
+ font-size: 18px;
+ }
+`;
+
+export default function RequiresOnline(props: Props) {
+ const status = useContext(StatusContext);
+
+ if (status === ClientStatus.CONNECTING) return ;
+ if (status !== ClientStatus.ONLINE && status !== ClientStatus.READY)
+ return (
+
+
+
+
+
+
+ );
+
+ return <>{ props.children }>;
+}
diff --git a/src/lib/conversion.ts b/src/lib/conversion.ts
new file mode 100644
index 00000000..61dcad9d
--- /dev/null
+++ b/src/lib/conversion.ts
@@ -0,0 +1,9 @@
+export function urlBase64ToUint8Array(base64String: string) {
+ const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
+ const base64 = (base64String + padding)
+ .replace(/\-/g, "+")
+ .replace(/_/g, "/");
+ const rawData = window.atob(base64);
+
+ return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
+}
diff --git a/src/lib/debounce.ts b/src/lib/debounce.ts
new file mode 100644
index 00000000..c7ac3830
--- /dev/null
+++ b/src/lib/debounce.ts
@@ -0,0 +1,15 @@
+export function debounce(cb: Function, duration: number) {
+ // Store the timer variable.
+ let timer: number;
+ // This function is given to React.
+ return (...args: any[]) => {
+ // Get rid of the old timer.
+ clearTimeout(timer);
+ // Set a new timer.
+ timer = setTimeout(() => {
+ // Instead calling the new function.
+ // (with the newer data)
+ cb(...args);
+ }, duration);
+ };
+}
diff --git a/src/lib/fileSize.ts b/src/lib/fileSize.ts
new file mode 100644
index 00000000..c6bba81d
--- /dev/null
+++ b/src/lib/fileSize.ts
@@ -0,0 +1,9 @@
+export function determineFileSize(size: number) {
+ if (size > 1e6) {
+ return `${(size / 1e6).toFixed(2)} MB`;
+ } else if (size > 1e3) {
+ return `${(size / 1e3).toFixed(2)} KB`;
+ }
+
+ return `${size} B`;
+}
diff --git a/src/main.tsx b/src/main.tsx
index 70a4359b..5415f66a 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,6 +1,6 @@
import { render } from "preact";
import "./styles/index.scss";
-import { App } from "./app";
+import { App } from "./pages/app";
import { registerSW } from 'virtual:pwa-register'
diff --git a/src/pages/App.tsx b/src/pages/RevoltApp.tsx
similarity index 50%
rename from src/pages/App.tsx
rename to src/pages/RevoltApp.tsx
index f0ff58d3..88738fd3 100644
--- a/src/pages/App.tsx
+++ b/src/pages/RevoltApp.tsx
@@ -11,7 +11,10 @@ import RightSidebar from "../components/navigation/RightSidebar";
import Home from './home/Home';
import Friends from "./friends/Friends";
+import Settings from './settings/Settings';
import Developer from "./developer/Developer";
+import ServerSettings from "./settings/ServerSettings";
+import ChannelSettings from "./settings/ChannelSettings";
const Routes = styled.div`
min-width: 0;
@@ -31,17 +34,19 @@ export default function App() {
docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
@@ -55,31 +60,6 @@ export default function App() {
*
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -89,21 +69,10 @@ export default function App() {
-
-
-
-
-
-
-
{/*
-
-
-
-
*/
diff --git a/src/app.tsx b/src/pages/app.tsx
similarity index 74%
rename from src/app.tsx
rename to src/pages/app.tsx
index ee559793..c6af90ae 100644
--- a/src/app.tsx
+++ b/src/pages/app.tsx
@@ -1,11 +1,11 @@
-import { CheckAuth } from "./context/revoltjs/CheckAuth";
-import Preloader from "./components/ui/Preloader";
+import { CheckAuth } from "../context/revoltjs/CheckAuth";
+import Preloader from "../components/ui/Preloader";
import { Route, Switch } from "react-router-dom";
-import Context from "./context";
+import Context from "../context";
import { lazy, Suspense } from "preact/compat";
-const Login = lazy(() => import('./pages/login/Login'));
-const RevoltApp = lazy(() => import('./pages/App'));
+const Login = lazy(() => import('./login/Login'));
+const RevoltApp = lazy(() => import('./RevoltApp'));
export function App() {
return (
diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx
index 78e0348c..01371fc7 100644
--- a/src/pages/home/Home.tsx
+++ b/src/pages/home/Home.tsx
@@ -3,14 +3,13 @@ import { Link } from "react-router-dom";
import { Text } from "preact-i18n";
import Header from "../../components/ui/Header";
-// import WideLogo from "../../../../../assets/wide.svg";
export default function Home() {
return (
- {/**/}
+
-
diff --git a/src/pages/login/forms/FormLogin.tsx b/src/pages/login/forms/FormLogin.tsx
index 6588a52d..d3af8a18 100644
--- a/src/pages/login/forms/FormLogin.tsx
+++ b/src/pages/login/forms/FormLogin.tsx
@@ -1,7 +1,7 @@
import { Form } from "./Form";
+import { detect } from "detect-browser";
import { useContext } from "preact/hooks";
import { useHistory } from "react-router-dom";
-import { deviceDetect } from "react-device-detect";
import { OperationsContext } from "../../../context/revoltjs/RevoltClient";
export function FormLogin() {
@@ -12,7 +12,7 @@ export function FormLogin() {