diff --git a/app/client/web/src/access/Access.tsx b/app/client/web/src/access/Access.tsx index e96fb058..ddf473eb 100644 --- a/app/client/web/src/access/Access.tsx +++ b/app/client/web/src/access/Access.tsx @@ -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: ( +
{state.strings.tryAgain}
+ ), + cancelProps: { display: 'none' }, + confirmProps: { display: 'none' }, + }); } } actions.setLoading(false) @@ -228,6 +240,7 @@ export function Access() { value={state.username} leftSectionPointerEvents="none" leftSection={} + rightSection={state.taken ? : 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 > actions.setNode(event.currentTarget.value)} /> - - {state.strings.tryAgain} -
diff --git a/app/client/web/src/identity/Identity.tsx b/app/client/web/src/identity/Identity.tsx index 377108a3..a6343e23 100644 --- a/app/client/web/src/identity/Identity.tsx +++ b/app/client/web/src/identity/Identity.tsx @@ -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: ( actions.setAll(ev.currentTarget.checked)} /> ), diff --git a/app/client/web/src/session/Session.tsx b/app/client/web/src/session/Session.tsx index 38177dfd..893e4d59 100644 --- a/app/client/web/src/session/Session.tsx +++ b/app/client/web/src/session/Session.tsx @@ -26,7 +26,7 @@ export function Session() { <>
{ tab === 'settings' && ( - + )}
@@ -61,7 +61,7 @@ export function Session() {
- +
)} diff --git a/app/client/web/src/settings/Settings.module.css b/app/client/web/src/settings/Settings.module.css index a009e5eb..a68b8062 100644 --- a/app/client/web/src/settings/Settings.module.css +++ b/app/client/web/src/settings/Settings.module.css @@ -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; diff --git a/app/client/web/src/settings/Settings.tsx b/app/client/web/src/settings/Settings.tsx index 2f62ee18..6e0db941 100644 --- a/app/client/web/src/settings/Settings.tsx +++ b/app/client/web/src/settings/Settings.tsx @@ -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: ( 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: ( + {state.strings.tryAgain} + ), + cancelProps: { display: 'none' }, + confirmProps: { display: 'none' }, + }); + + } + } + return ( <> { state.profileSet && ( @@ -93,12 +124,14 @@ export function Settings() {
-
-
- + { showLogout && ( +
+
+ +
+ { state.strings.logout }
- { state.strings.logout } -
+ )}
@@ -110,7 +143,7 @@ export function Settings() {
- { state.strings.changeLogin } + { state.strings.changeLogin }
@@ -205,6 +238,62 @@ export function Settings() {
)} + +
+ } + rightSection={state.taken ? : null} + placeholder={state.strings.username} + onChange={(event) => + actions.setUsername(event.currentTarget.value) + } + error={state.taken} + /> + } + placeholder={state.strings.password} + onChange={(event) => + actions.setPassword(event.currentTarget.value) + } + /> + } + placeholder={state.strings.confirmPassword} + onChange={(event) => + actions.setConfirm(event.currentTarget.value) + } + /> +
+ + +
+
+
); } diff --git a/app/client/web/src/settings/useSettings.hook.ts b/app/client/web/src/settings/useSettings.hook.ts index ba3c0d28..6735b577 100644 --- a/app/client/web/src/settings/useSettings.hook.ts +++ b/app/client/web/src/settings/useSettings.hook.ts @@ -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 } diff --git a/app/sdk/src/index.ts b/app/sdk/src/index.ts index 5e4e0bba..81433342 100644 --- a/app/sdk/src/index.ts +++ b/app/sdk/src/index.ts @@ -51,7 +51,7 @@ export class DatabagSDK { } public async username(name: string, token: string, node: string, secure: boolean): Promise { - 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 { diff --git a/app/sdk/src/net/getUsername.ts b/app/sdk/src/net/getUsername.ts index 73c6a180..5d5f410a 100644 --- a/app/sdk/src/net/getUsername.ts +++ b/app/sdk/src/net/getUsername.ts @@ -1,7 +1,7 @@ import axios from 'redaxios'; -export async function getUsername(name: string, token: string, node: string, secure: boolean): Promise { - const param = token ? `&token=${token}` : ''; +export async function getUsername(name: string, token: string | null, agent: string | null, node: string, secure: boolean): Promise { + 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); diff --git a/app/sdk/src/settings.ts b/app/sdk/src/settings.ts index 43c42fc8..beae7b61 100644 --- a/app/sdk/src/settings.ts +++ b/app/sdk/src/settings.ts @@ -211,7 +211,7 @@ export class SettingsModule implements Settings { public async getUsernameStatus(username: string): Promise { 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 {