diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2021-12-03 12:03:08 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2021-12-03 12:03:08 +0100 |
| commit | 91ba2ee587eab108c9f651215f8806ba353a3ecd (patch) | |
| tree | 7ead7cffea497a9ab264fc28101c5d4ea94b6280 | |
| parent | a671b5208ff4942a760239b761ed1b6e2487007c (diff) | |
| parent | 5e646f2ca5cd54092f7edc19be75bdfa74446e13 (diff) | |
| download | mullvadvpn-91ba2ee587eab108c9f651215f8806ba353a3ecd.tar.xz mullvadvpn-91ba2ee587eab108c9f651215f8806ba353a3ecd.zip | |
Merge branch 'update-account-number-label'
| -rw-r--r-- | CHANGELOG.md | 2 | ||||
| -rw-r--r-- | gui/assets/images/icon-copy.svg | 4 | ||||
| -rw-r--r-- | gui/assets/images/icon-obscure.svg | 4 | ||||
| -rw-r--r-- | gui/assets/images/icon-unobscure.svg | 4 | ||||
| -rw-r--r-- | gui/locales/messages.pot | 21 | ||||
| -rw-r--r-- | gui/src/renderer/components/AccountTokenLabel.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/ClipboardLabel.tsx | 114 | ||||
| -rw-r--r-- | gui/src/renderer/components/ExpiredAccountErrorView.tsx | 5 | ||||
| -rw-r--r-- | gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx | 6 | ||||
| -rw-r--r-- | gui/src/renderer/components/WireguardKeys.tsx | 6 |
10 files changed, 126 insertions, 42 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 548a2f3f51..618ddcdb31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ Line wrap the file at 100 chars. Th ### Changed - Keep unspecified constraints unchanged in the CLI when providing specific tunnel constraints instead of setting them to default values. +- Obscure account number in account view and add button for copying instead of copying when text is + pressed. #### Android - Avoid running in foreground when not connected. diff --git a/gui/assets/images/icon-copy.svg b/gui/assets/images/icon-copy.svg new file mode 100644 index 0000000000..6e1cf4a28d --- /dev/null +++ b/gui/assets/images/icon-copy.svg @@ -0,0 +1,4 @@ +<svg data-name="Icons/Copy" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> + <path data-name="Path 5" d="M0 0h24v24H0z" style="fill:none"/> + <path data-name="Path 6" d="M13.789 1H3.684A1.689 1.689 0 0 0 2 2.684v11.79a.749.749 0 0 0 .829.736.812.812 0 0 0 .855-.736V2.684h10.105a.9.9 0 0 0 .832-.825.91.91 0 0 0-.832-.859zm2.526 3.368H7.053a1.689 1.689 0 0 0-1.685 1.685v11.789a1.689 1.689 0 0 0 1.684 1.684h9.263A1.689 1.689 0 0 0 18 17.842V6.053a1.689 1.689 0 0 0-1.684-1.685zm0 13.474H7.053V6.053h9.263z" transform="translate(2 1.737)" style="fill:#294d73"/> +</svg> diff --git a/gui/assets/images/icon-obscure.svg b/gui/assets/images/icon-obscure.svg new file mode 100644 index 0000000000..23f4044080 --- /dev/null +++ b/gui/assets/images/icon-obscure.svg @@ -0,0 +1,4 @@ +<svg data-name="Icons/stop showing account number" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> + <path data-name="Path 1" d="M0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0z" style="fill:none"/> + <path data-name="Path 2" d="M12 6a9.77 9.77 0 0 1 8.82 5.5 9.647 9.647 0 0 1-2.41 3.12l1.41 1.41A11.8 11.8 0 0 0 23 11.5 11.834 11.834 0 0 0 8.36 4.57l1.65 1.65A10.108 10.108 0 0 1 12 6zm-1.07 1.14L13 9.21a2.5 2.5 0 0 1 1.28 1.28l2.07 2.07a4.679 4.679 0 0 0 .14-1.07A4.483 4.483 0 0 0 12 7a4.244 4.244 0 0 0-1.07.14zM2.01 3.87l2.68 2.68A11.738 11.738 0 0 0 1 11.5 11.827 11.827 0 0 0 12 19a11.73 11.73 0 0 0 4.32-.82l3.42 3.42a1 1 0 0 0 1.41-1.41C20.8 19.807 3.791 2.777 3.42 2.45a1.1 1.1 0 0 0-1.41 0 1.045 1.045 0 0 0 0 1.42zm7.5 7.5 2.61 2.61A.5.5 0 0 1 12 14a2.5 2.5 0 0 1-2.5-2.5c0-.05.01-.08.01-.13zm-3.4-3.4 1.75 1.75a4.6 4.6 0 0 0-.36 1.78 4.505 4.505 0 0 0 6.27 4.14l.98.98A10.432 10.432 0 0 1 12 17a9.77 9.77 0 0 1-8.82-5.5 9.9 9.9 0 0 1 2.93-3.53z" style="fill:#294d73"/> +</svg> diff --git a/gui/assets/images/icon-unobscure.svg b/gui/assets/images/icon-unobscure.svg new file mode 100644 index 0000000000..cef3c44ea2 --- /dev/null +++ b/gui/assets/images/icon-unobscure.svg @@ -0,0 +1,4 @@ +<svg data-name="Icons/show account number" xmlns="http://www.w3.org/2000/svg" width="24" height="24"> + <path data-name="Path 3" d="M0 0h24v24H0z" style="fill:none"/> + <path data-name="Path 4" d="M12 6a9.77 9.77 0 0 1 8.82 5.5 9.822 9.822 0 0 1-17.64 0A9.77 9.77 0 0 1 12 6m0-2a11.827 11.827 0 0 0-11 7.5 11.817 11.817 0 0 0 22 0A11.827 11.827 0 0 0 12 4zm0 5a2.5 2.5 0 1 1-2.5 2.5A2.5 2.5 0 0 1 12 9m0-2a4.5 4.5 0 1 0 4.5 4.5A4.507 4.507 0 0 0 12 7z" style="fill:#294d73"/> +</svg> diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index e12f70db2b..eb403901d0 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -93,9 +93,6 @@ msgstr "" msgid "Connect" msgstr "" -msgid "COPIED TO CLIPBOARD!" -msgstr "" - msgid "CREATING SECURE CONNECTION" msgstr "" @@ -176,6 +173,12 @@ msgctxt "accessibility" msgid "Collapse %(location)s" msgstr "" +#. Provided to accessibility tools such as screenreaders to describe a button +#. which copies the account number to the clipboard. +msgctxt "accessibility" +msgid "Copy account number" +msgstr "" + msgctxt "accessibility" msgid "Expand %(location)s" msgstr "" @@ -184,6 +187,12 @@ msgctxt "accessibility" msgid "Forget %(accountToken)s" msgstr "" +#. Provided to accessibility tools such as screenreaders to describe +#. the button which obscures the account number. +msgctxt "accessibility" +msgid "Hide account number" +msgstr "" + #. This is used by screenreaders to communicate the login button. msgctxt "accessibility" msgid "Login" @@ -201,6 +210,12 @@ msgctxt "accessibility" msgid "Select location. Current location is %(location)s" msgstr "" +#. Provided to accessibility tools such as screenreaders to describe +#. the button which unobscures the account number. +msgctxt "accessibility" +msgid "Show account number" +msgstr "" + #. Title label in navigation bar msgctxt "account-view" msgid "Account" diff --git a/gui/src/renderer/components/AccountTokenLabel.tsx b/gui/src/renderer/components/AccountTokenLabel.tsx index b2623a5ad2..704f41af10 100644 --- a/gui/src/renderer/components/AccountTokenLabel.tsx +++ b/gui/src/renderer/components/AccountTokenLabel.tsx @@ -3,6 +3,7 @@ import ClipboardLabel from './ClipboardLabel'; interface IAccountTokenLabelProps { accountToken: string; + obscureValue?: boolean; className?: string; } @@ -11,6 +12,7 @@ export default function AccountTokenLabel(props: IAccountTokenLabelProps) { <ClipboardLabel value={props.accountToken} displayValue={formatAccountToken(props.accountToken)} + obscureValue={props.obscureValue} className={props.className} /> ); diff --git a/gui/src/renderer/components/ClipboardLabel.tsx b/gui/src/renderer/components/ClipboardLabel.tsx index 43b0d2c764..071a6991a8 100644 --- a/gui/src/renderer/components/ClipboardLabel.tsx +++ b/gui/src/renderer/components/ClipboardLabel.tsx @@ -1,58 +1,104 @@ -import * as React from 'react'; +import { useCallback } from 'react'; import styled from 'styled-components'; +import { colors } from '../../config.json'; import { messages } from '../../shared/gettext'; import log from '../../shared/logging'; -import { Scheduler } from '../../shared/scheduler'; +import { useScheduler } from '../../shared/scheduler'; +import { useBoolean } from '../lib/utilityHooks'; +import ImageView from './ImageView'; + +const COPIED_ICON_DURATION = 2000; interface IProps { value: string; displayValue?: string; - delay: number; + obscureValue?: boolean; message?: string; className?: string; } -interface IState { - showsMessage: boolean; -} - -const Label = styled.span({ - cursor: 'pointer', +const StyledLabelContainer = styled.div({ + display: 'flex', + flex: 1, + height: '19px', + alignItems: 'center', }); -export default class ClipboardLabel extends React.Component<IProps, IState> { - public static defaultProps: Partial<IProps> = { - delay: 3000, - }; +const StyledLabel = styled.span({ + flex: 1, +}); - public state: IState = { - showsMessage: false, - }; +const StyledButton = styled.button({ + cursor: 'default', + padding: 0, + marginLeft: '20px', + backgroundColor: 'transparent', + border: 'none', +}); - private scheduler = new Scheduler(); +const StyledCopyButton = styled(StyledButton)({ + width: '24px', +}); - public componentWillUnmount() { - this.scheduler.cancel(); - } +export default function ClipboardLabel(props: IProps) { + const [obscured, , , toggleObscured] = useBoolean(props.obscureValue ?? true); + const [justCopied, setJustCopied, resetJustCopied] = useBoolean(false); - public render() { - const message = this.props.message ?? messages.gettext('COPIED TO CLIPBOARD!'); - const displayValue = this.props.displayValue ?? this.props.value; - return ( - <Label className={this.props.className} onClick={this.handlePress}> - {this.state.showsMessage ? message : displayValue} - </Label> - ); - } + const copiedScheduler = useScheduler(); - private handlePress = async () => { + const onCopy = useCallback(async () => { try { - await navigator.clipboard.writeText(this.props.value); - this.scheduler.schedule(() => this.setState({ showsMessage: false }), this.props.delay); - this.setState({ showsMessage: true }); + await navigator.clipboard.writeText(props.value); + copiedScheduler.schedule(resetJustCopied, COPIED_ICON_DURATION); + setJustCopied(); } catch (e) { const error = e as Error; log.error(`Failed to copy to clipboard: ${error.message}`); } - }; + }, [props.value, copiedScheduler, setJustCopied, resetJustCopied]); + + const value = props.displayValue ?? props.value; + return ( + <StyledLabelContainer> + <StyledLabel className={props.className} aria-hidden={obscured}> + {obscured ? '●●●● ●●●● ●●●● ●●●●' : value} + </StyledLabel> + {props.obscureValue !== false && ( + <StyledButton + onClick={toggleObscured} + aria-label={ + obscured + ? // This line is here to prevent the following one to be moved up here by prettier + // TRANSLATORS: Provided to accessibility tools such as screenreaders to describe + // TRANSLATORS: the button which unobscures the account number. + messages.pgettext('accessibility', 'Show account number') + : // This line is here to prevent the following one to be moved up here by prettier + // TRANSLATORS: Provided to accessibility tools such as screenreaders to describe + // TRANSLATORS: the button which obscures the account number. + messages.pgettext('accessibility', 'Hide account number') + }> + <ImageView + source={obscured ? 'icon-unobscure' : 'icon-obscure'} + tintColor={colors.white} + tintHoverColor={colors.white80} + width={24} + /> + </StyledButton> + )} + <StyledCopyButton + onClick={onCopy} + aria-label={ + // TRANSLATORS: Provided to accessibility tools such as screenreaders to describe a button + // TRANSLATORS: which copies the account number to the clipboard. + messages.pgettext('accessibility', 'Copy account number') + }> + <ImageView + source={justCopied ? 'icon-tick' : 'icon-copy'} + tintColor={justCopied ? colors.green : colors.white} + tintHoverColor={justCopied ? colors.green : colors.white80} + width={justCopied ? 22 : 24} + /> + </StyledCopyButton> + </StyledLabelContainer> + ); } diff --git a/gui/src/renderer/components/ExpiredAccountErrorView.tsx b/gui/src/renderer/components/ExpiredAccountErrorView.tsx index d4ac395c2c..0fedb67fdf 100644 --- a/gui/src/renderer/components/ExpiredAccountErrorView.tsx +++ b/gui/src/renderer/components/ExpiredAccountErrorView.tsx @@ -127,7 +127,10 @@ export default class ExpiredAccountErrorView extends React.Component< <StyledAccountTokenMessage> {messages.pgettext('connect-view', 'Here’s your account number. Save it!')} <StyledAccountTokenContainer> - <StyledAccountTokenLabel accountToken={this.props.accountToken || ''} /> + <StyledAccountTokenLabel + accountToken={this.props.accountToken || ''} + obscureValue={false} + /> </StyledAccountTokenContainer> </StyledAccountTokenMessage> diff --git a/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx b/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx index 8872bbb312..5aa4e6ea63 100644 --- a/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx +++ b/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx @@ -14,8 +14,8 @@ export const StyledHeader = styled(DefaultHeaderBar)({ export const StyledAccountTokenLabel = styled(AccountTokenLabel)({ fontFamily: 'Open Sans', - lineHeight: '24px', - fontSize: '24px', + lineHeight: '20px', + fontSize: '20px', fontWeight: 800, color: colors.white, }); @@ -80,6 +80,6 @@ export const StyledStatusIcon = styled.div({ export const StyledAccountTokenContainer = styled.div({ display: 'flex', - height: '68px', + height: '50px', alignItems: 'center', }); diff --git a/gui/src/renderer/components/WireguardKeys.tsx b/gui/src/renderer/components/WireguardKeys.tsx index 3201e0a708..be8c043458 100644 --- a/gui/src/renderer/components/WireguardKeys.tsx +++ b/gui/src/renderer/components/WireguardKeys.tsx @@ -242,7 +242,11 @@ export default class WireguardKeys extends React.Component<IProps, IState> { const publicKey = this.props.keyState.key.publicKey; return ( <StyledRowValue title={this.props.keyState.key.publicKey}> - <ClipboardLabel value={publicKey} displayValue={publicKey.substring(0, 20) + '...'} /> + <ClipboardLabel + value={publicKey} + displayValue={publicKey.substring(0, 20) + '...'} + obscureValue={false} + /> </StyledRowValue> ); } |
