some changes
parent
d7addbb496
commit
92913efdc9
Binary file not shown.
|
After Width: | Height: | Size: 366 KiB |
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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!');
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 => (
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
//
|
||||
//
|
||||
//
|
||||
// <a href="#" onClick={() => setReturning(false)}>Sign up</a>
|
||||
// </div>
|
||||
// ) : (
|
||||
// <>
|
||||
// <div>
|
||||
// <a href="#" onClick={() => setReturning(true)}>
|
||||
// Login
|
||||
// </a>
|
||||
//
|
||||
//
|
||||
//
|
||||
// <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>
|
||||
|
||||
|
||||
|
||||
<a href="#" onClick={() => setReturning(false)}>Sign up</a>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<a href="#" onClick={() => setReturning(true)}>
|
||||
Login
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
Reference in New Issue