diff --git a/package.json b/package.json
index 0b20ee71..7ae71682 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
"react-device-detect": "^1.17.0",
"react-helmet": "^6.1.0",
"react-hook-form": "6.3.0",
- "react-overlapping-panels": "1.1.2-patch.0",
+ "react-overlapping-panels": "1.2.1",
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.0",
"redux": "^4.1.0",
diff --git a/src/app.tsx b/src/app.tsx
index 28e68f59..ee559793 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,40 +1,31 @@
import { CheckAuth } from "./context/revoltjs/CheckAuth";
+import Preloader from "./components/ui/Preloader";
import { Route, Switch } from "react-router-dom";
import Context from "./context";
-import { Login } from "./pages/login/Login";
-
-import { useForceUpdate, useSelf, useUser } from "./context/revoltjs/hooks";
-
-function Test() {
- const ctx = useForceUpdate();
-
- let self = useSelf(ctx);
- let bree = useUser('01EZZJ98RM1YVB1FW9FG221CAN', ctx);
-
- return (
-
-
logged in as { self?.username }
- bree: { JSON.stringify(bree) }
-
- )
-}
+import { lazy, Suspense } from "preact/compat";
+const Login = lazy(() => import('./pages/login/Login'));
+const RevoltApp = lazy(() => import('./pages/App'));
export function App() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
+ {/*
+ // @ts-expect-error */}
+ }>
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/components/common/ChannelIcon.tsx b/src/components/common/ChannelIcon.tsx
new file mode 100644
index 00000000..8be9fe80
--- /dev/null
+++ b/src/components/common/ChannelIcon.tsx
@@ -0,0 +1,45 @@
+import { useContext } from "preact/hooks";
+import { Hash } from "@styled-icons/feather";
+import IconBase, { IconBaseProps } from "./IconBase";
+import { Channels } from "revolt.js/dist/api/objects";
+import { AppContext } from "../../context/revoltjs/RevoltClient";
+
+interface Props extends IconBaseProps {
+ isServerChannel?: boolean;
+}
+
+const fallback = '/assets/group.png';
+export default function ChannelIcon(props: Props & Omit, keyof Props>) {
+ const { client } = useContext(AppContext);
+
+ const { size, target, attachment, isServerChannel: server, animate, children, as, ...svgProps } = props;
+ const iconURL = client.generateFileURL(target?.icon ?? attachment, { max_side: 256 }, animate);
+ const isServerChannel = server || target?.channel_type === 'TextChannel';
+
+ if (typeof iconURL === 'undefined') {
+ if (isServerChannel) {
+ return (
+
+ )
+ }
+ }
+
+ return (
+
+
+
{
+ let el = e.currentTarget;
+ if (el.src !== fallback) {
+ el.src = fallback
+ }
+ } } />
+
+
+ );
+}
diff --git a/src/components/common/IconBase.tsx b/src/components/common/IconBase.tsx
new file mode 100644
index 00000000..fb1f75bd
--- /dev/null
+++ b/src/components/common/IconBase.tsx
@@ -0,0 +1,22 @@
+import { Attachment } from "revolt.js/dist/api/objects";
+import styled, { css } from "styled-components";
+
+export interface IconBaseProps {
+ target?: T;
+ attachment?: Attachment;
+
+ size: number;
+ animate?: boolean;
+}
+
+export default styled.svg<{ square?: boolean }>`
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+
+ ${ props => !props.square && css`
+ border-radius: 50%;
+ ` }
+ }
+`;
diff --git a/src/components/common/ServerIcon.tsx b/src/components/common/ServerIcon.tsx
new file mode 100644
index 00000000..1cf0297f
--- /dev/null
+++ b/src/components/common/ServerIcon.tsx
@@ -0,0 +1,57 @@
+import styled from "styled-components";
+import { useContext } from "preact/hooks";
+import { Server } from "revolt.js/dist/api/objects";
+import IconBase, { IconBaseProps } from "./IconBase";
+import { AppContext } from "../../context/revoltjs/RevoltClient";
+
+interface Props extends IconBaseProps {
+ server_name?: string;
+}
+
+const ServerText = styled.div`
+ display: grid;
+ padding: .2em;
+ overflow: hidden;
+ border-radius: 50%;
+ place-items: center;
+ color: var(--foreground);
+ background: var(--primary-background);
+`;
+
+const fallback = '/assets/group.png';
+export default function ServerIcon(props: Props & Omit, keyof Props>) {
+ const { client } = useContext(AppContext);
+
+ const { target, attachment, size, animate, server_name, children, as, ...svgProps } = props;
+ const iconURL = client.generateFileURL(target?.icon ?? attachment, { max_side: 256 }, animate);
+
+ if (typeof iconURL === 'undefined') {
+ const name = target?.name ?? server_name ?? '';
+
+ return (
+
+ { name.split(' ')
+ .map(x => x[0])
+ .filter(x => typeof x !== 'undefined') }
+
+ )
+ }
+
+ return (
+
+
+
{
+ let el = e.currentTarget;
+ if (el.src !== fallback) {
+ el.src = fallback
+ }
+ }} />
+
+
+ );
+}
diff --git a/src/components/common/UserIcon.tsx b/src/components/common/UserIcon.tsx
new file mode 100644
index 00000000..71e861dd
--- /dev/null
+++ b/src/components/common/UserIcon.tsx
@@ -0,0 +1,96 @@
+import { User } from "revolt.js";
+import { useContext } from "preact/hooks";
+import { MicOff } from "@styled-icons/feather";
+import styled, { css } from "styled-components";
+import { ThemeContext } from "../../context/Theme";
+import { Users } from "revolt.js/dist/api/objects";
+import IconBase, { IconBaseProps } from "./IconBase";
+import { AppContext } from "../../context/revoltjs/RevoltClient";
+
+type VoiceStatus = "muted";
+interface Props extends IconBaseProps {
+ status?: boolean;
+ voice?: VoiceStatus;
+}
+
+export function useStatusColour(user?: User) {
+ const theme = useContext(ThemeContext);
+
+ return (
+ user?.online &&
+ user?.status?.presence !== Users.Presence.Invisible
+ ? user?.status?.presence === Users.Presence.Idle
+ ? theme["status-away"]
+ : user?.status?.presence ===
+ Users.Presence.Busy
+ ? theme["status-busy"]
+ : theme["status-online"]
+ : theme["status-invisible"]
+ );
+}
+
+const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ svg {
+ stroke: white;
+ }
+
+ ${ props => props.status === 'muted' && css`
+ background: var(--error);
+ ` }
+`;
+
+const fallback = '/assets/user.png';
+export default function UserIcon(props: Props & Omit, keyof Props>) {
+ const { client } = useContext(AppContext);
+
+ const { target, attachment, size, voice, status, animate, children, as, ...svgProps } = props;
+ const iconURL = client.generateFileURL(target?.avatar ?? attachment, { max_side: 256 }, animate);
+
+ return (
+
+
+ {
+
{
+ let el = e.currentTarget;
+ if (el.src !== fallback) {
+ el.src = fallback
+ }
+ }} />
+ }
+
+ {props.status && (
+
+ )}
+ {props.voice && (
+
+
+ {props.voice === "muted" && }
+
+
+ )}
+
+ );
+}
diff --git a/src/context/Settings.tsx b/src/context/Settings.tsx
new file mode 100644
index 00000000..f45005ec
--- /dev/null
+++ b/src/context/Settings.tsx
@@ -0,0 +1,32 @@
+// This code is more or less redundant, but settings has so little state
+// updates that I can't be asked to pass everything through props each
+// time when I can just use the Context API.
+//
+// Replace references to SettingsContext with connectState in the future
+// if it does cause problems though.
+
+import { Settings } from "../redux/reducers/settings";
+import { connectState } from "../redux/connector";
+import { Children } from "../types/Preact";
+import { createContext } from "preact";
+
+export const SettingsContext = createContext({} as any);
+
+interface Props {
+ children?: Children,
+ settings: Settings
+}
+
+function Settings(props: Props) {
+ return (
+
+ { props.children }
+
+ )
+}
+
+export default connectState(Settings, state => {
+ return {
+ settings: state.settings
+ }
+});
diff --git a/src/context/revoltjs/RevoltClient.tsx b/src/context/revoltjs/RevoltClient.tsx
index dc919f20..c93b95a1 100644
--- a/src/context/revoltjs/RevoltClient.tsx
+++ b/src/context/revoltjs/RevoltClient.tsx
@@ -201,11 +201,12 @@ function Context({ auth, sync, children, dispatcher }: Props) {
}
}
} else {
- await client
- .fetchConfiguration()
- .catch(() =>
- console.error("Failed to connect to API server.")
- );
+ try {
+ await client.fetchConfiguration()
+ } catch (err) {
+ console.error("Failed to connect to API server.");
+ }
+
setStatus(ClientStatus.READY);
}
})();
diff --git a/src/lib/PaintCounter.tsx b/src/lib/PaintCounter.tsx
new file mode 100644
index 00000000..81251722
--- /dev/null
+++ b/src/lib/PaintCounter.tsx
@@ -0,0 +1,12 @@
+import { useState } from "preact/hooks";
+
+const counts: { [key: string]: number } = {};
+
+export default function PaintCounter() {
+ const [uniqueId] = useState('' + Math.random());
+ const count = counts[uniqueId] ?? 0;
+ counts[uniqueId] = count + 1;
+ return (
+ Painted {count + 1} time(s).
+ )
+}
diff --git a/src/lib/windowSize.ts b/src/lib/windowSize.ts
new file mode 100644
index 00000000..55089b4e
--- /dev/null
+++ b/src/lib/windowSize.ts
@@ -0,0 +1,24 @@
+import { useEffect, useState } from "preact/hooks";
+
+export function useWindowSize() {
+ const [windowSize, setWindowSize] = useState({
+ width: window.innerWidth,
+ height: window.innerHeight
+ });
+
+ useEffect(() => {
+ function handleResize() {
+ setWindowSize({
+ width: window.innerWidth,
+ height: window.innerHeight
+ });
+ }
+
+ window.addEventListener("resize", handleResize);
+ handleResize();
+
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ return windowSize;
+}
diff --git a/src/pages/App.tsx b/src/pages/App.tsx
new file mode 100644
index 00000000..6cfc6d0f
--- /dev/null
+++ b/src/pages/App.tsx
@@ -0,0 +1,18 @@
+import { OverlappingPanels } from "react-overlapping-panels";
+import { Switch, Route } from "react-router-dom";
+
+import Home from './home/Home';
+
+export default function App() {
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx
new file mode 100644
index 00000000..4ae75dbd
--- /dev/null
+++ b/src/pages/home/Home.tsx
@@ -0,0 +1,59 @@
+import { useChannels, useForceUpdate, useServers, useUser } from "../../context/revoltjs/hooks";
+import ChannelIcon from "../../components/common/ChannelIcon";
+import ServerIcon from "../../components/common/ServerIcon";
+import UserIcon from "../../components/common/UserIcon";
+import PaintCounter from "../../lib/PaintCounter";
+
+export function Nested() {
+ const ctx = useForceUpdate();
+
+ let user = useUser('01EX2NCWQ0CHS3QJF0FEQS1GR4', ctx)!;
+ let user2 = useUser('01EX40TVKYNV114H8Q8VWEGBWQ', ctx)!;
+ let user3 = useUser('01F5GV44HTXP3MTCD2VPV42DPE', ctx)!;
+
+ let channels = useChannels(undefined, ctx);
+ let servers = useServers(undefined, ctx);
+
+ return (
+ <>
+ Nested component
+
+ @{ user.username } is { user.online ? 'online' : 'offline' }
+
+ UserIcon Tests
+
+
+
+
+
+
+
+ Channels
+ { channels.map(channel =>
+ channel &&
+ channel.channel_type !== 'SavedMessages' &&
+ channel.channel_type !== 'DirectMessage' &&
+
+ ) }
+
+ Servers
+ { servers.map(server =>
+ server &&
+
+ ) }
+
+
+ { 'test long paragraph'.repeat(2000) }
+ >
+ )
+}
+
+export default function Home() {
+ return (
+
+ );
+}
diff --git a/src/pages/login/Login.module.scss b/src/pages/login/Login.module.scss
index 3b982bbb..27bd99ba 100644
--- a/src/pages/login/Login.module.scss
+++ b/src/pages/login/Login.module.scss
@@ -1,4 +1,7 @@
.login {
+ width: 100%;
+ height: 100%;
+
display: flex;
flex-direction: row;
diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx
index 3eda13ca..2334a174 100644
--- a/src/pages/login/Login.tsx
+++ b/src/pages/login/Login.tsx
@@ -15,7 +15,7 @@ import { FormCreate } from "./forms/FormCreate";
import { FormResend } from "./forms/FormResend";
import { FormReset, FormSendReset } from "./forms/FormReset";
-export const Login = () => {
+export default function Login() {
const theme = useContext(ThemeContext);
const { client } = useContext(AppContext);
diff --git a/src/styles/_page.scss b/src/styles/_page.scss
index 210e54c3..c99320ab 100644
--- a/src/styles/_page.scss
+++ b/src/styles/_page.scss
@@ -8,7 +8,7 @@
}
html {
- contain: content;
+ // contain: content;
background: var(--background);
background-size: cover !important;
background-repeat: no-repeat !important;
@@ -16,6 +16,8 @@ html {
html,
body {
+ margin: 0;
+ height: 100%;
font-family: "Open Sans", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@@ -29,3 +31,11 @@ body {
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
+
+#app {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
diff --git a/vite.config.ts b/vite.config.ts
index d53d1b3c..372f38d4 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -34,6 +34,7 @@ export default defineConfig({
})
],
build: {
+ sourcemap: true,
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
diff --git a/yarn.lock b/yarn.lock
index dd31e17e..76c2a992 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2969,10 +2969,10 @@ react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
-react-overlapping-panels@1.1.2-patch.0:
- version "1.1.2-patch.0"
- resolved "https://registry.yarnpkg.com/react-overlapping-panels/-/react-overlapping-panels-1.1.2-patch.0.tgz#335649735c029d334daea19ef6e30efc76b128fd"
- integrity sha512-PaXxk5HxBMYg46iADGGhkgXqqweJWo7yjSeT4/o0Q3s6Q7pl7Rz23lM3oW2gdJHBDOs/zBpZ+ZIP4j6grQlCOA==
+react-overlapping-panels@1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/react-overlapping-panels/-/react-overlapping-panels-1.2.1.tgz#3775a09ae6c83604d058d4082d1c8fed5cc59fe9"
+ integrity sha512-vkHLqX+X6HO13nAppZ5Z4tt4s8IMTA8sVf/FZFnnoqlQFIfTJAgdgZDa3LejMIrOJO6YMftVSVpzmusWTxvlUA==
react-redux@^7.2.4:
version "7.2.4"