some changes

main
Valerie 2022-08-03 01:05:22 -04:00
parent d7addbb496
commit 92913efdc9
21 changed files with 789 additions and 239 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

View File

@ -7,13 +7,14 @@
default-src 'self' data: https://ssl.gstatic.com https://fonts.gstatic.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
media-src *;
img-src 'self' data: content:;
img-src 'self' data: content: http://tinygraphs.com;
connect-src *;">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Vite App</title>
</head>
<body style=" margin: 0px; overflow: hidden;">
<div id="app" style="width: 100vw; height: 100vh;"></div>
<div id="portal-root"></div>
<script src="./src/index.tsx" type="module"></script>
</body>
</html>

View File

@ -1,7 +1,7 @@
import { createContext, useCallback, useEffect, useState, useMemo } from 'react';
import Channels from './pages/Channels';
import Chat from './pages/Chat';
import Sidebar from './components/Sidebar';
import Sidebar from './components/TwoPanel';
import NewAccount from './pages/NewAccount';
import ServerConnection from './components/ServerConnection';
import EphemeralState from './contexts/EphemeralState/EphemeralState';
@ -23,7 +23,7 @@ export default function App() {
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin='' />
<link href={"https://fonts.googleapis.com/css2?family=Fira+Sans&family=Josefin+Sans&family=Lato&family=Radio+Canada&family=Readex+Pro&family=Red+Hat+Text&family=Rubik&family=Signika&family=Telex&display=swap"} rel="stylesheet" />
<link href={"https://fonts.googleapis.com/css2?family=Fira+Sans&family=Josefin+Sans&family=Lato&family=Radio+Canada&family=Readex+Pro&family=Red+Hat+Text:wght@200;300;400;500;600;700;800;900&family=Rubik&family=Signika&family=Telex&display=swap"} rel="stylesheet" />
<style>{`
html {
--background: #282a36;
@ -53,7 +53,7 @@ export default function App() {
}
`}</style>
<div style={{
background: transparent ? 'rgba(0, 0, 0, 0)' : 'var(--background)',
background: transparent ? 'rgba(0, 0, 0, 0)' : 'var(--neutral-3)',
color: transparent ? 'black' : 'var(--foreground)',
fontSize: '16px',
fontFamily: "'Red Hat Text', sans-serif",

View File

@ -1,11 +1,15 @@
import { useContext } from "react";
import ServerConnection from "./components/ServerConnection";
import Sidebar from "./components/Sidebar";
import TwoPanel from "./components/TwoPanel";
import { SettingsContext } from "./contexts/EphemeralState/EphemeralState";
import useHomeServer from "./contexts/PersistentState/useHomeServerNative";
import useClientId from "./hooks/useClientId";
import useSessionToken from "./hooks/useSessionToken";
import Channels from "./pages/Channels";
import Chat from "./pages/Chat";
import NewAccount from "./pages/NewAccount";
import Settings from "./pages/Settings";
interface RouterProps {
[name: string]: React.ReactNode;
@ -17,6 +21,7 @@ export default function Router(props: RouterProps) {
const { clientId } = useClientId();
const { sessionToken } = useSessionToken();
const { homeServer } = useHomeServer();
const { isSettingsOpen } = useContext(SettingsContext);
const configured =
homeServer !== null &&
@ -26,13 +31,17 @@ export default function Router(props: RouterProps) {
return (
configured ? (
<ServerConnection url={homeServer}>
<Sidebar
threshold={800}
sidebar={300}
>
<Channels></Channels>
<Chat></Chat>
</Sidebar>
{isSettingsOpen ? (
<Settings></Settings>
) : (
<TwoPanel
threshold={800}
sidebar={300}
>
<Sidebar></Sidebar>
<Chat></Chat>
</TwoPanel>
)}
</ServerConnection>
) : (
<NewAccount></NewAccount>

View File

@ -0,0 +1,19 @@
import { useEffect } from "react";
import { createPortal } from "react-dom";
const Portal = ({children}: {children: React.ReactNode}) => {
const mount = document.getElementById("portal-root");
const el = document.createElement("div");
useEffect(() => {
if(mount === null) return;
mount.appendChild(el);
return () => {
mount.removeChild(el);
}
}, [el, mount]);
return createPortal(children, el)
};
export default Portal;

View File

@ -1,89 +1,116 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import useMediaQuery from '../lib/useMediaQueries';
import useHomeServer from "../hooks/useHomeServer";
import Channels from "../pages/Channels";
import pfp from '../../assets/pfp.jpg';
import { IoMdSettings } from 'react-icons/io';
import useHover from "../hooks/useHover";
import { useContext } from "react";
import { SettingsContext } from "../contexts/EphemeralState/EphemeralState";
export default function Sidebar() {
export default function Sidebar(props: {
threshold: number,
sidebar: number,
children: any[]
}) {
const bigScreen = useMediaQuery('(min-width:' + props.threshold + 'px)');
const [screenRef, setScreenRef] = useState<HTMLDivElement | null>(null);
const [startDrag, setStartDrag] = useState(0);
const [currentDrag, setCurrentDrag] = useState(0);
const [dragging, setDragging] = useState(false);
const [opened, setOpened] = useState(false);
return (
<div style={{
height: '100%',
display: 'grid',
gridTemplateRows: 'min-content 1fr min-content'
}}>
<TopSidebar></TopSidebar>
<Channels></Channels>
<MiniProfile></MiniProfile>
</div>
)
}
const difference = opened ?
Math.min(currentDrag - startDrag, 0) :
Math.max(currentDrag - startDrag, 0);
function TopSidebar() {
const pointerDown = useCallback((e: any) => {
setDragging(true);
setStartDrag(e.touches[0].clientX);
setCurrentDrag(e.touches[0].clientX);
}, [dragging, startDrag, currentDrag]);
const { homeServer } = useHomeServer();
const pointerUp = useCallback(() => {
setDragging(false);
if(difference > 0) {
setOpened(true);
} else if (difference < 0) {
setOpened(false);
}
}, [dragging, currentDrag, startDrag, opened]);
return (
<div style={{
lineHeight: '48px',
paddingLeft: '16px',
fontSize: '16px',
background: 'var(--neutral-3)',
boxShadow: 'black 0px 0px 3px 0px',
zIndex: '100',
fontWeight: '500',
}}>
{homeServer && new URL(homeServer).hostname.toLocaleLowerCase()}
</div>
)
}
const pointerMove = useCallback((e: any) => {
setCurrentDrag(e.touches[0].clientX);
}, [dragging, currentDrag]);
function MiniProfile() {
return (
<div style={{
fontSize: '16px',
background: 'var(--neutral-2)',
// boxShadow: 'black 0px 0px 3px 0px',
zIndex: '100',
fontWeight: '500',
display: 'grid',
gridTemplateColumns: 'min-content 1fr min-content'
}}>
<ProfilePicture></ProfilePicture>
<div style={{
display: 'grid',
placeItems: 'center left',
}}>
<div>
<div style={{
fontWeight: '400',
fontSize: '15px',
}}>Valerie</div>
<div style={{
fontWeight: '300',
fontSize: '13px',
}}>dev.valnet.xyz</div>
</div>
</div>
<div style={{
whiteSpace: 'nowrap',
display: 'grid',
gridAutoFlow: 'column',
placeItems: 'center right',
paddingRight: '8px',
}}>
<SettingsButton></SettingsButton>
{/* <SettingsButton></SettingsButton>
<SettingsButton></SettingsButton> */}
</div>
</div>
)
}
useEffect(() => {
if(screenRef === null) return;
screenRef.addEventListener('touchstart', pointerDown, { passive: true });
screenRef.addEventListener('touchend', pointerUp, { passive: true });
screenRef.addEventListener('touchmove', pointerMove, { passive: true });
// screenRef.addEventListener('pointercancel', pointerUp);
return () => {
screenRef.removeEventListener('touchstart', pointerDown);
screenRef.removeEventListener('touchend', pointerUp);
screenRef.removeEventListener('touchmove', pointerMove);
// screenRef.removeEventListener('pointercancel', pointerUp);
};
}, [screenRef, pointerUp, pointerDown]);
function SettingsButton() {
const [ref, hover] = useHover<HTMLDivElement>();
const { openSettings } = useContext(SettingsContext);
return <div ref={setScreenRef} style={{
width: '100%',
height: '100%',
position: 'relative',
userSelect: 'none',
// overflow: 'hidden',
}}>
<div
style={{
// background: 'red',
width: bigScreen ? (props.sidebar + 'px') : '100%',
height: '100%',
display: 'inline-block',
position: 'absolute',
top: '0px',
left: bigScreen ? '0px' : !dragging ? (opened ? '0px' : '-100%') : `calc(${difference}px ${opened ? '' : '- 100%'})`,
zIndex: '1',
transition: dragging ? 'none' : 'left 300ms linear, width 300ms linear',
}}
>{props.children[0]}</div>
<div
style={{
// background: 'green',
width: bigScreen ? 'calc(100% - ' + props.sidebar + 'px)' : '100%',
height: '100%',
display: 'inline-block',
position: 'absolute',
top: '0px',
left: bigScreen ? (props.sidebar + 'px') : '0px',
zIndex: '0',
transition: 'left 300ms linear, width 300ms linear',
}}
>{props.children[1]}</div>
</div>;
return <div ref={ref} className="settings" style={{
display: 'flex',
padding: '8px',
background: hover ?
'var(--neutral-4)' :
'initial',
borderRadius: '5px',
cursor: 'pointer',
}} onClick={openSettings}>
<IoMdSettings size="16"></IoMdSettings>
</div>
}
function ProfilePicture() {
const name = 'Val';
return <div style={{
backgroundImage: `url(${pfp})`,
width: '40px',
height: '40px',
margin: '12px',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
borderRadius: '50%',
}}></div>
}

View File

@ -0,0 +1,91 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import useMediaQuery from '../lib/useMediaQueries';
export default function TwoPanel(props: {
threshold: number,
sidebar: number,
children: any
}) {
const bigScreen = useMediaQuery('(min-width:' + props.threshold + 'px)');
const [screenRef, setScreenRef] = useState<HTMLDivElement | null>(null);
const [startDrag, setStartDrag] = useState(0);
const [currentDrag, setCurrentDrag] = useState(0);
const [dragging, setDragging] = useState(false);
const [opened, setOpened] = useState(false);
const difference = opened ?
Math.min(currentDrag - startDrag, 0) :
Math.max(currentDrag - startDrag, 0);
const pointerDown = useCallback((e: any) => {
setDragging(true);
setStartDrag(e.touches[0].clientX);
setCurrentDrag(e.touches[0].clientX);
}, [dragging, startDrag, currentDrag]);
const pointerUp = useCallback(() => {
setDragging(false);
if(difference > 0) {
setOpened(true);
} else if (difference < 0) {
setOpened(false);
}
}, [dragging, currentDrag, startDrag, opened]);
const pointerMove = useCallback((e: any) => {
setCurrentDrag(e.touches[0].clientX);
}, [dragging, currentDrag]);
useEffect(() => {
if(screenRef === null) return;
screenRef.addEventListener('touchstart', pointerDown, { passive: true });
screenRef.addEventListener('touchend', pointerUp, { passive: true });
screenRef.addEventListener('touchmove', pointerMove, { passive: true });
// screenRef.addEventListener('pointercancel', pointerUp);
return () => {
screenRef.removeEventListener('touchstart', pointerDown);
screenRef.removeEventListener('touchend', pointerUp);
screenRef.removeEventListener('touchmove', pointerMove);
// screenRef.removeEventListener('pointercancel', pointerUp);
};
}, [screenRef, pointerUp, pointerDown]);
return <div ref={setScreenRef} style={{
width: '100%',
height: '100%',
position: 'relative',
userSelect: 'none',
// overflow: 'hidden',
}}>
<div
style={{
// background: 'red',
width: bigScreen ? (props.sidebar + 'px') : '100%',
height: '100%',
display: 'inline-block',
position: 'absolute',
top: '0px',
left: bigScreen ? '0px' : !dragging ? (opened ? '0px' : '-100%') : `calc(${difference}px ${opened ? '' : '- 100%'})`,
zIndex: '1',
overflow: 'hidden',
transition: dragging ? 'none' : 'left 300ms linear, width 300ms linear',
}}
>{props.children[0]}</div>
<div
style={{
// background: 'green',
width: bigScreen ? 'calc(100% - ' + props.sidebar + 'px)' : '100%',
height: '100%',
display: 'inline-block',
position: 'absolute',
top: '0px',
left: bigScreen ? (props.sidebar + 'px') : '0px',
zIndex: '0',
overflow: 'hidden',
transition: 'left 300ms linear, width 300ms linear',
}}
>{props.children[1]}</div>
</div>;
}

View File

@ -8,7 +8,15 @@ export const ChannelContext = createContext<{
setChannel: () => {},
});
export const TransparencyContext = createContext<(transparent: boolean) => void>(() => {});
export const SettingsContext = createContext<{
openSettings: () => void,
closeSettings: () => void,
isSettingsOpen: boolean
}>({
openSettings() {},
closeSettings() {},
isSettingsOpen: false
});
export default function EphemeralState(props: {
onTransparencyChange: (value: boolean) => void,
@ -18,6 +26,8 @@ export default function EphemeralState(props: {
const [channel, setChannel] = useState<string | null>(null);
const [transparent, setTransparent] = useState(false);
const [settings, setSettings] = useState(true);
const channelContextValue = useMemo(() => {
return { channel, setChannel };
}, [channel, setChannel]);
@ -31,7 +41,13 @@ export default function EphemeralState(props: {
return (
<ChannelContext.Provider value={channelContextValue}>
<TransparencyContext.Provider value={setTransparent}>
{props.children}
<SettingsContext.Provider value={{
openSettings: () => setSettings(true),
closeSettings: () => setSettings(false),
isSettingsOpen: settings,
}}>
{props.children}
</SettingsContext.Provider>
</TransparencyContext.Provider>
</ChannelContext.Provider>
);

View File

@ -1,11 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import Sidebar from './components/Sidebar';
import Sidebar from './components/TwoPanel';
import App from './App';
import { createPortal } from 'react-dom';
const container = document.getElementById('app');
if(container !== null) {
const root = ReactDOM.createRoot(container)
const root = ReactDOM.createRoot(container);
// const portal = createPortal()
root.render(<App></App>);
} else {
throw new Error('Failed to initialize app, container not found!');

View File

@ -3,7 +3,7 @@ import { ServerConnectionContext } from '../components/ServerConnection';
import useSessionToken from '../hooks/useSessionToken';
import { Router, router, RouterObject } from './api';
export function useApi(actions: Router | RouterObject, deps: any[]) {
export function useApi(actions: Router | RouterObject = {}, deps: any[] = []) {
const connection = useContext(ServerConnectionContext);
const _router = typeof actions === 'object' ? router(actions) : actions;
const { sessionToken } = useSessionToken();

View File

@ -0,0 +1,70 @@
import useHover from "../hooks/useHover";
export function BigButton(props: any) {
const [ref, hover] = useHover<HTMLDivElement>();
const angle = props.angle ?? 20;
const width = props.width ?? 'auto';
const display = !!props.inline ? 'inline-grid' : 'grid';
return (
<div ref={ref} onClick={props.onClick ?? (() => { })} style={{
cursor: 'pointer',
width: width,
// margin: '4px',
display: display,
padding: '8px',
borderRadius: '8px',
gridAutoFlow: 'column',
gridTemplateColumns: 'min-content 1fr',
background: (!!props.bright) ? (
props.selected ? 'var(--neutral-6)' :
hover ? 'var(--neutral-6)' :
'var(--neutral-5)'
) : (
props.selected ? 'var(--neutral-5)' :
hover ? 'var(--neutral-4)' :
'inherit'
),
transform: `skew(-${angle}deg, 0deg)`,
boxSizing: 'border-box',
}}>
<div style={{
padding: '4px',
display: 'flex',
transform: `skew(${angle}deg, 0deg)`,
}}>
{props.icon({
size: 16,
color: !!props.color ? props.color : (!!props.bright) ? (
props.selected ? 'var(--neutral-9)' :
hover ? 'var(--neutral-8)' :
'var(--neutral-8)'
) : (
props.selected ? 'var(--neutral-9)' :
hover ? 'var(--neutral-7)' :
'var(--neutral-7)'
),
})}
</div>
<span style={{
lineHeight: '24px',
paddingLeft: '4px',
paddingRight: '4px',
color: !!props.color ? props.color : (!!props.bright) ? (
props.selected ? 'var(--neutral-9)' :
hover ? 'var(--neutral-9)' :
'var(--neutral-9)'
) : (
props.selected ? 'var(--neutral-9)' :
hover ? 'var(--neutral-9)' :
'var(--neutral-7)'
),
transform: `skew(${angle}deg, 0deg)`,
}}>
{props.text}
</span>
</div>
);
}

View File

@ -22,12 +22,12 @@ export default function Channel(props: ChannelProps) {
gridTemplateColumns: 'min-content 1fr',
color: selected ? 'cyan' : 'inherit',
cursor: 'pointer',
background: selected ? 'var(--neutral-4)' :
hover ? 'var(--neutral-3)' :
background: selected ? 'var(--neutral-5)' :
hover ? 'var(--neutral-4)' :
'inherit',
borderRadius: '8px',
// placeItems: 'left center',
// border: '1px solid white'
transform:'skew(-20deg, 0deg)',
transition: 'background 300ms, color 300ms',
}}
onClick={() => {
setChannel(uid);
@ -35,17 +35,21 @@ export default function Channel(props: ChannelProps) {
ref={ref}
>
<CgHashtag color={
selected ? 'var(--neutral-9)' :
hover ? 'var(--neutral-7)' :
'var(--neutral-7)'
} size={24} style={{
selected ? 'var(--neutral-9)' :
hover ? 'var(--neutral-7)' :
'var(--neutral-7)'
} size={24} style={{
margin: '4px',
transition: 'background 300ms, color 300ms',
transform:'skew(-5deg, 0deg)',
}}></CgHashtag>
<div style={{
lineHeight: '32px',
color: selected ? 'var(--neutral-9)' :
hover ? 'var(--neutral-9)' :
'var(--neutral-7)'
'var(--neutral-7)',
transform:'skew(20deg, 0deg)',
transition: 'background 300ms, color 300ms',
}}>
{name.toLowerCase().replaceAll(' ', '-').trim()}
</div>

View File

@ -24,7 +24,7 @@ export default function Channels() {
const [channels, setChannels] = useState<IChannel[]>([]);
const [unreads, setUnreads] = useState<IUnreads>({});
const { channel, setChannel } = useChannel()
const { clientId } = useClientId()
@ -44,10 +44,6 @@ export default function Channels() {
},
}, [channels, unreads]);
useEffect(() => {
// console.log('unreads', unreads);
}, [unreads]);
useEffect(() => {
if(channels.length === 0) {
send('channels:list');
@ -55,10 +51,8 @@ export default function Channels() {
}, [channels]);
useEffect(() => {
// console.log(channel, channels);
if(channels.length === 0) return;
if(channel !== null) return;
// console.log('this is what setChannel is', setChannel);
setChannel(channels[0].uid);
}, [channel, channels]);
@ -86,8 +80,10 @@ export default function Channels() {
return (
<div style={{
height: '100%',
background: '#21222c',
padding: '0px 8px'
background: 'var(--neutral-3)',
padding: '0px 8px',
overflowY: 'auto',
overflowX: 'hidden',
}}>
<br></br>
{channels.map(c => (

View File

@ -75,6 +75,7 @@ export default () => {
height: '100%',
width: '100%',
display: 'grid',
background: 'var(--neutral-4)',
gridTemplateColumns: `1fr ${CHATBOX_SIZE}px`,
gridTemplateRows: `1fr ${CHATBOX_SIZE}px`,
gridTemplateAreas: '"content content" "message send"',
@ -102,7 +103,7 @@ export default () => {
margin: PADDING + 'px',
marginRight: '0px',
borderRadius: ((CHATBOX_SIZE - PADDING*2) / 2) + 'px',
background: '#343746',
background: 'var(--neutral-5)',
gridArea: 'message',
display: 'grid',
placeItems: 'center center',

View File

@ -30,7 +30,7 @@ export function Message({
}}>
<span style={{
fontStyle: 'italic',
color: '#596793',
color: 'var(--neutral-6)',
textAlign: 'right',
userSelect: 'none',
marginRight: '16px',
@ -43,7 +43,7 @@ export function Message({
<span style={{
}}>
<div style={{
fontWeight: 'bold',
fontWeight: '500',
float: 'left',
paddingRight: firstLineIndent,
// marginRight: '16px',

View File

@ -1,9 +1,14 @@
import { useEffect, useState } from "react";
import { forwardRef, useEffect, useState } from "react";
import { useCallback, useContext, useRef } from "react"
import { BiLogIn } from "react-icons/bi";
import { FaUserPlus } from 'react-icons/fa';
import ServerConnection from "../components/ServerConnection";
import { useApi } from "../lib/useApi";
import QR from 'qrcode';
import useSessionToken from "../hooks/useSessionToken";
import useHomeServer from "../contexts/PersistentState/useHomeServerNative";
import { BigButton } from "./BigButton";
import { SignUp } from "./SignUp";
import { MdOutlineNavigateNext } from 'react-icons/md';
import useHover from "../hooks/useHover";
import { AiOutlineEdit } from "react-icons/ai";
export default function NewAccount() {
@ -12,7 +17,6 @@ export default function NewAccount() {
// const inputRef = useRef<HTMLInputElement>(null);
// const { setHomeServer } = useContext(HomeServerContext);
// const { setClientId } = useContext(ClientIdContext);
// const setTransparent = useContext(TransparencyContext);
@ -62,12 +66,26 @@ export default function NewAccount() {
// }
// }, [data, scanning])
// const [homeServer, setHomeServer] = useState<string | null>(null);
// const homeServerInputRef = useRef<HTMLInputElement>(null);
const { setHomeServer, homeServer } = useHomeServer();
const [homeServerInput, setHomeServerInput] = useState<string>(homeServer ?? '');
const [usernameInput, setUsernameInput] = useState('');
const [authCodeInput, setAuthCodeInput] = useState('');
const [returning, setReturning] = useState(true);
const homeServerInputRef = useRef<HTMLInputElement>(null);
const [homeServer, setHomeServer] = useState<string | null>(null);
const [connection, setConnection] = useState<WebSocket | null>(null);
const [connecting, setConnecting] = useState(false);
const [connectionError, setConnectionError] = useState('');
const [edittingHomeServer, setEdittingHomeServer] = useState(false);
const [homeServerInputRef, homeServerHovered] = useHover<HTMLInputElement>();
useEffect(() => {
if(homeServer === null) {
setEdittingHomeServer(true)
} else {
setEdittingHomeServer(false)
}
}, [homeServer]);
const connect = useCallback((url: string) => {
if(connecting) return;
@ -91,133 +109,240 @@ export default function NewAccount() {
ws.addEventListener('error', (e) => {
setConnectionError('Connection failed')
});
}, [connecting])
}, [connecting]);
// return (
// <div style={{
// display: 'grid',
// placeContent: 'center center',
// height: '100%',
// textAlign: 'center'
// }}>
// {returning ? (
// <div>
// <span>
// Login
// </span>
// &nbsp;
// &nbsp;
// &nbsp;
// <a href="#" onClick={() => setReturning(false)}>Sign up</a>
// </div>
// ) : (
// <>
// <div>
// <a href="#" onClick={() => setReturning(true)}>
// Login
// </a>
// &nbsp;
// &nbsp;
// &nbsp;
// <span>
// Sign up
// </span>
// </div>
// <br></br>
// <label>Home Server URL</label>
// <input style={{textAlign: 'center'}} ref={homeServerInputRef} defaultValue="wss://macos.valnet.xyz" disabled={connection !== null || connecting}></input>
// <button onClick={() => connect(homeServerInputRef.current?.value ?? '')} disabled={connection !== null || connecting}>Next</button>
// {connecting ? `Connecting...` : connectionError}
// <br></br>
// {connection !== null && (
// <ServerConnection url={homeServer ?? ''}>
// <SignUp>
// </SignUp>
// </ServerConnection>
// )}
// {/* Create New Account!! <br />
// Enter Home Server URL <br />
// <input defaultValue="wss://dev.valnet.xyz" ref={inputRef}></input> <br />
// <button onClick={go}> GO </button> <br />
// <br />
// or scan a QR! <br />
// <button onClick={scanQr}>SCAN</button><br></br>
// <pre>
// {data}
// {scanning ? 'SCANNING' : 'NOT SCANNING'}
// </pre> */}
// </>
// )}
// </div>
// );
return (
<div style={{
display: 'grid',
placeContent: 'center center',
width: '100%',
height: '100%',
textAlign: 'center'
display: 'grid',
placeItems: 'center center',
background: 'var(--neutral-3)',
}}>
{returning ? (
<div>
<span>
Login
</span>
&nbsp;
&nbsp;
&nbsp;
<a href="#" onClick={() => setReturning(false)}>Sign up</a>
</div>
) : (
<>
<div>
<a href="#" onClick={() => setReturning(true)}>
Login
</a>
&nbsp;
&nbsp;
&nbsp;
<span>
Sign up
</span>
<div style={{
width: '450px',
background: 'var(--neutral-4)',
boxShadow: '0px 4px 20px 0px var(--neutral-1)',
borderRadius: '8px',
transform: 'skew(-6deg, 0deg)',
}}>
<div style={{
transform: 'skew(6deg, 0deg)',
margin: '8px',
}}>
<div style={{
display: 'inline-block',
width: '50%',
paddingRight: '4px',
boxSizing: 'border-box',
}}>
<BigButton
icon={BiLogIn}
text="Login"
selected={returning}
angle={6}
width="100%"
inline={true}
onClick={() => setReturning(true)}
></BigButton>
</div>
<br></br>
<label>Home Server URL</label>
<input style={{textAlign: 'center'}} ref={homeServerInputRef} defaultValue="wss://macos.valnet.xyz" disabled={connection !== null || connecting}></input>
<button onClick={() => connect(homeServerInputRef.current?.value ?? '')} disabled={connection !== null || connecting}>Next</button>
{connecting ? `Connecting...` : connectionError}
<br></br>
{connection !== null && (
<ServerConnection url={homeServer ?? ''}>
<SignUp>
</SignUp>
</ServerConnection>
)}
{/* Create New Account!! <br />
Enter Home Server URL <br />
<input defaultValue="wss://dev.valnet.xyz" ref={inputRef}></input> <br />
<button onClick={go}> GO </button> <br />
<br />
or scan a QR! <br />
<button onClick={scanQr}>SCAN</button><br></br>
<pre>
{data}
{scanning ? 'SCANNING' : 'NOT SCANNING'}
</pre> */}
</>
)}
<div style={{
display: 'inline-block',
width: '50%',
paddingLeft: '4px',
boxSizing: 'border-box',
}}>
<BigButton
icon={FaUserPlus}
text="Sign up"
selected={!returning}
angle={6}
width="100%"
inline={true}
onClick={() => setReturning(false)}
></BigButton>
</div>
</div>
<Label>Home Server</Label>
<div style={{
transform: 'skew(6deg, 0deg)',
margin: '8px',
}}>
<AiOutlineEdit
style={{
display: homeServerHovered ? 'initial' : 'none',
float: 'right',
position: 'absolute',
top: '8px',
right: '12px',
zIndex: '1'
}}
size={24}
></AiOutlineEdit>
<Input
hoverRef={homeServerInputRef}
disabled={!edittingHomeServer}
value={homeServerInput}
setValue={setHomeServerInput}
onKeyPress={(e: any) => e.code === 'Enter' && (setHomeServer(homeServerInput))}
></Input>
</div>
<Label>Username</Label>
<div style={{
transform: 'skew(6deg, 0deg)',
margin: '8px',
}}>
<Input
disabled={edittingHomeServer}
value={usernameInput}
setValue={setUsernameInput}
></Input>
</div>
<Label>Auth Code</Label>
<div style={{
transform: 'skew(6deg, 0deg)',
margin: '8px',
}}>
<Input
disabled={edittingHomeServer}
value={authCodeInput}
setValue={setAuthCodeInput}
></Input>
</div>
<div style={{
transform: 'skew(6deg, 0deg)',
margin: '8px',
textAlign: 'right'
}}>
<BigButton
icon={MdOutlineNavigateNext}
text="Next"
selected={false}
angle={6}
width="auto"
inline={true}
onClick={() => {}}
></BigButton>
</div>
</div>
</div>
);
)
}
const SignUp = (props: any) => {
function Label(props: any) {
return <label style={{
paddingLeft: '24px',
fontWeight: 700,
fontSize: '12px',
textTransform: 'uppercase',
}}>{props.children}</label>
}
const usernameRef = useRef<HTMLInputElement>(null);
const displayNameRef = useRef<HTMLInputElement>(null);
const totpRef = useRef<HTMLInputElement>(null);
const [clientId, setClientId] = useState<string | null>(null);
// const [totpToken, setTotpToken] = useState<string | null>(null);
const [qr, setQr] = useState<string | null>(null);
const { setSessionToken } = useSessionToken();
interface InputProps {
value: string;
setValue: (s: string) => void;
default?: string;
onKeyPress?: (e: any) => void;
disabled?: boolean;
hoverRef?: React.LegacyRef<HTMLInputElement>
}
const { send } = useApi({
'client:new'(data: any) {
setClientId(data);
},
async 'totp:propose'(data: any) {
setQr(await QR.toDataURL(
'otpauth://totp/' +
(usernameRef.current?.value ?? '') +
'?secret=' +
data +
'&issuer=valnet-corner'
));
},
'totp:confirm'(data: any) {
setSessionToken(data.token);
console.log(data);
}
}, [setSessionToken]);
const Input = (props: InputProps) => {
const createAccount = useCallback(() => {
send('client:new', {
username: usernameRef.current?.value,
displayName: displayNameRef.current?.value,
})
}, []);
useEffect(() => {
if(clientId === null) return;
send('totp:propose', clientId);
}, [clientId]);
const changeTotp = useCallback(() => {
const value = totpRef.current?.value ?? '';
if(!(/[0-9]{6}/.test(value))) return;
send('totp:confirm', {
clientId,
code: value
})
}, [clientId]);
const _default = props.default ?? '';
const [focused, setFocused] = useState(false);
const disabled = props.disabled ?? false;
return (
<>
<label>Username</label>
<input defaultValue={'Test' + Math.floor(Math.random() * 1000)} disabled={clientId !== null} ref={usernameRef}></input>
<label>Display Name</label>
<input defaultValue="Val" disabled={clientId !== null} ref={displayNameRef}></input>
<button disabled={clientId !== null} onClick={createAccount}>Next</button>
{clientId && (
<>
<br></br>
<img src={qr ?? ''}></img>
<br></br>
<label>TOTP Code</label>
<input onChange={changeTotp} ref={totpRef}></input>
</>
)}
</>
<div style={{
width: '100%',
}}>
<input
ref={props.hoverRef}
onKeyPress={props.onKeyPress ?? (() => {})}
onFocus={(e) => !!props.disabled ? e.target.blur() : setFocused(true)}
onBlur={() => setFocused(false)}
disabled={disabled}
style={{
height: '40px',
width: '100%',
padding: '0px',
margin: '0px',
border: focused ? '1px solid var(--neutral-7)' : '1px solid rgba(0, 0, 0, 0)',
transform: 'skew(-6deg, 0deg)',
borderRadius: '8px',
outline: 'none',
fontSize: '20px',
paddingLeft: '12px',
paddingRight: '12px',
boxSizing: 'border-box',
background: disabled ? 'var(--neutral-3)' : focused ? 'var(--neutral-2)' : 'var(--neutral-1)',
color: disabled ? 'var(--neutral-6)' : 'var(--neutral-8)'
}}
spellCheck="false"
onChange={(e) => props.setValue(e.target.value)}
value={props.value}
></input>
</div>
)
}

View File

@ -0,0 +1,108 @@
import { useCallback, useContext, useState } from "react";
import { MdManageAccounts } from "react-icons/md";
import TwoPanel from "../components/TwoPanel";
import { SettingsContext } from "../contexts/EphemeralState/EphemeralState";
import { AiOutlineCloseCircle } from 'react-icons/ai';
import { BiLogOut } from 'react-icons/bi';
import { useApi } from "../lib/useApi";
import useSessionToken from "../hooks/useSessionToken";
import { BigButton } from "./BigButton";
const pages = [
['General', MdManageAccounts],
['Appearance', MdManageAccounts],
['Voice & Video', MdManageAccounts],
['Notifications', MdManageAccounts],
];
export default function Settings() {
const [page, setPage] = useState(0);
const { closeSettings } = useContext(SettingsContext);
const { setSessionToken } = useSessionToken()
const { send } = useApi();
const logout = useCallback(() => {
send('session:invalidate');
setSessionToken(null);
}, [send])
return <>
<div style={{
position: 'absolute',
top: '32px',
right: '32px',
zIndex: '1',
display: 'flex',
cursor: 'pointer',
borderRadius: '50%',
}} onClick={closeSettings}>
<AiOutlineCloseCircle
size={32}
></AiOutlineCloseCircle>
</div>
<TwoPanel
threshold={800}
sidebar={300}
>
<div style={{
background: 'var(--neutral-3)',
height: '100%',
marginLeft: '40%',
marginRight: '8px',
}}>
<br></br>
<br></br>
<br></br>
<br></br>
{pages.map((v, i) => (
<BigButton
key={i}
icon={pages[i][1]}
text={pages[i][0]}
selected={i === page}
onClick={() => setPage(i)}
></BigButton>
))}
<br></br>
<BigButton
icon={BiLogOut}
text="Logout"
selected={false}
color="var(--red)"
onClick={logout}
></BigButton>
</div>
<div style={{
background: 'var(--neutral-4)',
height: '100%',
paddingLeft: '32px'
}}>
<br></br>
<br></br>
{/* <br></br> */}
<div style={{
fontWeight: 700,
fontSize: '12px',
}}>
{pages[page][0].toString().toUpperCase()}
</div>
<br></br>
{(() => {
switch(page) {
case 0: return <GeneralSettings></GeneralSettings>
default: return <GeneralSettings></GeneralSettings>
}
})()}
</div>
</TwoPanel>
</>
}
function GeneralSettings() {
return (
<div>THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE THIS IS A PAGE </div>
)
}

View File

@ -0,0 +1,77 @@
import { useEffect, useState } from "react";
import { useCallback, useRef } from "react";
import { useApi } from "../lib/useApi";
import QR from 'qrcode';
import useSessionToken from "../hooks/useSessionToken";
export const SignUp = (props: any) => {
const usernameRef = useRef<HTMLInputElement>(null);
const displayNameRef = useRef<HTMLInputElement>(null);
const totpRef = useRef<HTMLInputElement>(null);
const [clientId, setClientId] = useState<string | null>(null);
// const [totpToken, setTotpToken] = useState<string | null>(null);
const [qr, setQr] = useState<string | null>(null);
const { setSessionToken } = useSessionToken();
const { send } = useApi({
'client:new'(data: any) {
setClientId(data);
},
async 'totp:propose'(data: any) {
setQr(await QR.toDataURL(
'otpauth://totp/' +
(usernameRef.current?.value ?? '') +
'?secret=' +
data +
'&issuer=valnet-corner'
));
},
'totp:confirm'(data: any) {
setSessionToken(data.token);
console.log(data);
}
}, [setSessionToken]);
const createAccount = useCallback(() => {
send('client:new', {
username: usernameRef.current?.value,
displayName: displayNameRef.current?.value,
});
}, []);
useEffect(() => {
if (clientId === null)
return;
send('totp:propose', clientId);
}, [clientId]);
const changeTotp = useCallback(() => {
const value = totpRef.current?.value ?? '';
if (!(/[0-9]{6}/.test(value)))
return;
send('totp:confirm', {
clientId,
code: value
});
}, [clientId]);
return (
<>
<label>Username</label>
<input defaultValue={'Test' + Math.floor(Math.random() * 1000)} disabled={clientId !== null} ref={usernameRef}></input>
<label>Display Name</label>
<input defaultValue="Val" disabled={clientId !== null} ref={displayNameRef}></input>
<button disabled={clientId !== null} onClick={createAccount}>Next</button>
{clientId && (
<>
<br></br>
<img src={qr ?? ''}></img>
<br></br>
<label>TOTP Code</label>
<input onChange={changeTotp} ref={totpRef}></input>
</>
)}
</>
);
};

View File

@ -17,6 +17,8 @@ const api = router({
client: client,
clients: client,
totp: totp,
session: session,
sessions: session,
});
expose(api, 3000);
@ -24,6 +26,7 @@ expose(api, 3000);
// -------------
import { update } from './db/migrate';
import session from './routers/session';
try {
update();

View File

@ -21,6 +21,7 @@ export function expose(router: Function, port: number) {
try {
if(typeof data === 'object' && 'sessionToken' in data) {
const auth = await validateSessionToken(data.sessionToken);
data.$sessionToken = data.sessionToken;
delete data['sessionToken'];
if(auth === null) return;
data.$clientId = auth;

View File

@ -6,8 +6,8 @@ import _get from '../db/snippets/session/get.sql'
import query from "../db/query";
export default router({
async 'invalidate'(token: string) {
await query(invalidate, token);
async 'invalidate'(data: any) {
await query(invalidate, data.$sessionToken);
return reply({
err: null
})