refactor(vpnapp): node location logic (#4223)
* refactor node location logic fontend: remove app local data from state remove node config from state use only country location state backend: add node location in state add set_node_location command * call backend to update node location * clean code
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="text-[14px]">
|
||||
<html lang="en" class="text-[12px]">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
|
||||
@@ -37,7 +37,7 @@ pub fn get_node_countries() -> Result<Vec<Country>, CmdError> {
|
||||
Ok(countries)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
#[instrument(skip(state))]
|
||||
#[tauri::command]
|
||||
pub async fn set_app_data(
|
||||
state: State<'_, SharedAppData>,
|
||||
@@ -56,7 +56,7 @@ pub async fn set_app_data(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
#[instrument(skip_all)]
|
||||
#[tauri::command]
|
||||
pub async fn get_app_data(
|
||||
state: State<'_, SharedAppData>,
|
||||
@@ -75,7 +75,7 @@ pub async fn get_app_data(
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
#[instrument(skip(data_state))]
|
||||
#[tauri::command]
|
||||
pub async fn set_ui_theme(
|
||||
data_state: State<'_, SharedAppData>,
|
||||
|
||||
@@ -182,7 +182,7 @@ pub async fn get_connection_start_time(
|
||||
Ok(app_state.connection_start_time.map(|t| t.unix_timestamp()))
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
#[instrument(skip(app_state, data_state))]
|
||||
#[tauri::command]
|
||||
pub async fn set_vpn_mode(
|
||||
app_state: State<'_, SharedAppState>,
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod app_data;
|
||||
pub mod connection;
|
||||
pub mod node_location;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
use tracing::{debug, instrument};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::{
|
||||
error::{CmdError, CmdErrorSource},
|
||||
states::{app::Country, SharedAppData, SharedAppState},
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, TS, Clone)]
|
||||
pub enum NodeType {
|
||||
Entry,
|
||||
Exit,
|
||||
}
|
||||
|
||||
#[instrument(skip(app_state, data_state))]
|
||||
#[tauri::command]
|
||||
pub async fn set_node_location(
|
||||
app_state: State<'_, SharedAppState>,
|
||||
data_state: State<'_, SharedAppData>,
|
||||
node_type: NodeType,
|
||||
country: Country,
|
||||
) -> Result<(), CmdError> {
|
||||
debug!("set_node_location");
|
||||
let mut state = app_state.lock().await;
|
||||
match node_type {
|
||||
NodeType::Entry => {
|
||||
state.entry_node_location = Some(country.clone());
|
||||
}
|
||||
NodeType::Exit => {
|
||||
state.exit_node_location = Some(country.clone());
|
||||
}
|
||||
}
|
||||
// TODO pick a node according to the selected country
|
||||
|
||||
// save the location on disk
|
||||
let mut app_data_store = data_state.lock().await;
|
||||
let mut app_data = app_data_store
|
||||
.read()
|
||||
.await
|
||||
.map_err(|e| CmdError::new(CmdErrorSource::InternalError, e.to_string()))?;
|
||||
|
||||
match node_type {
|
||||
NodeType::Entry => {
|
||||
app_data.entry_node_location = Some(country);
|
||||
}
|
||||
NodeType::Exit => {
|
||||
app_data.exit_node_location = Some(country);
|
||||
}
|
||||
}
|
||||
app_data_store.data = app_data;
|
||||
app_data_store
|
||||
.write()
|
||||
.await
|
||||
.map_err(|e| CmdError::new(CmdErrorSource::InternalError, e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::states::app::{NodeConfig, VpnMode};
|
||||
use crate::states::app::{Country, NodeConfig, VpnMode};
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, TS)]
|
||||
#[ts(export)]
|
||||
@@ -21,4 +21,6 @@ pub struct AppData {
|
||||
pub vpn_mode: Option<VpnMode>,
|
||||
pub entry_node: Option<NodeConfig>,
|
||||
pub exit_node: Option<NodeConfig>,
|
||||
pub entry_node_location: Option<Country>,
|
||||
pub exit_node_location: Option<Country>,
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ fn main() -> Result<()> {
|
||||
app_data::set_app_data,
|
||||
app_data::set_ui_theme,
|
||||
app_data::get_node_countries,
|
||||
node_location::set_node_location,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -6,7 +6,7 @@ use ts_rs::TS;
|
||||
#[ts(export)]
|
||||
pub struct NodeConfig {
|
||||
pub id: String,
|
||||
pub country: String,
|
||||
pub country: Country,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, TS)]
|
||||
@@ -42,6 +42,8 @@ pub struct AppState {
|
||||
pub vpn_mode: VpnMode,
|
||||
pub entry_node: Option<NodeConfig>,
|
||||
pub exit_node: Option<NodeConfig>,
|
||||
pub entry_node_location: Option<Country>,
|
||||
pub exit_node_location: Option<Country>,
|
||||
pub tunnel: Option<TunnelConfig>,
|
||||
pub connection_start_time: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
@@ -65,13 +65,15 @@ export function mockTauriIPC() {
|
||||
ui_theme: 'Dark',
|
||||
vpn_mode: 'TwoHop',
|
||||
entry_node: {
|
||||
country: QuickConnectCountry.name,
|
||||
country: QuickConnectCountry,
|
||||
id: QuickConnectCountry.code,
|
||||
},
|
||||
exit_node: {
|
||||
country: QuickConnectCountry.name,
|
||||
country: QuickConnectCountry,
|
||||
id: QuickConnectCountry.code,
|
||||
},
|
||||
entry_node_location: null,
|
||||
exit_node_location: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,14 +61,9 @@ function Home() {
|
||||
<NetworkModeSelect />
|
||||
<div className="py-2"></div>
|
||||
<HopSelect
|
||||
country={
|
||||
state.localAppData.exitNode ?? {
|
||||
country: QuickConnectCountry.name,
|
||||
id: QuickConnectCountry.code,
|
||||
}
|
||||
}
|
||||
country={state.exitNodeLocation ?? QuickConnectCountry}
|
||||
onClick={() => navigate(routes.exitNodeLocation)}
|
||||
nodeHop={{ type: 'exit' }}
|
||||
nodeHop="exit"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NodeConfig } from '../../types';
|
||||
import { NodeHop } from '../../types/general';
|
||||
import { Country, NodeHop } from '../../types';
|
||||
|
||||
interface HopSelectProps {
|
||||
country: NodeConfig;
|
||||
country: Country;
|
||||
onClick: () => void;
|
||||
nodeHop: NodeHop;
|
||||
}
|
||||
@@ -27,13 +26,13 @@ export default function HopSelect({
|
||||
readOnly={true}
|
||||
type="text"
|
||||
id="floating_outlined"
|
||||
value={country.country}
|
||||
value={country.name}
|
||||
className="dark:bg-baltic-sea cursor-pointer pl-11 dark:placeholder-white border border-gun-powder block px-2.5 pb-4 pt-4 w-full text-sm text-gray-900 bg-transparent rounded-lg border-1 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"
|
||||
/>
|
||||
<img
|
||||
src={`./flags/${country.id.toLowerCase()}.svg`}
|
||||
src={`./flags/${country.code.toLowerCase()}.svg`}
|
||||
className="h-8 scale-75 pointer-events-none absolute fill-current top-1/2 transform -translate-y-1/2 left-2"
|
||||
alt={country.id}
|
||||
alt={country.code}
|
||||
/>
|
||||
<span className="font-icon scale-125 pointer-events-none absolute fill-current top-1/4 transform -translate-x-1/2 right-2">
|
||||
arrow_right
|
||||
@@ -42,7 +41,7 @@ export default function HopSelect({
|
||||
htmlFor="floating_outlined"
|
||||
className="dark:text-white bg-blanc-nacre dark:bg-baltic-sea absolute text-sm text-gray-500 dark:text-gray-400 ml-4 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] dark:bg-gray-900 px-2 peer-placeholder-shown:px-2 peer-placeholder-shown:text-blue-600 peer-placeholder-shown:dark:text-blue-500 peer-placeholder-shown:top-2 peer-placeholder-shown:scale-75 peer-placeholder-shown:-translate-y-4 rtl:peer-placeholder-shown:translate-x-1/4 rtl:peer-placeholder-shown:left-auto start-1"
|
||||
>
|
||||
{nodeHop.type === 'entry' ? t('first-hop') : t('last-hop')}
|
||||
{nodeHop === 'entry' ? t('first-hop') : t('last-hop')}
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function CountryList({
|
||||
<ul className="flex flex-col w-full items-stretch p-1">
|
||||
{countries && countries.length > 0 ? (
|
||||
countries.map((country) => (
|
||||
<li key={t(country.name)} className="list-none w-full">
|
||||
<li key={country.code} className="list-none w-full">
|
||||
<div
|
||||
role="presentation"
|
||||
onKeyDown={() => onClick(country.name, country.code)}
|
||||
|
||||
@@ -3,24 +3,20 @@ import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { useMainDispatch, useMainState } from '../../contexts';
|
||||
import { InputEvent, NodeHop } from '../../types/general';
|
||||
import { Country, StateDispatch } from '../../types';
|
||||
import { Country, InputEvent, NodeHop, StateDispatch } from '../../types';
|
||||
import { routes } from '../../constants';
|
||||
import SearchBox from './SearchBox';
|
||||
import CountryList from './CountryList';
|
||||
import QuickConnect from './QuickConnect';
|
||||
|
||||
function NodeLocation({ type }: NodeHop) {
|
||||
const isEntryNodeSelectionScreen = type === 'entry';
|
||||
function NodeLocation({ node }: { node: NodeHop }) {
|
||||
const { t } = useTranslation('nodeLocation');
|
||||
const [countries, setCountries] = useState<Country[]>([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [foundCountries, setFoundCountries] = useState<Country[]>([]);
|
||||
|
||||
const {
|
||||
localAppData: { entryNode, exitNode },
|
||||
} = useMainState();
|
||||
const { entryNodeLocation, exitNodeLocation } = useMainState();
|
||||
const dispatch = useMainDispatch() as StateDispatch;
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -54,19 +50,24 @@ function NodeLocation({ type }: NodeHop) {
|
||||
};
|
||||
|
||||
const isCountrySelected = (code: string): boolean => {
|
||||
return isEntryNodeSelectionScreen
|
||||
? entryNode?.id === code
|
||||
: exitNode?.id === code;
|
||||
return node === 'entry'
|
||||
? entryNodeLocation?.code === code
|
||||
: exitNodeLocation?.code === code;
|
||||
};
|
||||
|
||||
const setNodeSelection = (name: string, code: string) => {
|
||||
const nodeType = isEntryNodeSelectionScreen
|
||||
? 'set-entry-node'
|
||||
: 'set-exit-node';
|
||||
dispatch({ type: nodeType, data: { country: name, id: code } });
|
||||
};
|
||||
const handleCountrySelection = (name: string, code: string) => {
|
||||
setNodeSelection(name, code);
|
||||
const handleCountrySelection = async (name: string, code: string) => {
|
||||
try {
|
||||
await invoke<void>('set_node_location', {
|
||||
nodeType: node === 'entry' ? 'Entry' : 'Exit',
|
||||
country: { name, code },
|
||||
});
|
||||
dispatch({
|
||||
type: 'set-node-location',
|
||||
payload: { hop: node, country: { name, code } },
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
navigate(routes.root);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { InputEvent } from '../../types/general';
|
||||
import { InputEvent } from '../../types';
|
||||
|
||||
interface SearchProps {
|
||||
value: string;
|
||||
onChange: (e: InputEvent) => void;
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
export default function SearchBox({
|
||||
value,
|
||||
onChange,
|
||||
|
||||
@@ -20,12 +20,12 @@ const router = createBrowserRouter([
|
||||
{
|
||||
path: routes.entryNodeLocation,
|
||||
// eslint-disable-next-line react/jsx-no-undef
|
||||
element: <NodeLocation type="entry" />,
|
||||
element: <NodeLocation node="entry" />,
|
||||
errorElement: <Error />,
|
||||
},
|
||||
{
|
||||
path: routes.exitNodeLocation,
|
||||
element: <NodeLocation type="exit" />,
|
||||
element: <NodeLocation node="exit" />,
|
||||
errorElement: <Error />,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
AppData,
|
||||
AppState,
|
||||
ConnectionState,
|
||||
NodeConfig,
|
||||
Country,
|
||||
NodeHop,
|
||||
UiTheme,
|
||||
VpnMode,
|
||||
} from '../types';
|
||||
import { QuickConnectCountry } from '../constants';
|
||||
|
||||
export type StateAction =
|
||||
| { type: 'set-partial-state'; partialState: Partial<AppState> }
|
||||
@@ -22,10 +21,8 @@ export type StateAction =
|
||||
| { type: 'set-connection-start-time'; startTime?: number | null }
|
||||
| { type: 'set-disconnected' }
|
||||
| { type: 'reset' }
|
||||
| { type: 'set-app-data'; data: AppData }
|
||||
| { type: 'set-ui-theme'; theme: UiTheme }
|
||||
| { type: 'set-exit-node'; data: NodeConfig }
|
||||
| { type: 'set-entry-node'; data: NodeConfig };
|
||||
| { type: 'set-node-location'; payload: { hop: NodeHop; country: Country } };
|
||||
|
||||
export const initialState: AppState = {
|
||||
state: 'Disconnected',
|
||||
@@ -34,30 +31,27 @@ export const initialState: AppState = {
|
||||
tunnel: { name: 'nym', id: 'nym' },
|
||||
uiTheme: 'Light',
|
||||
progressMessages: [],
|
||||
localAppData: {
|
||||
monitoring: false,
|
||||
autoconnect: false,
|
||||
killswitch: false,
|
||||
uiTheme: 'Light',
|
||||
vpnMode: 'TwoHop',
|
||||
entryNode: {
|
||||
country: QuickConnectCountry.name,
|
||||
id: QuickConnectCountry.code,
|
||||
},
|
||||
exitNode: {
|
||||
country: QuickConnectCountry.name,
|
||||
id: QuickConnectCountry.code,
|
||||
},
|
||||
},
|
||||
entryNodeLocation: null,
|
||||
exitNodeLocation: null,
|
||||
};
|
||||
|
||||
export function reducer(state: AppState, action: StateAction): AppState {
|
||||
switch (action.type) {
|
||||
case 'set-node-location':
|
||||
if (action.payload.hop === 'entry') {
|
||||
return {
|
||||
...state,
|
||||
entryNodeLocation: action.payload.country,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
exitNodeLocation: action.payload.country,
|
||||
};
|
||||
case 'set-vpn-mode':
|
||||
return {
|
||||
...state,
|
||||
vpnMode: action.mode,
|
||||
localAppData: { ...state.localAppData, vpnMode: action.mode },
|
||||
};
|
||||
case 'set-partial-state': {
|
||||
return { ...state, ...action.partialState };
|
||||
@@ -79,18 +73,6 @@ export function reducer(state: AppState, action: StateAction): AppState {
|
||||
case 'disconnect': {
|
||||
return { ...state, state: 'Disconnecting', loading: true };
|
||||
}
|
||||
case 'set-exit-node': {
|
||||
return {
|
||||
...state,
|
||||
localAppData: { ...state.localAppData, exitNode: action.data },
|
||||
};
|
||||
}
|
||||
case 'set-entry-node': {
|
||||
return {
|
||||
...state,
|
||||
localAppData: { ...state.localAppData, entryNode: action.data },
|
||||
};
|
||||
}
|
||||
case 'set-connected': {
|
||||
return {
|
||||
...state,
|
||||
@@ -115,9 +97,6 @@ export function reducer(state: AppState, action: StateAction): AppState {
|
||||
sessionStartDate:
|
||||
(action.startTime && dayjs.unix(action.startTime)) || null,
|
||||
};
|
||||
case 'set-app-data': {
|
||||
return { ...state, localAppData: action.data };
|
||||
}
|
||||
case 'set-error':
|
||||
return { ...state, error: action.error };
|
||||
case 'reset-error':
|
||||
@@ -131,7 +110,6 @@ export function reducer(state: AppState, action: StateAction): AppState {
|
||||
return {
|
||||
...state,
|
||||
uiTheme: action.theme,
|
||||
localAppData: { ...state.localAppData, uiTheme: action.theme },
|
||||
};
|
||||
case 'reset':
|
||||
return initialState;
|
||||
|
||||
@@ -43,29 +43,13 @@ export function MainStateProvider({ children }: Props) {
|
||||
getAppData()
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
dispatch({
|
||||
type: 'set-app-data',
|
||||
data: {
|
||||
autoconnect: data.autoconnect || false,
|
||||
monitoring: data.monitoring || false,
|
||||
killswitch: data.killswitch || false,
|
||||
uiTheme: data.ui_theme || 'Light',
|
||||
vpnMode: data.vpn_mode || 'TwoHop',
|
||||
entryNode: data.entry_node || {
|
||||
country: QuickConnectCountry.name,
|
||||
id: QuickConnectCountry.code,
|
||||
},
|
||||
exitNode: data.exit_node || {
|
||||
country: QuickConnectCountry.name,
|
||||
id: QuickConnectCountry.code,
|
||||
},
|
||||
},
|
||||
});
|
||||
dispatch({
|
||||
type: 'set-partial-state',
|
||||
partialState: {
|
||||
uiTheme: data.ui_theme || 'Light',
|
||||
vpnMode: data.vpn_mode || 'TwoHop',
|
||||
entryNodeLocation: data.entry_node_location || QuickConnectCountry,
|
||||
exitNodeLocation: data.exit_node_location || QuickConnectCountry,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ export type UiTheme = 'Dark' | 'Light';
|
||||
|
||||
export interface NodeConfig {
|
||||
id: string;
|
||||
country: string;
|
||||
country: Country;
|
||||
}
|
||||
|
||||
export type Country = {
|
||||
@@ -12,23 +12,15 @@ export type Country = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
export interface AppData {
|
||||
monitoring: boolean;
|
||||
autoconnect: boolean;
|
||||
killswitch: boolean;
|
||||
uiTheme: UiTheme;
|
||||
vpnMode: VpnMode;
|
||||
entryNode?: NodeConfig | null;
|
||||
exitNode?: NodeConfig | null;
|
||||
}
|
||||
|
||||
// tauri type, hence the use of snake_case
|
||||
export interface AppDataFromBackend {
|
||||
monitoring: boolean | null;
|
||||
autoconnect: boolean | null;
|
||||
killswitch: boolean | null;
|
||||
ui_theme: UiTheme | null;
|
||||
vpn_mode: VpnMode;
|
||||
vpn_mode: VpnMode | null;
|
||||
entry_node: NodeConfig | null;
|
||||
exit_node: NodeConfig | null;
|
||||
entry_node_location: Country | null;
|
||||
exit_node_location: Country | null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Dispatch } from 'react';
|
||||
import { Dayjs } from 'dayjs';
|
||||
import { StateAction } from '../state';
|
||||
import { AppData } from './app-data';
|
||||
import { Country } from './app-data';
|
||||
|
||||
export type ConnectionState =
|
||||
| 'Connected'
|
||||
@@ -26,7 +26,8 @@ export type AppState = {
|
||||
vpnMode: VpnMode;
|
||||
tunnel: TunnelConfig;
|
||||
uiTheme: 'Light' | 'Dark';
|
||||
localAppData: AppData;
|
||||
entryNodeLocation: Country | null;
|
||||
exitNodeLocation: Country | null;
|
||||
};
|
||||
|
||||
export type ConnectionEventPayload = {
|
||||
|
||||
@@ -2,6 +2,4 @@ import React from 'react';
|
||||
|
||||
export type InputEvent = React.ChangeEvent<HTMLInputElement>;
|
||||
|
||||
export type NodeHop = {
|
||||
type: 'entry' | 'exit';
|
||||
};
|
||||
export type NodeHop = 'entry' | 'exit';
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './app-state';
|
||||
export * from './app-data';
|
||||
export * from './tauri-ipc';
|
||||
export * from './routes';
|
||||
export * from './general';
|
||||
|
||||
Reference in New Issue
Block a user