summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2021-12-03 12:03:08 +0100
committerOskar Nyberg <oskar@mullvad.net>2021-12-03 12:03:08 +0100
commit91ba2ee587eab108c9f651215f8806ba353a3ecd (patch)
tree7ead7cffea497a9ab264fc28101c5d4ea94b6280
parenta671b5208ff4942a760239b761ed1b6e2487007c (diff)
parent5e646f2ca5cd54092f7edc19be75bdfa74446e13 (diff)
downloadmullvadvpn-91ba2ee587eab108c9f651215f8806ba353a3ecd.tar.xz
mullvadvpn-91ba2ee587eab108c9f651215f8806ba353a3ecd.zip
Merge branch 'update-account-number-label'
-rw-r--r--CHANGELOG.md2
-rw-r--r--gui/assets/images/icon-copy.svg4
-rw-r--r--gui/assets/images/icon-obscure.svg4
-rw-r--r--gui/assets/images/icon-unobscure.svg4
-rw-r--r--gui/locales/messages.pot21
-rw-r--r--gui/src/renderer/components/AccountTokenLabel.tsx2
-rw-r--r--gui/src/renderer/components/ClipboardLabel.tsx114
-rw-r--r--gui/src/renderer/components/ExpiredAccountErrorView.tsx5
-rw-r--r--gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx6
-rw-r--r--gui/src/renderer/components/WireguardKeys.tsx6
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>
);
}