diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 4417382b20b..9179085caba 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 00000000000..97f3b4c527d --- /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 7036575cd1e..6f11fa12bd5 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -75,7 +75,7 @@ type IProps = DynamicHtmlElementProps onClick: ((e: ButtonEvent) => void | Promise) | null; }; -interface IAccessibleButtonProps extends React.InputHTMLAttributes { +export interface IAccessibleButtonProps extends React.InputHTMLAttributes { ref?: React.Ref; } diff --git a/src/components/views/elements/LearnMore.tsx b/src/components/views/elements/LearnMore.tsx new file mode 100644 index 00000000000..1a96e3d8f47 --- /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 = ({ + title, + description, + ...rest +}) => { + const onClick = () => { + Modal.createDialog( + InfoDialog, + { + title, + description, + button: _t('Got it'), + hasCloseButton: true, + }, + ); + }; + + return + { _t('Learn more') } + ; +}; + +export default LearnMore; diff --git a/src/components/views/settings/devices/FilteredDeviceList.tsx b/src/components/views/settings/devices/FilteredDeviceList.tsx index 9bc216a086e..a2afcc22f64 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.Verified]: { + title: _t('Verified sessions'), + description: _t('For best security, sign out from any session that you don\'t recognize or use anymore.'), + learnMoreDescription: <> +

{ _t('Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.') } +

+

+ { _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.`, + ) + } +

+ , + }, + [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: <> +

{ _t('Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.') } +

+

+ { _t( + `You should make especially certain that you recognise these sessions ` + + `as they could represent an unauthorised use of your account.`, + ) + } +

+ , + }, + [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: <> +

{ _t('Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.') } +

+

+ { _t( + `Removing inactive sessions improves security and performance, ` + + `and makes it easier for you to identify if a new session is suspicious.`, + ) + } +

+ , + }, + }; + +const isSecurityVariation = (filter?: DeviceFilterKey): filter is DeviceSecurityVariation => + Object.values(DeviceSecurityVariation).includes(filter); + const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter }) => { - switch (filter) { - case DeviceSecurityVariation.Verified: - return
- -
- ; - case DeviceSecurityVariation.Unverified: - return
- -
- ; - case DeviceSecurityVariation.Inactive: - return
- -
- ; - default: - return null; + if (isSecurityVariation(filter)) { + const { title, description, learnMoreDescription } = securityCardContent[filter]; + return
+ + { description } + + } + /> +
+ ; } + + return null; }; const getNoResultsMessage = (filter?: DeviceSecurityVariation): string => { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a33de8ea150..8af41255fce 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 00000000000..6ae577543cb --- /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('', () => { + const defaultProps = { + title: 'Test', + description: 'test test test', + ['data-testid']: 'testid', + }; + const getComponent = (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 00000000000..41904877c83 --- /dev/null +++ b/test/components/views/elements/__snapshots__/LearnMore-test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders button 1`] = ` +
+ +
+`; 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 c0f5b9af98c..62a6cd94d1e 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 [

- Consider signing out from old sessions (90 days or older) you don't use anymore + + Consider signing out from old sessions (90 days or older) you don't use anymore. +

+

@@ -72,7 +81,16 @@ HTMLCollection [

- 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. +

+

@@ -107,7 +125,16 @@ HTMLCollection [

- 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. +

+