implementing settings screen

This commit is contained in:
balzack 2024-09-07 21:46:23 -07:00
parent 8f4bd44c35
commit 4c7f5315cc
9 changed files with 173 additions and 27 deletions

View File

@ -17,15 +17,15 @@ import left from '../images/login.png'
import {
IconLock,
IconUser,
IconUsers,
IconSettings,
IconServer,
IconKey,
} from '@tabler/icons-react'
import { modals } from '@mantine/modals';
export function Access() {
const { state, actions } = useAccess()
const [alertOpened, { open: alertOpen, close: alertClose }] =
useDisclosure(false)
const [urlOpened, { open: urlOpen, close: urlClose }] = useDisclosure(false)
const [otpOpened, { open: otpOpen, close: otpClose }] = useDisclosure(false)
const [disabled, setDisabled] = useState(false)
@ -58,7 +58,19 @@ export function Access() {
}
otpOpen()
} else {
alertOpen()
modals.openConfirmModal({
title: state.strings.operationFailed,
withCloseButton: true,
overlayProps: {
backgroundOpacity: 0.55,
blur: 3,
},
children: (
<div>{state.strings.tryAgain}</div>
),
cancelProps: { display: 'none' },
confirmProps: { display: 'none' },
});
}
}
actions.setLoading(false)
@ -228,6 +240,7 @@ export function Access() {
value={state.username}
leftSectionPointerEvents="none"
leftSection={<IconUser />}
rightSection={state.taken ? <IconUsers /> : null}
placeholder={state.strings.username}
onChange={(event) =>
actions.setUsername(event.currentTarget.value)
@ -331,6 +344,7 @@ export function Access() {
opened={urlOpened}
onClose={urlClose}
withCloseButton={false}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
centered
>
<TextInput
@ -346,17 +360,11 @@ export function Access() {
onChange={(event) => actions.setNode(event.currentTarget.value)}
/>
</Modal>
<Modal
opened={alertOpened}
onClose={alertClose}
title={state.strings.operationFailed}
>
{state.strings.tryAgain}
</Modal>
<Modal
opened={otpOpened}
onClose={otpClose}
withCloseButton={false}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
centered
>
<div className={classes.mfa}>

View File

@ -18,6 +18,10 @@ export function Identity({ settings, contacts }: { settings: () => void, contact
const logout = () => modals.openConfirmModal({
title: state.strings.confirmLogout,
withCloseButton: false,
overlayProps: {
backgroundOpacity: 0.55,
blur: 3,
},
children: (
<Switch label={state.strings.allDevices} size="md" onChange={(ev) => actions.setAll(ev.currentTarget.checked)} />
),

View File

@ -26,7 +26,7 @@ export function Session() {
<>
<div className={classes.screen}>
{ tab === 'settings' && (
<Settings />
<Settings showLogout={true} />
)}
</div>
<div className={classes.tabs}>
@ -61,7 +61,7 @@ export function Session() {
<div className={classes.right}>
</div>
<Drawer opened={settings} onClose={closeSettings} withCloseButton={false} size="sm" position="right">
<Settings />
<Settings showLogout={false} />
</Drawer>
</div>
)}

View File

@ -1,3 +1,16 @@
.change {
display: flex;
flex-direction: column;
gap: 8px;
}
.control {
display: flex;
gap: 16px;
padding-top: 8px;
justify-content: flex-end;
}
.settings {
display: flex;
flex-direction: column;

View File

@ -1,16 +1,22 @@
import { useSettings } from './useSettings.hook';
import { Radio, Group, Select, Switch, Text, Image, Button, UnstyledButton } from '@mantine/core';
import { Modal, TextInput, PasswordInput, Radio, Group, Select, Switch, Text, Image, Button, UnstyledButton } from '@mantine/core';
import classes from './Settings.module.css';
import { IconClock, IconCalendar, IconVideo, IconMicrophone, IconWorld, IconBrightness, IconTicket, IconCloudLock, IconBell, IconEye, IconBook, IconMapPin, IconLogout, IconLogin } from '@tabler/icons-react'
import { IconLock, IconUser, IconClock, IconCalendar, IconUsers, IconVideo, IconMicrophone, IconWorld, IconBrightness, IconTicket, IconCloudLock, IconBell, IconEye, IconBook, IconMapPin, IconLogout, IconLogin } from '@tabler/icons-react'
import avatar from '../images/avatar.png'
import { modals } from '@mantine/modals';
import { useDisclosure } from '@mantine/hooks'
export function Settings() {
export function Settings({ showLogout }) {
const { state, actions } = useSettings();
const [changeOpened, { open: changeOpen, close: changeClose }] = useDisclosure(false)
const logout = () => modals.openConfirmModal({
title: state.strings.confirmLogout,
withCloseButton: false,
overlayProps: {
backgroundOpacity: 0.55,
blur: 3,
},
children: (
<Switch label={state.strings.allDevices} size="md" onChange={(ev) => actions.setAll(ev.currentTarget.checked)} />
),
@ -18,6 +24,31 @@ export function Settings() {
onConfirm: actions.logout,
});
const setLogin = async () => {
try {
throw new Error("NO");
await actions.setLogin();
changeClose();
}
catch (err) {
console.log(err);
modals.openConfirmModal({
title: state.strings.operationFailed,
withCloseButton: true,
overlayProps: {
backgroundOpacity: 0.55,
blur: 3,
},
children: (
<Text>{state.strings.tryAgain}</Text>
),
cancelProps: { display: 'none' },
confirmProps: { display: 'none' },
});
}
}
return (
<>
{ state.profileSet && (
@ -93,12 +124,14 @@ export function Settings() {
<Switch className={classes.entryControl} />
</div>
<div className={classes.divider} />
<div className={classes.entry}>
<div className={classes.entryIcon}>
<IconLogout />
{ showLogout && (
<div className={classes.entry}>
<div className={classes.entryIcon}>
<IconLogout />
</div>
<Text className={classes.entryLabel} onClick={logout}>{ state.strings.logout }</Text>
</div>
<Text className={classes.entryLabel} onClick={logout}>{ state.strings.logout }</Text>
</div>
)}
<div className={classes.entry}>
<div className={classes.entryIcon}>
<IconTicket />
@ -110,7 +143,7 @@ export function Settings() {
<div className={classes.entryIcon}>
<IconLogin />
</div>
<Text className={classes.entryLabel}>{ state.strings.changeLogin }</Text>
<Text className={classes.entryLabel} onClick={changeOpen}>{ state.strings.changeLogin }</Text>
</div>
<div className={classes.divider} />
@ -205,6 +238,62 @@ export function Settings() {
</div>
</div>
)}
<Modal
title={state.strings.changeLogin}
opened={changeOpened}
onClose={changeClose}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
centered
>
<div className={classes.change}>
<TextInput
className={classes.input}
size="md"
value={state.username}
leftSectionPointerEvents="none"
leftSection={<IconUser />}
rightSection={state.taken ? <IconUsers /> : null}
placeholder={state.strings.username}
onChange={(event) =>
actions.setUsername(event.currentTarget.value)
}
error={state.taken}
/>
<PasswordInput
className={classes.input}
size="md"
value={state.password}
leftSection={<IconLock />}
placeholder={state.strings.password}
onChange={(event) =>
actions.setPassword(event.currentTarget.value)
}
/>
<PasswordInput
className={classes.input}
size="md"
value={state.confirm}
leftSection={<IconLock />}
placeholder={state.strings.confirmPassword}
onChange={(event) =>
actions.setConfirm(event.currentTarget.value)
}
/>
<div className={classes.control}>
<Button variant="default" onClick={changeClose}>
{state.strings.cancel}
</Button>
<Button
variant="filled"
onClick={setLogin}
loading={state.loading}
disabled={state.taken || !state.checked || !state.username || !state.password || state.confirm !== state.password}
>
{state.strings.save}
</Button>
</div>
</div>
</Modal>
</>
);
}

View File

@ -1,13 +1,16 @@
import { useEffect, useState, useContext } from 'react'
import { useEffect, useState, useContext, useRef } from 'react'
import { AppContext } from '../context/AppContext';
import { DisplayContext } from '../context/DisplayContext';
import { ContextType } from '../context/ContextType';
import { Session, Settings, Identity, type Profile, type Config } from 'databag-client-sdk'
const DEBOUNCE_MS = 1000;
export function useSettings() {
const display = useContext(DisplayContext) as ContextType;
const app = useContext(AppContext) as ContextType;
const debounce = useRef(useRef(setTimeout(() => {}, 0)));
const [state, setState] = useState({
config: {} as Config,
@ -26,6 +29,11 @@ export function useSettings() {
timeFormat: '12h',
dateFormat: 'mm/dd',
all: false,
password: '',
confirm: '',
username: '',
taken: false,
checked: true,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -50,7 +58,7 @@ export function useSettings() {
}
settings.addConfigListener(setConfig);
const setProfile = (profile: Profile) => {
updateState({ profile, profileSet: true, imageUrl: identity.getProfileImageUrl() })
updateState({ profile, username: profile.handle, profileSet: true, imageUrl: identity.getProfileImageUrl() })
}
identity.addProfileListener(setProfile)
return () => {
@ -168,6 +176,30 @@ console.log(audioInputs, videoInputs);
logout: async () => {
await app.actions.accountLogout(state.all);
},
setUsername: (username) => {
updateState({ username, taken: false, checked: false });
clearTimeout(debounce.current);
if (!username || username === state.profile.handle) {
updateState({ available: true, checked: true});
}
else {
debounce.current = setTimeout(async () => {
const { settings } = getSession();
const available = await settings.getUsernameStatus(username);
updateState({ taken: !available, checked: true });
}, DEBOUNCE_MS);
}
},
setPassword: (password) => {
updateState({ password });
},
setConfirm: (confirm) => {
updateState({ confirm });
},
setLogin: async () => {
const { settings } = getSession();
await settings.setLogin(state.username, state.password);
}
}
return { state, actions }

View File

@ -51,7 +51,7 @@ export class DatabagSDK {
}
public async username(name: string, token: string, node: string, secure: boolean): Promise<boolean> {
return await getUsername(name, token, node, secure);
return await getUsername(name, token, null, node, secure);
}
public async login(handle: string, password: string, node: string, secure: boolean, mfaCode: string | null, params: SessionParams): Promise<Session> {

View File

@ -1,7 +1,7 @@
import axios from 'redaxios';
export async function getUsername(name: string, token: string, node: string, secure: boolean): Promise<boolean> {
const param = token ? `&token=${token}` : '';
export async function getUsername(name: string, token: string | null, agent: string | null, node: string, secure: boolean): Promise<boolean> {
const param = token ? `&token=${token}` : agent ? `&agent=${agent}` : '';
const username = encodeURIComponent(name)
const endpoint = `http${secure ? 's' : ''}://${node}/account/username?name=${username}${param}`;
const response = await axios.get(endpoint);

View File

@ -211,7 +211,7 @@ export class SettingsModule implements Settings {
public async getUsernameStatus(username: string): Promise<boolean> {
const { node, secure, token } = this;
return await getUsername(username, token, node, secure);
return await getUsername(username, null, token, node, secure);
}
public async setLogin(username: string, password: string): Promise<void> {