summaryrefslogtreecommitdiffhomepage
path: root/gui
diff options
context:
space:
mode:
Diffstat (limited to 'gui')
-rw-r--r--gui/changelog.txt4
-rw-r--r--gui/locales/messages.pot5
-rw-r--r--gui/src/main/changelog.ts65
-rw-r--r--gui/src/main/gui-settings.ts14
-rw-r--r--gui/src/main/index.ts11
-rw-r--r--gui/src/renderer/app.tsx27
-rw-r--r--gui/src/renderer/components/Account.tsx141
-rw-r--r--gui/src/renderer/components/AdvancedSettings.tsx253
-rw-r--r--gui/src/renderer/components/AppRouter.tsx73
-rw-r--r--gui/src/renderer/components/Changelog.tsx70
-rw-r--r--gui/src/renderer/components/Connect.tsx11
-rw-r--r--gui/src/renderer/components/ErrorBoundary.tsx7
-rw-r--r--gui/src/renderer/components/ExpiredAccountErrorView.tsx48
-rw-r--r--gui/src/renderer/components/Modal.tsx22
-rw-r--r--gui/src/renderer/components/OpenVPNSettings.tsx276
-rw-r--r--gui/src/renderer/components/Preferences.tsx507
-rw-r--r--gui/src/renderer/components/SplitTunnelingSettings.tsx64
-rw-r--r--gui/src/renderer/components/Support.tsx56
-rw-r--r--gui/src/renderer/components/WireguardSettings.tsx320
-rw-r--r--gui/src/renderer/redux/settings/reducers.ts1
-rw-r--r--gui/src/renderer/redux/userinterface/actions.ts17
-rw-r--r--gui/src/renderer/redux/userinterface/reducers.ts9
-rw-r--r--gui/src/shared/gui-settings-state.ts4
-rw-r--r--gui/src/shared/ipc-schema.ts4
-rw-r--r--gui/src/shared/ipc-types.ts2
-rw-r--r--gui/src/shared/localization-contexts.ts1
-rw-r--r--gui/tasks/distribution.js22
-rw-r--r--gui/test/setup/changelog.spec.ts65
28 files changed, 1181 insertions, 918 deletions
diff --git a/gui/changelog.txt b/gui/changelog.txt
new file mode 100644
index 0000000000..20fab22b44
--- /dev/null
+++ b/gui/changelog.txt
@@ -0,0 +1,4 @@
+Add multihop with WireGuard tunnels. Can be enabled under the advanced settings.
+Add malware blocking. Can be found under preferences.
+[macOS] Fix slow wakeups from sleep. The internet and VPN tunnels should now come up instantly.
+[Windows] Make WireGuardNT the default driver for WireGuard. If you use WireGuard, you can potentially see significant performance increases.
diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot
index 721cc61bb1..6e17e41731 100644
--- a/gui/locales/messages.pot
+++ b/gui/locales/messages.pot
@@ -114,6 +114,7 @@ msgstr ""
msgid "Filter"
msgstr ""
+#. This is a button which closes a dialog.
msgid "Got it!"
msgstr ""
@@ -363,6 +364,10 @@ msgctxt "auth-failure"
msgid "You are logged in with an invalid account number. Please log out and try another one."
msgstr ""
+msgctxt "changelog"
+msgid "Changes in this version:"
+msgstr ""
+
#. The selected location label displayed on the main view, when a user selected a specific host to connect to.
#. Example: Malmö (se-mma-001)
#. Available placeholders:
diff --git a/gui/src/main/changelog.ts b/gui/src/main/changelog.ts
new file mode 100644
index 0000000000..4c169b055b
--- /dev/null
+++ b/gui/src/main/changelog.ts
@@ -0,0 +1,65 @@
+import fs from 'fs';
+import path from 'path';
+import { IChangelog } from '../shared/ipc-types';
+import log from '../shared/logging';
+
+// Reads and parses the changelog file.
+export function readChangelog(): IChangelog {
+ try {
+ const changelogPath = path.join(__dirname, '..', '..', '..', 'changelog.txt');
+ const contents = fs.readFileSync(changelogPath).toString();
+ return parseChangelog(contents);
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to read changelog.txt', error.message);
+ return [];
+ }
+}
+
+// Parses the contents of the changelog file and returns all relevant items.
+export function parseChangelog(changelog: string): IChangelog {
+ const items = changelog
+ .split('\n')
+ .map((item) => item.trim())
+ .filter((item) => item !== '');
+ return filterForPlatform(items);
+}
+
+// Filters the changelog items based on platform
+function filterForPlatform(items: Array<string>): IChangelog {
+ return items
+ .map((item) => {
+ // Extracts the platforms from from the string if there are any specified. Platforms are
+ // specified within brackets with separated with a comma.
+ const platforms = item
+ .match(/^\[.*?\]/)
+ ?.flatMap((match) => match.slice(1, -1).split(','))
+ .map((platform) => platform.trim());
+ if (!platforms || isPlatform(platforms)) {
+ // If there are no platforms specified or if the current platform matches one of the
+ // specified, then the item is included.
+ return item.replace(/^\[.*?\]/, '').trim();
+ } else {
+ return undefined;
+ }
+ })
+ .filter((item): item is string => item !== undefined);
+}
+
+// Checks if an OS name corresponds to the current platform.
+function isPlatform(platformNames: Array<string>): boolean {
+ const platforms = platformNames.map((platformName) => {
+ switch (platformName.toLowerCase()) {
+ case 'windows':
+ return 'win32';
+ case 'macos':
+ return 'darwin';
+ case 'linux':
+ return 'linux';
+ default:
+ return platformName;
+ }
+ });
+
+ return platforms.includes(process.platform);
+}
diff --git a/gui/src/main/gui-settings.ts b/gui/src/main/gui-settings.ts
index d5432e2321..73c546a24b 100644
--- a/gui/src/main/gui-settings.ts
+++ b/gui/src/main/gui-settings.ts
@@ -4,7 +4,7 @@ import * as path from 'path';
import { IGuiSettingsState, SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state';
import log from '../shared/logging';
-const settingsSchema = {
+const settingsSchema: Record<keyof IGuiSettingsState, string> = {
preferredLocale: 'string',
autoConnect: 'boolean',
enableSystemNotifications: 'boolean',
@@ -12,6 +12,7 @@ const settingsSchema = {
startMinimized: 'boolean',
unpinnedWindow: 'boolean',
browsedForSplitTunnelingApplications: 'Array<string>',
+ changelogDisplayedForVersion: 'string',
};
const defaultSettings: IGuiSettingsState = {
@@ -22,6 +23,7 @@ const defaultSettings: IGuiSettingsState = {
startMinimized: false,
unpinnedWindow: process.platform !== 'win32' && process.platform !== 'darwin',
browsedForSplitTunnelingApplications: [],
+ changelogDisplayedForVersion: '',
};
export default class GuiSettings {
@@ -92,6 +94,16 @@ export default class GuiSettings {
return this.stateValue.browsedForSplitTunnelingApplications;
}
+ set changelogDisplayedForVersion(newValue: string | undefined) {
+ this.changeStateAndNotify({ ...this.stateValue, changelogDisplayedForVersion: newValue ?? '' });
+ }
+
+ get changelogDisplayedForVersion(): string | undefined {
+ return this.stateValue.changelogDisplayedForVersion === ''
+ ? undefined
+ : this.stateValue.changelogDisplayedForVersion;
+ }
+
public load() {
try {
const settingsFile = this.filePath();
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index f6567e47ad..a9daa98367 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -41,8 +41,9 @@ import { messages, relayLocations } from '../shared/gettext';
import { SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state';
import log, { ConsoleOutput, Logger } from '../shared/logging';
import { LogLevel } from '../shared/logging-types';
+import { readChangelog } from './changelog';
import { IpcMainEventChannel } from './ipc-event-channel';
-import { ICurrentAppVersionInfo } from '../shared/ipc-types';
+import { IChangelog, ICurrentAppVersionInfo } from '../shared/ipc-types';
import {
AccountExpiredNotificationProvider,
CloseToAccountExpiryNotificationProvider,
@@ -260,6 +261,8 @@ class ApplicationMain {
private quitWithoutDisconnect = false;
+ private changelog?: IChangelog;
+
public run() {
// Remove window animations to combat window flickering when opening window. Can be removed when
// this issue has been resolved: https://github.com/electron/electron/issues/12130
@@ -301,6 +304,7 @@ class ApplicationMain {
}
this.guiSettings.load();
+ this.changelog = readChangelog();
app.on('render-process-gone', (_event, _webContents, details) => {
log.error(
@@ -1228,6 +1232,7 @@ class ApplicationMain {
translations: this.translations,
windowsSplitTunnelingApplications: this.windowsSplitTunnelingApplications,
macOsScrollbarVisibility: this.macOsScrollbarVisibility,
+ changelog: this.changelog ?? [],
}));
IpcMainEventChannel.settings.handleSetAllowLan((allowLan: boolean) =>
@@ -1461,6 +1466,10 @@ class ApplicationMain {
return response;
});
+ IpcMainEventChannel.currentVersion.handleDisplayedChangelog(() => {
+ this.guiSettings.changelogDisplayedForVersion = this.currentVersion.gui;
+ });
+
if (windowsSplitTunneling) {
this.guiSettings.browsedForSplitTunnelingApplications.forEach(
windowsSplitTunneling.addApplicationPathToCache,
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 7b4b63c969..56ac8887d4 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -5,6 +5,7 @@ import { bindActionCreators } from 'redux';
import AppRouter from './components/AppRouter';
import MacOsScrollbarDetection from './components/MacOsScrollbarDetection';
import ErrorBoundary from './components/ErrorBoundary';
+import PlatformWindowContainer from './containers/PlatformWindowContainer';
import { AppContext } from './context';
import accountActions from './redux/account/actions';
@@ -15,7 +16,7 @@ import configureStore from './redux/store';
import userInterfaceActions from './redux/userinterface/actions';
import versionActions from './redux/version/actions';
-import { ICurrentAppVersionInfo } from '../shared/ipc-types';
+import { IChangelog, ICurrentAppVersionInfo } from '../shared/ipc-types';
import { IApplication, ILinuxSplitTunnelingApplication } from '../shared/application-types';
import { IGuiSettingsState, SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state';
import { messages, relayLocations } from '../shared/gettext';
@@ -45,6 +46,8 @@ import {
import { LogLevel } from '../shared/logging-types';
import IpcOutput from './lib/logging';
import { RoutePath } from './lib/routes';
+import { Changelog } from './components/Changelog';
+import { ModalContainer } from './components/Modal';
const IpcRendererEventChannel = window.ipc;
@@ -212,6 +215,7 @@ export default class AppRenderer {
this.setGuiSettings(initialState.guiSettings);
this.storeAutoStart(initialState.autoStart);
this.setWireguardPublicKey(initialState.wireguardPublicKey);
+ this.setChangelog(initialState.changelog);
if (initialState.macOsScrollbarVisibility !== undefined) {
this.reduxActions.userInterface.setMacOsScrollbarVisibility(
@@ -248,10 +252,15 @@ export default class AppRenderer {
<AppContext.Provider value={{ app: this }}>
<Provider store={this.reduxStore}>
<Router history={this.history.asHistory}>
- <ErrorBoundary>
- <AppRouter />
- {window.env.platform === 'darwin' && <MacOsScrollbarDetection />}
- </ErrorBoundary>
+ <PlatformWindowContainer>
+ <ErrorBoundary>
+ <ModalContainer>
+ <AppRouter />
+ <Changelog />
+ {window.env.platform === 'darwin' && <MacOsScrollbarDetection />}
+ </ModalContainer>
+ </ErrorBoundary>
+ </PlatformWindowContainer>
</Router>
</Provider>
</AppContext.Provider>
@@ -533,6 +542,10 @@ export default class AppRenderer {
return preferredLocale ? preferredLocale.name : '';
}
+ public setDisplayedChangelog = (): void => {
+ IpcRendererEventChannel.currentVersion.displayedChangelog();
+ };
+
// Make sure that the content height is correct and log if it isn't. This is mostly for debugging
// purposes since there's a bug in Electron that causes the app height to be another value than
// the one we have set.
@@ -833,6 +846,10 @@ export default class AppRenderer {
this.reduxActions.settings.setWireguardKey(publicKey);
}
+ private setChangelog(changelog: IChangelog) {
+ this.reduxActions.userInterface.setChangelog(changelog);
+ }
+
private async updateLocation() {
switch (this.tunnelState.state) {
case 'disconnected': {
diff --git a/gui/src/renderer/components/Account.tsx b/gui/src/renderer/components/Account.tsx
index 02e2b9f081..86a70db271 100644
--- a/gui/src/renderer/components/Account.tsx
+++ b/gui/src/renderer/components/Account.tsx
@@ -17,7 +17,6 @@ import AccountTokenLabel from './AccountTokenLabel';
import * as AppButton from './AppButton';
import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import { Layout } from './Layout';
-import { ModalContainer } from './Modal';
import { BackBarItem, NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
@@ -41,84 +40,80 @@ export default class Account extends React.Component<IProps> {
public render() {
return (
- <ModalContainer>
- <Layout>
- <StyledContainer>
- <NavigationBar>
- <NavigationItems>
- <BackBarItem action={this.props.onClose}>
- {
- // TRANSLATORS: Back button in navigation bar
- messages.pgettext('navigation-bar', 'Settings')
- }
- </BackBarItem>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('account-view', 'Account')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <Layout>
+ <StyledContainer>
+ <NavigationBar>
+ <NavigationItems>
+ <BackBarItem action={this.props.onClose}>
+ {
+ // TRANSLATORS: Back button in navigation bar
+ messages.pgettext('navigation-bar', 'Settings')
+ }
+ </BackBarItem>
+ <TitleBarItem>
+ {
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('account-view', 'Account')
+ }
+ </TitleBarItem>
+ </NavigationItems>
+ </NavigationBar>
- <AccountContainer>
- <SettingsHeader>
- <HeaderTitle>{messages.pgettext('account-view', 'Account')}</HeaderTitle>
- </SettingsHeader>
+ <AccountContainer>
+ <SettingsHeader>
+ <HeaderTitle>{messages.pgettext('account-view', 'Account')}</HeaderTitle>
+ </SettingsHeader>
- <AccountRows>
- <AccountRow>
- <AccountRowLabel>
- {messages.pgettext('account-view', 'Account number')}
- </AccountRowLabel>
- <AccountRowValue
- as={AccountTokenLabel}
- accountToken={this.props.accountToken || ''}
- />
- </AccountRow>
+ <AccountRows>
+ <AccountRow>
+ <AccountRowLabel>
+ {messages.pgettext('account-view', 'Account number')}
+ </AccountRowLabel>
+ <AccountRowValue
+ as={AccountTokenLabel}
+ accountToken={this.props.accountToken || ''}
+ />
+ </AccountRow>
- <AccountRow>
- <AccountRowLabel>
- {messages.pgettext('account-view', 'Paid until')}
- </AccountRowLabel>
- <FormattedAccountExpiry
- expiry={this.props.accountExpiry}
- locale={this.props.expiryLocale}
- />
- </AccountRow>
- </AccountRows>
+ <AccountRow>
+ <AccountRowLabel>{messages.pgettext('account-view', 'Paid until')}</AccountRowLabel>
+ <FormattedAccountExpiry
+ expiry={this.props.accountExpiry}
+ locale={this.props.expiryLocale}
+ />
+ </AccountRow>
+ </AccountRows>
- <AccountFooter>
- <AppButton.BlockingButton
- disabled={this.props.isOffline}
- onClick={this.props.onBuyMore}>
- <AriaDescriptionGroup>
- <AriaDescribed>
- <StyledBuyCreditButton>
- <AppButton.Label>{messages.gettext('Buy more credit')}</AppButton.Label>
- <AriaDescription>
- <AppButton.Icon
- source="icon-extLink"
- height={16}
- width={16}
- aria-label={messages.pgettext('accessibility', 'Opens externally')}
- />
- </AriaDescription>
- </StyledBuyCreditButton>
- </AriaDescribed>
- </AriaDescriptionGroup>
- </AppButton.BlockingButton>
+ <AccountFooter>
+ <AppButton.BlockingButton
+ disabled={this.props.isOffline}
+ onClick={this.props.onBuyMore}>
+ <AriaDescriptionGroup>
+ <AriaDescribed>
+ <StyledBuyCreditButton>
+ <AppButton.Label>{messages.gettext('Buy more credit')}</AppButton.Label>
+ <AriaDescription>
+ <AppButton.Icon
+ source="icon-extLink"
+ height={16}
+ width={16}
+ aria-label={messages.pgettext('accessibility', 'Opens externally')}
+ />
+ </AriaDescription>
+ </StyledBuyCreditButton>
+ </AriaDescribed>
+ </AriaDescriptionGroup>
+ </AppButton.BlockingButton>
- <StyledRedeemVoucherButton />
+ <StyledRedeemVoucherButton />
- <AppButton.RedButton onClick={this.props.onLogout}>
- {messages.pgettext('account-view', 'Log out')}
- </AppButton.RedButton>
- </AccountFooter>
- </AccountContainer>
- </StyledContainer>
- </Layout>
- </ModalContainer>
+ <AppButton.RedButton onClick={this.props.onLogout}>
+ {messages.pgettext('account-view', 'Log out')}
+ </AppButton.RedButton>
+ </AccountFooter>
+ </AccountContainer>
+ </StyledContainer>
+ </Layout>
);
}
}
diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx
index cbd5c6d7f7..084459b79c 100644
--- a/gui/src/renderer/components/AdvancedSettings.tsx
+++ b/gui/src/renderer/components/AdvancedSettings.tsx
@@ -15,7 +15,7 @@ import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGro
import * as Cell from './cell';
import CustomDnsSettings from './CustomDnsSettings';
import { Layout, SettingsContainer } from './Layout';
-import { ModalAlert, ModalAlertType, ModalContainer, ModalMessage } from './Modal';
+import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
import {
BackBarItem,
NavigationBar,
@@ -58,150 +58,143 @@ export default class AdvancedSettings extends React.Component<IProps, IState> {
const hasWireguardKey = this.props.wireguardKeyState.type === 'key-set';
return (
- <ModalContainer>
- <Layout>
- <SettingsContainer>
- <NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <BackBarItem action={this.props.onClose}>
- {
- // TRANSLATORS: Back button in navigation bar
- messages.pgettext('navigation-bar', 'Settings')
- }
- </BackBarItem>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('advanced-settings-nav', 'Advanced')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <Layout>
+ <SettingsContainer>
+ <NavigationContainer>
+ <NavigationBar>
+ <NavigationItems>
+ <BackBarItem action={this.props.onClose}>
+ {
+ // TRANSLATORS: Back button in navigation bar
+ messages.pgettext('navigation-bar', 'Settings')
+ }
+ </BackBarItem>
+ <TitleBarItem>
+ {
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('advanced-settings-nav', 'Advanced')
+ }
+ </TitleBarItem>
+ </NavigationItems>
+ </NavigationBar>
- <StyledNavigationScrollbars>
- <SettingsHeader>
- <HeaderTitle>
- {messages.pgettext('advanced-settings-view', 'Advanced')}
- </HeaderTitle>
- </SettingsHeader>
+ <StyledNavigationScrollbars>
+ <SettingsHeader>
+ <HeaderTitle>{messages.pgettext('advanced-settings-view', 'Advanced')}</HeaderTitle>
+ </SettingsHeader>
- <AriaInputGroup>
- <Cell.Container>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('advanced-settings-view', 'Enable IPv6')}
- </Cell.InputLabel>
- </AriaLabel>
- <AriaInput>
- <Cell.Switch
- isOn={this.props.enableIpv6}
- onChange={this.props.setEnableIpv6}
- />
- </AriaInput>
- </Cell.Container>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {messages.pgettext(
- 'advanced-settings-view',
- 'Enable IPv6 communication through the tunnel.',
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('advanced-settings-view', 'Enable IPv6')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch isOn={this.props.enableIpv6} onChange={this.props.setEnableIpv6} />
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {messages.pgettext(
+ 'advanced-settings-view',
+ 'Enable IPv6 communication through the tunnel.',
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
- <AriaInputGroup>
- <Cell.Container>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('advanced-settings-view', 'Always require VPN')}
- </Cell.InputLabel>
- </AriaLabel>
- <AriaInput>
- <Cell.Switch
- ref={this.blockWhenDisconnectedRef}
- isOn={this.props.blockWhenDisconnected}
- onChange={this.setBlockWhenDisconnected}
- />
- </AriaInput>
- </Cell.Container>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {messages.pgettext(
- 'advanced-settings-view',
- 'If you disconnect or quit the app, this setting will block your internet.',
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
-
- {(window.env.platform === 'linux' || window.env.platform === 'win32') && (
- <Cell.CellButtonGroup>
- <Cell.CellButton onClick={this.props.onViewSplitTunneling}>
- <Cell.Label>
- {messages.pgettext('advanced-settings-view', 'Split tunneling')}
- </Cell.Label>
- <Cell.Icon height={12} width={7} source="icon-chevron" />
- </Cell.CellButton>
- </Cell.CellButtonGroup>
- )}
-
- <AriaInputGroup>
- <StyledTunnelProtocolContainer>
- <StyledSelectorForFooter
- title={messages.pgettext('advanced-settings-view', 'Tunnel protocol')}
- values={this.tunnelProtocolItems(hasWireguardKey)}
- value={this.props.tunnelProtocol}
- onSelect={this.onSelectTunnelProtocol}
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('advanced-settings-view', 'Always require VPN')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch
+ ref={this.blockWhenDisconnectedRef}
+ isOn={this.props.blockWhenDisconnected}
+ onChange={this.setBlockWhenDisconnected}
/>
- {!hasWireguardKey && (
- <StyledNoWireguardKeyErrorContainer>
- <AriaDescription>
- <StyledNoWireguardKeyError>
- {messages.pgettext(
- 'advanced-settings-view',
- 'To enable WireGuard, generate a key under the "WireGuard key" setting below.',
- )}
- </StyledNoWireguardKeyError>
- </AriaDescription>
- </StyledNoWireguardKeyErrorContainer>
- )}
- </StyledTunnelProtocolContainer>
- </AriaInputGroup>
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {messages.pgettext(
+ 'advanced-settings-view',
+ 'If you disconnect or quit the app, this setting will block your internet.',
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+ {(window.env.platform === 'linux' || window.env.platform === 'win32') && (
<Cell.CellButtonGroup>
- <Cell.CellButton
- onClick={this.props.onViewWireguardSettings}
- disabled={this.props.tunnelProtocol === 'openvpn'}>
- <Cell.Label>
- {messages.pgettext('advanced-settings-view', 'WireGuard settings')}
- </Cell.Label>
- <Cell.Icon height={12} width={7} source="icon-chevron" />
- </Cell.CellButton>
-
- <Cell.CellButton
- onClick={this.props.onViewOpenVpnSettings}
- disabled={this.props.tunnelProtocol === 'wireguard'}>
+ <Cell.CellButton onClick={this.props.onViewSplitTunneling}>
<Cell.Label>
- {messages.pgettext('advanced-settings-view', 'OpenVPN settings')}
+ {messages.pgettext('advanced-settings-view', 'Split tunneling')}
</Cell.Label>
<Cell.Icon height={12} width={7} source="icon-chevron" />
</Cell.CellButton>
</Cell.CellButtonGroup>
+ )}
+
+ <AriaInputGroup>
+ <StyledTunnelProtocolContainer>
+ <StyledSelectorForFooter
+ title={messages.pgettext('advanced-settings-view', 'Tunnel protocol')}
+ values={this.tunnelProtocolItems(hasWireguardKey)}
+ value={this.props.tunnelProtocol}
+ onSelect={this.onSelectTunnelProtocol}
+ />
+ {!hasWireguardKey && (
+ <StyledNoWireguardKeyErrorContainer>
+ <AriaDescription>
+ <StyledNoWireguardKeyError>
+ {messages.pgettext(
+ 'advanced-settings-view',
+ 'To enable WireGuard, generate a key under the "WireGuard key" setting below.',
+ )}
+ </StyledNoWireguardKeyError>
+ </AriaDescription>
+ </StyledNoWireguardKeyErrorContainer>
+ )}
+ </StyledTunnelProtocolContainer>
+ </AriaInputGroup>
+
+ <Cell.CellButtonGroup>
+ <Cell.CellButton
+ onClick={this.props.onViewWireguardSettings}
+ disabled={this.props.tunnelProtocol === 'openvpn'}>
+ <Cell.Label>
+ {messages.pgettext('advanced-settings-view', 'WireGuard settings')}
+ </Cell.Label>
+ <Cell.Icon height={12} width={7} source="icon-chevron" />
+ </Cell.CellButton>
+
+ <Cell.CellButton
+ onClick={this.props.onViewOpenVpnSettings}
+ disabled={this.props.tunnelProtocol === 'wireguard'}>
+ <Cell.Label>
+ {messages.pgettext('advanced-settings-view', 'OpenVPN settings')}
+ </Cell.Label>
+ <Cell.Icon height={12} width={7} source="icon-chevron" />
+ </Cell.CellButton>
+ </Cell.CellButtonGroup>
- <CustomDnsSettings />
- </StyledNavigationScrollbars>
- </NavigationContainer>
- </SettingsContainer>
- </Layout>
+ <CustomDnsSettings />
+ </StyledNavigationScrollbars>
+ </NavigationContainer>
+ </SettingsContainer>
{this.state.showConfirmBlockWhenDisconnectedAlert &&
this.renderConfirmBlockWhenDisconnectedAlert()}
- </ModalContainer>
+ </Layout>
);
}
diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx
index 9921109d23..f72de697e3 100644
--- a/gui/src/renderer/components/AppRouter.tsx
+++ b/gui/src/renderer/components/AppRouter.tsx
@@ -11,7 +11,6 @@ import AccountPage from '../containers/AccountPage';
import AdvancedSettingsPage from '../containers/AdvancedSettingsPage';
import LoginPage from '../containers/LoginPage';
import OpenVPNSettingsPage from '../containers/OpenVPNSettingsPage';
-import PlatformWindowContainer from '../containers/PlatformWindowContainer';
import PreferencesPage from '../containers/PreferencesPage';
import SelectLanguagePage from '../containers/SelectLanguagePage';
import SelectLocationPage from '../containers/SelectLocationPage';
@@ -71,45 +70,39 @@ class AppRouter extends React.Component<IHistoryProps, IAppRoutesState> {
const location = this.state.currentLocation;
return (
- <PlatformWindowContainer>
- <KeyboardNavigation>
- <Focus ref={this.focusRef}>
- <TransitionContainer onTransitionEnd={this.onNavigation} {...this.state.transition}>
- <TransitionView viewId={location.key || ''}>
- <Switch key={location.key} location={location}>
- <Route exact path={RoutePath.launch} component={Launch} />
- <Route exact path={RoutePath.login} component={LoginPage} />
- <Route exact path={RoutePath.main} component={MainView} />
- <Route exact path={RoutePath.redeemVoucher} component={VoucherInput} />
- <Route
- exact
- path={RoutePath.voucherSuccess}
- component={VoucherVerificationSuccess}
- />
- <Route exact path={RoutePath.timeAdded} component={TimeAdded} />
- <Route exact path={RoutePath.setupFinished} component={SetupFinished} />
- <Route exact path={RoutePath.settings} component={SettingsPage} />
- <Route exact path={RoutePath.selectLanguage} component={SelectLanguagePage} />
- <Route exact path={RoutePath.accountSettings} component={AccountPage} />
- <Route exact path={RoutePath.preferences} component={PreferencesPage} />
- <Route exact path={RoutePath.advancedSettings} component={AdvancedSettingsPage} />
- <Route
- exact
- path={RoutePath.wireguardSettings}
- component={WireguardSettingsPage}
- />
- <Route exact path={RoutePath.wireguardKeys} component={WireguardKeysPage} />
- <Route exact path={RoutePath.openVpnSettings} component={OpenVPNSettingsPage} />
- <Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} />
- <Route exact path={RoutePath.support} component={SupportPage} />
- <Route exact path={RoutePath.selectLocation} component={SelectLocationPage} />
- <Route exact path={RoutePath.filterByProvider} component={FilterByProvider} />
- </Switch>
- </TransitionView>
- </TransitionContainer>
- </Focus>
- </KeyboardNavigation>
- </PlatformWindowContainer>
+ <KeyboardNavigation>
+ <Focus ref={this.focusRef}>
+ <TransitionContainer onTransitionEnd={this.onNavigation} {...this.state.transition}>
+ <TransitionView viewId={location.key || ''}>
+ <Switch key={location.key} location={location}>
+ <Route exact path={RoutePath.launch} component={Launch} />
+ <Route exact path={RoutePath.login} component={LoginPage} />
+ <Route exact path={RoutePath.main} component={MainView} />
+ <Route exact path={RoutePath.redeemVoucher} component={VoucherInput} />
+ <Route
+ exact
+ path={RoutePath.voucherSuccess}
+ component={VoucherVerificationSuccess}
+ />
+ <Route exact path={RoutePath.timeAdded} component={TimeAdded} />
+ <Route exact path={RoutePath.setupFinished} component={SetupFinished} />
+ <Route exact path={RoutePath.settings} component={SettingsPage} />
+ <Route exact path={RoutePath.selectLanguage} component={SelectLanguagePage} />
+ <Route exact path={RoutePath.accountSettings} component={AccountPage} />
+ <Route exact path={RoutePath.preferences} component={PreferencesPage} />
+ <Route exact path={RoutePath.advancedSettings} component={AdvancedSettingsPage} />
+ <Route exact path={RoutePath.wireguardSettings} component={WireguardSettingsPage} />
+ <Route exact path={RoutePath.wireguardKeys} component={WireguardKeysPage} />
+ <Route exact path={RoutePath.openVpnSettings} component={OpenVPNSettingsPage} />
+ <Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} />
+ <Route exact path={RoutePath.support} component={SupportPage} />
+ <Route exact path={RoutePath.selectLocation} component={SelectLocationPage} />
+ <Route exact path={RoutePath.filterByProvider} component={FilterByProvider} />
+ </Switch>
+ </TransitionView>
+ </TransitionContainer>
+ </Focus>
+ </KeyboardNavigation>
);
}
diff --git a/gui/src/renderer/components/Changelog.tsx b/gui/src/renderer/components/Changelog.tsx
new file mode 100644
index 0000000000..83326a18bd
--- /dev/null
+++ b/gui/src/renderer/components/Changelog.tsx
@@ -0,0 +1,70 @@
+import styled from 'styled-components';
+import { colors } from '../../config.json';
+import { messages } from '../../shared/gettext';
+import { useAppContext } from '../context';
+import { useSelector } from '../redux/store';
+import * as AppButton from './AppButton';
+import { bigText, smallText } from './common-styles';
+import { ModalAlert, ModalMessage } from './Modal';
+
+const StyledTitle = styled.h1(bigText, {
+ fontColor: colors.white,
+ textAlign: 'center',
+ margin: '7px 0 4px',
+});
+
+const StyledSubTitle = styled.span(smallText, {
+ marginTop: '10px',
+ fontWeight: 800,
+});
+
+const StyledList = styled.ul({
+ listStyle: 'disc outside',
+ marginLeft: '20px',
+});
+
+const StyledMessage = styled(ModalMessage)({
+ fontSize: '12px',
+ marginTop: '6px',
+});
+
+export function Changelog() {
+ const currentVersion = useSelector((state) => state.version.current);
+ const changelogDisplayedForVersion = useSelector(
+ (state) => state.settings.guiSettings.changelogDisplayedForVersion,
+ );
+ const changelog = useSelector((state) => state.userInterface.changelog);
+
+ const { setDisplayedChangelog } = useAppContext();
+
+ if (
+ changelogDisplayedForVersion === currentVersion ||
+ changelog.length === 0 ||
+ window.env.development ||
+ /-dev-[0-9a-f]{6}$/.test(currentVersion)
+ ) {
+ return null;
+ }
+
+ return (
+ <ModalAlert
+ buttons={[
+ <AppButton.BlueButton key="close" onClick={setDisplayedChangelog}>
+ {
+ // TRANSLATORS: This is a button which closes a dialog.
+ messages.gettext('Got it!')
+ }
+ </AppButton.BlueButton>,
+ ]}>
+ <StyledTitle>{currentVersion}</StyledTitle>
+ <StyledSubTitle>{messages.pgettext('changelog', 'Changes in this version:')}</StyledSubTitle>
+ <StyledMessage>
+ <StyledList>
+ {changelog.map((item, i) => (
+ <li key={i}>{item}</li>
+ ))}
+ </StyledList>
+ </StyledMessage>
+ </ModalAlert>
+ );
+}
diff --git a/gui/src/renderer/components/Connect.tsx b/gui/src/renderer/components/Connect.tsx
index a150da90f1..d861ab51f6 100644
--- a/gui/src/renderer/components/Connect.tsx
+++ b/gui/src/renderer/components/Connect.tsx
@@ -9,7 +9,6 @@ import { calculateHeaderBarStyle, DefaultHeaderBar } from './HeaderBar';
import ImageView from './ImageView';
import { Container, Layout } from './Layout';
import Map, { MarkerStyle, ZoomLevel } from './Map';
-import { ModalContainer } from './Modal';
import TunnelControl from './TunnelControl';
interface IProps {
@@ -85,12 +84,10 @@ export default class Connect extends React.Component<IProps, IState> {
public render() {
return (
- <ModalContainer>
- <Layout>
- <DefaultHeaderBar barStyle={calculateHeaderBarStyle(this.props.connection.status)} />
- <StyledContainer>{this.renderMap()}</StyledContainer>
- </Layout>
- </ModalContainer>
+ <Layout>
+ <DefaultHeaderBar barStyle={calculateHeaderBarStyle(this.props.connection.status)} />
+ <StyledContainer>{this.renderMap()}</StyledContainer>
+ </Layout>
);
}
diff --git a/gui/src/renderer/components/ErrorBoundary.tsx b/gui/src/renderer/components/ErrorBoundary.tsx
index d29c029c93..d9f2bf370e 100644
--- a/gui/src/renderer/components/ErrorBoundary.tsx
+++ b/gui/src/renderer/components/ErrorBoundary.tsx
@@ -3,7 +3,6 @@ import styled from 'styled-components';
import { supportEmail } from '../../config.json';
import { messages } from '../../shared/gettext';
import log from '../../shared/logging';
-import PlatformWindowContainer from '../containers/PlatformWindowContainer';
import ErrorView from './ErrorView';
interface IProps {
@@ -42,11 +41,7 @@ export default class ErrorBoundary extends React.Component<IProps, IState> {
.split('%(email)s', 2);
reachBackMessage.splice(1, 0, <Email>{supportEmail}</Email>);
- return (
- <PlatformWindowContainer>
- <ErrorView>{reachBackMessage}</ErrorView>
- </PlatformWindowContainer>
- );
+ return <ErrorView>{reachBackMessage}</ErrorView>;
} else {
return this.props.children;
}
diff --git a/gui/src/renderer/components/ExpiredAccountErrorView.tsx b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
index c24c79da45..6f29b3dad2 100644
--- a/gui/src/renderer/components/ExpiredAccountErrorView.tsx
+++ b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
@@ -26,7 +26,7 @@ import {
import { calculateHeaderBarStyle, HeaderBarStyle } from './HeaderBar';
import ImageView from './ImageView';
import { Layout } from './Layout';
-import { ModalAlert, ModalAlertType, ModalContainer, ModalMessage } from './Modal';
+import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
export enum RecoveryAction {
openBrowser,
@@ -65,34 +65,32 @@ export default class ExpiredAccountErrorView extends React.Component<
: calculateHeaderBarStyle(this.props.tunnelState);
return (
- <ModalContainer>
- <Layout>
- <StyledHeader barStyle={headerBarStyle} />
- <StyledCustomScrollbars fillContainer>
- <StyledContainer>
- <StyledBody>{this.renderContent()}</StyledBody>
+ <Layout>
+ <StyledHeader barStyle={headerBarStyle} />
+ <StyledCustomScrollbars fillContainer>
+ <StyledContainer>
+ <StyledBody>{this.renderContent()}</StyledBody>
- <StyledFooter>
- {this.getRecoveryAction() === RecoveryAction.disconnect && (
- <AppButton.BlockingButton onClick={this.props.onDisconnect}>
- <StyledDisconnectButton>
- {messages.pgettext('connect-view', 'Disconnect')}
- </StyledDisconnectButton>
- </AppButton.BlockingButton>
- )}
+ <StyledFooter>
+ {this.getRecoveryAction() === RecoveryAction.disconnect && (
+ <AppButton.BlockingButton onClick={this.props.onDisconnect}>
+ <StyledDisconnectButton>
+ {messages.pgettext('connect-view', 'Disconnect')}
+ </StyledDisconnectButton>
+ </AppButton.BlockingButton>
+ )}
- {this.renderExternalPaymentButton()}
+ {this.renderExternalPaymentButton()}
- <AppButton.GreenButton onClick={this.props.navigateToRedeemVoucher}>
- {messages.pgettext('connect-view', 'Redeem voucher')}
- </AppButton.GreenButton>
- </StyledFooter>
+ <AppButton.GreenButton onClick={this.props.navigateToRedeemVoucher}>
+ {messages.pgettext('connect-view', 'Redeem voucher')}
+ </AppButton.GreenButton>
+ </StyledFooter>
- {this.state.showBlockWhenDisconnectedAlert && this.renderBlockWhenDisconnectedAlert()}
- </StyledContainer>
- </StyledCustomScrollbars>
- </Layout>
- </ModalContainer>
+ {this.state.showBlockWhenDisconnectedAlert && this.renderBlockWhenDisconnectedAlert()}
+ </StyledContainer>
+ </StyledCustomScrollbars>
+ </Layout>
);
}
diff --git a/gui/src/renderer/components/Modal.tsx b/gui/src/renderer/components/Modal.tsx
index 97cf4dce22..19fda5133a 100644
--- a/gui/src/renderer/components/Modal.tsx
+++ b/gui/src/renderer/components/Modal.tsx
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
import styled from 'styled-components';
import { colors } from '../../config.json';
import log from '../../shared/logging';
+import CustomScrollbars from './CustomScrollbars';
import ImageView from './ImageView';
const MODAL_CONTAINER_ID = 'modal-container';
@@ -105,7 +106,12 @@ const StyledModalAlert = styled.div({
flexDirection: 'column',
backgroundColor: colors.darkBlue,
borderRadius: '11px',
- padding: '16px',
+ padding: '16px 0 16px 16px',
+ maxHeight: '80vh',
+});
+
+const StyledCustomScrollbars = styled(CustomScrollbars)({
+ paddingRight: '16px',
});
const ModalAlertIcon = styled.div({
@@ -118,6 +124,7 @@ const ModalAlertButtonContainer = styled.div({
display: 'flex',
flexDirection: 'column',
marginTop: '18px',
+ marginRight: '16px',
});
interface IModalAlertProps {
@@ -178,11 +185,14 @@ class ModalAlertWithContext extends React.Component<IModalAlertProps & IModalCon
<ModalBackground>
<ModalAlertContainer>
<StyledModalAlert ref={this.modalRef} tabIndex={-1} role="dialog" aria-modal>
- {this.props.type && (
- <ModalAlertIcon>{this.renderTypeIcon(this.props.type)}</ModalAlertIcon>
- )}
- {this.props.message && <ModalMessage>{this.props.message}</ModalMessage>}
- {this.props.children}
+ <StyledCustomScrollbars>
+ {this.props.type && (
+ <ModalAlertIcon>{this.renderTypeIcon(this.props.type)}</ModalAlertIcon>
+ )}
+ {this.props.message && <ModalMessage>{this.props.message}</ModalMessage>}
+ {this.props.children}
+ </StyledCustomScrollbars>
+
{this.props.buttons.map((button, index) => (
<ModalAlertButtonContainer key={index}>{button}</ModalAlertButtonContainer>
))}
diff --git a/gui/src/renderer/components/OpenVPNSettings.tsx b/gui/src/renderer/components/OpenVPNSettings.tsx
index b029038d06..ce2cbe264b 100644
--- a/gui/src/renderer/components/OpenVPNSettings.tsx
+++ b/gui/src/renderer/components/OpenVPNSettings.tsx
@@ -7,7 +7,7 @@ import * as AppButton from './AppButton';
import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
import * as Cell from './cell';
import { Layout, SettingsContainer } from './Layout';
-import { ModalAlert, ModalAlertType, ModalContainer } from './Modal';
+import { ModalAlert, ModalAlertType } from './Modal';
import {
BackBarItem,
NavigationBar,
@@ -90,155 +90,153 @@ export default class OpenVpnSettings extends React.Component<IProps, IState> {
public render() {
return (
- <ModalContainer>
- <Layout>
- <SettingsContainer>
- <NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <BackBarItem action={this.props.onClose}>
- {
- // TRANSLATORS: Back button in navigation bar
- messages.pgettext('navigation-bar', 'Advanced')
- }
- </BackBarItem>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('openvpn-settings-nav', 'OpenVPN settings')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <Layout>
+ <SettingsContainer>
+ <NavigationContainer>
+ <NavigationBar>
+ <NavigationItems>
+ <BackBarItem action={this.props.onClose}>
+ {
+ // TRANSLATORS: Back button in navigation bar
+ messages.pgettext('navigation-bar', 'Advanced')
+ }
+ </BackBarItem>
+ <TitleBarItem>
+ {
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('openvpn-settings-nav', 'OpenVPN settings')
+ }
+ </TitleBarItem>
+ </NavigationItems>
+ </NavigationBar>
- <StyledNavigationScrollbars>
- <SettingsHeader>
- <HeaderTitle>
- {messages.pgettext('openvpn-settings-view', 'OpenVPN settings')}
- </HeaderTitle>
- </SettingsHeader>
+ <StyledNavigationScrollbars>
+ <SettingsHeader>
+ <HeaderTitle>
+ {messages.pgettext('openvpn-settings-view', 'OpenVPN settings')}
+ </HeaderTitle>
+ </SettingsHeader>
- <StyledSelectorContainer>
- <AriaInputGroup>
- <Selector
- title={messages.pgettext('openvpn-settings-view', 'Transport protocol')}
- values={this.protocolItems(this.props.bridgeState !== 'on')}
- value={this.props.openvpn.protocol}
- onSelect={this.onSelectOpenvpnProtocol}
- hasFooter={this.props.bridgeState === 'on'}
- />
- {this.props.bridgeState === 'on' && (
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {formatMarkdown(
- // TRANSLATORS: This is used to instruct users how to make UDP mode
- // TRANSLATORS: available.
- messages.pgettext(
- 'openvpn-settings-view',
- 'To activate UDP, change **Bridge mode** to **Automatic** or **Off**.',
- ),
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- )}
- </AriaInputGroup>
- </StyledSelectorContainer>
-
- <StyledSelectorContainer>
- <AriaInputGroup>
- {this.props.openvpn.protocol ? (
- <Selector
- title={sprintf(
- // TRANSLATORS: The title for the port selector section.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(portType)s - a selected protocol (either TCP or UDP)
- messages.pgettext('openvpn-settings-view', '%(portType)s port'),
- {
- portType: this.props.openvpn.protocol.toUpperCase(),
- },
- )}
- values={this.portItems[this.props.openvpn.protocol]}
- value={this.props.openvpn.port}
- onSelect={this.onSelectOpenVpnPort}
- />
- ) : undefined}
- </AriaInputGroup>
- </StyledSelectorContainer>
+ <StyledSelectorContainer>
+ <AriaInputGroup>
+ <Selector
+ title={messages.pgettext('openvpn-settings-view', 'Transport protocol')}
+ values={this.protocolItems(this.props.bridgeState !== 'on')}
+ value={this.props.openvpn.protocol}
+ onSelect={this.onSelectOpenvpnProtocol}
+ hasFooter={this.props.bridgeState === 'on'}
+ />
+ {this.props.bridgeState === 'on' && (
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {formatMarkdown(
+ // TRANSLATORS: This is used to instruct users how to make UDP mode
+ // TRANSLATORS: available.
+ messages.pgettext(
+ 'openvpn-settings-view',
+ 'To activate UDP, change **Bridge mode** to **Automatic** or **Off**.',
+ ),
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ )}
+ </AriaInputGroup>
+ </StyledSelectorContainer>
+ <StyledSelectorContainer>
<AriaInputGroup>
- <StyledSelectorContainer>
+ {this.props.openvpn.protocol ? (
<Selector
- title={
- // TRANSLATORS: The title for the shadowsocks bridge selector section.
- messages.pgettext('openvpn-settings-view', 'Bridge mode')
- }
- values={this.bridgeStateItems(
- this.props.bridgeModeAvailablity === BridgeModeAvailability.available,
+ title={sprintf(
+ // TRANSLATORS: The title for the port selector section.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(portType)s - a selected protocol (either TCP or UDP)
+ messages.pgettext('openvpn-settings-view', '%(portType)s port'),
+ {
+ portType: this.props.openvpn.protocol.toUpperCase(),
+ },
)}
- value={this.props.bridgeState}
- onSelect={this.onSelectBridgeState}
- hasFooter
+ values={this.portItems[this.props.openvpn.protocol]}
+ value={this.props.openvpn.port}
+ onSelect={this.onSelectOpenVpnPort}
/>
- </StyledSelectorContainer>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>{this.bridgeModeFooterText()}</Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
+ ) : undefined}
</AriaInputGroup>
+ </StyledSelectorContainer>
- <AriaInputGroup>
- <Cell.Container>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('openvpn-settings-view', 'Mssfix')}
- </Cell.InputLabel>
- </AriaLabel>
- <StyledInputFrame>
- <AriaInput>
- <Cell.AutoSizingTextInput
- value={this.props.mssfix ? this.props.mssfix.toString() : ''}
- inputMode={'numeric'}
- maxLength={4}
- placeholder={messages.gettext('Default')}
- onSubmitValue={this.onMssfixSubmit}
- validateValue={OpenVpnSettings.mssfixIsValid}
- submitOnBlur={true}
- modifyValue={OpenVpnSettings.removeNonNumericCharacters}
- />
- </AriaInput>
- </StyledInputFrame>
- </Cell.Container>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {sprintf(
- // TRANSLATORS: The hint displayed below the Mssfix input field.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(max)d - the maximum possible mssfix value
- // TRANSLATORS: %(min)d - the minimum possible mssfix value
- messages.pgettext(
- 'openvpn-settings-view',
- 'Set OpenVPN MSS value. Valid range: %(min)d - %(max)d.',
- ),
- {
- min: MIN_MSSFIX_VALUE,
- max: MAX_MSSFIX_VALUE,
- },
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
- </StyledNavigationScrollbars>
- </NavigationContainer>
- </SettingsContainer>
- </Layout>
+ <AriaInputGroup>
+ <StyledSelectorContainer>
+ <Selector
+ title={
+ // TRANSLATORS: The title for the shadowsocks bridge selector section.
+ messages.pgettext('openvpn-settings-view', 'Bridge mode')
+ }
+ values={this.bridgeStateItems(
+ this.props.bridgeModeAvailablity === BridgeModeAvailability.available,
+ )}
+ value={this.props.bridgeState}
+ onSelect={this.onSelectBridgeState}
+ hasFooter
+ />
+ </StyledSelectorContainer>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>{this.bridgeModeFooterText()}</Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('openvpn-settings-view', 'Mssfix')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <StyledInputFrame>
+ <AriaInput>
+ <Cell.AutoSizingTextInput
+ value={this.props.mssfix ? this.props.mssfix.toString() : ''}
+ inputMode={'numeric'}
+ maxLength={4}
+ placeholder={messages.gettext('Default')}
+ onSubmitValue={this.onMssfixSubmit}
+ validateValue={OpenVpnSettings.mssfixIsValid}
+ submitOnBlur={true}
+ modifyValue={OpenVpnSettings.removeNonNumericCharacters}
+ />
+ </AriaInput>
+ </StyledInputFrame>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {sprintf(
+ // TRANSLATORS: The hint displayed below the Mssfix input field.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(max)d - the maximum possible mssfix value
+ // TRANSLATORS: %(min)d - the minimum possible mssfix value
+ messages.pgettext(
+ 'openvpn-settings-view',
+ 'Set OpenVPN MSS value. Valid range: %(min)d - %(max)d.',
+ ),
+ {
+ min: MIN_MSSFIX_VALUE,
+ max: MAX_MSSFIX_VALUE,
+ },
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+ </StyledNavigationScrollbars>
+ </NavigationContainer>
+ </SettingsContainer>
{this.state.showBridgeStateConfirmationDialog && this.renderBridgeStateConfirmation()}
- </ModalContainer>
+ </Layout>
);
}
diff --git a/gui/src/renderer/components/Preferences.tsx b/gui/src/renderer/components/Preferences.tsx
index 7adcfcf3e4..46b5a54054 100644
--- a/gui/src/renderer/components/Preferences.tsx
+++ b/gui/src/renderer/components/Preferences.tsx
@@ -9,7 +9,7 @@ import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGro
import * as Cell from './cell';
import ImageView from './ImageView';
import { Layout } from './Layout';
-import { ModalAlert, ModalAlertType, ModalContainer } from './Modal';
+import { ModalAlert, ModalAlertType } from './Modal';
import {
BackBarItem,
NavigationBar,
@@ -53,204 +53,228 @@ export default class Preferences extends React.Component<IProps, IState> {
public render() {
return (
- <ModalContainer>
- <Layout>
- <StyledContainer>
- <NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <BackBarItem action={this.props.onClose}>
- {
- // TRANSLATORS: Back button in navigation bar
- messages.pgettext('navigation-bar', 'Settings')
- }
- </BackBarItem>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('preferences-nav', 'Preferences')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <Layout>
+ <StyledContainer>
+ <NavigationContainer>
+ <NavigationBar>
+ <NavigationItems>
+ <BackBarItem action={this.props.onClose}>
+ {
+ // TRANSLATORS: Back button in navigation bar
+ messages.pgettext('navigation-bar', 'Settings')
+ }
+ </BackBarItem>
+ <TitleBarItem>
+ {
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('preferences-nav', 'Preferences')
+ }
+ </TitleBarItem>
+ </NavigationItems>
+ </NavigationBar>
- <NavigationScrollbars>
- <SettingsHeader>
- <HeaderTitle>{messages.pgettext('preferences-view', 'Preferences')}</HeaderTitle>
- </SettingsHeader>
+ <NavigationScrollbars>
+ <SettingsHeader>
+ <HeaderTitle>{messages.pgettext('preferences-view', 'Preferences')}</HeaderTitle>
+ </SettingsHeader>
- <StyledContent>
- <Cell.CellButton onClick={this.showKillSwitchInfo}>
- <Cell.InputLabel>
- {messages.pgettext('preferences-view', 'Kill switch')}
- </Cell.InputLabel>
- <ImageView source="icon-info" width={24} tintColor={colors.white} />
- </Cell.CellButton>
- <StyledSeparator height={20} />
+ <StyledContent>
+ <Cell.CellButton onClick={this.showKillSwitchInfo}>
+ <Cell.InputLabel>
+ {messages.pgettext('preferences-view', 'Kill switch')}
+ </Cell.InputLabel>
+ <ImageView source="icon-info" width={24} tintColor={colors.white} />
+ </Cell.CellButton>
+ <StyledSeparator height={20} />
- <AriaInputGroup>
- <Cell.Container>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('preferences-view', 'Launch app on start-up')}
- </Cell.InputLabel>
- </AriaLabel>
- <AriaInput>
- <Cell.Switch
- isOn={this.props.autoStart}
- onChange={this.props.setAutoStart}
- />
- </AriaInput>
- </Cell.Container>
- </AriaInputGroup>
- <StyledSeparator />
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('preferences-view', 'Launch app on start-up')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch isOn={this.props.autoStart} onChange={this.props.setAutoStart} />
+ </AriaInput>
+ </Cell.Container>
+ </AriaInputGroup>
+ <StyledSeparator />
- <AriaInputGroup>
- <Cell.Container>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('preferences-view', 'Auto-connect')}
- </Cell.InputLabel>
- </AriaLabel>
- <AriaInput>
- <Cell.Switch
- isOn={this.props.autoConnect}
- onChange={this.props.setAutoConnect}
- />
- </AriaInput>
- </Cell.Container>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {messages.pgettext(
- 'preferences-view',
- 'Automatically connect to a server when the app launches.',
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('preferences-view', 'Auto-connect')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch
+ isOn={this.props.autoConnect}
+ onChange={this.props.setAutoConnect}
+ />
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {messages.pgettext(
+ 'preferences-view',
+ 'Automatically connect to a server when the app launches.',
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
- <AriaInputGroup>
- <Cell.Container disabled={this.props.dns.state === 'custom'}>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('preferences-view', 'Block ads')}
- </Cell.InputLabel>
- </AriaLabel>
- <AriaInput>
- <Cell.Switch
- isOn={
- this.props.dns.state === 'default' &&
- this.props.dns.defaultOptions.blockAds
- }
- onChange={this.setBlockAds}
- />
- </AriaInput>
- </Cell.Container>
- </AriaInputGroup>
- <StyledSeparator />
- <AriaInputGroup>
- <Cell.Container disabled={this.props.dns.state === 'custom'}>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('preferences-view', 'Block trackers')}
- </Cell.InputLabel>
- </AriaLabel>
- <AriaInput>
- <Cell.Switch
- isOn={
- this.props.dns.state === 'default' &&
- this.props.dns.defaultOptions.blockTrackers
- }
- onChange={this.setBlockTrackers}
- />
- </AriaInput>
- </Cell.Container>
- </AriaInputGroup>
- <StyledSeparator />
- <AriaInputGroup>
- <Cell.Container disabled={this.props.dns.state === 'custom'}>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('preferences-view', 'Block malware')}
- </Cell.InputLabel>
- </AriaLabel>
- <AriaInput>
- <Cell.Switch
- isOn={
- this.props.dns.state === 'default' &&
- this.props.dns.defaultOptions.blockMalware
- }
- onChange={this.setBlockMalware}
- />
- </AriaInput>
- </Cell.Container>
- {this.props.dns.state === 'custom' && <CustomDnsEnabledFooter />}
- </AriaInputGroup>
+ <AriaInputGroup>
+ <Cell.Container disabled={this.props.dns.state === 'custom'}>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('preferences-view', 'Block ads')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch
+ isOn={
+ this.props.dns.state === 'default' &&
+ this.props.dns.defaultOptions.blockAds
+ }
+ onChange={this.setBlockAds}
+ />
+ </AriaInput>
+ </Cell.Container>
+ </AriaInputGroup>
+ <StyledSeparator />
+ <AriaInputGroup>
+ <Cell.Container disabled={this.props.dns.state === 'custom'}>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('preferences-view', 'Block trackers')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch
+ isOn={
+ this.props.dns.state === 'default' &&
+ this.props.dns.defaultOptions.blockTrackers
+ }
+ onChange={this.setBlockTrackers}
+ />
+ </AriaInput>
+ </Cell.Container>
+ </AriaInputGroup>
+ <StyledSeparator />
+ <AriaInputGroup>
+ <Cell.Container disabled={this.props.dns.state === 'custom'}>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('preferences-view', 'Block malware')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch
+ isOn={
+ this.props.dns.state === 'default' &&
+ this.props.dns.defaultOptions.blockMalware
+ }
+ onChange={this.setBlockMalware}
+ />
+ </AriaInput>
+ </Cell.Container>
+ {this.props.dns.state === 'custom' && <CustomDnsEnabledFooter />}
+ </AriaInputGroup>
- {this.props.dns.state !== 'custom' && <StyledSeparator height={20} />}
+ {this.props.dns.state !== 'custom' && <StyledSeparator height={20} />}
- <AriaInputGroup>
- <Cell.Container>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('preferences-view', 'Local network sharing')}
- </Cell.InputLabel>
- </AriaLabel>
- <AriaInput>
- <Cell.Switch isOn={this.props.allowLan} onChange={this.props.setAllowLan} />
- </AriaInput>
- </Cell.Container>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {messages.pgettext(
- 'preferences-view',
- 'Allows access to other devices on the same network for sharing, printing etc.',
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('preferences-view', 'Local network sharing')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch isOn={this.props.allowLan} onChange={this.props.setAllowLan} />
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {messages.pgettext(
+ 'preferences-view',
+ 'Allows access to other devices on the same network for sharing, printing etc.',
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
- <AriaInputGroup>
- <Cell.Container>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('preferences-view', 'Notifications')}
- </Cell.InputLabel>
- </AriaLabel>
- <AriaInput>
- <Cell.Switch
- isOn={this.props.enableSystemNotifications}
- onChange={this.props.setEnableSystemNotifications}
- />
- </AriaInput>
- </Cell.Container>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {messages.pgettext(
- 'preferences-view',
- 'Enable or disable system notifications. The critical notifications will always be displayed.',
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('preferences-view', 'Notifications')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch
+ isOn={this.props.enableSystemNotifications}
+ onChange={this.props.setEnableSystemNotifications}
+ />
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {messages.pgettext(
+ 'preferences-view',
+ 'Enable or disable system notifications. The critical notifications will always be displayed.',
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('preferences-view', 'Monochromatic tray icon')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch
+ isOn={this.props.monochromaticIcon}
+ onChange={this.props.setMonochromaticIcon}
+ />
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {messages.pgettext(
+ 'preferences-view',
+ 'Use a monochromatic tray icon instead of a colored one.',
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+
+ {(window.env.platform === 'win32' ||
+ (window.env.platform === 'darwin' && window.env.development)) && (
<AriaInputGroup>
<Cell.Container>
<AriaLabel>
<Cell.InputLabel>
- {messages.pgettext('preferences-view', 'Monochromatic tray icon')}
+ {messages.pgettext('preferences-view', 'Unpin app from taskbar')}
</Cell.InputLabel>
</AriaLabel>
<AriaInput>
<Cell.Switch
- isOn={this.props.monochromaticIcon}
- onChange={this.props.setMonochromaticIcon}
+ isOn={this.props.unpinnedWindow}
+ onChange={this.props.setUnpinnedWindow}
/>
</AriaInput>
</Cell.Container>
@@ -259,26 +283,27 @@ export default class Preferences extends React.Component<IProps, IState> {
<Cell.FooterText>
{messages.pgettext(
'preferences-view',
- 'Use a monochromatic tray icon instead of a colored one.',
+ 'Enable to move the app around as a free-standing window.',
)}
</Cell.FooterText>
</AriaDescription>
</Cell.Footer>
</AriaInputGroup>
+ )}
- {(window.env.platform === 'win32' ||
- (window.env.platform === 'darwin' && window.env.development)) && (
+ {this.props.unpinnedWindow && (
+ <React.Fragment>
<AriaInputGroup>
<Cell.Container>
<AriaLabel>
<Cell.InputLabel>
- {messages.pgettext('preferences-view', 'Unpin app from taskbar')}
+ {messages.pgettext('preferences-view', 'Start minimized')}
</Cell.InputLabel>
</AriaLabel>
<AriaInput>
<Cell.Switch
- isOn={this.props.unpinnedWindow}
- onChange={this.props.setUnpinnedWindow}
+ isOn={this.props.startMinimized}
+ onChange={this.props.setStartMinimized}
/>
</AriaInput>
</Cell.Container>
@@ -287,79 +312,49 @@ export default class Preferences extends React.Component<IProps, IState> {
<Cell.FooterText>
{messages.pgettext(
'preferences-view',
- 'Enable to move the app around as a free-standing window.',
+ 'Show only the tray icon when the app starts.',
)}
</Cell.FooterText>
</AriaDescription>
</Cell.Footer>
</AriaInputGroup>
- )}
-
- {this.props.unpinnedWindow && (
- <React.Fragment>
- <AriaInputGroup>
- <Cell.Container>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('preferences-view', 'Start minimized')}
- </Cell.InputLabel>
- </AriaLabel>
- <AriaInput>
- <Cell.Switch
- isOn={this.props.startMinimized}
- onChange={this.props.setStartMinimized}
- />
- </AriaInput>
- </Cell.Container>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {messages.pgettext(
- 'preferences-view',
- 'Show only the tray icon when the app starts.',
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
- </React.Fragment>
- )}
+ </React.Fragment>
+ )}
- <AriaInputGroup>
- <Cell.Container disabled={this.props.isBeta}>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('preferences-view', 'Beta program')}
- </Cell.InputLabel>
- </AriaLabel>
- <AriaInput>
- <Cell.Switch
- isOn={this.props.showBetaReleases}
- onChange={this.props.setShowBetaReleases}
- />
- </AriaInput>
- </Cell.Container>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {this.props.isBeta
- ? messages.pgettext(
- 'preferences-view',
- 'This option is unavailable while using a beta version.',
- )
- : messages.pgettext(
- 'preferences-view',
- 'Enable to get notified when new beta versions of the app are released.',
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
- </StyledContent>
- </NavigationScrollbars>
- </NavigationContainer>
- </StyledContainer>
- </Layout>
+ <AriaInputGroup>
+ <Cell.Container disabled={this.props.isBeta}>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('preferences-view', 'Beta program')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch
+ isOn={this.props.showBetaReleases}
+ onChange={this.props.setShowBetaReleases}
+ />
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {this.props.isBeta
+ ? messages.pgettext(
+ 'preferences-view',
+ 'This option is unavailable while using a beta version.',
+ )
+ : messages.pgettext(
+ 'preferences-view',
+ 'Enable to get notified when new beta versions of the app are released.',
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+ </StyledContent>
+ </NavigationScrollbars>
+ </NavigationContainer>
+ </StyledContainer>
{this.state.showKillSwitchInfo && (
<ModalAlert
@@ -376,7 +371,7 @@ export default class Preferences extends React.Component<IProps, IState> {
close={this.hideKillSwitchInfo}
/>
)}
- </ModalContainer>
+ </Layout>
);
}
diff --git a/gui/src/renderer/components/SplitTunnelingSettings.tsx b/gui/src/renderer/components/SplitTunnelingSettings.tsx
index 5f6f125d39..a0ea98bcd9 100644
--- a/gui/src/renderer/components/SplitTunnelingSettings.tsx
+++ b/gui/src/renderer/components/SplitTunnelingSettings.tsx
@@ -15,7 +15,7 @@ import { CustomScrollbarsRef } from './CustomScrollbars';
import ImageView from './ImageView';
import { Layout } from './Layout';
import List from './List';
-import { ModalContainer, ModalAlert, ModalAlertType } from './Modal';
+import { ModalAlert, ModalAlertType } from './Modal';
import {
BackBarItem,
NavigationBar,
@@ -59,39 +59,37 @@ export default function SplitTunneling() {
return (
<>
<StyledPageCover show={browsing} />
- <ModalContainer>
- <Layout>
- <StyledContainer>
- <NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <BackBarItem action={pop}>
- {
- // TRANSLATORS: Back button in navigation bar
- messages.pgettext('navigation-bar', 'Advanced')
- }
- </BackBarItem>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('split-tunneling-nav', 'Split tunneling')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <Layout>
+ <StyledContainer>
+ <NavigationContainer>
+ <NavigationBar>
+ <NavigationItems>
+ <BackBarItem action={pop}>
+ {
+ // TRANSLATORS: Back button in navigation bar
+ messages.pgettext('navigation-bar', 'Advanced')
+ }
+ </BackBarItem>
+ <TitleBarItem>
+ {
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('split-tunneling-nav', 'Split tunneling')
+ }
+ </TitleBarItem>
+ </NavigationItems>
+ </NavigationBar>
- <StyledNavigationScrollbars ref={scrollbarsRef}>
- <StyledContent>
- <PlatformSpecificSplitTunnelingSettings
- setBrowsing={setBrowsing}
- scrollToTop={scrollToTop}
- />
- </StyledContent>
- </StyledNavigationScrollbars>
- </NavigationContainer>
- </StyledContainer>
- </Layout>
- </ModalContainer>
+ <StyledNavigationScrollbars ref={scrollbarsRef}>
+ <StyledContent>
+ <PlatformSpecificSplitTunnelingSettings
+ setBrowsing={setBrowsing}
+ scrollToTop={scrollToTop}
+ />
+ </StyledContent>
+ </StyledNavigationScrollbars>
+ </NavigationContainer>
+ </StyledContainer>
+ </Layout>
</>
);
}
diff --git a/gui/src/renderer/components/Support.tsx b/gui/src/renderer/components/Support.tsx
index d34e6b6b08..eaea64d789 100644
--- a/gui/src/renderer/components/Support.tsx
+++ b/gui/src/renderer/components/Support.tsx
@@ -5,7 +5,7 @@ import * as AppButton from './AppButton';
import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import ImageView from './ImageView';
import { Layout } from './Layout';
-import { ModalAlert, ModalAlertType, ModalContainer } from './Modal';
+import { ModalAlert, ModalAlertType } from './Modal';
import { BackBarItem, NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
import {
@@ -150,35 +150,33 @@ export default class Support extends React.Component<ISupportProps, ISupportStat
const content = this.renderContent();
return (
- <ModalContainer>
- <Layout>
- <StyledContainer>
- <NavigationBar>
- <NavigationItems>
- <BackBarItem action={this.props.onClose}>
- {
- // TRANSLATORS: Back button in navigation bar
- messages.pgettext('navigation-bar', 'Settings')
- }
- </BackBarItem>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('support-view', 'Report a problem')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
- <StyledContentContainer>
- {header}
- {content}
- </StyledContentContainer>
+ <Layout>
+ <StyledContainer>
+ <NavigationBar>
+ <NavigationItems>
+ <BackBarItem action={this.props.onClose}>
+ {
+ // TRANSLATORS: Back button in navigation bar
+ messages.pgettext('navigation-bar', 'Settings')
+ }
+ </BackBarItem>
+ <TitleBarItem>
+ {
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('support-view', 'Report a problem')
+ }
+ </TitleBarItem>
+ </NavigationItems>
+ </NavigationBar>
+ <StyledContentContainer>
+ {header}
+ {content}
+ </StyledContentContainer>
- {sendState === SendState.confirm && this.renderNoEmailDialog()}
- {showOutdatedVersionWarning && this.renderOutdateVersionWarningDialog()}
- </StyledContainer>
- </Layout>
- </ModalContainer>
+ {sendState === SendState.confirm && this.renderNoEmailDialog()}
+ {showOutdatedVersionWarning && this.renderOutdateVersionWarningDialog()}
+ </StyledContainer>
+ </Layout>
);
}
diff --git a/gui/src/renderer/components/WireguardSettings.tsx b/gui/src/renderer/components/WireguardSettings.tsx
index 6a90c307f8..8d5d47f38c 100644
--- a/gui/src/renderer/components/WireguardSettings.tsx
+++ b/gui/src/renderer/components/WireguardSettings.tsx
@@ -7,7 +7,7 @@ import * as AppButton from './AppButton';
import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
import * as Cell from './cell';
import { Layout, SettingsContainer } from './Layout';
-import { ModalAlert, ModalAlertType, ModalContainer } from './Modal';
+import { ModalAlert, ModalAlertType } from './Modal';
import {
BackBarItem,
NavigationBar,
@@ -101,176 +101,174 @@ export default class WireguardSettings extends React.Component<IProps, IState> {
public render() {
return (
- <ModalContainer>
- <Layout>
- <SettingsContainer>
- <NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <BackBarItem action={this.props.onClose}>
- {
- // TRANSLATORS: Back button in navigation bar
- messages.pgettext('navigation-bar', 'Advanced')
- }
- </BackBarItem>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('wireguard-settings-nav', 'WireGuard settings')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <Layout>
+ <SettingsContainer>
+ <NavigationContainer>
+ <NavigationBar>
+ <NavigationItems>
+ <BackBarItem action={this.props.onClose}>
+ {
+ // TRANSLATORS: Back button in navigation bar
+ messages.pgettext('navigation-bar', 'Advanced')
+ }
+ </BackBarItem>
+ <TitleBarItem>
+ {
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('wireguard-settings-nav', 'WireGuard settings')
+ }
+ </TitleBarItem>
+ </NavigationItems>
+ </NavigationBar>
- <StyledNavigationScrollbars>
- <SettingsHeader>
- <HeaderTitle>
- {messages.pgettext('wireguard-settings-view', 'WireGuard settings')}
- </HeaderTitle>
- </SettingsHeader>
+ <StyledNavigationScrollbars>
+ <SettingsHeader>
+ <HeaderTitle>
+ {messages.pgettext('wireguard-settings-view', 'WireGuard settings')}
+ </HeaderTitle>
+ </SettingsHeader>
- <AriaInputGroup>
- <StyledSelectorContainer>
- <StyledSelectorForFooter
- // TRANSLATORS: The title for the WireGuard port selector.
- title={messages.pgettext('wireguard-settings-view', 'Port')}
- values={this.wireguardPortItems}
- value={this.props.wireguard.port}
- onSelect={this.props.setWireguardPort}
+ <AriaInputGroup>
+ <StyledSelectorContainer>
+ <StyledSelectorForFooter
+ // TRANSLATORS: The title for the WireGuard port selector.
+ title={messages.pgettext('wireguard-settings-view', 'Port')}
+ values={this.wireguardPortItems}
+ value={this.props.wireguard.port}
+ onSelect={this.props.setWireguardPort}
+ />
+ </StyledSelectorContainer>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {
+ // TRANSLATORS: The hint displayed below the WireGuard port selector.
+ messages.pgettext(
+ 'wireguard-settings-view',
+ 'The automatic setting will randomly choose from a wide range of ports.',
+ )
+ }
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {
+ // TRANSLATORS: The label next to the multihop settings toggle.
+ messages.pgettext('advanced-settings-view', 'Enable multihop')
+ }
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch
+ ref={this.multihopRef}
+ isOn={this.props.wireguardMultihop}
+ onChange={this.setWireguardMultihop}
/>
- </StyledSelectorContainer>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {
- // TRANSLATORS: The hint displayed below the WireGuard port selector.
- messages.pgettext(
- 'wireguard-settings-view',
- 'The automatic setting will randomly choose from a wide range of ports.',
- )
- }
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {
+ // TRANSLATORS: Description for multihop settings toggle.
+ messages.pgettext(
+ 'advanced-settings-view',
+ 'Increases anonymity by routing your traffic into one WireGuard server and out another, making it harder to trace.',
+ )
+ }
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
- <AriaInputGroup>
- <Cell.Container>
- <AriaLabel>
- <Cell.InputLabel>
- {
- // TRANSLATORS: The label next to the multihop settings toggle.
- messages.pgettext('advanced-settings-view', 'Enable multihop')
- }
- </Cell.InputLabel>
- </AriaLabel>
+ <AriaInputGroup>
+ <StyledSelectorContainer>
+ <StyledSelectorForFooter
+ // TRANSLATORS: The title for the WireGuard IP version selector.
+ title={messages.pgettext('wireguard-settings-view', 'IP version')}
+ values={this.wireguardIpVersionItems}
+ value={this.props.wireguard.ipVersion}
+ onSelect={this.props.setWireguardIpVersion}
+ />
+ </StyledSelectorContainer>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {
+ // TRANSLATORS: The hint displayed below the WireGuard IP version selector.
+ messages.pgettext(
+ 'wireguard-settings-view',
+ 'This allows access to WireGuard for devices that only support IPv6.',
+ )
+ }
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+
+ <Cell.CellButtonGroup>
+ <Cell.CellButton onClick={this.props.onViewWireguardKeys}>
+ <Cell.Label>
+ {messages.pgettext('wireguard-settings-view', 'WireGuard key')}
+ </Cell.Label>
+ <Cell.Icon height={12} width={7} source="icon-chevron" />
+ </Cell.CellButton>
+ </Cell.CellButtonGroup>
+
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('wireguard-settings-view', 'MTU')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <StyledInputFrame>
<AriaInput>
- <Cell.Switch
- ref={this.multihopRef}
- isOn={this.props.wireguardMultihop}
- onChange={this.setWireguardMultihop}
+ <Cell.AutoSizingTextInput
+ value={this.props.wireguardMtu ? this.props.wireguardMtu.toString() : ''}
+ inputMode={'numeric'}
+ maxLength={4}
+ placeholder={messages.gettext('Default')}
+ onSubmitValue={this.onWireguardMtuSubmit}
+ validateValue={WireguardSettings.wireguarMtuIsValid}
+ submitOnBlur={true}
+ modifyValue={WireguardSettings.removeNonNumericCharacters}
/>
</AriaInput>
- </Cell.Container>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
+ </StyledInputFrame>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {sprintf(
+ // TRANSLATORS: The hint displayed below the WireGuard MTU input field.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(max)d - the maximum possible wireguard mtu value
+ // TRANSLATORS: %(min)d - the minimum possible wireguard mtu value
+ messages.pgettext(
+ 'wireguard-settings-view',
+ 'Set WireGuard MTU value. Valid range: %(min)d - %(max)d.',
+ ),
{
- // TRANSLATORS: Description for multihop settings toggle.
- messages.pgettext(
- 'advanced-settings-view',
- 'Increases anonymity by routing your traffic into one WireGuard server and out another, making it harder to trace.',
- )
- }
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
-
- <AriaInputGroup>
- <StyledSelectorContainer>
- <StyledSelectorForFooter
- // TRANSLATORS: The title for the WireGuard IP version selector.
- title={messages.pgettext('wireguard-settings-view', 'IP version')}
- values={this.wireguardIpVersionItems}
- value={this.props.wireguard.ipVersion}
- onSelect={this.props.setWireguardIpVersion}
- />
- </StyledSelectorContainer>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {
- // TRANSLATORS: The hint displayed below the WireGuard IP version selector.
- messages.pgettext(
- 'wireguard-settings-view',
- 'This allows access to WireGuard for devices that only support IPv6.',
- )
- }
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
-
- <Cell.CellButtonGroup>
- <Cell.CellButton onClick={this.props.onViewWireguardKeys}>
- <Cell.Label>
- {messages.pgettext('wireguard-settings-view', 'WireGuard key')}
- </Cell.Label>
- <Cell.Icon height={12} width={7} source="icon-chevron" />
- </Cell.CellButton>
- </Cell.CellButtonGroup>
-
- <AriaInputGroup>
- <Cell.Container>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('wireguard-settings-view', 'MTU')}
- </Cell.InputLabel>
- </AriaLabel>
- <StyledInputFrame>
- <AriaInput>
- <Cell.AutoSizingTextInput
- value={this.props.wireguardMtu ? this.props.wireguardMtu.toString() : ''}
- inputMode={'numeric'}
- maxLength={4}
- placeholder={messages.gettext('Default')}
- onSubmitValue={this.onWireguardMtuSubmit}
- validateValue={WireguardSettings.wireguarMtuIsValid}
- submitOnBlur={true}
- modifyValue={WireguardSettings.removeNonNumericCharacters}
- />
- </AriaInput>
- </StyledInputFrame>
- </Cell.Container>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {sprintf(
- // TRANSLATORS: The hint displayed below the WireGuard MTU input field.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(max)d - the maximum possible wireguard mtu value
- // TRANSLATORS: %(min)d - the minimum possible wireguard mtu value
- messages.pgettext(
- 'wireguard-settings-view',
- 'Set WireGuard MTU value. Valid range: %(min)d - %(max)d.',
- ),
- {
- min: MIN_WIREGUARD_MTU_VALUE,
- max: MAX_WIREGUARD_MTU_VALUE,
- },
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
- </StyledNavigationScrollbars>
- </NavigationContainer>
- </SettingsContainer>
- </Layout>
+ min: MIN_WIREGUARD_MTU_VALUE,
+ max: MAX_WIREGUARD_MTU_VALUE,
+ },
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+ </StyledNavigationScrollbars>
+ </NavigationContainer>
+ </SettingsContainer>
{this.state.showMultihopConfirmationDialog && this.renderMultihopConfirmation()}
- </ModalContainer>
+ </Layout>
);
}
diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts
index 401edaf5c4..449ff2b563 100644
--- a/gui/src/renderer/redux/settings/reducers.ts
+++ b/gui/src/renderer/redux/settings/reducers.ts
@@ -155,6 +155,7 @@ const initialState: ISettingsReduxState = {
startMinimized: false,
unpinnedWindow: window.env.platform !== 'win32' && window.env.platform !== 'darwin',
browsedForSplitTunnelingApplications: [],
+ changelogDisplayedForVersion: '',
},
relaySettings: {
normal: {
diff --git a/gui/src/renderer/redux/userinterface/actions.ts b/gui/src/renderer/redux/userinterface/actions.ts
index e6010d42f2..78b5bae756 100644
--- a/gui/src/renderer/redux/userinterface/actions.ts
+++ b/gui/src/renderer/redux/userinterface/actions.ts
@@ -1,4 +1,5 @@
import { MacOsScrollbarVisibility } from '../../../shared/ipc-schema';
+import { IChangelog } from '../../../shared/ipc-types';
export interface IUpdateLocaleAction {
type: 'UPDATE_LOCALE';
@@ -40,6 +41,11 @@ export interface ISetConnectedToDaemon {
connectedToDaemon: boolean;
}
+export interface ISetChangelog {
+ type: 'SET_CHANGELOG';
+ changelog: IChangelog;
+}
+
export type UserInterfaceAction =
| IUpdateLocaleAction
| IUpdateWindowArrowPositionAction
@@ -48,7 +54,8 @@ export type UserInterfaceAction =
| IAddScrollPosition
| IRemoveScrollPosition
| ISetMacOsScrollbarVisibility
- | ISetConnectedToDaemon;
+ | ISetConnectedToDaemon
+ | ISetChangelog;
function updateLocale(locale: string): IUpdateLocaleAction {
return {
@@ -108,6 +115,13 @@ function setConnectedToDaemon(connectedToDaemon: boolean): ISetConnectedToDaemon
};
}
+function setChangelog(changelog: IChangelog): ISetChangelog {
+ return {
+ type: 'SET_CHANGELOG',
+ changelog,
+ };
+}
+
export default {
updateLocale,
updateWindowArrowPosition,
@@ -117,4 +131,5 @@ export default {
removeScrollPosition,
setMacOsScrollbarVisibility,
setConnectedToDaemon,
+ setChangelog,
};
diff --git a/gui/src/renderer/redux/userinterface/reducers.ts b/gui/src/renderer/redux/userinterface/reducers.ts
index 9247dc8a5f..58107b455e 100644
--- a/gui/src/renderer/redux/userinterface/reducers.ts
+++ b/gui/src/renderer/redux/userinterface/reducers.ts
@@ -1,4 +1,5 @@
import { MacOsScrollbarVisibility } from '../../../shared/ipc-schema';
+import { IChangelog } from '../../../shared/ipc-types';
import { ReduxAction } from '../store';
export interface IUserInterfaceReduxState {
@@ -9,6 +10,7 @@ export interface IUserInterfaceReduxState {
scrollPosition: Record<string, [number, number]>;
macOsScrollbarVisibility?: MacOsScrollbarVisibility;
connectedToDaemon: boolean;
+ changelog: IChangelog;
}
const initialState: IUserInterfaceReduxState = {
@@ -18,6 +20,7 @@ const initialState: IUserInterfaceReduxState = {
scrollPosition: {},
macOsScrollbarVisibility: undefined,
connectedToDaemon: false,
+ changelog: [],
};
export default function (
@@ -55,6 +58,12 @@ export default function (
case 'SET_CONNECTED_TO_DAEMON':
return { ...state, connectedToDaemon: action.connectedToDaemon };
+ case 'SET_CHANGELOG':
+ return {
+ ...state,
+ changelog: action.changelog,
+ };
+
default:
return state;
}
diff --git a/gui/src/shared/gui-settings-state.ts b/gui/src/shared/gui-settings-state.ts
index dc5bea66f5..e1286101b7 100644
--- a/gui/src/shared/gui-settings-state.ts
+++ b/gui/src/shared/gui-settings-state.ts
@@ -27,4 +27,8 @@ export interface IGuiSettingsState {
// Conains a list of filepaths to applications added to the list of applications, in the split
// tunneling view, by the user.
browsedForSplitTunnelingApplications: Array<string>;
+
+ // The last version that the changelog dialog was shown for. This is used to only show the
+ // changelog after upgrade.
+ changelogDisplayedForVersion: string;
}
diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts
index cd39ac8c8a..0ed0985bf1 100644
--- a/gui/src/shared/ipc-schema.ts
+++ b/gui/src/shared/ipc-schema.ts
@@ -24,7 +24,7 @@ interface ILogEntry {
message: string;
}
import { invoke, invokeSync, notifyRenderer, send } from './ipc-helpers';
-import { ICurrentAppVersionInfo, IWindowShapeParameters } from './ipc-types';
+import { IChangelog, ICurrentAppVersionInfo, IWindowShapeParameters } from './ipc-types';
export interface ITranslations {
locale: string;
@@ -60,6 +60,7 @@ export interface IAppStateSnapshot {
translations: ITranslations;
windowsSplitTunnelingApplications?: IApplication[];
macOsScrollbarVisibility?: MacOsScrollbarVisibility;
+ changelog: IChangelog;
}
// The different types of requests are:
@@ -122,6 +123,7 @@ export const ipcSchema = {
},
currentVersion: {
'': notifyRenderer<ICurrentAppVersionInfo>(),
+ displayedChangelog: send<void>(),
},
upgradeVersion: {
'': notifyRenderer<IAppVersionInfo>(),
diff --git a/gui/src/shared/ipc-types.ts b/gui/src/shared/ipc-types.ts
index 691d54b61b..0624ac95cd 100644
--- a/gui/src/shared/ipc-types.ts
+++ b/gui/src/shared/ipc-types.ts
@@ -8,3 +8,5 @@ export interface ICurrentAppVersionInfo {
export interface IWindowShapeParameters {
arrowPosition?: number;
}
+
+export type IChangelog = Array<string>;
diff --git a/gui/src/shared/localization-contexts.ts b/gui/src/shared/localization-contexts.ts
index bf458444ca..b402278571 100644
--- a/gui/src/shared/localization-contexts.ts
+++ b/gui/src/shared/localization-contexts.ts
@@ -1,4 +1,5 @@
export type LocalizationContexts =
+ | 'changelog'
| 'accessibility'
| 'login-view'
| 'auth-failure'
diff --git a/gui/tasks/distribution.js b/gui/tasks/distribution.js
index 15cdd23d87..798883b0ad 100644
--- a/gui/tasks/distribution.js
+++ b/gui/tasks/distribution.js
@@ -35,6 +35,7 @@ const config = {
files: [
'package.json',
+ 'changelog.txt',
'init.js',
'build/',
'!build/src/renderer',
@@ -51,7 +52,7 @@ const config = {
afterPack: (context) => {
const resources = context.packager.platformSpecificBuildOptions.extraResources;
for (const resource of resources) {
- const filePath = resource.from.replace(/\$\{env\.(.*)\}/, function(match, captureGroup) {
+ const filePath = resource.from.replace(/\$\{env\.(.*)\}/, function (match, captureGroup) {
return process.env[captureGroup];
});
@@ -123,9 +124,22 @@ const config = {
{ from: distAssets('mullvad-problem-report.exe'), to: '.' },
{ from: distAssets('mullvad-daemon.exe'), to: '.' },
{ from: distAssets('talpid_openvpn_plugin.dll'), to: '.' },
- { from: root(path.join('windows', 'winfw', 'bin', 'x64-${env.CPP_BUILD_MODE}', 'winfw.dll')), to: '.' },
- { from: root(path.join('windows', 'windns', 'bin', 'x64-${env.CPP_BUILD_MODE}', 'windns.dll')), to: '.' },
- { from: root(path.join('windows', 'winnet', 'bin', 'x64-${env.CPP_BUILD_MODE}', 'winnet.dll')), to: '.' },
+ {
+ from: root(path.join('windows', 'winfw', 'bin', 'x64-${env.CPP_BUILD_MODE}', 'winfw.dll')),
+ to: '.',
+ },
+ {
+ from: root(
+ path.join('windows', 'windns', 'bin', 'x64-${env.CPP_BUILD_MODE}', 'windns.dll'),
+ ),
+ to: '.',
+ },
+ {
+ from: root(
+ path.join('windows', 'winnet', 'bin', 'x64-${env.CPP_BUILD_MODE}', 'winnet.dll'),
+ ),
+ to: '.',
+ },
{ from: distAssets('binaries/x86_64-pc-windows-msvc/openvpn.exe'), to: '.' },
{ from: distAssets('binaries/x86_64-pc-windows-msvc/sslocal.exe'), to: '.' },
{ from: root('build/lib/x86_64-pc-windows-msvc/libwg.dll'), to: '.' },
diff --git a/gui/test/setup/changelog.spec.ts b/gui/test/setup/changelog.spec.ts
new file mode 100644
index 0000000000..249c26e1db
--- /dev/null
+++ b/gui/test/setup/changelog.spec.ts
@@ -0,0 +1,65 @@
+import { expect } from 'chai';
+import { after, it, describe } from 'mocha';
+import { parseChangelog } from '../../src/main/changelog';
+
+// It should be handled the same no matter if the platforms are split with a space or not.
+const changelogItems = [
+ 'Changelog item 1',
+ '[Windows] Changelog item 2',
+ '[macOS] Changelog item 3',
+ '[linux] Changelog item 4',
+ '[Windows, macOS] Changelog item 5',
+ '[Windows,linux] Changelog item 6',
+ '[Windows, macOS,linux] Changelog item 7',
+];
+
+const changelogString = changelogItems.join('\n');
+
+const mockPlatform = (platform: string) => {
+ Object.defineProperty(process, 'platform', { value: platform });
+};
+
+describe('Changelog parser', () => {
+ const platform = process.platform;
+
+ after(() => {
+ mockPlatform(platform);
+ });
+
+ it('should show Windows items', () => {
+ mockPlatform('win32');
+
+ const changelog = parseChangelog(changelogString);
+
+ expect(changelog).to.have.length(5);
+ expect(changelogItems[0].endsWith(changelog[0])).to.be.true;
+ expect(changelogItems[1].endsWith(changelog[1])).to.be.true;
+ expect(changelogItems[4].endsWith(changelog[2])).to.be.true;
+ expect(changelogItems[5].endsWith(changelog[3])).to.be.true;
+ expect(changelogItems[6].endsWith(changelog[4])).to.be.true;
+ });
+
+ it('should show macOS items', () => {
+ mockPlatform('darwin');
+
+ const changelog = parseChangelog(changelogString);
+
+ expect(changelog).to.have.length(4);
+ expect(changelogItems[0].endsWith(changelog[0])).to.be.true;
+ expect(changelogItems[2].endsWith(changelog[1])).to.be.true;
+ expect(changelogItems[4].endsWith(changelog[2])).to.be.true;
+ expect(changelogItems[6].endsWith(changelog[3])).to.be.true;
+ });
+
+ it('should show Linux items', () => {
+ mockPlatform('linux');
+
+ const changelog = parseChangelog(changelogString);
+
+ expect(changelog).to.have.length(4);
+ expect(changelogItems[0].endsWith(changelog[0])).to.be.true;
+ expect(changelogItems[3].endsWith(changelog[1])).to.be.true;
+ expect(changelogItems[5].endsWith(changelog[2])).to.be.true;
+ expect(changelogItems[6].endsWith(changelog[3])).to.be.true;
+ });
+});