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:
parent
0c22b15bba
commit
10bb10539b
13 changed files with 371 additions and 49 deletions
|
@ -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}
|
||||
/>
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue