+
diff --git a/nym-vpn/ui/src/pages/home/ConnectionStatus.tsx b/nym-vpn/ui/src/pages/home/ConnectionStatus.tsx
new file mode 100644
index 0000000000..0da9232f4a
--- /dev/null
+++ b/nym-vpn/ui/src/pages/home/ConnectionStatus.tsx
@@ -0,0 +1,84 @@
+import clsx from 'clsx';
+import { useTranslation } from 'react-i18next';
+import { ConnectionState } from '../../types';
+import { useMainState } from '../../contexts';
+import ConnectionTimer from './ConnectionTimer';
+
+function ConnectionStatus() {
+ const state = useMainState();
+
+ const { t } = useTranslation('home');
+ const statusBadgeDynStyles = {
+ Connected: [
+ 'bg-blanc-nacre-icicle',
+ 'text-vert-menthe',
+ 'dark:bg-baltic-sea-quartzite',
+ ],
+ Disconnected: [
+ 'bg-blanc-nacre-platinum',
+ 'text-coal-mine-light',
+ 'dark:bg-baltic-sea-oil',
+ 'dark:text-coal-mine-dark',
+ ],
+ Connecting: [
+ 'bg-blanc-nacre-platinum',
+ 'text-baltic-sea',
+ 'dark:bg-baltic-sea-oil',
+ 'dark:text-white',
+ ],
+ Disconnecting: [
+ 'bg-blanc-nacre-platinum',
+ 'text-baltic-sea',
+ 'dark:bg-baltic-sea-oil',
+ 'dark:text-white',
+ ],
+ Unknown: [
+ 'bg-blanc-nacre-platinum',
+ 'text-coal-mine-light',
+ 'dark:bg-baltic-sea-oil',
+ 'dark:text-coal-mine-dark',
+ ],
+ };
+
+ const getStatusText = (state: ConnectionState) => {
+ switch (state) {
+ case 'Connected':
+ return t('status.connected');
+ case 'Disconnected':
+ return t('status.disconnected');
+ case 'Connecting':
+ return t('status.connecting');
+ case 'Disconnecting':
+ return t('status.disconnecting');
+ case 'Unknown':
+ return t('status.unknown');
+ }
+ };
+ return (
+
+
+
+ {getStatusText(state.state)}
+
+
+
+ {state.loading && state.progressMessages.length > 0 && (
+
+ {state.progressMessages[state.progressMessages.length - 1]}
+
+ )}
+ {state.state === 'Connected' &&
}
+ {state.error && (
+
{state.error}
+ )}
+
+
+ );
+}
+
+export default ConnectionStatus;
diff --git a/nym-vpn/ui/src/ui/ConnectionTimer.tsx b/nym-vpn/ui/src/pages/home/ConnectionTimer.tsx
similarity index 95%
rename from nym-vpn/ui/src/ui/ConnectionTimer.tsx
rename to nym-vpn/ui/src/pages/home/ConnectionTimer.tsx
index 68b139e188..bc06d93102 100644
--- a/nym-vpn/ui/src/ui/ConnectionTimer.tsx
+++ b/nym-vpn/ui/src/pages/home/ConnectionTimer.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
-import { useMainState } from '../contexts';
+import { useMainState } from '../../contexts';
function ConnectionTimer() {
const { sessionStartDate } = useMainState();
diff --git a/nym-vpn/ui/src/pages/home/Home.tsx b/nym-vpn/ui/src/pages/home/Home.tsx
new file mode 100644
index 0000000000..cc53da9df3
--- /dev/null
+++ b/nym-vpn/ui/src/pages/home/Home.tsx
@@ -0,0 +1,79 @@
+import { useTranslation } from 'react-i18next';
+import { invoke } from '@tauri-apps/api';
+import clsx from 'clsx';
+import { Button } from '@mui/base';
+import { useMainDispatch, useMainState } from '../../contexts';
+import { ConnectionState, StateDispatch } from '../../types';
+import NetworkModeSelect from './NetworkModeSelect';
+import ConnectionStatus from './ConnectionStatus';
+
+function Home() {
+ const state = useMainState();
+ const dispatch = useMainDispatch() as StateDispatch;
+
+ const { t } = useTranslation('home');
+
+ const handleClick = async () => {
+ if (state.state === 'Connected') {
+ dispatch({ type: 'disconnect' });
+ invoke('disconnect').then((result) => {
+ console.log(result);
+ });
+ } else if (state.state === 'Disconnected') {
+ dispatch({ type: 'connect' });
+ invoke('connect').then((result) => {
+ console.log(result);
+ });
+ }
+ };
+
+ const getButtonText = (state: ConnectionState) => {
+ switch (state) {
+ case 'Connected':
+ return t('disconnect');
+ case 'Disconnected':
+ return t('connect');
+ case 'Connecting':
+ return (
+
+ autorenew
+
+ );
+ case 'Disconnecting':
+ return (
+
+ autorenew
+
+ );
+ case 'Unknown':
+ return t('status.unknown');
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export default Home;
diff --git a/nym-vpn/ui/src/pages/home/NetworkModeSelect.tsx b/nym-vpn/ui/src/pages/home/NetworkModeSelect.tsx
new file mode 100644
index 0000000000..432233ff60
--- /dev/null
+++ b/nym-vpn/ui/src/pages/home/NetworkModeSelect.tsx
@@ -0,0 +1,116 @@
+import { useState } from 'react';
+import { RadioGroup } from '@headlessui/react';
+import { invoke } from '@tauri-apps/api';
+import clsx from 'clsx';
+import { useTranslation } from 'react-i18next';
+import { useMainDispatch, useMainState } from '../../contexts';
+import { StateDispatch, VpnMode } from '../../types';
+
+type VpnModeOption = { name: VpnMode; title: string; desc: string };
+
+function NetworkModeSelect() {
+ const state = useMainState();
+ const dispatch = useMainDispatch() as StateDispatch;
+ const [selected, setSelected] = useState(state.vpnMode);
+ const [loading, setLoading] = useState(false);
+
+ const { t } = useTranslation('home');
+
+ const handleNetworkModeChange = async (value: VpnMode) => {
+ if (state.state === 'Disconnected' && value !== state.vpnMode) {
+ setLoading(true);
+ try {
+ await invoke
('set_vpn_mode', { mode: value });
+ dispatch({ type: 'set-vpn-mode', mode: value });
+ } catch (e) {
+ console.log(e);
+ } finally {
+ setLoading(false);
+ }
+ }
+ };
+
+ const vpnModes: VpnModeOption[] = [
+ {
+ name: 'Mixnet',
+ title: t('mixnet-mode.title'),
+ desc: t('mixnet-mode.desc'),
+ },
+ {
+ name: 'TwoHop',
+ title: t('twohop-mode.title'),
+ desc: t('twohop-mode.desc'),
+ },
+ ];
+
+ const handleSelect = (value: VpnMode) => {
+ setSelected(value);
+ handleNetworkModeChange(value);
+ };
+
+ return (
+
+
+
+ {t('select-network-label')}
+
+
+ {vpnModes.map((mode) => (
+
+ clsx([
+ 'bg-white dark:bg-baltic-sea-jaguar relative flex rounded-lg px-5 py-3 shadow-md focus:outline-none',
+ (state.state !== 'Disconnected' || loading) &&
+ 'cursor-not-allowed',
+ checked &&
+ 'ring-0 ring-melon ring-offset-2 ring-offset-melon',
+ state.state === 'Disconnected' && 'cursor-pointer',
+ ])
+ }
+ disabled={state.state !== 'Disconnected' || loading}
+ >
+ {({ checked }) => {
+ return (
+
+ {checked ? (
+
+ radio_button_checked
+
+ ) : (
+
+ radio_button_unchecked
+
+ )}
+
+
+
+ {mode.title}
+
+
+ {mode.desc}
+
+
+
+
+ );
+ }}
+
+ ))}
+
+
+
+ );
+}
+
+export default NetworkModeSelect;
diff --git a/nym-vpn/ui/src/pages/home/index.ts b/nym-vpn/ui/src/pages/home/index.ts
new file mode 100644
index 0000000000..c2d92a5746
--- /dev/null
+++ b/nym-vpn/ui/src/pages/home/index.ts
@@ -0,0 +1 @@
+export { default as Home } from './Home';
diff --git a/nym-vpn/ui/src/pages/index.ts b/nym-vpn/ui/src/pages/index.ts
index 69154de097..c0d83e6544 100644
--- a/nym-vpn/ui/src/pages/index.ts
+++ b/nym-vpn/ui/src/pages/index.ts
@@ -1,5 +1,5 @@
+export * from './home';
export { default as NavLayout } from './NavLayout';
-export { default as Home } from './Home';
export { default as Settings } from './Settings';
export { default as NodeLocation } from './NodeLocation';
export { default as Error } from './Error';
diff --git a/nym-vpn/ui/src/state/main.ts b/nym-vpn/ui/src/state/main.ts
index e339ec781c..d54627a219 100644
--- a/nym-vpn/ui/src/state/main.ts
+++ b/nym-vpn/ui/src/state/main.ts
@@ -1,9 +1,10 @@
import dayjs from 'dayjs';
-import { AppData, AppState, ConnectionState } from '../types';
+import { AppData, AppState, ConnectionState, VpnMode } from '../types';
export type StateAction =
| { type: 'set-partial-state'; partialState: Partial }
| { type: 'change-connection-state'; state: ConnectionState }
+ | { type: 'set-vpn-mode'; mode: VpnMode }
| { type: 'set-error'; error: string }
| { type: 'reset-error' }
| { type: 'new-progress-message'; message: string }
@@ -18,16 +19,16 @@ export type StateAction =
export const initialState: AppState = {
state: 'Disconnected',
loading: false,
- privacyMode: 'High',
+ vpnMode: 'TwoHop',
tunnel: { name: 'nym', id: 'nym' },
- uiMode: 'Light',
+ uiTheme: 'Light',
progressMessages: [],
localAppData: {
monitoring: false,
autoconnect: false,
killswitch: false,
- uiMode: 'Light',
- privacyMode: 'High',
+ uiTheme: 'Light',
+ vpnMode: 'TwoHop',
entryNode: null,
exitNode: null,
},
@@ -35,6 +36,12 @@ export const initialState: AppState = {
export function reducer(state: AppState, action: StateAction): AppState {
switch (action.type) {
+ case 'set-vpn-mode':
+ return {
+ ...state,
+ vpnMode: action.mode,
+ localAppData: { ...state.localAppData, vpnMode: action.mode },
+ };
case 'set-partial-state': {
return { ...state, ...action.partialState };
}
diff --git a/nym-vpn/ui/src/state/provider.tsx b/nym-vpn/ui/src/state/provider.tsx
index 60eeedc028..981a23973a 100644
--- a/nym-vpn/ui/src/state/provider.tsx
+++ b/nym-vpn/ui/src/state/provider.tsx
@@ -48,8 +48,8 @@ export function MainStateProvider({ children }: Props) {
autoconnect: data.autoconnect || false,
monitoring: data.monitoring || false,
killswitch: data.killswitch || false,
- uiMode: data.ui_mode || 'Light',
- privacyMode: data.privacy_mode || 'High',
+ uiTheme: data.ui_theme || 'Light',
+ vpnMode: data.vpn_mode || 'TwoHop',
entryNode: data.entry_node,
exitNode: data.exit_node,
},
@@ -57,8 +57,8 @@ export function MainStateProvider({ children }: Props) {
dispatch({
type: 'set-partial-state',
partialState: {
- uiMode: data.ui_mode || 'Light',
- privacyMode: data.privacy_mode || 'High',
+ uiTheme: data.ui_theme || 'Light',
+ vpnMode: data.vpn_mode || 'TwoHop',
},
});
})
diff --git a/nym-vpn/ui/src/types/app-data.ts b/nym-vpn/ui/src/types/app-data.ts
index 940fb182b0..73d6f0c189 100644
--- a/nym-vpn/ui/src/types/app-data.ts
+++ b/nym-vpn/ui/src/types/app-data.ts
@@ -1,6 +1,6 @@
-import { PrivacyMode } from './app-state';
+import { VpnMode } from './app-state';
-export type UiMode = 'Dark' | 'Light';
+export type UiTheme = 'Dark' | 'Light';
export interface NodeConfig {
id: string;
@@ -11,8 +11,8 @@ export interface AppData {
monitoring: boolean;
autoconnect: boolean;
killswitch: boolean;
- uiMode: UiMode;
- privacyMode: PrivacyMode;
+ uiTheme: UiTheme;
+ vpnMode: VpnMode;
entryNode?: NodeConfig | null;
exitNode?: NodeConfig | null;
}
@@ -22,8 +22,8 @@ export interface AppDataFromBackend {
monitoring: boolean | null;
autoconnect: boolean | null;
killswitch: boolean | null;
- ui_mode: UiMode | null;
- privacy_mode: PrivacyMode;
+ ui_theme: UiTheme | null;
+ vpn_mode: VpnMode;
entry_node: NodeConfig | null;
exit_node: NodeConfig | null;
}
diff --git a/nym-vpn/ui/src/types/app-state.ts b/nym-vpn/ui/src/types/app-state.ts
index 0b190ff37d..c30f0aa5e9 100644
--- a/nym-vpn/ui/src/types/app-state.ts
+++ b/nym-vpn/ui/src/types/app-state.ts
@@ -10,7 +10,7 @@ export type ConnectionState =
| 'Disconnecting'
| 'Unknown';
-export type PrivacyMode = 'High' | 'Medium' | 'Low';
+export type VpnMode = 'TwoHop' | 'Mixnet';
export interface TunnelConfig {
id: string;
@@ -23,9 +23,9 @@ export type AppState = {
error?: string | null;
progressMessages: string[];
sessionStartDate?: Dayjs | null;
- privacyMode: PrivacyMode;
+ vpnMode: VpnMode;
tunnel: TunnelConfig;
- uiMode: 'Light' | 'Dark';
+ uiTheme: 'Light' | 'Dark';
localAppData: AppData;
};
diff --git a/nym-vpn/ui/src/types/routes.ts b/nym-vpn/ui/src/types/routes.ts
index b30777985c..a18280924e 100644
--- a/nym-vpn/ui/src/types/routes.ts
+++ b/nym-vpn/ui/src/types/routes.ts
@@ -1,3 +1,3 @@
-import { routes } from '../constants.ts';
+import { routes } from '../constants';
export type Routes = (typeof routes)[keyof typeof routes];
diff --git a/nym-vpn/ui/src/ui/ThemeSetter.tsx b/nym-vpn/ui/src/ui/ThemeSetter.tsx
index 3e43226846..99941e74cc 100644
--- a/nym-vpn/ui/src/ui/ThemeSetter.tsx
+++ b/nym-vpn/ui/src/ui/ThemeSetter.tsx
@@ -7,10 +7,10 @@ export default function ThemeSetter({
}: {
children: React.ReactNode;
}) {
- const { uiMode } = useMainState();
+ const { uiTheme } = useMainState();
return (
-
+
{children}
);
diff --git a/nym-vpn/ui/src/ui/TopBar.tsx b/nym-vpn/ui/src/ui/TopBar.tsx
index 3785dc4bc9..db31a5fcb3 100644
--- a/nym-vpn/ui/src/ui/TopBar.tsx
+++ b/nym-vpn/ui/src/ui/TopBar.tsx
@@ -67,7 +67,7 @@ export default function TopBar() {
}, [location.pathname, navBarData]);
return (
-