diff --git a/res/css/components/views/settings/devices/_DeviceDetails.pcss b/res/css/components/views/settings/devices/_DeviceDetails.pcss index 3017935bb7b..d53dcee02b7 100644 --- a/res/css/components/views/settings/devices/_DeviceDetails.pcss +++ b/res/css/components/views/settings/devices/_DeviceDetails.pcss @@ -34,6 +34,7 @@ limitations under the License. display: grid; grid-gap: $spacing-16; + justify-content: left; &:last-child { padding-bottom: 0; @@ -46,7 +47,7 @@ limitations under the License. margin: 0; } -.mxDeviceDetails_metadataTable { +.mx_DeviceDetails_metadataTable { font-size: $font-12px; color: $secondary-content; diff --git a/res/css/views/elements/_AccessibleButton.pcss b/res/css/views/elements/_AccessibleButton.pcss index 8718d862337..bb4d4924811 100644 --- a/res/css/views/elements/_AccessibleButton.pcss +++ b/res/css/views/elements/_AccessibleButton.pcss @@ -138,15 +138,25 @@ limitations under the License. } &.mx_AccessibleButton_kind_link, - &.mx_AccessibleButton_kind_link_inline { - color: $accent; + &.mx_AccessibleButton_kind_link_inline, + &.mx_AccessibleButton_kind_danger_inline { font-size: inherit; font-weight: normal; line-height: inherit; padding: 0; } + &.mx_AccessibleButton_kind_link, &.mx_AccessibleButton_kind_link_inline { + color: $accent; + } + + &.mx_AccessibleButton_kind_danger_inline { + color: $alert; + } + + &.mx_AccessibleButton_kind_link_inline, + &.mx_AccessibleButton_kind_danger_inline { display: inline; } diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index a2337444cfa..c90293aff4e 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -29,6 +29,7 @@ type AccessibleButtonKind = | 'primary' | 'danger' | 'danger_outline' | 'danger_sm' + | 'danger_inline' | 'link' | 'link_inline' | 'link_sm' diff --git a/src/components/views/settings/devices/CurrentDeviceSection.tsx b/src/components/views/settings/devices/CurrentDeviceSection.tsx index ca4e1494901..8db70aa2b1b 100644 --- a/src/components/views/settings/devices/CurrentDeviceSection.tsx +++ b/src/components/views/settings/devices/CurrentDeviceSection.tsx @@ -29,10 +29,14 @@ interface Props { device?: DeviceWithVerification; isLoading: boolean; onVerifyCurrentDevice: () => void; + onSignOutCurrentDevice: () => void; } const CurrentDeviceSection: React.FC = ({ - device, isLoading, onVerifyCurrentDevice, + device, + isLoading, + onVerifyCurrentDevice, + onSignOutCurrentDevice, }) => { const [isExpanded, setIsExpanded] = useState(false); @@ -51,7 +55,12 @@ const CurrentDeviceSection: React.FC = ({ onClick={() => setIsExpanded(!isExpanded)} /> - { isExpanded && } + { isExpanded && + + }
diff --git a/src/components/views/settings/devices/DeviceDetails.tsx b/src/components/views/settings/devices/DeviceDetails.tsx index 779e29b8d15..48c32ad1a79 100644 --- a/src/components/views/settings/devices/DeviceDetails.tsx +++ b/src/components/views/settings/devices/DeviceDetails.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { formatDate } from '../../../../DateUtils'; import { _t } from '../../../../languageHandler'; +import AccessibleButton from '../../elements/AccessibleButton'; import Heading from '../../typography/Heading'; import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard'; import { DeviceWithVerification } from './types'; @@ -25,6 +26,9 @@ import { DeviceWithVerification } from './types'; interface Props { device: DeviceWithVerification; onVerifyDevice?: () => void; + // @TODO(kerry) optional while signout only implemented + // for current device (PSG-744) + onSignOutDevice?: () => void; } interface MetadataTable { @@ -35,6 +39,7 @@ interface MetadataTable { const DeviceDetails: React.FC = ({ device, onVerifyDevice, + onSignOutDevice, }) => { const metadata: MetadataTable[] = [ { @@ -64,7 +69,7 @@ const DeviceDetails: React.FC = ({

{ _t('Session details') }

{ metadata.map(({ heading, values }, index) => { heading && @@ -82,6 +87,15 @@ const DeviceDetails: React.FC = ({
, ) }
+ { !!onSignOutDevice &&
+ + { _t('Sign out of this session') } + +
} ; }; diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts index 3f1beecae78..1a4b1e6bb20 100644 --- a/src/components/views/settings/devices/useOwnDevices.ts +++ b/src/components/views/settings/devices/useOwnDevices.ts @@ -19,7 +19,7 @@ import { IMyDevice, MatrixClient } from "matrix-js-sdk/src/matrix"; import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { User } from "matrix-js-sdk/src/models/user"; -import { MatrixError } from "matrix-js-sdk/src/matrix"; +import { MatrixError } from "matrix-js-sdk/src/http-api"; import { logger } from "matrix-js-sdk/src/logger"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 024b71c5310..14521f84dad 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -27,6 +27,7 @@ import SettingsTab from '../SettingsTab'; import Modal from '../../../../../Modal'; import SetupEncryptionDialog from '../../../dialogs/security/SetupEncryptionDialog'; import VerificationRequestDialog from '../../../dialogs/VerificationRequestDialog'; +import LogoutDialog from '../../../dialogs/LogoutDialog'; const SessionManagerTab: React.FC = () => { const { @@ -90,6 +91,15 @@ const SessionManagerTab: React.FC = () => { }); }, [requestDeviceVerification, refreshDevices, currentUserMember]); + const onSignOutCurrentDevice = () => { + if (!currentDevice) { + return; + } + Modal.createDialog(LogoutDialog, + /* props= */{}, /* className= */undefined, + /* isPriority= */false, /* isStatic= */true); + }; + useEffect(() => () => { clearTimeout(scrollIntoViewTimeoutRef.current); }, [scrollIntoViewTimeoutRef]); @@ -104,6 +114,7 @@ const SessionManagerTab: React.FC = () => { device={currentDevice} isLoading={isLoading} onVerifyCurrentDevice={onVerifyCurrentDevice} + onSignOutCurrentDevice={onSignOutCurrentDevice} /> { shouldShowOtherSessions && diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index df41c832399..ab35c67f368 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1712,6 +1712,7 @@ "Device": "Device", "IP address": "IP address", "Session details": "Session details", + "Sign out of this session": "Sign out of this session", "Toggle device details": "Toggle device details", "Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days", "Verified": "Verified", diff --git a/test/components/views/settings/devices/CurrentDeviceSection-test.tsx b/test/components/views/settings/devices/CurrentDeviceSection-test.tsx index 197e50a4bf4..1fd6b2ec436 100644 --- a/test/components/views/settings/devices/CurrentDeviceSection-test.tsx +++ b/test/components/views/settings/devices/CurrentDeviceSection-test.tsx @@ -35,6 +35,7 @@ describe('', () => { const defaultProps = { device: alicesVerifiedDevice, onVerifyCurrentDevice: jest.fn(), + onSignOutCurrentDevice: jest.fn(), isLoading: false, }; const getComponent = (props = {}): React.ReactElement => diff --git a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap index e20ce4f48d7..5215e7f2086 100644 --- a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap @@ -50,7 +50,7 @@ HTMLCollection [ Session details

@@ -78,7 +78,7 @@ HTMLCollection [
@@ -101,6 +101,18 @@ HTMLCollection [
+
+
+ Sign out of this session +
+
, ] `; diff --git a/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap index d83ca383150..4a3d7afade0 100644 --- a/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap @@ -50,7 +50,7 @@ exports[` renders a verified device 1`] = ` Session details

@@ -78,7 +78,7 @@ exports[` renders a verified device 1`] = `
@@ -155,7 +155,7 @@ exports[` renders device with metadata 1`] = ` Session details

@@ -185,7 +185,7 @@ exports[` renders device with metadata 1`] = `
@@ -264,7 +264,7 @@ exports[` renders device without metadata 1`] = ` Session details

@@ -292,7 +292,7 @@ exports[` renders device without metadata 1`] = `
diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index bea02207e24..ee538b27dd8 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -30,6 +30,7 @@ import { mockClientMethodsUser, } from '../../../../../test-utils'; import Modal from '../../../../../../src/Modal'; +import LogoutDialog from '../../../../../../src/components/views/dialogs/LogoutDialog'; jest.useFakeTimers(); @@ -53,7 +54,7 @@ describe('', () => { const mockCrossSigningInfo = { checkDeviceTrust: jest.fn(), }; - const mockVerificationRequest = { cancel: jest.fn() } as unknown as VerificationRequest; + const mockVerificationRequest = { cancel: jest.fn(), on: jest.fn() } as unknown as VerificationRequest; const mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsUser(aliceId), getStoredCrossSigningForUser: jest.fn().mockReturnValue(mockCrossSigningInfo), @@ -374,4 +375,29 @@ describe('', () => { expect(mockClient.getDevices).toHaveBeenCalled(); }); }); + + describe('Sign out', () => { + it('Signs out of current device', async () => { + const modalSpy = jest.spyOn(Modal, 'createDialog'); + + mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice] }); + const { getByTestId } = render(getComponent()); + + await act(async () => { + await flushPromisesWithFakeTimers(); + }); + + // open device detail + const tile1 = getByTestId(`device-tile-${alicesDevice.device_id}`); + const toggle1 = tile1.querySelector('[aria-label="Toggle device details"]') as Element; + fireEvent.click(toggle1); + + const signOutButton = getByTestId('device-detail-sign-out-cta'); + expect(signOutButton).toMatchSnapshot(); + fireEvent.click(signOutButton); + + // logout dialog opened + expect(modalSpy).toHaveBeenCalledWith(LogoutDialog, {}, undefined, false, true); + }); + }); }); diff --git a/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap index 87f9aab9523..6cec91242d5 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap @@ -1,5 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[` Sign out Signs out of current device 1`] = ` +
+ Sign out of this session +
+`; + exports[` goes to filtered list from security recommendations 1`] = `