diff options
Diffstat (limited to 'gui')
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; + }); +}); |
