Device manager - logout of other session (PSG-744) (#9280)

* add sign out of current device section in device details

* lint

* add sign out cta for other sessions

* test other device sign out

* add pending sign out loader

* tidy

* fix strict error

* use gap instead of nbsp

* use more specific assertions in tests, tweak formatting

* tweak test
This commit is contained in:
Kerry 2022-09-14 18:18:32 +02:00 committed by GitHub
parent 0c22b15bba
commit 10bb10539b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 371 additions and 49 deletions

View file

@ -28,6 +28,7 @@ import { DeviceWithVerification } from './types';
interface Props {
device?: DeviceWithVerification;
isLoading: boolean;
isSigningOut: boolean;
onVerifyCurrentDevice: () => void;
onSignOutCurrentDevice: () => void;
}
@ -35,6 +36,7 @@ interface Props {
const CurrentDeviceSection: React.FC<Props> = ({
device,
isLoading,
isSigningOut,
onVerifyCurrentDevice,
onSignOutCurrentDevice,
}) => {
@ -58,6 +60,7 @@ const CurrentDeviceSection: React.FC<Props> = ({
{ isExpanded &&
<DeviceDetails
device={device}
isSigningOut={isSigningOut}
onSignOutDevice={onSignOutCurrentDevice}
/>
}

View file

@ -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 {
@ -38,6 +38,7 @@ interface MetadataTable {
const DeviceDetails: React.FC<Props> = ({
device,
isSigningOut,
onVerifyDevice,
onSignOutDevice,
}) => {
@ -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>;
};

View file

@ -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;
}
@ -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
@ -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>;
/**
@ -158,8 +174,10 @@ export const FilteredDeviceList =
devices,
filter,
expandedDeviceIds,
signingOutDeviceIds,
onFilterChange,
onDeviceExpandToggle,
onSignOutDevices,
onRequestDeviceVerification,
}: Props, ref: ForwardedRef<HTMLDivElement>) => {
const sortedDevices = getFilteredSortedDevices(devices, filter);
@ -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)

View file

@ -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";
@ -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>;
@ -135,7 +133,6 @@ export const useOwnDevices = (): DevicesState => {
return {
devices,
currentDeviceId,
currentUserMember: userId && matrixClient.getUser(userId) || undefined,
requestDeviceVerification,
refreshDevices,
isLoading,

View file

@ -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';
@ -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,
@ -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));
@ -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);
@ -113,6 +168,7 @@ const SessionManagerTab: React.FC = () => {
<CurrentDeviceSection
device={currentDevice}
isLoading={isLoading}
isSigningOut={signingOutDeviceIds.includes(currentDevice?.device_id)}
onVerifyCurrentDevice={onVerifyCurrentDevice}
onSignOutCurrentDevice={onSignOutCurrentDevice}
/>
@ -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>