From 69642544acfa94ab425ab164fcf593cd773007a3 Mon Sep 17 00:00:00 2001 From: Kerry <kerrya@element.io> Date: Wed, 26 Oct 2022 11:04:16 +0200 Subject: [PATCH] Device manager - add learn more popups to filtered sessions section (#9497) * add learn more to filtered sessions * fullstop * update tests and i18n for fullstop * remove unused switch * whitespace * use correct card type --- res/css/_components.pcss | 7 +- .../components/views/elements/_LearnMore.pcss | 19 +++ .../views/elements/AccessibleButton.tsx | 2 +- src/components/views/elements/LearnMore.tsx | 56 ++++++++ .../settings/devices/FilteredDeviceList.tsx | 121 ++++++++++++------ src/i18n/strings/en_EN.json | 9 +- .../views/elements/LearnMore-test.tsx | 57 +++++++++ .../__snapshots__/LearnMore-test.tsx.snap | 14 ++ .../FilteredDeviceList-test.tsx.snap | 33 ++++- 9 files changed, 270 insertions(+), 48 deletions(-) create mode 100644 res/css/components/views/elements/_LearnMore.pcss create mode 100644 src/components/views/elements/LearnMore.tsx create mode 100644 test/components/views/elements/LearnMore-test.tsx create mode 100644 test/components/views/elements/__snapshots__/LearnMore-test.tsx.snap diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 4417382b20..9179085cab 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -4,7 +4,6 @@ @import "./_font-sizes.pcss"; @import "./_font-weights.pcss"; @import "./_spacing.pcss"; -@import "./compound/_Icon.pcss"; @import "./components/views/beacon/_BeaconListItem.pcss"; @import "./components/views/beacon/_BeaconStatus.pcss"; @import "./components/views/beacon/_BeaconStatusTooltip.pcss"; @@ -19,6 +18,7 @@ @import "./components/views/beacon/_StyledLiveBeaconIcon.pcss"; @import "./components/views/context_menus/_KebabContextMenu.pcss"; @import "./components/views/elements/_FilterDropdown.pcss"; +@import "./components/views/elements/_LearnMore.pcss"; @import "./components/views/location/_EnableLiveShare.pcss"; @import "./components/views/location/_LiveDurationDropdown.pcss"; @import "./components/views/location/_LocationShareMenu.pcss"; @@ -44,6 +44,7 @@ @import "./components/views/settings/shared/_SettingsSubsectionHeading.pcss"; @import "./components/views/spaces/_QuickThemeSwitcher.pcss"; @import "./components/views/typography/_Caption.pcss"; +@import "./compound/_Icon.pcss"; @import "./structures/_AutoHideScrollbar.pcss"; @import "./structures/_BackdropPanel.pcss"; @import "./structures/_CompatibilityPage.pcss"; @@ -299,10 +300,10 @@ @import "./views/rooms/_TopUnreadMessagesBar.pcss"; @import "./views/rooms/_VoiceRecordComposerTile.pcss"; @import "./views/rooms/_WhoIsTypingTile.pcss"; +@import "./views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss"; +@import "./views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss"; @import "./views/rooms/wysiwyg_composer/components/_Editor.pcss"; @import "./views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss"; -@import "./views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss"; -@import "./views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss"; @import "./views/settings/_AvatarSetting.pcss"; @import "./views/settings/_CrossSigningPanel.pcss"; @import "./views/settings/_CryptographyPanel.pcss"; diff --git a/res/css/components/views/elements/_LearnMore.pcss b/res/css/components/views/elements/_LearnMore.pcss new file mode 100644 index 0000000000..97f3b4c527 --- /dev/null +++ b/res/css/components/views/elements/_LearnMore.pcss @@ -0,0 +1,19 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_LearnMore_button { + margin-left: $spacing-4; +} diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 7036575cd1..6f11fa12bd 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -75,7 +75,7 @@ type IProps<T extends keyof JSX.IntrinsicElements> = DynamicHtmlElementProps<T> onClick: ((e: ButtonEvent) => void | Promise<void>) | null; }; -interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> { +export interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> { ref?: React.Ref<Element>; } diff --git a/src/components/views/elements/LearnMore.tsx b/src/components/views/elements/LearnMore.tsx new file mode 100644 index 0000000000..1a96e3d8f4 --- /dev/null +++ b/src/components/views/elements/LearnMore.tsx @@ -0,0 +1,56 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { _t } from '../../../languageHandler'; +import Modal from '../../../Modal'; +import InfoDialog from '../dialogs/InfoDialog'; +import AccessibleButton, { IAccessibleButtonProps } from './AccessibleButton'; + +interface Props extends IAccessibleButtonProps { + title: string; + description: string | React.ReactNode; +} + +const LearnMore: React.FC<Props> = ({ + title, + description, + ...rest +}) => { + const onClick = () => { + Modal.createDialog( + InfoDialog, + { + title, + description, + button: _t('Got it'), + hasCloseButton: true, + }, + ); + }; + + return <AccessibleButton + {...rest} + kind='link_inline' + onClick={onClick} + className='mx_LearnMore_button' + > + { _t('Learn more') } + </AccessibleButton>; +}; + +export default LearnMore; diff --git a/src/components/views/settings/devices/FilteredDeviceList.tsx b/src/components/views/settings/devices/FilteredDeviceList.tsx index 9bc216a086..a2afcc22f6 100644 --- a/src/components/views/settings/devices/FilteredDeviceList.tsx +++ b/src/components/views/settings/devices/FilteredDeviceList.tsx @@ -38,6 +38,7 @@ import { import { DevicesState } from './useOwnDevices'; import FilteredDeviceListHeader from './FilteredDeviceListHeader'; import Spinner from '../../elements/Spinner'; +import LearnMore from '../../elements/LearnMore'; interface Props { devices: DevicesDictionary; @@ -73,48 +74,88 @@ const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: DeviceSec const ALL_FILTER_ID = 'ALL'; type DeviceFilterKey = DeviceSecurityVariation | typeof ALL_FILTER_ID; +const securityCardContent: Record<DeviceSecurityVariation, { + title: string; + description: string; + learnMoreDescription: React.ReactNode | string; + }> = { + [DeviceSecurityVariation.Verified]: { + title: _t('Verified sessions'), + description: _t('For best security, sign out from any session that you don\'t recognize or use anymore.'), + learnMoreDescription: <> + <p>{ _t('Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.') } + </p> + <p> + { _t( + `This means they hold encryption keys for your previous messages, ` + + `and confirm to other users you are communicating with that these sessions are really you.`, + ) + } + </p> + </>, + }, + [DeviceSecurityVariation.Unverified]: { + title: _t('Unverified sessions'), + description: _t( + `Verify your sessions for enhanced secure messaging or ` + + `sign out from those you don't recognize or use anymore.`, + ), + learnMoreDescription: <> + <p>{ _t('Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.') } + </p> + <p> + { _t( + `You should make especially certain that you recognise these sessions ` + + `as they could represent an unauthorised use of your account.`, + ) + } + </p> + </>, + }, + [DeviceSecurityVariation.Inactive]: { + title: _t('Inactive sessions'), + description: _t( + `Consider signing out from old sessions ` + + `(%(inactiveAgeDays)s days or older) you don't use anymore.`, + { inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS }, + ), + learnMoreDescription: <> + <p>{ _t('Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.') } + </p> + <p> + { _t( + `Removing inactive sessions improves security and performance, ` + + `and makes it easier for you to identify if a new session is suspicious.`, + ) + } + </p> + </>, + }, + }; + +const isSecurityVariation = (filter?: DeviceFilterKey): filter is DeviceSecurityVariation => + Object.values<string>(DeviceSecurityVariation).includes(filter); + const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter }) => { - switch (filter) { - case DeviceSecurityVariation.Verified: - return <div className='mx_FilteredDeviceList_securityCard'> - <DeviceSecurityCard - variation={DeviceSecurityVariation.Verified} - heading={_t('Verified sessions')} - description={_t( - `For best security, sign out from any session` + - ` that you don't recognize or use anymore.`, - )} - /> - </div> - ; - case DeviceSecurityVariation.Unverified: - return <div className='mx_FilteredDeviceList_securityCard'> - <DeviceSecurityCard - variation={DeviceSecurityVariation.Unverified} - heading={_t('Unverified sessions')} - description={_t( - `Verify your sessions for enhanced secure messaging or sign out` - + ` from those you don't recognize or use anymore.`, - )} - /> - </div> - ; - case DeviceSecurityVariation.Inactive: - return <div className='mx_FilteredDeviceList_securityCard'> - <DeviceSecurityCard - variation={DeviceSecurityVariation.Inactive} - heading={_t('Inactive sessions')} - description={_t( - `Consider signing out from old sessions ` + - `(%(inactiveAgeDays)s days or older) you don't use anymore`, - { inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS }, - )} - /> - </div> - ; - default: - return null; + if (isSecurityVariation(filter)) { + const { title, description, learnMoreDescription } = securityCardContent[filter]; + return <div className='mx_FilteredDeviceList_securityCard'> + <DeviceSecurityCard + variation={filter} + heading={title} + description={<span> + { description } + <LearnMore + title={title} + description={learnMoreDescription} + /> + </span>} + /> + </div> + ; } + + return null; }; const getNoResultsMessage = (filter?: DeviceSecurityVariation): string => { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a33de8ea15..8af41255fc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1778,10 +1778,16 @@ "Verify session": "Verify session", "Verified sessions": "Verified sessions", "For best security, sign out from any session that you don't recognize or use anymore.": "For best security, sign out from any session that you don't recognize or use anymore.", + "Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.": "Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.", + "This means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you.": "This means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you.", "Unverified sessions": "Unverified sessions", "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.", + "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.": "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.", + "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.": "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.", "Inactive sessions": "Inactive sessions", - "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.", + "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.": "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.", + "Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.": "Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.", "No verified sessions found.": "No verified sessions found.", "No unverified sessions found.": "No unverified sessions found.", "No inactive sessions found.": "No inactive sessions found.", @@ -1801,6 +1807,7 @@ "Security recommendations": "Security recommendations", "Improve your account security by following these recommendations": "Improve your account security by following these recommendations", "View all": "View all", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore", "Failed to set pusher state": "Failed to set pusher state", "Unable to remove contact information": "Unable to remove contact information", "Remove %(email)s?": "Remove %(email)s?", diff --git a/test/components/views/elements/LearnMore-test.tsx b/test/components/views/elements/LearnMore-test.tsx new file mode 100644 index 0000000000..6ae577543c --- /dev/null +++ b/test/components/views/elements/LearnMore-test.tsx @@ -0,0 +1,57 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import LearnMore from '../../../../src/components/views/elements/LearnMore'; +import Modal from '../../../../src/Modal'; +import InfoDialog from '../../../../src/components/views/dialogs/InfoDialog'; + +describe('<LearnMore />', () => { + const defaultProps = { + title: 'Test', + description: 'test test test', + ['data-testid']: 'testid', + }; + const getComponent = (props = {}) => + (<LearnMore {...defaultProps} {...props} />); + + const modalSpy = jest.spyOn(Modal, 'createDialog').mockReturnValue(undefined); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders button', () => { + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); + }); + + it('opens modal on click', async () => { + const { getByTestId } = render(getComponent()); + fireEvent.click(getByTestId('testid')); + + expect(modalSpy).toHaveBeenCalledWith( + InfoDialog, + { + button: 'Got it', + description: defaultProps.description, + hasCloseButton: true, + title: defaultProps.title, + }); + }); +}); diff --git a/test/components/views/elements/__snapshots__/LearnMore-test.tsx.snap b/test/components/views/elements/__snapshots__/LearnMore-test.tsx.snap new file mode 100644 index 0000000000..41904877c8 --- /dev/null +++ b/test/components/views/elements/__snapshots__/LearnMore-test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`<LearnMore /> renders button 1`] = ` +<div> + <div + class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline" + data-testid="testid" + role="button" + tabindex="0" + > + Learn more + </div> +</div> +`; diff --git a/test/components/views/settings/devices/__snapshots__/FilteredDeviceList-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/FilteredDeviceList-test.tsx.snap index c0f5b9af98..62a6cd94d1 100644 --- a/test/components/views/settings/devices/__snapshots__/FilteredDeviceList-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/FilteredDeviceList-test.tsx.snap @@ -37,7 +37,16 @@ HTMLCollection [ <p class="mx_DeviceSecurityCard_description" > - Consider signing out from old sessions (90 days or older) you don't use anymore + <span> + Consider signing out from old sessions (90 days or older) you don't use anymore. + <div + class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline" + role="button" + tabindex="0" + > + Learn more + </div> + </span> </p> </div> </div> @@ -72,7 +81,16 @@ HTMLCollection [ <p class="mx_DeviceSecurityCard_description" > - Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore. + <span> + Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore. + <div + class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline" + role="button" + tabindex="0" + > + Learn more + </div> + </span> </p> </div> </div> @@ -107,7 +125,16 @@ HTMLCollection [ <p class="mx_DeviceSecurityCard_description" > - For best security, sign out from any session that you don't recognize or use anymore. + <span> + For best security, sign out from any session that you don't recognize or use anymore. + <div + class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline" + role="button" + tabindex="0" + > + Learn more + </div> + </span> </p> </div> </div>