Apply prettier formatting
This commit is contained in:
parent
1cac306093
commit
526645c791
1576 changed files with 65385 additions and 62478 deletions
|
@ -14,20 +14,20 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { LocalNotificationSettings } from 'matrix-js-sdk/src/@types/local_notifications';
|
||||
import React, { useState } from "react";
|
||||
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
|
||||
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import Spinner from '../../elements/Spinner';
|
||||
import SettingsSubsection from '../shared/SettingsSubsection';
|
||||
import { SettingsSubsectionHeading } from '../shared/SettingsSubsectionHeading';
|
||||
import DeviceDetails from './DeviceDetails';
|
||||
import { DeviceExpandDetailsButton } from './DeviceExpandDetailsButton';
|
||||
import DeviceTile from './DeviceTile';
|
||||
import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard';
|
||||
import { ExtendedDevice } from './types';
|
||||
import { KebabContextMenu } from '../../context_menus/KebabContextMenu';
|
||||
import { IconizedContextMenuOption } from '../../context_menus/IconizedContextMenu';
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import Spinner from "../../elements/Spinner";
|
||||
import SettingsSubsection from "../shared/SettingsSubsection";
|
||||
import { SettingsSubsectionHeading } from "../shared/SettingsSubsectionHeading";
|
||||
import DeviceDetails from "./DeviceDetails";
|
||||
import { DeviceExpandDetailsButton } from "./DeviceExpandDetailsButton";
|
||||
import DeviceTile from "./DeviceTile";
|
||||
import { DeviceVerificationStatusCard } from "./DeviceVerificationStatusCard";
|
||||
import { ExtendedDevice } from "./types";
|
||||
import { KebabContextMenu } from "../../context_menus/KebabContextMenu";
|
||||
import { IconizedContextMenuOption } from "../../context_menus/IconizedContextMenu";
|
||||
|
||||
interface Props {
|
||||
device?: ExtendedDevice;
|
||||
|
@ -41,9 +41,9 @@ interface Props {
|
|||
saveDeviceName: (deviceName: string) => Promise<void>;
|
||||
}
|
||||
|
||||
type CurrentDeviceSectionHeadingProps =
|
||||
Pick<Props, 'onSignOutCurrentDevice' | 'signOutAllOtherSessions'>
|
||||
& { disabled?: boolean };
|
||||
type CurrentDeviceSectionHeadingProps = Pick<Props, "onSignOutCurrentDevice" | "signOutAllOtherSessions"> & {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const CurrentDeviceSectionHeading: React.FC<CurrentDeviceSectionHeadingProps> = ({
|
||||
onSignOutCurrentDevice,
|
||||
|
@ -53,30 +53,31 @@ const CurrentDeviceSectionHeading: React.FC<CurrentDeviceSectionHeadingProps> =
|
|||
const menuOptions = [
|
||||
<IconizedContextMenuOption
|
||||
key="sign-out"
|
||||
label={_t('Sign out')}
|
||||
label={_t("Sign out")}
|
||||
onClick={onSignOutCurrentDevice}
|
||||
isDestructive
|
||||
/>,
|
||||
...(signOutAllOtherSessions
|
||||
? [
|
||||
<IconizedContextMenuOption
|
||||
key="sign-out-all-others"
|
||||
label={_t('Sign out all other sessions')}
|
||||
onClick={signOutAllOtherSessions}
|
||||
isDestructive
|
||||
/>,
|
||||
]
|
||||
: []
|
||||
),
|
||||
<IconizedContextMenuOption
|
||||
key="sign-out-all-others"
|
||||
label={_t("Sign out all other sessions")}
|
||||
onClick={signOutAllOtherSessions}
|
||||
isDestructive
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
];
|
||||
return <SettingsSubsectionHeading heading={_t('Current session')}>
|
||||
<KebabContextMenu
|
||||
disabled={disabled}
|
||||
title={_t('Options')}
|
||||
options={menuOptions}
|
||||
data-testid='current-session-menu'
|
||||
/>
|
||||
</SettingsSubsectionHeading>;
|
||||
return (
|
||||
<SettingsSubsectionHeading heading={_t("Current session")}>
|
||||
<KebabContextMenu
|
||||
disabled={disabled}
|
||||
title={_t("Options")}
|
||||
options={menuOptions}
|
||||
data-testid="current-session-menu"
|
||||
/>
|
||||
</SettingsSubsectionHeading>
|
||||
);
|
||||
};
|
||||
|
||||
const CurrentDeviceSection: React.FC<Props> = ({
|
||||
|
@ -92,43 +93,45 @@ const CurrentDeviceSection: React.FC<Props> = ({
|
|||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return <SettingsSubsection
|
||||
data-testid='current-session-section'
|
||||
heading={<CurrentDeviceSectionHeading
|
||||
onSignOutCurrentDevice={onSignOutCurrentDevice}
|
||||
signOutAllOtherSessions={signOutAllOtherSessions}
|
||||
disabled={isLoading || !device || isSigningOut}
|
||||
/>}
|
||||
>
|
||||
{ /* only show big spinner on first load */ }
|
||||
{ isLoading && !device && <Spinner /> }
|
||||
{ !!device && <>
|
||||
<DeviceTile
|
||||
device={device}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<DeviceExpandDetailsButton
|
||||
data-testid='current-session-toggle-details'
|
||||
isExpanded={isExpanded}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
/>
|
||||
</DeviceTile>
|
||||
{ isExpanded &&
|
||||
<DeviceDetails
|
||||
device={device}
|
||||
localNotificationSettings={localNotificationSettings}
|
||||
setPushNotifications={setPushNotifications}
|
||||
isSigningOut={isSigningOut}
|
||||
onVerifyDevice={onVerifyCurrentDevice}
|
||||
onSignOutDevice={onSignOutCurrentDevice}
|
||||
saveDeviceName={saveDeviceName}
|
||||
return (
|
||||
<SettingsSubsection
|
||||
data-testid="current-session-section"
|
||||
heading={
|
||||
<CurrentDeviceSectionHeading
|
||||
onSignOutCurrentDevice={onSignOutCurrentDevice}
|
||||
signOutAllOtherSessions={signOutAllOtherSessions}
|
||||
disabled={isLoading || !device || isSigningOut}
|
||||
/>
|
||||
}
|
||||
<br />
|
||||
<DeviceVerificationStatusCard device={device} onVerifyDevice={onVerifyCurrentDevice} />
|
||||
</>
|
||||
}
|
||||
</SettingsSubsection>;
|
||||
>
|
||||
{/* only show big spinner on first load */}
|
||||
{isLoading && !device && <Spinner />}
|
||||
{!!device && (
|
||||
<>
|
||||
<DeviceTile device={device} onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<DeviceExpandDetailsButton
|
||||
data-testid="current-session-toggle-details"
|
||||
isExpanded={isExpanded}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
/>
|
||||
</DeviceTile>
|
||||
{isExpanded && (
|
||||
<DeviceDetails
|
||||
device={device}
|
||||
localNotificationSettings={localNotificationSettings}
|
||||
setPushNotifications={setPushNotifications}
|
||||
isSigningOut={isSigningOut}
|
||||
onVerifyDevice={onVerifyCurrentDevice}
|
||||
onSignOutDevice={onSignOutCurrentDevice}
|
||||
saveDeviceName={saveDeviceName}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
<DeviceVerificationStatusCard device={device} onVerifyDevice={onVerifyCurrentDevice} />
|
||||
</>
|
||||
)}
|
||||
</SettingsSubsection>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentDeviceSection;
|
||||
|
|
|
@ -14,35 +14,32 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FormEvent, useEffect, useState } from 'react';
|
||||
import React, { FormEvent, useEffect, useState } from "react";
|
||||
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import AccessibleButton from '../../elements/AccessibleButton';
|
||||
import Field from '../../elements/Field';
|
||||
import LearnMore from '../../elements/LearnMore';
|
||||
import Spinner from '../../elements/Spinner';
|
||||
import { Caption } from '../../typography/Caption';
|
||||
import Heading from '../../typography/Heading';
|
||||
import { ExtendedDevice } from './types';
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import Field from "../../elements/Field";
|
||||
import LearnMore from "../../elements/LearnMore";
|
||||
import Spinner from "../../elements/Spinner";
|
||||
import { Caption } from "../../typography/Caption";
|
||||
import Heading from "../../typography/Heading";
|
||||
import { ExtendedDevice } from "./types";
|
||||
|
||||
interface Props {
|
||||
device: ExtendedDevice;
|
||||
saveDeviceName: (deviceName: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({
|
||||
device, saveDeviceName, stopEditing,
|
||||
}) => {
|
||||
const [deviceName, setDeviceName] = useState(device.display_name || '');
|
||||
const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({ device, saveDeviceName, stopEditing }) => {
|
||||
const [deviceName, setDeviceName] = useState(device.display_name || "");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setDeviceName(device.display_name || '');
|
||||
setDeviceName(device.display_name || "");
|
||||
}, [device.display_name]);
|
||||
|
||||
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>): void =>
|
||||
setDeviceName(event.target.value);
|
||||
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>): void => setDeviceName(event.target.value);
|
||||
|
||||
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
setIsLoading(true);
|
||||
|
@ -52,7 +49,7 @@ const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({
|
|||
await saveDeviceName(deviceName);
|
||||
stopEditing();
|
||||
} catch (error) {
|
||||
setError(_t('Failed to set display name'));
|
||||
setError(_t("Failed to set display name"));
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
@ -60,102 +57,92 @@ const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({
|
|||
const headingId = `device-rename-${device.device_id}`;
|
||||
const descriptionId = `device-rename-description-${device.device_id}`;
|
||||
|
||||
return <form
|
||||
aria-disabled={isLoading}
|
||||
className="mx_DeviceDetailHeading_renameForm"
|
||||
onSubmit={onSubmit}
|
||||
method="post"
|
||||
>
|
||||
<p
|
||||
id={headingId}
|
||||
className="mx_DeviceDetailHeading_renameFormHeading"
|
||||
>
|
||||
{ _t('Rename session') }
|
||||
</p>
|
||||
<div>
|
||||
<Field
|
||||
data-testid='device-rename-input'
|
||||
type="text"
|
||||
value={deviceName}
|
||||
autoComplete="off"
|
||||
onChange={onInputChange}
|
||||
autoFocus
|
||||
disabled={isLoading}
|
||||
aria-labelledby={headingId}
|
||||
aria-describedby={descriptionId}
|
||||
className="mx_DeviceDetailHeading_renameFormInput"
|
||||
maxLength={100}
|
||||
/>
|
||||
<Caption
|
||||
id={descriptionId}
|
||||
>
|
||||
{ _t('Please be aware that session names are also visible to people you communicate with.') }
|
||||
<LearnMore
|
||||
title={_t('Renaming sessions')}
|
||||
description={<>
|
||||
<p>
|
||||
{ _t(`Other users in direct messages and rooms that you join ` +
|
||||
`are able to view a full list of your sessions.`,
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t(`This provides them with confidence that they are really speaking to you, ` +
|
||||
`but it also means they can see the session name you enter here.`,
|
||||
) }
|
||||
</p>
|
||||
</>}
|
||||
return (
|
||||
<form aria-disabled={isLoading} className="mx_DeviceDetailHeading_renameForm" onSubmit={onSubmit} method="post">
|
||||
<p id={headingId} className="mx_DeviceDetailHeading_renameFormHeading">
|
||||
{_t("Rename session")}
|
||||
</p>
|
||||
<div>
|
||||
<Field
|
||||
data-testid="device-rename-input"
|
||||
type="text"
|
||||
value={deviceName}
|
||||
autoComplete="off"
|
||||
onChange={onInputChange}
|
||||
autoFocus
|
||||
disabled={isLoading}
|
||||
aria-labelledby={headingId}
|
||||
aria-describedby={descriptionId}
|
||||
className="mx_DeviceDetailHeading_renameFormInput"
|
||||
maxLength={100}
|
||||
/>
|
||||
{ !!error &&
|
||||
<span
|
||||
data-testid="device-rename-error"
|
||||
className='mx_DeviceDetailHeading_renameFormError'>
|
||||
{ error }
|
||||
</span>
|
||||
}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className="mx_DeviceDetailHeading_renameFormButtons">
|
||||
<AccessibleButton
|
||||
onClick={onSubmit}
|
||||
kind="primary"
|
||||
data-testid='device-rename-submit-cta'
|
||||
disabled={isLoading}
|
||||
>
|
||||
{ _t('Save') }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={stopEditing}
|
||||
kind="secondary"
|
||||
data-testid='device-rename-cancel-cta'
|
||||
disabled={isLoading}
|
||||
>
|
||||
{ _t('Cancel') }
|
||||
</AccessibleButton>
|
||||
{ isLoading && <Spinner w={16} h={16} /> }
|
||||
</div>
|
||||
</form>;
|
||||
<Caption id={descriptionId}>
|
||||
{_t("Please be aware that session names are also visible to people you communicate with.")}
|
||||
<LearnMore
|
||||
title={_t("Renaming sessions")}
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
{_t(
|
||||
`Other users in direct messages and rooms that you join ` +
|
||||
`are able to view a full list of your sessions.`,
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
`This provides them with confidence that they are really speaking to you, ` +
|
||||
`but it also means they can see the session name you enter here.`,
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{!!error && (
|
||||
<span data-testid="device-rename-error" className="mx_DeviceDetailHeading_renameFormError">
|
||||
{error}
|
||||
</span>
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className="mx_DeviceDetailHeading_renameFormButtons">
|
||||
<AccessibleButton
|
||||
onClick={onSubmit}
|
||||
kind="primary"
|
||||
data-testid="device-rename-submit-cta"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={stopEditing}
|
||||
kind="secondary"
|
||||
data-testid="device-rename-cancel-cta"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{_t("Cancel")}
|
||||
</AccessibleButton>
|
||||
{isLoading && <Spinner w={16} h={16} />}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const DeviceDetailHeading: React.FC<Props> = ({
|
||||
device, saveDeviceName,
|
||||
}) => {
|
||||
export const DeviceDetailHeading: React.FC<Props> = ({ device, saveDeviceName }) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
return isEditing
|
||||
? <DeviceNameEditor
|
||||
device={device}
|
||||
saveDeviceName={saveDeviceName}
|
||||
stopEditing={() => setIsEditing(false)}
|
||||
/>
|
||||
: <div className='mx_DeviceDetailHeading' data-testid='device-detail-heading'>
|
||||
<Heading size='h3'>{ device.display_name || device.device_id }</Heading>
|
||||
return isEditing ? (
|
||||
<DeviceNameEditor device={device} saveDeviceName={saveDeviceName} stopEditing={() => setIsEditing(false)} />
|
||||
) : (
|
||||
<div className="mx_DeviceDetailHeading" data-testid="device-detail-heading">
|
||||
<Heading size="h3">{device.display_name || device.device_id}</Heading>
|
||||
<AccessibleButton
|
||||
kind='link_inline'
|
||||
kind="link_inline"
|
||||
onClick={() => setIsEditing(true)}
|
||||
className='mx_DeviceDetailHeading_renameCta'
|
||||
data-testid='device-heading-rename-cta'
|
||||
className="mx_DeviceDetailHeading_renameCta"
|
||||
data-testid="device-heading-rename-cta"
|
||||
>
|
||||
{ _t('Rename') }
|
||||
{_t("Rename")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -14,19 +14,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { IPusher } from 'matrix-js-sdk/src/@types/PushRules';
|
||||
import { PUSHER_ENABLED } from 'matrix-js-sdk/src/@types/event';
|
||||
import { LocalNotificationSettings } from 'matrix-js-sdk/src/@types/local_notifications';
|
||||
import React from "react";
|
||||
import { IPusher } from "matrix-js-sdk/src/@types/PushRules";
|
||||
import { PUSHER_ENABLED } from "matrix-js-sdk/src/@types/event";
|
||||
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
|
||||
|
||||
import { formatDate } from '../../../../DateUtils';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import AccessibleButton from '../../elements/AccessibleButton';
|
||||
import Spinner from '../../elements/Spinner';
|
||||
import ToggleSwitch from '../../elements/ToggleSwitch';
|
||||
import { DeviceDetailHeading } from './DeviceDetailHeading';
|
||||
import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard';
|
||||
import { ExtendedDevice } from './types';
|
||||
import { formatDate } from "../../../../DateUtils";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import Spinner from "../../elements/Spinner";
|
||||
import ToggleSwitch from "../../elements/ToggleSwitch";
|
||||
import { DeviceDetailHeading } from "./DeviceDetailHeading";
|
||||
import { DeviceVerificationStatusCard } from "./DeviceVerificationStatusCard";
|
||||
import { ExtendedDevice } from "./types";
|
||||
|
||||
interface Props {
|
||||
device: ExtendedDevice;
|
||||
|
@ -43,7 +43,7 @@ interface Props {
|
|||
interface MetadataTable {
|
||||
id: string;
|
||||
heading?: string;
|
||||
values: { label: string, value?: string | React.ReactNode }[];
|
||||
values: { label: string; value?: string | React.ReactNode }[];
|
||||
}
|
||||
|
||||
const DeviceDetails: React.FC<Props> = ({
|
||||
|
@ -59,40 +59,43 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
}) => {
|
||||
const metadata: MetadataTable[] = [
|
||||
{
|
||||
id: 'session',
|
||||
id: "session",
|
||||
values: [
|
||||
{ label: _t('Session ID'), value: device.device_id },
|
||||
{ label: _t("Session ID"), value: device.device_id },
|
||||
{
|
||||
label: _t('Last activity'),
|
||||
label: _t("Last activity"),
|
||||
value: device.last_seen_ts && formatDate(new Date(device.last_seen_ts)),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'application',
|
||||
heading: _t('Application'),
|
||||
id: "application",
|
||||
heading: _t("Application"),
|
||||
values: [
|
||||
{ label: _t('Name'), value: device.appName },
|
||||
{ label: _t('Version'), value: device.appVersion },
|
||||
{ label: _t('URL'), value: device.url },
|
||||
{ label: _t("Name"), value: device.appName },
|
||||
{ label: _t("Version"), value: device.appVersion },
|
||||
{ label: _t("URL"), value: device.url },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'device',
|
||||
heading: _t('Device'),
|
||||
id: "device",
|
||||
heading: _t("Device"),
|
||||
values: [
|
||||
{ label: _t('Model'), value: device.deviceModel },
|
||||
{ label: _t('Operating system'), value: device.deviceOperatingSystem },
|
||||
{ label: _t('Browser'), value: device.client },
|
||||
{ label: _t('IP address'), value: device.last_seen_ip },
|
||||
{ label: _t("Model"), value: device.deviceModel },
|
||||
{ label: _t("Operating system"), value: device.deviceOperatingSystem },
|
||||
{ label: _t("Browser"), value: device.client },
|
||||
{ label: _t("IP address"), value: device.last_seen_ip },
|
||||
],
|
||||
},
|
||||
].map(section =>
|
||||
// filter out falsy values
|
||||
({ ...section, values: section.values.filter(row => !!row.value) }))
|
||||
.filter(section =>
|
||||
// then filter out sections with no values
|
||||
section.values.length,
|
||||
]
|
||||
.map((section) =>
|
||||
// filter out falsy values
|
||||
({ ...section, values: section.values.filter((row) => !!row.value) }),
|
||||
)
|
||||
.filter(
|
||||
(section) =>
|
||||
// then filter out sections with no values
|
||||
section.values.length,
|
||||
);
|
||||
|
||||
const showPushNotificationSection = !!pusher || !!localNotificationSettings;
|
||||
|
@ -109,75 +112,75 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
return false;
|
||||
}
|
||||
|
||||
return <div className='mx_DeviceDetails' data-testid={`device-detail-${device.device_id}`}>
|
||||
<section className='mx_DeviceDetails_section'>
|
||||
<DeviceDetailHeading
|
||||
device={device}
|
||||
saveDeviceName={saveDeviceName}
|
||||
/>
|
||||
<DeviceVerificationStatusCard
|
||||
device={device}
|
||||
onVerifyDevice={onVerifyDevice}
|
||||
/>
|
||||
</section>
|
||||
<section className='mx_DeviceDetails_section'>
|
||||
<p className='mx_DeviceDetails_sectionHeading'>{ _t('Session details') }</p>
|
||||
{ metadata.map(({ heading, values, id }, index) => <table
|
||||
className='mx_DeviceDetails_metadataTable'
|
||||
key={index}
|
||||
data-testid={`device-detail-metadata-${id}`}
|
||||
>
|
||||
{ heading &&
|
||||
<thead>
|
||||
<tr><th>{ heading }</th></tr>
|
||||
</thead>
|
||||
}
|
||||
<tbody>
|
||||
|
||||
{ values.map(({ label, value }) => <tr key={label}>
|
||||
<td className='mxDeviceDetails_metadataLabel'>{ label }</td>
|
||||
<td className='mxDeviceDetails_metadataValue'>{ value }</td>
|
||||
</tr>) }
|
||||
</tbody>
|
||||
</table>,
|
||||
) }
|
||||
</section>
|
||||
{ showPushNotificationSection && (
|
||||
<section
|
||||
className='mx_DeviceDetails_section mx_DeviceDetails_pushNotifications'
|
||||
data-testid='device-detail-push-notification'
|
||||
>
|
||||
<ToggleSwitch
|
||||
// For backwards compatibility, if `enabled` is missing
|
||||
// default to `true`
|
||||
checked={isPushNotificationsEnabled(pusher, localNotificationSettings)}
|
||||
disabled={isCheckboxDisabled(pusher, localNotificationSettings)}
|
||||
onChange={checked => setPushNotifications?.(device.device_id, checked)}
|
||||
title={_t("Toggle push notifications on this session.")}
|
||||
data-testid='device-detail-push-notification-checkbox'
|
||||
/>
|
||||
<p className='mx_DeviceDetails_sectionHeading'>
|
||||
{ _t('Push notifications') }
|
||||
<small className='mx_DeviceDetails_sectionSubheading'>
|
||||
{ _t('Receive push notifications on this session.') }
|
||||
</small>
|
||||
</p>
|
||||
return (
|
||||
<div className="mx_DeviceDetails" data-testid={`device-detail-${device.device_id}`}>
|
||||
<section className="mx_DeviceDetails_section">
|
||||
<DeviceDetailHeading device={device} saveDeviceName={saveDeviceName} />
|
||||
<DeviceVerificationStatusCard device={device} onVerifyDevice={onVerifyDevice} />
|
||||
</section>
|
||||
) }
|
||||
<section className='mx_DeviceDetails_section'>
|
||||
<AccessibleButton
|
||||
onClick={onSignOutDevice}
|
||||
kind='danger_inline'
|
||||
disabled={isSigningOut}
|
||||
data-testid='device-detail-sign-out-cta'
|
||||
>
|
||||
<span className='mx_DeviceDetails_signOutButtonContent'>
|
||||
{ _t('Sign out of this session') }
|
||||
{ isSigningOut && <Spinner w={16} h={16} /> }
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
</section>
|
||||
</div>;
|
||||
<section className="mx_DeviceDetails_section">
|
||||
<p className="mx_DeviceDetails_sectionHeading">{_t("Session details")}</p>
|
||||
{metadata.map(({ heading, values, id }, index) => (
|
||||
<table
|
||||
className="mx_DeviceDetails_metadataTable"
|
||||
key={index}
|
||||
data-testid={`device-detail-metadata-${id}`}
|
||||
>
|
||||
{heading && (
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{heading}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
)}
|
||||
<tbody>
|
||||
{values.map(({ label, value }) => (
|
||||
<tr key={label}>
|
||||
<td className="mxDeviceDetails_metadataLabel">{label}</td>
|
||||
<td className="mxDeviceDetails_metadataValue">{value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
))}
|
||||
</section>
|
||||
{showPushNotificationSection && (
|
||||
<section
|
||||
className="mx_DeviceDetails_section mx_DeviceDetails_pushNotifications"
|
||||
data-testid="device-detail-push-notification"
|
||||
>
|
||||
<ToggleSwitch
|
||||
// For backwards compatibility, if `enabled` is missing
|
||||
// default to `true`
|
||||
checked={isPushNotificationsEnabled(pusher, localNotificationSettings)}
|
||||
disabled={isCheckboxDisabled(pusher, localNotificationSettings)}
|
||||
onChange={(checked) => setPushNotifications?.(device.device_id, checked)}
|
||||
title={_t("Toggle push notifications on this session.")}
|
||||
data-testid="device-detail-push-notification-checkbox"
|
||||
/>
|
||||
<p className="mx_DeviceDetails_sectionHeading">
|
||||
{_t("Push notifications")}
|
||||
<small className="mx_DeviceDetails_sectionSubheading">
|
||||
{_t("Receive push notifications on this session.")}
|
||||
</small>
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
<section className="mx_DeviceDetails_section">
|
||||
<AccessibleButton
|
||||
onClick={onSignOutDevice}
|
||||
kind="danger_inline"
|
||||
disabled={isSigningOut}
|
||||
data-testid="device-detail-sign-out-cta"
|
||||
>
|
||||
<span className="mx_DeviceDetails_signOutButtonContent">
|
||||
{_t("Sign out of this session")}
|
||||
{isSigningOut && <Spinner w={16} h={16} />}
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeviceDetails;
|
||||
|
|
|
@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { Icon as CaretIcon } from '../../../../../res/img/feather-customised/dropdown-arrow.svg';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';
|
||||
import { Icon as CaretIcon } from "../../../../../res/img/feather-customised/dropdown-arrow.svg";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton";
|
||||
|
||||
interface Props extends React.ComponentProps<typeof AccessibleTooltipButton> {
|
||||
isExpanded: boolean;
|
||||
|
@ -27,17 +27,19 @@ interface Props extends React.ComponentProps<typeof AccessibleTooltipButton> {
|
|||
}
|
||||
|
||||
export const DeviceExpandDetailsButton: React.FC<Props> = ({ isExpanded, onClick, ...rest }) => {
|
||||
const label = isExpanded ? _t('Hide details') : _t('Show details');
|
||||
return <AccessibleTooltipButton
|
||||
{...rest}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
kind='icon'
|
||||
className={classNames('mx_DeviceExpandDetailsButton', {
|
||||
mx_DeviceExpandDetailsButton_expanded: isExpanded,
|
||||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
<CaretIcon className='mx_DeviceExpandDetailsButton_icon' />
|
||||
</AccessibleTooltipButton>;
|
||||
const label = isExpanded ? _t("Hide details") : _t("Show details");
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
{...rest}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
kind="icon"
|
||||
className={classNames("mx_DeviceExpandDetailsButton", {
|
||||
mx_DeviceExpandDetailsButton_expanded: isExpanded,
|
||||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
<CaretIcon className="mx_DeviceExpandDetailsButton_icon" />
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { Icon as VerifiedIcon } from '../../../../../res/img/e2e/verified.svg';
|
||||
import { Icon as UnverifiedIcon } from '../../../../../res/img/e2e/warning.svg';
|
||||
import { Icon as InactiveIcon } from '../../../../../res/img/element-icons/settings/inactive.svg';
|
||||
import { DeviceSecurityVariation } from './types';
|
||||
import { Icon as VerifiedIcon } from "../../../../../res/img/e2e/verified.svg";
|
||||
import { Icon as UnverifiedIcon } from "../../../../../res/img/e2e/warning.svg";
|
||||
import { Icon as InactiveIcon } from "../../../../../res/img/element-icons/settings/inactive.svg";
|
||||
import { DeviceSecurityVariation } from "./types";
|
||||
interface Props {
|
||||
variation: DeviceSecurityVariation;
|
||||
heading: string;
|
||||
|
@ -37,22 +37,24 @@ const VariationIcon: Record<DeviceSecurityVariation, React.FC<React.SVGProps<SVG
|
|||
|
||||
const DeviceSecurityIcon: React.FC<{ variation: DeviceSecurityVariation }> = ({ variation }) => {
|
||||
const Icon = VariationIcon[variation];
|
||||
return <div className={classNames('mx_DeviceSecurityCard_icon', variation)}>
|
||||
<Icon height={16} width={16} />
|
||||
</div>;
|
||||
return (
|
||||
<div className={classNames("mx_DeviceSecurityCard_icon", variation)}>
|
||||
<Icon height={16} width={16} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DeviceSecurityCard: React.FC<Props> = ({ variation, heading, description, children }) => {
|
||||
return <div className='mx_DeviceSecurityCard'>
|
||||
<DeviceSecurityIcon variation={variation} />
|
||||
<div className='mx_DeviceSecurityCard_content'>
|
||||
<p className='mx_DeviceSecurityCard_heading'>{ heading }</p>
|
||||
<p className='mx_DeviceSecurityCard_description'>{ description }</p>
|
||||
{ !!children && <div className='mx_DeviceSecurityCard_actions'>
|
||||
{ children }
|
||||
</div> }
|
||||
return (
|
||||
<div className="mx_DeviceSecurityCard">
|
||||
<DeviceSecurityIcon variation={variation} />
|
||||
<div className="mx_DeviceSecurityCard_content">
|
||||
<p className="mx_DeviceSecurityCard_heading">{heading}</p>
|
||||
<p className="mx_DeviceSecurityCard_description">{description}</p>
|
||||
{!!children && <div className="mx_DeviceSecurityCard_actions">{children}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
};
|
||||
|
||||
export default DeviceSecurityCard;
|
||||
|
|
|
@ -14,83 +14,98 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import LearnMore, { LearnMoreProps } from "../../elements/LearnMore";
|
||||
import { DeviceSecurityVariation } from "./types";
|
||||
|
||||
interface Props extends Omit<LearnMoreProps, 'title' | 'description'> {
|
||||
interface Props extends Omit<LearnMoreProps, "title" | "description"> {
|
||||
variation: DeviceSecurityVariation;
|
||||
}
|
||||
|
||||
const securityCardContent: Record<DeviceSecurityVariation, {
|
||||
title: string;
|
||||
description: React.ReactNode | string;
|
||||
}> = {
|
||||
[DeviceSecurityVariation.Verified]: {
|
||||
title: _t('Verified sessions'),
|
||||
description: <>
|
||||
<p>{ _t('Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.') }
|
||||
</p>
|
||||
<p>
|
||||
{ _t(
|
||||
`This means that you have all the keys needed to unlock your encrypted messages ` +
|
||||
`and confirm to other users that you trust this session.`,
|
||||
)
|
||||
}
|
||||
</p>
|
||||
</>,
|
||||
},
|
||||
[DeviceSecurityVariation.Unverified]: {
|
||||
title: _t('Unverified sessions'),
|
||||
description: <>
|
||||
<p>{ _t('Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.') }
|
||||
</p>
|
||||
<p>
|
||||
{ _t(
|
||||
`You should make especially certain that you recognise these sessions ` +
|
||||
`as they could represent an unauthorised use of your account.`,
|
||||
)
|
||||
}
|
||||
</p>
|
||||
</>,
|
||||
},
|
||||
// unverifiable uses single-session case
|
||||
// because it is only ever displayed on a single session detail
|
||||
[DeviceSecurityVariation.Unverifiable]: {
|
||||
title: _t('Unverified session'),
|
||||
description: <>
|
||||
<p>{ _t(`This session doesn't support encryption, so it can't be verified.`) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t(
|
||||
`You won't be able to participate in rooms where encryption is enabled when using this session.`,
|
||||
)
|
||||
}
|
||||
</p><p>
|
||||
{ _t(
|
||||
`For best security and privacy, it is recommended to use Matrix clients that support encryption.`,
|
||||
)
|
||||
}
|
||||
</p>
|
||||
</>,
|
||||
},
|
||||
[DeviceSecurityVariation.Inactive]: {
|
||||
title: _t('Inactive sessions'),
|
||||
description: <>
|
||||
<p>{ _t('Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.') }
|
||||
</p>
|
||||
<p>
|
||||
{ _t(
|
||||
`Removing inactive sessions improves security and performance, ` +
|
||||
`and makes it easier for you to identify if a new session is suspicious.`,
|
||||
)
|
||||
}
|
||||
</p>
|
||||
</>,
|
||||
},
|
||||
};
|
||||
const securityCardContent: Record<
|
||||
DeviceSecurityVariation,
|
||||
{
|
||||
title: string;
|
||||
description: React.ReactNode | string;
|
||||
}
|
||||
> = {
|
||||
[DeviceSecurityVariation.Verified]: {
|
||||
title: _t("Verified sessions"),
|
||||
description: (
|
||||
<>
|
||||
<p>
|
||||
{_t(
|
||||
"Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
`This means that you have all the keys needed to unlock your encrypted messages ` +
|
||||
`and confirm to other users that you trust this session.`,
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
[DeviceSecurityVariation.Unverified]: {
|
||||
title: _t("Unverified sessions"),
|
||||
description: (
|
||||
<>
|
||||
<p>
|
||||
{_t(
|
||||
"Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
`You should make especially certain that you recognise these sessions ` +
|
||||
`as they could represent an unauthorised use of your account.`,
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
// unverifiable uses single-session case
|
||||
// because it is only ever displayed on a single session detail
|
||||
[DeviceSecurityVariation.Unverifiable]: {
|
||||
title: _t("Unverified session"),
|
||||
description: (
|
||||
<>
|
||||
<p>{_t(`This session doesn't support encryption, so it can't be verified.`)}</p>
|
||||
<p>
|
||||
{_t(
|
||||
`You won't be able to participate in rooms where encryption is enabled when using this session.`,
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
`For best security and privacy, it is recommended to use Matrix clients that support encryption.`,
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
[DeviceSecurityVariation.Inactive]: {
|
||||
title: _t("Inactive sessions"),
|
||||
description: (
|
||||
<>
|
||||
<p>
|
||||
{_t(
|
||||
"Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
`Removing inactive sessions improves security and performance, ` +
|
||||
`and makes it easier for you to identify if a new session is suspicious.`,
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* LearnMore with content for device security warnings
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React, { Fragment } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { Icon as InactiveIcon } from '../../../../../res/img/element-icons/settings/inactive.svg';
|
||||
import { Icon as InactiveIcon } from "../../../../../res/img/element-icons/settings/inactive.svg";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { formatDate, formatRelativeTime } from "../../../../DateUtils";
|
||||
import Heading from "../../typography/Heading";
|
||||
|
@ -33,9 +33,7 @@ export interface DeviceTileProps {
|
|||
}
|
||||
|
||||
const DeviceTileName: React.FC<{ device: ExtendedDevice }> = ({ device }) => {
|
||||
return <Heading size='h4'>
|
||||
{ device.display_name || device.device_id }
|
||||
</Heading>;
|
||||
return <Heading size="h4">{device.display_name || device.device_id}</Heading>;
|
||||
};
|
||||
|
||||
const MS_DAY = 24 * 60 * 60 * 1000;
|
||||
|
@ -50,76 +48,66 @@ const formatLastActivity = (timestamp: number, now = new Date().getTime()): stri
|
|||
return formatRelativeTime(new Date(timestamp));
|
||||
};
|
||||
|
||||
const getInactiveMetadata = (device: ExtendedDevice): { id: string, value: React.ReactNode } | undefined => {
|
||||
const getInactiveMetadata = (device: ExtendedDevice): { id: string; value: React.ReactNode } | undefined => {
|
||||
const isInactive = isDeviceInactive(device);
|
||||
|
||||
if (!isInactive) {
|
||||
return undefined;
|
||||
}
|
||||
return { id: 'inactive', value: (
|
||||
<>
|
||||
<InactiveIcon className="mx_DeviceTile_inactiveIcon" />
|
||||
{
|
||||
_t('Inactive for %(inactiveAgeDays)s+ days', { inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS }) +
|
||||
` (${formatLastActivity(device.last_seen_ts)})`
|
||||
}
|
||||
</>),
|
||||
return {
|
||||
id: "inactive",
|
||||
value: (
|
||||
<>
|
||||
<InactiveIcon className="mx_DeviceTile_inactiveIcon" />
|
||||
{_t("Inactive for %(inactiveAgeDays)s+ days", { inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS }) +
|
||||
` (${formatLastActivity(device.last_seen_ts)})`}
|
||||
</>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const DeviceMetadata: React.FC<{ value: string | React.ReactNode, id: string }> = ({ value, id }) => (
|
||||
value ? <span data-testid={`device-metadata-${id}`}>{ value }</span> : null
|
||||
);
|
||||
const DeviceMetadata: React.FC<{ value: string | React.ReactNode; id: string }> = ({ value, id }) =>
|
||||
value ? <span data-testid={`device-metadata-${id}`}>{value}</span> : null;
|
||||
|
||||
const DeviceTile: React.FC<DeviceTileProps> = ({
|
||||
device,
|
||||
children,
|
||||
isSelected,
|
||||
onClick,
|
||||
}) => {
|
||||
const DeviceTile: React.FC<DeviceTileProps> = ({ device, children, isSelected, onClick }) => {
|
||||
const inactive = getInactiveMetadata(device);
|
||||
const lastActivity = device.last_seen_ts && `${_t('Last activity')} ${formatLastActivity(device.last_seen_ts)}`;
|
||||
const verificationStatus = device.isVerified ? _t('Verified') : _t('Unverified');
|
||||
const lastActivity = device.last_seen_ts && `${_t("Last activity")} ${formatLastActivity(device.last_seen_ts)}`;
|
||||
const verificationStatus = device.isVerified ? _t("Verified") : _t("Unverified");
|
||||
// if device is inactive, don't display last activity or verificationStatus
|
||||
const metadata = inactive
|
||||
? [inactive, { id: 'lastSeenIp', value: device.last_seen_ip }]
|
||||
? [inactive, { id: "lastSeenIp", value: device.last_seen_ip }]
|
||||
: [
|
||||
{ id: 'isVerified', value: verificationStatus },
|
||||
{ id: 'lastActivity', value: lastActivity },
|
||||
{ id: 'lastSeenIp', value: device.last_seen_ip },
|
||||
{ id: 'deviceId', value: device.device_id },
|
||||
];
|
||||
{ id: "isVerified", value: verificationStatus },
|
||||
{ id: "lastActivity", value: lastActivity },
|
||||
{ id: "lastSeenIp", value: device.last_seen_ip },
|
||||
{ id: "deviceId", value: device.device_id },
|
||||
];
|
||||
|
||||
return <div
|
||||
className={classNames(
|
||||
"mx_DeviceTile",
|
||||
{ "mx_DeviceTile_interactive": !!onClick },
|
||||
)}
|
||||
data-testid={`device-tile-${device.device_id}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<DeviceTypeIcon
|
||||
isVerified={device.isVerified}
|
||||
isSelected={isSelected}
|
||||
deviceType={device.deviceType}
|
||||
/>
|
||||
<div className="mx_DeviceTile_info">
|
||||
<DeviceTileName device={device} />
|
||||
<div className="mx_DeviceTile_metadata">
|
||||
{ metadata.map(({ id, value }, index) =>
|
||||
!!value
|
||||
? <Fragment key={id}>
|
||||
{ !!index && ' · ' }
|
||||
<DeviceMetadata id={id} value={value} />
|
||||
</Fragment>
|
||||
: null,
|
||||
) }
|
||||
return (
|
||||
<div
|
||||
className={classNames("mx_DeviceTile", { mx_DeviceTile_interactive: !!onClick })}
|
||||
data-testid={`device-tile-${device.device_id}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<DeviceTypeIcon isVerified={device.isVerified} isSelected={isSelected} deviceType={device.deviceType} />
|
||||
<div className="mx_DeviceTile_info">
|
||||
<DeviceTileName device={device} />
|
||||
<div className="mx_DeviceTile_metadata">
|
||||
{metadata.map(({ id, value }, index) =>
|
||||
!!value ? (
|
||||
<Fragment key={id}>
|
||||
{!!index && " · "}
|
||||
<DeviceMetadata id={id} value={value} />
|
||||
</Fragment>
|
||||
) : null,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_DeviceTile_actions" onClick={preventDefaultWrapper(() => {})}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_DeviceTile_actions" onClick={preventDefaultWrapper(() => {})}>
|
||||
{ children }
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
};
|
||||
|
||||
export default DeviceTile;
|
||||
|
|
|
@ -14,21 +14,21 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { Icon as UnknownDeviceIcon } from '../../../../../res/img/element-icons/settings/unknown-device.svg';
|
||||
import { Icon as DesktopIcon } from '../../../../../res/img/element-icons/settings/desktop.svg';
|
||||
import { Icon as WebIcon } from '../../../../../res/img/element-icons/settings/web.svg';
|
||||
import { Icon as MobileIcon } from '../../../../../res/img/element-icons/settings/mobile.svg';
|
||||
import { Icon as VerifiedIcon } from '../../../../../res/img/e2e/verified.svg';
|
||||
import { Icon as UnverifiedIcon } from '../../../../../res/img/e2e/warning.svg';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { ExtendedDevice } from './types';
|
||||
import { DeviceType } from '../../../../utils/device/parseUserAgent';
|
||||
import { Icon as UnknownDeviceIcon } from "../../../../../res/img/element-icons/settings/unknown-device.svg";
|
||||
import { Icon as DesktopIcon } from "../../../../../res/img/element-icons/settings/desktop.svg";
|
||||
import { Icon as WebIcon } from "../../../../../res/img/element-icons/settings/web.svg";
|
||||
import { Icon as MobileIcon } from "../../../../../res/img/element-icons/settings/mobile.svg";
|
||||
import { Icon as VerifiedIcon } from "../../../../../res/img/e2e/verified.svg";
|
||||
import { Icon as UnverifiedIcon } from "../../../../../res/img/e2e/warning.svg";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { ExtendedDevice } from "./types";
|
||||
import { DeviceType } from "../../../../utils/device/parseUserAgent";
|
||||
|
||||
interface Props {
|
||||
isVerified?: ExtendedDevice['isVerified'];
|
||||
isVerified?: ExtendedDevice["isVerified"];
|
||||
isSelected?: boolean;
|
||||
deviceType?: DeviceType;
|
||||
}
|
||||
|
@ -40,44 +40,37 @@ const deviceTypeIcon: Record<DeviceType, React.FC<React.SVGProps<SVGSVGElement>>
|
|||
[DeviceType.Unknown]: UnknownDeviceIcon,
|
||||
};
|
||||
const deviceTypeLabel: Record<DeviceType, string> = {
|
||||
[DeviceType.Desktop]: _t('Desktop session'),
|
||||
[DeviceType.Mobile]: _t('Mobile session'),
|
||||
[DeviceType.Web]: _t('Web session'),
|
||||
[DeviceType.Unknown]: _t('Unknown session type'),
|
||||
[DeviceType.Desktop]: _t("Desktop session"),
|
||||
[DeviceType.Mobile]: _t("Mobile session"),
|
||||
[DeviceType.Web]: _t("Web session"),
|
||||
[DeviceType.Unknown]: _t("Unknown session type"),
|
||||
};
|
||||
|
||||
export const DeviceTypeIcon: React.FC<Props> = ({
|
||||
isVerified,
|
||||
isSelected,
|
||||
deviceType,
|
||||
}) => {
|
||||
export const DeviceTypeIcon: React.FC<Props> = ({ isVerified, isSelected, deviceType }) => {
|
||||
const Icon = deviceTypeIcon[deviceType] || deviceTypeIcon[DeviceType.Unknown];
|
||||
const label = deviceTypeLabel[deviceType] || deviceTypeLabel[DeviceType.Unknown];
|
||||
return (
|
||||
<div className={classNames('mx_DeviceTypeIcon', {
|
||||
mx_DeviceTypeIcon_selected: isSelected,
|
||||
})}
|
||||
<div
|
||||
className={classNames("mx_DeviceTypeIcon", {
|
||||
mx_DeviceTypeIcon_selected: isSelected,
|
||||
})}
|
||||
>
|
||||
<div className='mx_DeviceTypeIcon_deviceIconWrapper'>
|
||||
<Icon
|
||||
className='mx_DeviceTypeIcon_deviceIcon'
|
||||
role='img'
|
||||
aria-label={label}
|
||||
/>
|
||||
<div className="mx_DeviceTypeIcon_deviceIconWrapper">
|
||||
<Icon className="mx_DeviceTypeIcon_deviceIcon" role="img" aria-label={label} />
|
||||
</div>
|
||||
{
|
||||
isVerified
|
||||
? <VerifiedIcon
|
||||
className={classNames('mx_DeviceTypeIcon_verificationIcon', 'verified')}
|
||||
role='img'
|
||||
aria-label={_t('Verified')}
|
||||
/>
|
||||
: <UnverifiedIcon
|
||||
className={classNames('mx_DeviceTypeIcon_verificationIcon', 'unverified')}
|
||||
role='img'
|
||||
aria-label={_t('Unverified')}
|
||||
/>
|
||||
}
|
||||
</div>);
|
||||
{isVerified ? (
|
||||
<VerifiedIcon
|
||||
className={classNames("mx_DeviceTypeIcon_verificationIcon", "verified")}
|
||||
role="img"
|
||||
aria-label={_t("Verified")}
|
||||
/>
|
||||
) : (
|
||||
<UnverifiedIcon
|
||||
className={classNames("mx_DeviceTypeIcon_verificationIcon", "unverified")}
|
||||
role="img"
|
||||
aria-label={_t("Unverified")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -14,23 +14,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import AccessibleButton from '../../elements/AccessibleButton';
|
||||
import DeviceSecurityCard from './DeviceSecurityCard';
|
||||
import { DeviceSecurityLearnMore } from './DeviceSecurityLearnMore';
|
||||
import {
|
||||
DeviceSecurityVariation,
|
||||
ExtendedDevice,
|
||||
} from './types';
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import DeviceSecurityCard from "./DeviceSecurityCard";
|
||||
import { DeviceSecurityLearnMore } from "./DeviceSecurityLearnMore";
|
||||
import { DeviceSecurityVariation, ExtendedDevice } from "./types";
|
||||
|
||||
interface Props {
|
||||
device: ExtendedDevice;
|
||||
onVerifyDevice?: () => void;
|
||||
}
|
||||
|
||||
const getCardProps = (device: ExtendedDevice): {
|
||||
const getCardProps = (
|
||||
device: ExtendedDevice,
|
||||
): {
|
||||
variation: DeviceSecurityVariation;
|
||||
heading: string;
|
||||
description: React.ReactNode;
|
||||
|
@ -38,52 +37,55 @@ const getCardProps = (device: ExtendedDevice): {
|
|||
if (device.isVerified) {
|
||||
return {
|
||||
variation: DeviceSecurityVariation.Verified,
|
||||
heading: _t('Verified session'),
|
||||
description: <>
|
||||
{ _t('This session is ready for secure messaging.') }
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Verified} />
|
||||
</>,
|
||||
heading: _t("Verified session"),
|
||||
description: (
|
||||
<>
|
||||
{_t("This session is ready for secure messaging.")}
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Verified} />
|
||||
</>
|
||||
),
|
||||
};
|
||||
}
|
||||
if (device.isVerified === null) {
|
||||
return {
|
||||
variation: DeviceSecurityVariation.Unverified,
|
||||
heading: _t('Unverified session'),
|
||||
description: <>
|
||||
{ _t(`This session doesn't support encryption and thus can't be verified.`) }
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverifiable} />
|
||||
</>,
|
||||
heading: _t("Unverified session"),
|
||||
description: (
|
||||
<>
|
||||
{_t(`This session doesn't support encryption and thus can't be verified.`)}
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverifiable} />
|
||||
</>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
variation: DeviceSecurityVariation.Unverified,
|
||||
heading: _t('Unverified session'),
|
||||
description: <>
|
||||
{ _t('Verify or sign out from this session for best security and reliability.') }
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
|
||||
</>,
|
||||
heading: _t("Unverified session"),
|
||||
description: (
|
||||
<>
|
||||
{_t("Verify or sign out from this session for best security and reliability.")}
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
|
||||
</>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const DeviceVerificationStatusCard: React.FC<Props> = ({
|
||||
device,
|
||||
onVerifyDevice,
|
||||
}) => {
|
||||
export const DeviceVerificationStatusCard: React.FC<Props> = ({ device, onVerifyDevice }) => {
|
||||
const securityCardProps = getCardProps(device);
|
||||
|
||||
return <DeviceSecurityCard
|
||||
{...securityCardProps}
|
||||
>
|
||||
{ /* check for explicit false to exclude unverifiable devices */ }
|
||||
{ device.isVerified === false && !!onVerifyDevice &&
|
||||
<AccessibleButton
|
||||
kind='primary'
|
||||
onClick={onVerifyDevice}
|
||||
data-testid={`verification-status-button-${device.device_id}`}
|
||||
>
|
||||
{ _t('Verify session') }
|
||||
</AccessibleButton>
|
||||
}
|
||||
</DeviceSecurityCard>;
|
||||
return (
|
||||
<DeviceSecurityCard {...securityCardProps}>
|
||||
{/* check for explicit false to exclude unverifiable devices */}
|
||||
{device.isVerified === false && !!onVerifyDevice && (
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={onVerifyDevice}
|
||||
data-testid={`verification-status-button-${device.device_id}`}
|
||||
>
|
||||
{_t("Verify session")}
|
||||
</AccessibleButton>
|
||||
)}
|
||||
</DeviceSecurityCard>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -14,122 +14,117 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ForwardedRef, forwardRef } from 'react';
|
||||
import { IPusher } from 'matrix-js-sdk/src/@types/PushRules';
|
||||
import { PUSHER_DEVICE_ID } from 'matrix-js-sdk/src/@types/event';
|
||||
import { LocalNotificationSettings } from 'matrix-js-sdk/src/@types/local_notifications';
|
||||
import React, { ForwardedRef, forwardRef } from "react";
|
||||
import { IPusher } from "matrix-js-sdk/src/@types/PushRules";
|
||||
import { PUSHER_DEVICE_ID } from "matrix-js-sdk/src/@types/event";
|
||||
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
|
||||
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import AccessibleButton from '../../elements/AccessibleButton';
|
||||
import { FilterDropdown, FilterDropdownOption } from '../../elements/FilterDropdown';
|
||||
import DeviceDetails from './DeviceDetails';
|
||||
import { DeviceExpandDetailsButton } from './DeviceExpandDetailsButton';
|
||||
import DeviceSecurityCard from './DeviceSecurityCard';
|
||||
import {
|
||||
filterDevicesBySecurityRecommendation,
|
||||
FilterVariation,
|
||||
INACTIVE_DEVICE_AGE_DAYS,
|
||||
} from './filter';
|
||||
import SelectableDeviceTile from './SelectableDeviceTile';
|
||||
import {
|
||||
DevicesDictionary,
|
||||
DeviceSecurityVariation,
|
||||
ExtendedDevice,
|
||||
} from './types';
|
||||
import { DevicesState } from './useOwnDevices';
|
||||
import FilteredDeviceListHeader from './FilteredDeviceListHeader';
|
||||
import Spinner from '../../elements/Spinner';
|
||||
import { DeviceSecurityLearnMore } from './DeviceSecurityLearnMore';
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import { FilterDropdown, FilterDropdownOption } from "../../elements/FilterDropdown";
|
||||
import DeviceDetails from "./DeviceDetails";
|
||||
import { DeviceExpandDetailsButton } from "./DeviceExpandDetailsButton";
|
||||
import DeviceSecurityCard from "./DeviceSecurityCard";
|
||||
import { filterDevicesBySecurityRecommendation, FilterVariation, INACTIVE_DEVICE_AGE_DAYS } from "./filter";
|
||||
import SelectableDeviceTile from "./SelectableDeviceTile";
|
||||
import { DevicesDictionary, DeviceSecurityVariation, ExtendedDevice } from "./types";
|
||||
import { DevicesState } from "./useOwnDevices";
|
||||
import FilteredDeviceListHeader from "./FilteredDeviceListHeader";
|
||||
import Spinner from "../../elements/Spinner";
|
||||
import { DeviceSecurityLearnMore } from "./DeviceSecurityLearnMore";
|
||||
|
||||
interface Props {
|
||||
devices: DevicesDictionary;
|
||||
pushers: IPusher[];
|
||||
localNotificationSettings: Map<string, LocalNotificationSettings>;
|
||||
expandedDeviceIds: ExtendedDevice['device_id'][];
|
||||
signingOutDeviceIds: ExtendedDevice['device_id'][];
|
||||
selectedDeviceIds: ExtendedDevice['device_id'][];
|
||||
expandedDeviceIds: ExtendedDevice["device_id"][];
|
||||
signingOutDeviceIds: ExtendedDevice["device_id"][];
|
||||
selectedDeviceIds: ExtendedDevice["device_id"][];
|
||||
filter?: FilterVariation;
|
||||
onFilterChange: (filter: FilterVariation | undefined) => void;
|
||||
onDeviceExpandToggle: (deviceId: ExtendedDevice['device_id']) => void;
|
||||
onSignOutDevices: (deviceIds: ExtendedDevice['device_id'][]) => void;
|
||||
saveDeviceName: DevicesState['saveDeviceName'];
|
||||
onRequestDeviceVerification?: (deviceId: ExtendedDevice['device_id']) => void;
|
||||
onDeviceExpandToggle: (deviceId: ExtendedDevice["device_id"]) => void;
|
||||
onSignOutDevices: (deviceIds: ExtendedDevice["device_id"][]) => void;
|
||||
saveDeviceName: DevicesState["saveDeviceName"];
|
||||
onRequestDeviceVerification?: (deviceId: ExtendedDevice["device_id"]) => void;
|
||||
setPushNotifications: (deviceId: string, enabled: boolean) => Promise<void>;
|
||||
setSelectedDeviceIds: (deviceIds: ExtendedDevice['device_id'][]) => void;
|
||||
setSelectedDeviceIds: (deviceIds: ExtendedDevice["device_id"][]) => void;
|
||||
supportsMSC3881?: boolean | undefined;
|
||||
}
|
||||
|
||||
const isDeviceSelected = (
|
||||
deviceId: ExtendedDevice['device_id'],
|
||||
selectedDeviceIds: ExtendedDevice['device_id'][],
|
||||
) => selectedDeviceIds.includes(deviceId);
|
||||
const isDeviceSelected = (deviceId: ExtendedDevice["device_id"], selectedDeviceIds: ExtendedDevice["device_id"][]) =>
|
||||
selectedDeviceIds.includes(deviceId);
|
||||
|
||||
// devices without timestamp metadata should be sorted last
|
||||
const sortDevicesByLatestActivityThenDisplayName = (left: ExtendedDevice, right: ExtendedDevice) =>
|
||||
(right.last_seen_ts || 0) - (left.last_seen_ts || 0)
|
||||
|| ((left.display_name || left.device_id).localeCompare(right.display_name || right.device_id));
|
||||
(right.last_seen_ts || 0) - (left.last_seen_ts || 0) ||
|
||||
(left.display_name || left.device_id).localeCompare(right.display_name || right.device_id);
|
||||
|
||||
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: FilterVariation) =>
|
||||
filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : [])
|
||||
.sort(sortDevicesByLatestActivityThenDisplayName);
|
||||
filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : []).sort(
|
||||
sortDevicesByLatestActivityThenDisplayName,
|
||||
);
|
||||
|
||||
const ALL_FILTER_ID = 'ALL';
|
||||
const ALL_FILTER_ID = "ALL";
|
||||
type DeviceFilterKey = FilterVariation | typeof ALL_FILTER_ID;
|
||||
|
||||
const securityCardContent: Record<DeviceSecurityVariation, {
|
||||
title: string;
|
||||
description: string;
|
||||
}> = {
|
||||
[DeviceSecurityVariation.Verified]: {
|
||||
title: _t('Verified sessions'),
|
||||
description: _t('For best security, sign out from any session that you don\'t recognize or use anymore.'),
|
||||
},
|
||||
[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.`,
|
||||
),
|
||||
},
|
||||
[DeviceSecurityVariation.Unverifiable]: {
|
||||
title: _t('Unverified session'),
|
||||
description: _t(
|
||||
`This session doesn't support encryption and thus can't be verified.`,
|
||||
),
|
||||
},
|
||||
[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 },
|
||||
),
|
||||
},
|
||||
};
|
||||
const securityCardContent: Record<
|
||||
DeviceSecurityVariation,
|
||||
{
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
> = {
|
||||
[DeviceSecurityVariation.Verified]: {
|
||||
title: _t("Verified sessions"),
|
||||
description: _t("For best security, sign out from any session that you don't recognize or use anymore."),
|
||||
},
|
||||
[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.`,
|
||||
),
|
||||
},
|
||||
[DeviceSecurityVariation.Unverifiable]: {
|
||||
title: _t("Unverified session"),
|
||||
description: _t(`This session doesn't support encryption and thus can't be verified.`),
|
||||
},
|
||||
[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 },
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
const isSecurityVariation = (filter?: DeviceFilterKey): filter is FilterVariation =>
|
||||
!!filter && ([
|
||||
DeviceSecurityVariation.Inactive,
|
||||
DeviceSecurityVariation.Unverified,
|
||||
DeviceSecurityVariation.Verified,
|
||||
] as string[]).includes(filter);
|
||||
!!filter &&
|
||||
(
|
||||
[
|
||||
DeviceSecurityVariation.Inactive,
|
||||
DeviceSecurityVariation.Unverified,
|
||||
DeviceSecurityVariation.Verified,
|
||||
] as string[]
|
||||
).includes(filter);
|
||||
|
||||
const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter }) => {
|
||||
if (isSecurityVariation(filter)) {
|
||||
const { title, description } = securityCardContent[filter];
|
||||
return <div className='mx_FilteredDeviceList_securityCard'>
|
||||
<DeviceSecurityCard
|
||||
variation={filter}
|
||||
heading={title}
|
||||
description={<span>
|
||||
{ description }
|
||||
<DeviceSecurityLearnMore
|
||||
variation={filter}
|
||||
/>
|
||||
</span>}
|
||||
/>
|
||||
</div>
|
||||
;
|
||||
return (
|
||||
<div className="mx_FilteredDeviceList_securityCard">
|
||||
<DeviceSecurityCard
|
||||
variation={filter}
|
||||
heading={title}
|
||||
description={
|
||||
<span>
|
||||
{description}
|
||||
<DeviceSecurityLearnMore variation={filter} />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -138,34 +133,35 @@ const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter })
|
|||
const getNoResultsMessage = (filter?: FilterVariation): string => {
|
||||
switch (filter) {
|
||||
case DeviceSecurityVariation.Verified:
|
||||
return _t('No verified sessions found.');
|
||||
return _t("No verified sessions found.");
|
||||
case DeviceSecurityVariation.Unverified:
|
||||
return _t('No unverified sessions found.');
|
||||
return _t("No unverified sessions found.");
|
||||
case DeviceSecurityVariation.Inactive:
|
||||
return _t('No inactive sessions found.');
|
||||
return _t("No inactive sessions found.");
|
||||
default:
|
||||
return _t('No sessions found.');
|
||||
return _t("No sessions found.");
|
||||
}
|
||||
};
|
||||
interface NoResultsProps { filter?: FilterVariation, clearFilter: () => void}
|
||||
const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) =>
|
||||
<div className='mx_FilteredDeviceList_noResults'>
|
||||
{ getNoResultsMessage(filter) }
|
||||
interface NoResultsProps {
|
||||
filter?: FilterVariation;
|
||||
clearFilter: () => void;
|
||||
}
|
||||
const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) => (
|
||||
<div className="mx_FilteredDeviceList_noResults">
|
||||
{getNoResultsMessage(filter)}
|
||||
{
|
||||
/* No clear filter button when filter is falsy (ie 'All') */
|
||||
!!filter &&
|
||||
<>
|
||||
|
||||
<AccessibleButton
|
||||
kind='link_inline'
|
||||
onClick={clearFilter}
|
||||
data-testid='devices-clear-filter-btn'
|
||||
>
|
||||
{ _t('Show all') }
|
||||
</AccessibleButton>
|
||||
</>
|
||||
!!filter && (
|
||||
<>
|
||||
|
||||
<AccessibleButton kind="link_inline" onClick={clearFilter} data-testid="devices-clear-filter-btn">
|
||||
{_t("Show all")}
|
||||
</AccessibleButton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>;
|
||||
</div>
|
||||
);
|
||||
|
||||
const DeviceListItem: React.FC<{
|
||||
device: ExtendedDevice;
|
||||
|
@ -195,96 +191,96 @@ const DeviceListItem: React.FC<{
|
|||
setPushNotifications,
|
||||
toggleSelected,
|
||||
supportsMSC3881,
|
||||
}) => <li className='mx_FilteredDeviceList_listItem'>
|
||||
<SelectableDeviceTile
|
||||
isSelected={isSelected}
|
||||
onSelect={toggleSelected}
|
||||
onClick={onDeviceExpandToggle}
|
||||
device={device}
|
||||
>
|
||||
{ isSigningOut && <Spinner w={16} h={16} /> }
|
||||
<DeviceExpandDetailsButton
|
||||
isExpanded={isExpanded}
|
||||
}) => (
|
||||
<li className="mx_FilteredDeviceList_listItem">
|
||||
<SelectableDeviceTile
|
||||
isSelected={isSelected}
|
||||
onSelect={toggleSelected}
|
||||
onClick={onDeviceExpandToggle}
|
||||
/>
|
||||
</SelectableDeviceTile>
|
||||
{
|
||||
isExpanded &&
|
||||
<DeviceDetails
|
||||
device={device}
|
||||
pusher={pusher}
|
||||
localNotificationSettings={localNotificationSettings}
|
||||
isSigningOut={isSigningOut}
|
||||
onVerifyDevice={onRequestDeviceVerification}
|
||||
onSignOutDevice={onSignOutDevice}
|
||||
saveDeviceName={saveDeviceName}
|
||||
setPushNotifications={setPushNotifications}
|
||||
supportsMSC3881={supportsMSC3881}
|
||||
/>
|
||||
}
|
||||
</li>;
|
||||
>
|
||||
{isSigningOut && <Spinner w={16} h={16} />}
|
||||
<DeviceExpandDetailsButton isExpanded={isExpanded} onClick={onDeviceExpandToggle} />
|
||||
</SelectableDeviceTile>
|
||||
{isExpanded && (
|
||||
<DeviceDetails
|
||||
device={device}
|
||||
pusher={pusher}
|
||||
localNotificationSettings={localNotificationSettings}
|
||||
isSigningOut={isSigningOut}
|
||||
onVerifyDevice={onRequestDeviceVerification}
|
||||
onSignOutDevice={onSignOutDevice}
|
||||
saveDeviceName={saveDeviceName}
|
||||
setPushNotifications={setPushNotifications}
|
||||
supportsMSC3881={supportsMSC3881}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
|
||||
/**
|
||||
* Filtered list of devices
|
||||
* Sorted by latest activity descending
|
||||
*/
|
||||
export const FilteredDeviceList =
|
||||
forwardRef(({
|
||||
devices,
|
||||
pushers,
|
||||
localNotificationSettings,
|
||||
filter,
|
||||
expandedDeviceIds,
|
||||
signingOutDeviceIds,
|
||||
selectedDeviceIds,
|
||||
onFilterChange,
|
||||
onDeviceExpandToggle,
|
||||
saveDeviceName,
|
||||
onSignOutDevices,
|
||||
onRequestDeviceVerification,
|
||||
setPushNotifications,
|
||||
setSelectedDeviceIds,
|
||||
supportsMSC3881,
|
||||
}: Props, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
export const FilteredDeviceList = forwardRef(
|
||||
(
|
||||
{
|
||||
devices,
|
||||
pushers,
|
||||
localNotificationSettings,
|
||||
filter,
|
||||
expandedDeviceIds,
|
||||
signingOutDeviceIds,
|
||||
selectedDeviceIds,
|
||||
onFilterChange,
|
||||
onDeviceExpandToggle,
|
||||
saveDeviceName,
|
||||
onSignOutDevices,
|
||||
onRequestDeviceVerification,
|
||||
setPushNotifications,
|
||||
setSelectedDeviceIds,
|
||||
supportsMSC3881,
|
||||
}: Props,
|
||||
ref: ForwardedRef<HTMLDivElement>,
|
||||
) => {
|
||||
const sortedDevices = getFilteredSortedDevices(devices, filter);
|
||||
|
||||
function getPusherForDevice(device: ExtendedDevice): IPusher | undefined {
|
||||
return pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === device.device_id);
|
||||
return pushers.find((pusher) => pusher[PUSHER_DEVICE_ID.name] === device.device_id);
|
||||
}
|
||||
|
||||
const toggleSelection = (deviceId: ExtendedDevice['device_id']): void => {
|
||||
const toggleSelection = (deviceId: ExtendedDevice["device_id"]): void => {
|
||||
if (isDeviceSelected(deviceId, selectedDeviceIds)) {
|
||||
// remove from selection
|
||||
setSelectedDeviceIds(selectedDeviceIds.filter(id => id !== deviceId));
|
||||
setSelectedDeviceIds(selectedDeviceIds.filter((id) => id !== deviceId));
|
||||
} else {
|
||||
setSelectedDeviceIds([...selectedDeviceIds, deviceId]);
|
||||
}
|
||||
};
|
||||
|
||||
const options: FilterDropdownOption<DeviceFilterKey>[] = [
|
||||
{ id: ALL_FILTER_ID, label: _t('All') },
|
||||
{ id: ALL_FILTER_ID, label: _t("All") },
|
||||
{
|
||||
id: DeviceSecurityVariation.Verified,
|
||||
label: _t('Verified'),
|
||||
description: _t('Ready for secure messaging'),
|
||||
label: _t("Verified"),
|
||||
description: _t("Ready for secure messaging"),
|
||||
},
|
||||
{
|
||||
id: DeviceSecurityVariation.Unverified,
|
||||
label: _t('Unverified'),
|
||||
description: _t('Not ready for secure messaging'),
|
||||
label: _t("Unverified"),
|
||||
description: _t("Not ready for secure messaging"),
|
||||
},
|
||||
{
|
||||
id: DeviceSecurityVariation.Inactive,
|
||||
label: _t('Inactive'),
|
||||
description: _t(
|
||||
'Inactive for %(inactiveAgeDays)s days or longer',
|
||||
{ inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS },
|
||||
),
|
||||
label: _t("Inactive"),
|
||||
description: _t("Inactive for %(inactiveAgeDays)s days or longer", {
|
||||
inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const onFilterOptionChange = (filterId: DeviceFilterKey) => {
|
||||
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as FilterVariation);
|
||||
onFilterChange(filterId === ALL_FILTER_ID ? undefined : (filterId as FilterVariation));
|
||||
};
|
||||
|
||||
const isAllSelected = selectedDeviceIds.length >= sortedDevices.length;
|
||||
|
@ -292,77 +288,82 @@ export const FilteredDeviceList =
|
|||
if (isAllSelected) {
|
||||
setSelectedDeviceIds([]);
|
||||
} else {
|
||||
setSelectedDeviceIds(sortedDevices.map(device => device.device_id));
|
||||
setSelectedDeviceIds(sortedDevices.map((device) => device.device_id));
|
||||
}
|
||||
};
|
||||
|
||||
const isSigningOut = !!signingOutDeviceIds.length;
|
||||
|
||||
return <div className='mx_FilteredDeviceList' ref={ref}>
|
||||
<FilteredDeviceListHeader
|
||||
selectedDeviceCount={selectedDeviceIds.length}
|
||||
isAllSelected={isAllSelected}
|
||||
toggleSelectAll={toggleSelectAll}
|
||||
>
|
||||
{ selectedDeviceIds.length
|
||||
? <>
|
||||
<AccessibleButton
|
||||
data-testid='sign-out-selection-cta'
|
||||
kind='danger_inline'
|
||||
disabled={isSigningOut}
|
||||
onClick={() => onSignOutDevices(selectedDeviceIds)}
|
||||
className='mx_FilteredDeviceList_headerButton'
|
||||
>
|
||||
{ isSigningOut && <Spinner w={16} h={16} /> }
|
||||
{ _t('Sign out') }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
data-testid='cancel-selection-cta'
|
||||
kind='content_inline'
|
||||
disabled={isSigningOut}
|
||||
onClick={() => setSelectedDeviceIds([])}
|
||||
className='mx_FilteredDeviceList_headerButton'
|
||||
>
|
||||
{ _t('Cancel') }
|
||||
</AccessibleButton>
|
||||
</>
|
||||
: <FilterDropdown<DeviceFilterKey>
|
||||
id='device-list-filter'
|
||||
label={_t('Filter devices')}
|
||||
value={filter || ALL_FILTER_ID}
|
||||
onOptionChange={onFilterOptionChange}
|
||||
options={options}
|
||||
selectedLabel={_t('Show')}
|
||||
/>
|
||||
}
|
||||
</FilteredDeviceListHeader>
|
||||
{ !!sortedDevices.length
|
||||
? <FilterSecurityCard filter={filter} />
|
||||
: <NoResults filter={filter} clearFilter={() => onFilterChange(undefined)} />
|
||||
}
|
||||
<ol className='mx_FilteredDeviceList_list'>
|
||||
{ sortedDevices.map((device) => <DeviceListItem
|
||||
key={device.device_id}
|
||||
device={device}
|
||||
pusher={getPusherForDevice(device)}
|
||||
localNotificationSettings={localNotificationSettings.get(device.device_id)}
|
||||
isExpanded={expandedDeviceIds.includes(device.device_id)}
|
||||
isSigningOut={signingOutDeviceIds.includes(device.device_id)}
|
||||
isSelected={isDeviceSelected(device.device_id, selectedDeviceIds)}
|
||||
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
|
||||
onSignOutDevice={() => onSignOutDevices([device.device_id])}
|
||||
saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)}
|
||||
onRequestDeviceVerification={
|
||||
onRequestDeviceVerification
|
||||
? () => onRequestDeviceVerification(device.device_id)
|
||||
: undefined
|
||||
}
|
||||
setPushNotifications={setPushNotifications}
|
||||
toggleSelected={() => toggleSelection(device.device_id)}
|
||||
supportsMSC3881={supportsMSC3881}
|
||||
/>,
|
||||
) }
|
||||
</ol>
|
||||
</div>;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx_FilteredDeviceList" ref={ref}>
|
||||
<FilteredDeviceListHeader
|
||||
selectedDeviceCount={selectedDeviceIds.length}
|
||||
isAllSelected={isAllSelected}
|
||||
toggleSelectAll={toggleSelectAll}
|
||||
>
|
||||
{selectedDeviceIds.length ? (
|
||||
<>
|
||||
<AccessibleButton
|
||||
data-testid="sign-out-selection-cta"
|
||||
kind="danger_inline"
|
||||
disabled={isSigningOut}
|
||||
onClick={() => onSignOutDevices(selectedDeviceIds)}
|
||||
className="mx_FilteredDeviceList_headerButton"
|
||||
>
|
||||
{isSigningOut && <Spinner w={16} h={16} />}
|
||||
{_t("Sign out")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
data-testid="cancel-selection-cta"
|
||||
kind="content_inline"
|
||||
disabled={isSigningOut}
|
||||
onClick={() => setSelectedDeviceIds([])}
|
||||
className="mx_FilteredDeviceList_headerButton"
|
||||
>
|
||||
{_t("Cancel")}
|
||||
</AccessibleButton>
|
||||
</>
|
||||
) : (
|
||||
<FilterDropdown<DeviceFilterKey>
|
||||
id="device-list-filter"
|
||||
label={_t("Filter devices")}
|
||||
value={filter || ALL_FILTER_ID}
|
||||
onOptionChange={onFilterOptionChange}
|
||||
options={options}
|
||||
selectedLabel={_t("Show")}
|
||||
/>
|
||||
)}
|
||||
</FilteredDeviceListHeader>
|
||||
{!!sortedDevices.length ? (
|
||||
<FilterSecurityCard filter={filter} />
|
||||
) : (
|
||||
<NoResults filter={filter} clearFilter={() => onFilterChange(undefined)} />
|
||||
)}
|
||||
<ol className="mx_FilteredDeviceList_list">
|
||||
{sortedDevices.map((device) => (
|
||||
<DeviceListItem
|
||||
key={device.device_id}
|
||||
device={device}
|
||||
pusher={getPusherForDevice(device)}
|
||||
localNotificationSettings={localNotificationSettings.get(device.device_id)}
|
||||
isExpanded={expandedDeviceIds.includes(device.device_id)}
|
||||
isSigningOut={signingOutDeviceIds.includes(device.device_id)}
|
||||
isSelected={isDeviceSelected(device.device_id, selectedDeviceIds)}
|
||||
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
|
||||
onSignOutDevice={() => onSignOutDevices([device.device_id])}
|
||||
saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)}
|
||||
onRequestDeviceVerification={
|
||||
onRequestDeviceVerification
|
||||
? () => onRequestDeviceVerification(device.device_id)
|
||||
: undefined
|
||||
}
|
||||
setPushNotifications={setPushNotifications}
|
||||
toggleSelected={() => toggleSelection(device.device_id)}
|
||||
supportsMSC3881={supportsMSC3881}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { HTMLProps } from 'react';
|
||||
import React, { HTMLProps } from "react";
|
||||
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import StyledCheckbox, { CheckboxStyle } from '../../elements/StyledCheckbox';
|
||||
import { Alignment } from '../../elements/Tooltip';
|
||||
import TooltipTarget from '../../elements/TooltipTarget';
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import StyledCheckbox, { CheckboxStyle } from "../../elements/StyledCheckbox";
|
||||
import { Alignment } from "../../elements/Tooltip";
|
||||
import TooltipTarget from "../../elements/TooltipTarget";
|
||||
|
||||
interface Props extends Omit<HTMLProps<HTMLDivElement>, 'className'> {
|
||||
interface Props extends Omit<HTMLProps<HTMLDivElement>, "className"> {
|
||||
selectedDeviceCount: number;
|
||||
isAllSelected: boolean;
|
||||
toggleSelectAll: () => void;
|
||||
|
@ -35,29 +35,27 @@ const FilteredDeviceListHeader: React.FC<Props> = ({
|
|||
children,
|
||||
...rest
|
||||
}) => {
|
||||
const checkboxLabel = isAllSelected ? _t('Deselect all') : _t('Select all');
|
||||
return <div className='mx_FilteredDeviceListHeader' {...rest}>
|
||||
<TooltipTarget
|
||||
label={checkboxLabel}
|
||||
alignment={Alignment.Top}
|
||||
>
|
||||
<StyledCheckbox
|
||||
kind={CheckboxStyle.Solid}
|
||||
checked={isAllSelected}
|
||||
onChange={toggleSelectAll}
|
||||
id='device-select-all-checkbox'
|
||||
data-testid='device-select-all-checkbox'
|
||||
aria-label={checkboxLabel}
|
||||
/>
|
||||
</TooltipTarget>
|
||||
<span className='mx_FilteredDeviceListHeader_label'>
|
||||
{ selectedDeviceCount > 0
|
||||
? _t('%(selectedDeviceCount)s sessions selected', { selectedDeviceCount })
|
||||
: _t('Sessions')
|
||||
}
|
||||
</span>
|
||||
{ children }
|
||||
</div>;
|
||||
const checkboxLabel = isAllSelected ? _t("Deselect all") : _t("Select all");
|
||||
return (
|
||||
<div className="mx_FilteredDeviceListHeader" {...rest}>
|
||||
<TooltipTarget label={checkboxLabel} alignment={Alignment.Top}>
|
||||
<StyledCheckbox
|
||||
kind={CheckboxStyle.Solid}
|
||||
checked={isAllSelected}
|
||||
onChange={toggleSelectAll}
|
||||
id="device-select-all-checkbox"
|
||||
data-testid="device-select-all-checkbox"
|
||||
aria-label={checkboxLabel}
|
||||
/>
|
||||
</TooltipTarget>
|
||||
<span className="mx_FilteredDeviceListHeader_label">
|
||||
{selectedDeviceCount > 0
|
||||
? _t("%(selectedDeviceCount)s sessions selected", { selectedDeviceCount })
|
||||
: _t("Sessions")}
|
||||
</span>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilteredDeviceListHeader;
|
||||
|
|
|
@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import type { IServerVersions } from 'matrix-js-sdk/src/matrix';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import AccessibleButton from '../../elements/AccessibleButton';
|
||||
import SettingsSubsection from '../shared/SettingsSubsection';
|
||||
import SettingsStore from '../../../../settings/SettingsStore';
|
||||
import type { IServerVersions } from "matrix-js-sdk/src/matrix";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import SettingsSubsection from "../shared/SettingsSubsection";
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
|
||||
interface IProps {
|
||||
onShowQr: () => void;
|
||||
|
@ -33,31 +33,32 @@ export default class LoginWithQRSection extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
public render(): JSX.Element | null {
|
||||
const msc3882Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3882'];
|
||||
const msc3886Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3886'];
|
||||
const msc3882Supported = !!this.props.versions?.unstable_features?.["org.matrix.msc3882"];
|
||||
const msc3886Supported = !!this.props.versions?.unstable_features?.["org.matrix.msc3886"];
|
||||
|
||||
// Needs to be enabled as a feature + server support MSC3886 or have a default rendezvous server configured:
|
||||
const offerShowQr = SettingsStore.getValue("feature_qr_signin_reciprocate_show") &&
|
||||
msc3882Supported && msc3886Supported; // We don't support configuration of a fallback at the moment so we just check the MSCs
|
||||
const offerShowQr =
|
||||
SettingsStore.getValue("feature_qr_signin_reciprocate_show") && msc3882Supported && msc3886Supported; // We don't support configuration of a fallback at the moment so we just check the MSCs
|
||||
|
||||
// don't show anything if no method is available
|
||||
if (!offerShowQr) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SettingsSubsection
|
||||
heading={_t('Sign in with QR code')}
|
||||
>
|
||||
<div className="mx_LoginWithQRSection">
|
||||
<p className="mx_SettingsTab_subsectionText">{
|
||||
_t("You can use this device to sign in a new device with a QR code. You will need to " +
|
||||
"scan the QR code shown on this device with your device that's signed out.")
|
||||
}</p>
|
||||
<AccessibleButton
|
||||
onClick={this.props.onShowQr}
|
||||
kind="primary"
|
||||
>{ _t("Show QR code") }</AccessibleButton>
|
||||
</div>
|
||||
</SettingsSubsection>;
|
||||
return (
|
||||
<SettingsSubsection heading={_t("Sign in with QR code")}>
|
||||
<div className="mx_LoginWithQRSection">
|
||||
<p className="mx_SettingsTab_subsectionText">
|
||||
{_t(
|
||||
"You can use this device to sign in a new device with a QR code. You will need to " +
|
||||
"scan the QR code shown on this device with your device that's signed out.",
|
||||
)}
|
||||
</p>
|
||||
<AccessibleButton onClick={this.props.onShowQr} kind="primary">
|
||||
{_t("Show QR code")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</SettingsSubsection>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,46 +14,35 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import AccessibleButton from '../../elements/AccessibleButton';
|
||||
import SettingsSubsection from '../shared/SettingsSubsection';
|
||||
import DeviceSecurityCard from './DeviceSecurityCard';
|
||||
import { DeviceSecurityLearnMore } from './DeviceSecurityLearnMore';
|
||||
import { filterDevicesBySecurityRecommendation, FilterVariation, INACTIVE_DEVICE_AGE_DAYS } from './filter';
|
||||
import {
|
||||
DeviceSecurityVariation,
|
||||
ExtendedDevice,
|
||||
DevicesDictionary,
|
||||
} from './types';
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import SettingsSubsection from "../shared/SettingsSubsection";
|
||||
import DeviceSecurityCard from "./DeviceSecurityCard";
|
||||
import { DeviceSecurityLearnMore } from "./DeviceSecurityLearnMore";
|
||||
import { filterDevicesBySecurityRecommendation, FilterVariation, INACTIVE_DEVICE_AGE_DAYS } from "./filter";
|
||||
import { DeviceSecurityVariation, ExtendedDevice, DevicesDictionary } from "./types";
|
||||
|
||||
interface Props {
|
||||
devices: DevicesDictionary;
|
||||
currentDeviceId: ExtendedDevice['device_id'];
|
||||
currentDeviceId: ExtendedDevice["device_id"];
|
||||
goToFilteredList: (filter: FilterVariation) => void;
|
||||
}
|
||||
|
||||
const SecurityRecommendations: React.FC<Props> = ({
|
||||
devices,
|
||||
currentDeviceId,
|
||||
goToFilteredList,
|
||||
}) => {
|
||||
const SecurityRecommendations: React.FC<Props> = ({ devices, currentDeviceId, goToFilteredList }) => {
|
||||
const devicesArray = Object.values<ExtendedDevice>(devices);
|
||||
|
||||
const unverifiedDevicesCount = filterDevicesBySecurityRecommendation(
|
||||
devicesArray,
|
||||
[DeviceSecurityVariation.Unverified],
|
||||
)
|
||||
const unverifiedDevicesCount = filterDevicesBySecurityRecommendation(devicesArray, [
|
||||
DeviceSecurityVariation.Unverified,
|
||||
])
|
||||
// filter out the current device
|
||||
// as unverfied warning and actions
|
||||
// will be shown in current session section
|
||||
.filter((device) => device.device_id !== currentDeviceId)
|
||||
.length;
|
||||
const inactiveDevicesCount = filterDevicesBySecurityRecommendation(
|
||||
devicesArray,
|
||||
[DeviceSecurityVariation.Inactive],
|
||||
).length;
|
||||
.filter((device) => device.device_id !== currentDeviceId).length;
|
||||
const inactiveDevicesCount = filterDevicesBySecurityRecommendation(devicesArray, [
|
||||
DeviceSecurityVariation.Inactive,
|
||||
]).length;
|
||||
|
||||
if (!(unverifiedDevicesCount | inactiveDevicesCount)) {
|
||||
return null;
|
||||
|
@ -61,61 +50,64 @@ const SecurityRecommendations: React.FC<Props> = ({
|
|||
|
||||
const inactiveAgeDays = INACTIVE_DEVICE_AGE_DAYS;
|
||||
|
||||
return <SettingsSubsection
|
||||
heading={_t('Security recommendations')}
|
||||
description={_t('Improve your account security by following these recommendations')}
|
||||
data-testid='security-recommendations-section'
|
||||
>
|
||||
{
|
||||
!!unverifiedDevicesCount &&
|
||||
<DeviceSecurityCard
|
||||
variation={DeviceSecurityVariation.Unverified}
|
||||
heading={_t('Unverified sessions')}
|
||||
description={<>
|
||||
{ _t(
|
||||
`Verify your sessions for enhanced secure messaging` +
|
||||
` or sign out from those you don't recognize or use anymore.`,
|
||||
) }
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
|
||||
</>}
|
||||
>
|
||||
<AccessibleButton
|
||||
kind='link_inline'
|
||||
onClick={() => goToFilteredList(DeviceSecurityVariation.Unverified)}
|
||||
data-testid='unverified-devices-cta'
|
||||
>
|
||||
{ _t('View all') + ` (${unverifiedDevicesCount})` }
|
||||
</AccessibleButton>
|
||||
</DeviceSecurityCard>
|
||||
}
|
||||
{
|
||||
!!inactiveDevicesCount &&
|
||||
<>
|
||||
{ !!unverifiedDevicesCount && <div className='mx_SecurityRecommendations_spacing' /> }
|
||||
return (
|
||||
<SettingsSubsection
|
||||
heading={_t("Security recommendations")}
|
||||
description={_t("Improve your account security by following these recommendations")}
|
||||
data-testid="security-recommendations-section"
|
||||
>
|
||||
{!!unverifiedDevicesCount && (
|
||||
<DeviceSecurityCard
|
||||
variation={DeviceSecurityVariation.Inactive}
|
||||
heading={_t('Inactive sessions')}
|
||||
description={<>
|
||||
{ _t(
|
||||
`Consider signing out from old sessions ` +
|
||||
`(%(inactiveAgeDays)s days or older) you don't use anymore`,
|
||||
{ inactiveAgeDays },
|
||||
) }
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Inactive} />
|
||||
</>
|
||||
variation={DeviceSecurityVariation.Unverified}
|
||||
heading={_t("Unverified sessions")}
|
||||
description={
|
||||
<>
|
||||
{_t(
|
||||
`Verify your sessions for enhanced secure messaging` +
|
||||
` or sign out from those you don't recognize or use anymore.`,
|
||||
)}
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<AccessibleButton
|
||||
kind='link_inline'
|
||||
onClick={() => goToFilteredList(DeviceSecurityVariation.Inactive)}
|
||||
data-testid='inactive-devices-cta'
|
||||
kind="link_inline"
|
||||
onClick={() => goToFilteredList(DeviceSecurityVariation.Unverified)}
|
||||
data-testid="unverified-devices-cta"
|
||||
>
|
||||
{ _t('View all') + ` (${inactiveDevicesCount})` }
|
||||
{_t("View all") + ` (${unverifiedDevicesCount})`}
|
||||
</AccessibleButton>
|
||||
</DeviceSecurityCard>
|
||||
</>
|
||||
}
|
||||
</SettingsSubsection>;
|
||||
)}
|
||||
{!!inactiveDevicesCount && (
|
||||
<>
|
||||
{!!unverifiedDevicesCount && <div className="mx_SecurityRecommendations_spacing" />}
|
||||
<DeviceSecurityCard
|
||||
variation={DeviceSecurityVariation.Inactive}
|
||||
heading={_t("Inactive sessions")}
|
||||
description={
|
||||
<>
|
||||
{_t(
|
||||
`Consider signing out from old sessions ` +
|
||||
`(%(inactiveAgeDays)s days or older) you don't use anymore`,
|
||||
{ inactiveAgeDays },
|
||||
)}
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Inactive} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<AccessibleButton
|
||||
kind="link_inline"
|
||||
onClick={() => goToFilteredList(DeviceSecurityVariation.Inactive)}
|
||||
data-testid="inactive-devices-cta"
|
||||
>
|
||||
{_t("View all") + ` (${inactiveDevicesCount})`}
|
||||
</AccessibleButton>
|
||||
</DeviceSecurityCard>
|
||||
</>
|
||||
)}
|
||||
</SettingsSubsection>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecurityRecommendations;
|
||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import StyledCheckbox, { CheckboxStyle } from '../../elements/StyledCheckbox';
|
||||
import DeviceTile, { DeviceTileProps } from './DeviceTile';
|
||||
import StyledCheckbox, { CheckboxStyle } from "../../elements/StyledCheckbox";
|
||||
import DeviceTile, { DeviceTileProps } from "./DeviceTile";
|
||||
|
||||
interface Props extends DeviceTileProps {
|
||||
isSelected: boolean;
|
||||
|
@ -25,26 +25,22 @@ interface Props extends DeviceTileProps {
|
|||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const SelectableDeviceTile: React.FC<Props> = ({
|
||||
children,
|
||||
device,
|
||||
isSelected,
|
||||
onSelect,
|
||||
onClick,
|
||||
}) => {
|
||||
return <div className='mx_SelectableDeviceTile'>
|
||||
<StyledCheckbox
|
||||
kind={CheckboxStyle.Solid}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
className='mx_SelectableDeviceTile_checkbox'
|
||||
id={`device-tile-checkbox-${device.device_id}`}
|
||||
data-testid={`device-tile-checkbox-${device.device_id}`}
|
||||
/>
|
||||
<DeviceTile device={device} onClick={onClick} isSelected={isSelected}>
|
||||
{ children }
|
||||
</DeviceTile>
|
||||
</div>;
|
||||
const SelectableDeviceTile: React.FC<Props> = ({ children, device, isSelected, onSelect, onClick }) => {
|
||||
return (
|
||||
<div className="mx_SelectableDeviceTile">
|
||||
<StyledCheckbox
|
||||
kind={CheckboxStyle.Solid}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
className="mx_SelectableDeviceTile_checkbox"
|
||||
id={`device-tile-checkbox-${device.device_id}`}
|
||||
data-testid={`device-tile-checkbox-${device.device_id}`}
|
||||
/>
|
||||
<DeviceTile device={device} onClick={onClick} isSelected={isSelected}>
|
||||
{children}
|
||||
</DeviceTile>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectableDeviceTile;
|
||||
|
|
|
@ -23,14 +23,16 @@ import { InteractiveAuthCallback } from "../../../structures/InteractiveAuth";
|
|||
import { SSOAuthEntry } from "../../auth/InteractiveAuthEntryComponents";
|
||||
import InteractiveAuthDialog from "../../dialogs/InteractiveAuthDialog";
|
||||
|
||||
const makeDeleteRequest = (
|
||||
matrixClient: MatrixClient, deviceIds: string[],
|
||||
) => async (auth?: IAuthData): Promise<void> => {
|
||||
await matrixClient.deleteMultipleDevices(deviceIds, auth);
|
||||
};
|
||||
const makeDeleteRequest =
|
||||
(matrixClient: MatrixClient, deviceIds: string[]) =>
|
||||
async (auth?: IAuthData): Promise<void> => {
|
||||
await matrixClient.deleteMultipleDevices(deviceIds, auth);
|
||||
};
|
||||
|
||||
export const deleteDevicesWithInteractiveAuth = async (
|
||||
matrixClient: MatrixClient, deviceIds: string[], onFinished?: InteractiveAuthCallback,
|
||||
matrixClient: MatrixClient,
|
||||
deviceIds: string[],
|
||||
onFinished?: InteractiveAuthCallback,
|
||||
) => {
|
||||
if (!deviceIds.length) {
|
||||
return;
|
||||
|
|
|
@ -19,19 +19,20 @@ import { ExtendedDevice, DeviceSecurityVariation } from "./types";
|
|||
type DeviceFilterCondition = (device: ExtendedDevice) => boolean;
|
||||
|
||||
const MS_DAY = 24 * 60 * 60 * 1000;
|
||||
export const INACTIVE_DEVICE_AGE_MS = 7.776e+9; // 90 days
|
||||
export const INACTIVE_DEVICE_AGE_MS = 7.776e9; // 90 days
|
||||
export const INACTIVE_DEVICE_AGE_DAYS = INACTIVE_DEVICE_AGE_MS / MS_DAY;
|
||||
|
||||
export type FilterVariation = DeviceSecurityVariation.Verified
|
||||
export type FilterVariation =
|
||||
| DeviceSecurityVariation.Verified
|
||||
| DeviceSecurityVariation.Inactive
|
||||
| DeviceSecurityVariation.Unverified;
|
||||
|
||||
export const isDeviceInactive: DeviceFilterCondition = device =>
|
||||
export const isDeviceInactive: DeviceFilterCondition = (device) =>
|
||||
!!device.last_seen_ts && device.last_seen_ts < Date.now() - INACTIVE_DEVICE_AGE_MS;
|
||||
|
||||
const filters: Record<FilterVariation, DeviceFilterCondition> = {
|
||||
[DeviceSecurityVariation.Verified]: device => !!device.isVerified,
|
||||
[DeviceSecurityVariation.Unverified]: device => !device.isVerified,
|
||||
[DeviceSecurityVariation.Verified]: (device) => !!device.isVerified,
|
||||
[DeviceSecurityVariation.Unverified]: (device) => !device.isVerified,
|
||||
[DeviceSecurityVariation.Inactive]: isDeviceInactive,
|
||||
};
|
||||
|
||||
|
@ -39,9 +40,9 @@ export const filterDevicesBySecurityRecommendation = (
|
|||
devices: ExtendedDevice[],
|
||||
securityVariations: FilterVariation[],
|
||||
) => {
|
||||
const activeFilters = securityVariations.map(variation => filters[variation]);
|
||||
const activeFilters = securityVariations.map((variation) => filters[variation]);
|
||||
if (!activeFilters.length) {
|
||||
return devices;
|
||||
}
|
||||
return devices.filter(device => activeFilters.every(filter => filter(device)));
|
||||
return devices.filter((device) => activeFilters.every((filter) => filter(device)));
|
||||
};
|
||||
|
|
|
@ -26,13 +26,13 @@ export type ExtendedDeviceAppInfo = {
|
|||
url?: string;
|
||||
};
|
||||
export type ExtendedDevice = DeviceWithVerification & ExtendedDeviceAppInfo & ExtendedDeviceInformation;
|
||||
export type DevicesDictionary = Record<ExtendedDevice['device_id'], ExtendedDevice>;
|
||||
export type DevicesDictionary = Record<ExtendedDevice["device_id"], ExtendedDevice>;
|
||||
|
||||
export enum DeviceSecurityVariation {
|
||||
Verified = 'Verified',
|
||||
Unverified = 'Unverified',
|
||||
Inactive = 'Inactive',
|
||||
Verified = "Verified",
|
||||
Unverified = "Unverified",
|
||||
Inactive = "Inactive",
|
||||
// sessions that do not support encryption
|
||||
// eg a session that logged in via api to get an access token
|
||||
Unverifiable = 'Unverifiable'
|
||||
Unverifiable = "Unverifiable",
|
||||
}
|
||||
|
|
|
@ -48,18 +48,13 @@ const isDeviceVerified = (
|
|||
try {
|
||||
const userId = matrixClient.getUserId();
|
||||
if (!userId) {
|
||||
throw new Error('No user id');
|
||||
throw new Error("No user id");
|
||||
}
|
||||
const deviceInfo = matrixClient.getStoredDevice(userId, device.device_id);
|
||||
if (!deviceInfo) {
|
||||
throw new Error('No device info available');
|
||||
throw new Error("No device info available");
|
||||
}
|
||||
return crossSigningInfo.checkDeviceTrust(
|
||||
crossSigningInfo,
|
||||
deviceInfo,
|
||||
false,
|
||||
true,
|
||||
).isCrossSigningVerified();
|
||||
return crossSigningInfo.checkDeviceTrust(crossSigningInfo, deviceInfo, false, true).isCrossSigningVerified();
|
||||
} catch (error) {
|
||||
logger.error("Error getting device cross-signing info", error);
|
||||
return null;
|
||||
|
@ -79,27 +74,30 @@ const parseDeviceExtendedInformation = (matrixClient: MatrixClient, device: IMyD
|
|||
const fetchDevicesWithVerification = async (
|
||||
matrixClient: MatrixClient,
|
||||
userId: string,
|
||||
): Promise<DevicesState['devices']> => {
|
||||
): Promise<DevicesState["devices"]> => {
|
||||
const { devices } = await matrixClient.getDevices();
|
||||
|
||||
const crossSigningInfo = matrixClient.getStoredCrossSigningForUser(userId);
|
||||
|
||||
const devicesDict = devices.reduce((acc, device: IMyDevice) => ({
|
||||
...acc,
|
||||
[device.device_id]: {
|
||||
...device,
|
||||
isVerified: isDeviceVerified(matrixClient, crossSigningInfo, device),
|
||||
...parseDeviceExtendedInformation(matrixClient, device),
|
||||
...parseUserAgent(device[UNSTABLE_MSC3852_LAST_SEEN_UA.name]),
|
||||
},
|
||||
}), {});
|
||||
const devicesDict = devices.reduce(
|
||||
(acc, device: IMyDevice) => ({
|
||||
...acc,
|
||||
[device.device_id]: {
|
||||
...device,
|
||||
isVerified: isDeviceVerified(matrixClient, crossSigningInfo, device),
|
||||
...parseDeviceExtendedInformation(matrixClient, device),
|
||||
...parseUserAgent(device[UNSTABLE_MSC3852_LAST_SEEN_UA.name]),
|
||||
},
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
return devicesDict;
|
||||
};
|
||||
|
||||
export enum OwnDevicesError {
|
||||
Unsupported = 'Unsupported',
|
||||
Default = 'Default',
|
||||
Unsupported = "Unsupported",
|
||||
Default = "Default",
|
||||
}
|
||||
export type DevicesState = {
|
||||
devices: DevicesDictionary;
|
||||
|
@ -108,10 +106,10 @@ export type DevicesState = {
|
|||
currentDeviceId: string;
|
||||
isLoadingDeviceList: boolean;
|
||||
// not provided when current session cannot request verification
|
||||
requestDeviceVerification?: (deviceId: ExtendedDevice['device_id']) => Promise<VerificationRequest>;
|
||||
requestDeviceVerification?: (deviceId: ExtendedDevice["device_id"]) => Promise<VerificationRequest>;
|
||||
refreshDevices: () => Promise<void>;
|
||||
saveDeviceName: (deviceId: ExtendedDevice['device_id'], deviceName: string) => Promise<void>;
|
||||
setPushNotifications: (deviceId: ExtendedDevice['device_id'], enabled: boolean) => Promise<void>;
|
||||
saveDeviceName: (deviceId: ExtendedDevice["device_id"], deviceName: string) => Promise<void>;
|
||||
setPushNotifications: (deviceId: ExtendedDevice["device_id"], enabled: boolean) => Promise<void>;
|
||||
error?: OwnDevicesError;
|
||||
supportsMSC3881?: boolean | undefined;
|
||||
};
|
||||
|
@ -121,17 +119,18 @@ export const useOwnDevices = (): DevicesState => {
|
|||
const currentDeviceId = matrixClient.getDeviceId();
|
||||
const userId = matrixClient.getUserId();
|
||||
|
||||
const [devices, setDevices] = useState<DevicesState['devices']>({});
|
||||
const [pushers, setPushers] = useState<DevicesState['pushers']>([]);
|
||||
const [localNotificationSettings, setLocalNotificationSettings]
|
||||
= useState<DevicesState['localNotificationSettings']>(new Map<string, LocalNotificationSettings>());
|
||||
const [devices, setDevices] = useState<DevicesState["devices"]>({});
|
||||
const [pushers, setPushers] = useState<DevicesState["pushers"]>([]);
|
||||
const [localNotificationSettings, setLocalNotificationSettings] = useState<
|
||||
DevicesState["localNotificationSettings"]
|
||||
>(new Map<string, LocalNotificationSettings>());
|
||||
const [isLoadingDeviceList, setIsLoadingDeviceList] = useState(true);
|
||||
const [supportsMSC3881, setSupportsMSC3881] = useState(true); // optimisticly saying yes!
|
||||
|
||||
const [error, setError] = useState<OwnDevicesError>();
|
||||
|
||||
useEffect(() => {
|
||||
matrixClient.doesServerSupportUnstableFeature("org.matrix.msc3881").then(hasSupport => {
|
||||
matrixClient.doesServerSupportUnstableFeature("org.matrix.msc3881").then((hasSupport) => {
|
||||
setSupportsMSC3881(hasSupport);
|
||||
});
|
||||
}, [matrixClient]);
|
||||
|
@ -142,7 +141,7 @@ export const useOwnDevices = (): DevicesState => {
|
|||
// realistically we should never hit this
|
||||
// but it satisfies types
|
||||
if (!userId) {
|
||||
throw new Error('Cannot fetch devices without user id');
|
||||
throw new Error("Cannot fetch devices without user id");
|
||||
}
|
||||
const devices = await fetchDevicesWithVerification(matrixClient, userId);
|
||||
setDevices(devices);
|
||||
|
@ -155,10 +154,7 @@ export const useOwnDevices = (): DevicesState => {
|
|||
const eventType = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
|
||||
const event = matrixClient.getAccountData(eventType);
|
||||
if (event) {
|
||||
notificationSettings.set(
|
||||
deviceId,
|
||||
event.getContent(),
|
||||
);
|
||||
notificationSettings.set(deviceId, event.getContent());
|
||||
}
|
||||
});
|
||||
setLocalNotificationSettings(notificationSettings);
|
||||
|
@ -198,17 +194,15 @@ export const useOwnDevices = (): DevicesState => {
|
|||
|
||||
const isCurrentDeviceVerified = !!devices[currentDeviceId]?.isVerified;
|
||||
|
||||
const requestDeviceVerification = isCurrentDeviceVerified && userId
|
||||
? async (deviceId: ExtendedDevice['device_id']) => {
|
||||
return await matrixClient.requestVerification(
|
||||
userId,
|
||||
[deviceId],
|
||||
);
|
||||
}
|
||||
: undefined;
|
||||
const requestDeviceVerification =
|
||||
isCurrentDeviceVerified && userId
|
||||
? async (deviceId: ExtendedDevice["device_id"]) => {
|
||||
return await matrixClient.requestVerification(userId, [deviceId]);
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const saveDeviceName = useCallback(
|
||||
async (deviceId: ExtendedDevice['device_id'], deviceName: string): Promise<void> => {
|
||||
async (deviceId: ExtendedDevice["device_id"], deviceName: string): Promise<void> => {
|
||||
const device = devices[deviceId];
|
||||
|
||||
// no change
|
||||
|
@ -217,21 +211,20 @@ export const useOwnDevices = (): DevicesState => {
|
|||
}
|
||||
|
||||
try {
|
||||
await matrixClient.setDeviceDetails(
|
||||
deviceId,
|
||||
{ display_name: deviceName },
|
||||
);
|
||||
await matrixClient.setDeviceDetails(deviceId, { display_name: deviceName });
|
||||
await refreshDevices();
|
||||
} catch (error) {
|
||||
logger.error("Error setting session display name", error);
|
||||
throw new Error(_t("Failed to set display name"));
|
||||
}
|
||||
}, [matrixClient, devices, refreshDevices]);
|
||||
},
|
||||
[matrixClient, devices, refreshDevices],
|
||||
);
|
||||
|
||||
const setPushNotifications = useCallback(
|
||||
async (deviceId: ExtendedDevice['device_id'], enabled: boolean): Promise<void> => {
|
||||
async (deviceId: ExtendedDevice["device_id"], enabled: boolean): Promise<void> => {
|
||||
try {
|
||||
const pusher = pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === deviceId);
|
||||
const pusher = pushers.find((pusher) => pusher[PUSHER_DEVICE_ID.name] === deviceId);
|
||||
if (pusher) {
|
||||
await matrixClient.setPusher({
|
||||
...pusher,
|
||||
|
@ -248,7 +241,8 @@ export const useOwnDevices = (): DevicesState => {
|
|||
} finally {
|
||||
await refreshDevices();
|
||||
}
|
||||
}, [matrixClient, pushers, localNotificationSettings, refreshDevices],
|
||||
},
|
||||
[matrixClient, pushers, localNotificationSettings, refreshDevices],
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue