Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,10 @@ limitations under the License.
color: $primary-content;
}
}

.mx_DeviceDetails_signOutButtonContent {
display: flex;
flex-direction: row;
align-items: center;
gap: $spacing-4;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ import { DeviceWithVerification } from './types';
interface Props {
device?: DeviceWithVerification;
isLoading: boolean;
isSigningOut: boolean;
onVerifyCurrentDevice: () => void;
onSignOutCurrentDevice: () => void;
}

const CurrentDeviceSection: React.FC<Props> = ({
device,
isLoading,
isSigningOut,
onVerifyCurrentDevice,
onSignOutCurrentDevice,
}) => {
Expand All @@ -58,6 +60,7 @@ const CurrentDeviceSection: React.FC<Props> = ({
{ isExpanded &&
<DeviceDetails
device={device}
isSigningOut={isSigningOut}
onSignOutDevice={onSignOutCurrentDevice}
/>
}
Expand Down
17 changes: 11 additions & 6 deletions src/components/views/settings/devices/DeviceDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ import React from 'react';
import { formatDate } from '../../../../DateUtils';
import { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton';
import Spinner from '../../elements/Spinner';
import Heading from '../../typography/Heading';
import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard';
import { DeviceWithVerification } from './types';

interface Props {
device: DeviceWithVerification;
isSigningOut: boolean;
onVerifyDevice?: () => void;
// @TODO(kerry) optional while signout only implemented
// for current device (PSG-744)
onSignOutDevice?: () => void;
onSignOutDevice: () => void;
}

interface MetadataTable {
Expand All @@ -38,6 +38,7 @@ interface MetadataTable {

const DeviceDetails: React.FC<Props> = ({
device,
isSigningOut,
onVerifyDevice,
onSignOutDevice,
}) => {
Expand Down Expand Up @@ -87,15 +88,19 @@ const DeviceDetails: React.FC<Props> = ({
</table>,
) }
</section>
{ !!onSignOutDevice && <section className='mx_DeviceDetails_section'>
<section className='mx_DeviceDetails_section'>
<AccessibleButton
onClick={onSignOutDevice}
kind='danger_inline'
disabled={isSigningOut}
data-testid='device-detail-sign-out-cta'
>
{ _t('Sign out of this session') }
<span className='mx_DeviceDetails_signOutButtonContent'>
{ _t('Sign out of this session') }
{ isSigningOut && <Spinner w={16} h={16} /> }
</span>
</AccessibleButton>
</section> }
</section>
</div>;
};

Expand Down
24 changes: 22 additions & 2 deletions src/components/views/settings/devices/FilteredDeviceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ import {
interface Props {
devices: DevicesDictionary;
expandedDeviceIds: DeviceWithVerification['device_id'][];
signingOutDeviceIds: DeviceWithVerification['device_id'][];
filter?: DeviceSecurityVariation;
onFilterChange: (filter: DeviceSecurityVariation | undefined) => void;
onDeviceExpandToggle: (deviceId: DeviceWithVerification['device_id']) => void;
onSignOutDevices: (deviceIds: DeviceWithVerification['device_id'][]) => void;
onRequestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => void;
}

Expand Down Expand Up @@ -132,10 +134,16 @@ const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) =>
const DeviceListItem: React.FC<{
device: DeviceWithVerification;
isExpanded: boolean;
isSigningOut: boolean;
onDeviceExpandToggle: () => void;
onSignOutDevice: () => void;
onRequestDeviceVerification?: () => void;
}> = ({
device, isExpanded, onDeviceExpandToggle,
device,
isExpanded,
isSigningOut,
onDeviceExpandToggle,
onSignOutDevice,
onRequestDeviceVerification,
}) => <li className='mx_FilteredDeviceList_listItem'>
<DeviceTile
Expand All @@ -146,7 +154,15 @@ const DeviceListItem: React.FC<{
onClick={onDeviceExpandToggle}
/>
</DeviceTile>
{ isExpanded && <DeviceDetails device={device} onVerifyDevice={onRequestDeviceVerification} /> }
{
isExpanded &&
<DeviceDetails
device={device}
isSigningOut={isSigningOut}
onVerifyDevice={onRequestDeviceVerification}
onSignOutDevice={onSignOutDevice}
/>
}
</li>;

/**
Expand All @@ -158,8 +174,10 @@ export const FilteredDeviceList =
devices,
filter,
expandedDeviceIds,
signingOutDeviceIds,
onFilterChange,
onDeviceExpandToggle,
onSignOutDevices,
onRequestDeviceVerification,
}: Props, ref: ForwardedRef<HTMLDivElement>) => {
const sortedDevices = getFilteredSortedDevices(devices, filter);
Expand Down Expand Up @@ -213,7 +231,9 @@ export const FilteredDeviceList =
key={device.device_id}
device={device}
isExpanded={expandedDeviceIds.includes(device.device_id)}
isSigningOut={signingOutDeviceIds.includes(device.device_id)}
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
onSignOutDevice={() => onSignOutDevices([device.device_id])}
onRequestDeviceVerification={
onRequestDeviceVerification
? () => onRequestDeviceVerification(device.device_id)
Expand Down
5 changes: 1 addition & 4 deletions src/components/views/settings/devices/useOwnDevices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { useCallback, useContext, useEffect, useState } from "react";
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/http-api";
import { logger } from "matrix-js-sdk/src/logger";

Expand Down Expand Up @@ -74,10 +73,9 @@ export enum OwnDevicesError {
Unsupported = 'Unsupported',
Default = 'Default',
}
type DevicesState = {
export type DevicesState = {
devices: DevicesDictionary;
currentDeviceId: string;
currentUserMember?: User;
isLoading: boolean;
// not provided when current session cannot request verification
requestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => Promise<VerificationRequest>;
Expand Down Expand Up @@ -135,7 +133,6 @@ export const useOwnDevices = (): DevicesState => {
return {
devices,
currentDeviceId,
currentUserMember: userId && matrixClient.getUser(userId) || undefined,
requestDeviceVerification,
refreshDevices,
isLoading,
Expand Down
80 changes: 69 additions & 11 deletions src/components/views/settings/tabs/user/SessionManagerTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { logger } from 'matrix-js-sdk/src/logger';

import { _t } from "../../../../../languageHandler";
import { useOwnDevices } from '../../devices/useOwnDevices';
import { DevicesState, useOwnDevices } from '../../devices/useOwnDevices';
import SettingsSubsection from '../../shared/SettingsSubsection';
import { FilteredDeviceList } from '../../devices/FilteredDeviceList';
import CurrentDeviceSection from '../../devices/CurrentDeviceSection';
Expand All @@ -28,12 +30,64 @@ import Modal from '../../../../../Modal';
import SetupEncryptionDialog from '../../../dialogs/security/SetupEncryptionDialog';
import VerificationRequestDialog from '../../../dialogs/VerificationRequestDialog';
import LogoutDialog from '../../../dialogs/LogoutDialog';
import MatrixClientContext from '../../../../../contexts/MatrixClientContext';
import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices';

const useSignOut = (
matrixClient: MatrixClient,
refreshDevices: DevicesState['refreshDevices'],
): {
onSignOutCurrentDevice: () => void;
onSignOutOtherDevices: (deviceIds: DeviceWithVerification['device_id'][]) => Promise<void>;
signingOutDeviceIds: DeviceWithVerification['device_id'][];
} => {
const [signingOutDeviceIds, setSigningOutDeviceIds] = useState<DeviceWithVerification['device_id'][]>([]);

const onSignOutCurrentDevice = () => {
Modal.createDialog(
LogoutDialog,
{}, // props,
undefined, // className
false, // isPriority
true, // isStatic
);
};

const onSignOutOtherDevices = async (deviceIds: DeviceWithVerification['device_id'][]) => {
if (!deviceIds.length) {
return;
}
try {
setSigningOutDeviceIds([...signingOutDeviceIds, ...deviceIds]);
await deleteDevicesWithInteractiveAuth(
matrixClient,
deviceIds,
async (success) => {
if (success) {
// @TODO(kerrya) clear selection if was bulk deletion
// when added in PSG-659
await refreshDevices();
}
setSigningOutDeviceIds(signingOutDeviceIds.filter(deviceId => !deviceIds.includes(deviceId)));
},
);
} catch (error) {
logger.error("Error deleting sessions", error);
setSigningOutDeviceIds(signingOutDeviceIds.filter(deviceId => !deviceIds.includes(deviceId)));
}
};

return {
onSignOutCurrentDevice,
onSignOutOtherDevices,
signingOutDeviceIds,
};
};

const SessionManagerTab: React.FC = () => {
const {
devices,
currentDeviceId,
currentUserMember,
isLoading,
requestDeviceVerification,
refreshDevices,
Expand All @@ -43,6 +97,10 @@ const SessionManagerTab: React.FC = () => {
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
const scrollIntoViewTimeoutRef = useRef<ReturnType<typeof setTimeout>>();

const matrixClient = useContext(MatrixClientContext);
const userId = matrixClient.getUserId();
const currentUserMember = userId && matrixClient.getUser(userId) || undefined;

const onDeviceExpandToggle = (deviceId: DeviceWithVerification['device_id']): void => {
if (expandedDeviceIds.includes(deviceId)) {
setExpandedDeviceIds(expandedDeviceIds.filter(id => id !== deviceId));
Expand Down Expand Up @@ -91,14 +149,11 @@ const SessionManagerTab: React.FC = () => {
});
}, [requestDeviceVerification, refreshDevices, currentUserMember]);

const onSignOutCurrentDevice = () => {
if (!currentDevice) {
return;
}
Modal.createDialog(LogoutDialog,
/* props= */{}, /* className= */undefined,
/* isPriority= */false, /* isStatic= */true);
};
const {
onSignOutCurrentDevice,
onSignOutOtherDevices,
signingOutDeviceIds,
} = useSignOut(matrixClient, refreshDevices);

useEffect(() => () => {
clearTimeout(scrollIntoViewTimeoutRef.current);
Expand All @@ -113,6 +168,7 @@ const SessionManagerTab: React.FC = () => {
<CurrentDeviceSection
device={currentDevice}
isLoading={isLoading}
isSigningOut={signingOutDeviceIds.includes(currentDevice?.device_id)}
onVerifyCurrentDevice={onVerifyCurrentDevice}
onSignOutCurrentDevice={onSignOutCurrentDevice}
/>
Expand All @@ -130,9 +186,11 @@ const SessionManagerTab: React.FC = () => {
devices={otherDevices}
filter={filter}
expandedDeviceIds={expandedDeviceIds}
signingOutDeviceIds={signingOutDeviceIds}
onFilterChange={setFilter}
onDeviceExpandToggle={onDeviceExpandToggle}
onRequestDeviceVerification={requestDeviceVerification ? onTriggerDeviceVerification : undefined}
onSignOutDevices={onSignOutOtherDevices}
ref={filteredDeviceListRef}
/>
</SettingsSubsection>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe('<CurrentDeviceSection />', () => {
onVerifyCurrentDevice: jest.fn(),
onSignOutCurrentDevice: jest.fn(),
isLoading: false,
isSigningOut: false,
};
const getComponent = (props = {}): React.ReactElement =>
(<CurrentDeviceSection {...defaultProps} {...props} />);
Expand Down
12 changes: 12 additions & 0 deletions test/components/views/settings/devices/DeviceDetails-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ describe('<DeviceDetails />', () => {
};
const defaultProps = {
device: baseDevice,
isSigningOut: false,
onSignOutDevice: jest.fn(),
};
const getComponent = (props = {}) => <DeviceDetails {...defaultProps} {...props} />;
// 14.03.2022 16:15
Expand Down Expand Up @@ -60,4 +62,14 @@ describe('<DeviceDetails />', () => {
const { container } = render(getComponent({ device }));
expect(container).toMatchSnapshot();
});

it('disables sign out button while sign out is pending', () => {
const device = {
...baseDevice,
};
const { getByTestId } = render(getComponent({ device, isSigningOut: true }));
expect(
getByTestId('device-detail-sign-out-cta').getAttribute('aria-disabled'),
).toEqual("true");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ describe('<FilteredDeviceList />', () => {
const defaultProps = {
onFilterChange: jest.fn(),
onDeviceExpandToggle: jest.fn(),
onSignOutDevices: jest.fn(),
expandedDeviceIds: [],
signingOutDeviceIds: [],
devices: {
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,11 @@ HTMLCollection [
role="button"
tabindex="0"
>
Sign out of this session
<span
class="mx_DeviceDetails_signOutButtonContent"
>
Sign out of this session
</span>
</div>
</section>
</div>,
Expand Down
Loading