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 ( + + ); +} 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 ( + + ); +} 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 ( + + ); +} 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 ( +
+

HOME

+ + +
+ ); +} 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"