= ({
+ isVerified,
+ isSelected,
+ deviceType,
+}) => (
+
{ /* TODO(kerrya) all devices have an unknown type until PSG-650 */ }
{
isVerified
?
:
diff --git a/src/components/views/settings/devices/DeviceVerificationStatusCard.tsx b/src/components/views/settings/devices/DeviceVerificationStatusCard.tsx
index 11e806e54e..127f5eedf6 100644
--- a/src/components/views/settings/devices/DeviceVerificationStatusCard.tsx
+++ b/src/components/views/settings/devices/DeviceVerificationStatusCard.tsx
@@ -21,11 +21,11 @@ import AccessibleButton from '../../elements/AccessibleButton';
import DeviceSecurityCard from './DeviceSecurityCard';
import {
DeviceSecurityVariation,
- DeviceWithVerification,
+ ExtendedDevice,
} from './types';
interface Props {
- device: DeviceWithVerification;
+ device: ExtendedDevice;
onVerifyDevice?: () => void;
}
diff --git a/src/components/views/settings/devices/FilteredDeviceList.tsx b/src/components/views/settings/devices/FilteredDeviceList.tsx
index baee4e1d0e..c2e8786052 100644
--- a/src/components/views/settings/devices/FilteredDeviceList.tsx
+++ b/src/components/views/settings/devices/FilteredDeviceList.tsx
@@ -17,6 +17,7 @@ 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 { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton';
@@ -24,35 +25,44 @@ import { FilterDropdown, FilterDropdownOption } from '../../elements/FilterDropd
import DeviceDetails from './DeviceDetails';
import DeviceExpandDetailsButton from './DeviceExpandDetailsButton';
import DeviceSecurityCard from './DeviceSecurityCard';
-import DeviceTile from './DeviceTile';
import {
filterDevicesBySecurityRecommendation,
INACTIVE_DEVICE_AGE_DAYS,
} from './filter';
+import SelectableDeviceTile from './SelectableDeviceTile';
import {
DevicesDictionary,
DeviceSecurityVariation,
- DeviceWithVerification,
+ ExtendedDevice,
} from './types';
import { DevicesState } from './useOwnDevices';
+import FilteredDeviceListHeader from './FilteredDeviceListHeader';
interface Props {
devices: DevicesDictionary;
pushers: IPusher[];
- expandedDeviceIds: DeviceWithVerification['device_id'][];
- signingOutDeviceIds: DeviceWithVerification['device_id'][];
+ localNotificationSettings: Map;
+ expandedDeviceIds: ExtendedDevice['device_id'][];
+ signingOutDeviceIds: ExtendedDevice['device_id'][];
+ selectedDeviceIds: ExtendedDevice['device_id'][];
filter?: DeviceSecurityVariation;
onFilterChange: (filter: DeviceSecurityVariation | undefined) => void;
- onDeviceExpandToggle: (deviceId: DeviceWithVerification['device_id']) => void;
- onSignOutDevices: (deviceIds: DeviceWithVerification['device_id'][]) => void;
+ onDeviceExpandToggle: (deviceId: ExtendedDevice['device_id']) => void;
+ onSignOutDevices: (deviceIds: ExtendedDevice['device_id'][]) => void;
saveDeviceName: DevicesState['saveDeviceName'];
- onRequestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => void;
- setPusherEnabled: (deviceId: string, enabled: boolean) => Promise;
+ onRequestDeviceVerification?: (deviceId: ExtendedDevice['device_id']) => void;
+ setPushNotifications: (deviceId: string, enabled: boolean) => Promise;
+ setSelectedDeviceIds: (deviceIds: ExtendedDevice['device_id'][]) => void;
supportsMSC3881?: boolean | undefined;
}
+const isDeviceSelected = (
+ deviceId: ExtendedDevice['device_id'],
+ selectedDeviceIds: ExtendedDevice['device_id'][],
+) => selectedDeviceIds.includes(deviceId);
+
// devices without timestamp metadata should be sorted last
-const sortDevicesByLatestActivity = (left: DeviceWithVerification, right: DeviceWithVerification) =>
+const sortDevicesByLatestActivity = (left: ExtendedDevice, right: ExtendedDevice) =>
(right.last_seen_ts || 0) - (left.last_seen_ts || 0);
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: DeviceSecurityVariation) =>
@@ -139,46 +149,55 @@ const NoResults: React.FC = ({ filter, clearFilter }) =>
;
const DeviceListItem: React.FC<{
- device: DeviceWithVerification;
+ device: ExtendedDevice;
pusher?: IPusher | undefined;
+ localNotificationSettings?: LocalNotificationSettings | undefined;
isExpanded: boolean;
isSigningOut: boolean;
+ isSelected: boolean;
onDeviceExpandToggle: () => void;
onSignOutDevice: () => void;
saveDeviceName: (deviceName: string) => Promise
;
onRequestDeviceVerification?: () => void;
- setPusherEnabled: (deviceId: string, enabled: boolean) => Promise;
+ toggleSelected: () => void;
+ setPushNotifications: (deviceId: string, enabled: boolean) => Promise;
supportsMSC3881?: boolean | undefined;
}> = ({
device,
pusher,
+ localNotificationSettings,
isExpanded,
isSigningOut,
+ isSelected,
onDeviceExpandToggle,
onSignOutDevice,
saveDeviceName,
onRequestDeviceVerification,
- setPusherEnabled,
+ setPushNotifications,
+ toggleSelected,
supportsMSC3881,
}) =>
-
-
+
{
isExpanded &&
}
@@ -192,23 +211,35 @@ export const FilteredDeviceList =
forwardRef(({
devices,
pushers,
+ localNotificationSettings,
filter,
expandedDeviceIds,
signingOutDeviceIds,
+ selectedDeviceIds,
onFilterChange,
onDeviceExpandToggle,
saveDeviceName,
onSignOutDevices,
onRequestDeviceVerification,
- setPusherEnabled,
+ setPushNotifications,
+ setSelectedDeviceIds,
supportsMSC3881,
}: Props, ref: ForwardedRef) => {
const sortedDevices = getFilteredSortedDevices(devices, filter);
- function getPusherForDevice(device: DeviceWithVerification): IPusher | undefined {
+ function getPusherForDevice(device: ExtendedDevice): IPusher | undefined {
return pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === device.device_id);
}
+ const toggleSelection = (deviceId: ExtendedDevice['device_id']): void => {
+ if (isDeviceSelected(deviceId, selectedDeviceIds)) {
+ // remove from selection
+ setSelectedDeviceIds(selectedDeviceIds.filter(id => id !== deviceId));
+ } else {
+ setSelectedDeviceIds([...selectedDeviceIds, deviceId]);
+ }
+ };
+
const options: FilterDropdownOption[] = [
{ id: ALL_FILTER_ID, label: _t('All') },
{
@@ -235,20 +266,50 @@ export const FilteredDeviceList =
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as DeviceSecurityVariation);
};
+ const isAllSelected = selectedDeviceIds.length >= sortedDevices.length;
+ const toggleSelectAll = () => {
+ if (isAllSelected) {
+ setSelectedDeviceIds([]);
+ } else {
+ setSelectedDeviceIds(sortedDevices.map(device => device.device_id));
+ }
+ };
+
return
-
-
- { _t('Sessions') }
-
-
- id='device-list-filter'
- label={_t('Filter devices')}
- value={filter || ALL_FILTER_ID}
- onOptionChange={onFilterOptionChange}
- options={options}
- selectedLabel={_t('Show')}
- />
-
+
+ { selectedDeviceIds.length
+ ? <>
+ onSignOutDevices(selectedDeviceIds)}
+ className='mx_FilteredDeviceList_headerButton'
+ >
+ { _t('Sign out') }
+
+ setSelectedDeviceIds([])}
+ className='mx_FilteredDeviceList_headerButton'
+ >
+ { _t('Cancel') }
+
+ >
+ :
+ id='device-list-filter'
+ label={_t('Filter devices')}
+ value={filter || ALL_FILTER_ID}
+ onOptionChange={onFilterOptionChange}
+ options={options}
+ selectedLabel={_t('Show')}
+ />
+ }
+
{ !!sortedDevices.length
?
:
onFilterChange(undefined)} />
@@ -258,8 +319,10 @@ export const FilteredDeviceList =
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)}
@@ -268,7 +331,8 @@ export const FilteredDeviceList =
? () => onRequestDeviceVerification(device.device_id)
: undefined
}
- setPusherEnabled={setPusherEnabled}
+ setPushNotifications={setPushNotifications}
+ toggleSelected={() => toggleSelection(device.device_id)}
supportsMSC3881={supportsMSC3881}
/>,
) }
diff --git a/src/components/views/settings/devices/FilteredDeviceListHeader.tsx b/src/components/views/settings/devices/FilteredDeviceListHeader.tsx
new file mode 100644
index 0000000000..561431d12e
--- /dev/null
+++ b/src/components/views/settings/devices/FilteredDeviceListHeader.tsx
@@ -0,0 +1,63 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+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';
+
+interface Props extends Omit, 'className'> {
+ selectedDeviceCount: number;
+ isAllSelected: boolean;
+ toggleSelectAll: () => void;
+ children?: React.ReactNode;
+}
+
+const FilteredDeviceListHeader: React.FC = ({
+ selectedDeviceCount,
+ isAllSelected,
+ toggleSelectAll,
+ children,
+ ...rest
+}) => {
+ const checkboxLabel = isAllSelected ? _t('Deselect all') : _t('Select all');
+ return
+
+
+
+
+ { selectedDeviceCount > 0
+ ? _t('%(selectedDeviceCount)s sessions selected', { selectedDeviceCount })
+ : _t('Sessions')
+ }
+
+ { children }
+
;
+};
+
+export default FilteredDeviceListHeader;
diff --git a/src/components/views/settings/devices/SecurityRecommendations.tsx b/src/components/views/settings/devices/SecurityRecommendations.tsx
index 3132eba38a..ddeb2f2e2e 100644
--- a/src/components/views/settings/devices/SecurityRecommendations.tsx
+++ b/src/components/views/settings/devices/SecurityRecommendations.tsx
@@ -23,13 +23,13 @@ import DeviceSecurityCard from './DeviceSecurityCard';
import { filterDevicesBySecurityRecommendation, INACTIVE_DEVICE_AGE_DAYS } from './filter';
import {
DeviceSecurityVariation,
- DeviceWithVerification,
+ ExtendedDevice,
DevicesDictionary,
} from './types';
interface Props {
devices: DevicesDictionary;
- currentDeviceId: DeviceWithVerification['device_id'];
+ currentDeviceId: ExtendedDevice['device_id'];
goToFilteredList: (filter: DeviceSecurityVariation) => void;
}
@@ -38,7 +38,7 @@ const SecurityRecommendations: React.FC = ({
currentDeviceId,
goToFilteredList,
}) => {
- const devicesArray = Object.values(devices);
+ const devicesArray = Object.values(devices);
const unverifiedDevicesCount = filterDevicesBySecurityRecommendation(
devicesArray,
diff --git a/src/components/views/settings/devices/SelectableDeviceTile.tsx b/src/components/views/settings/devices/SelectableDeviceTile.tsx
index e232e5ff50..d784472a84 100644
--- a/src/components/views/settings/devices/SelectableDeviceTile.tsx
+++ b/src/components/views/settings/devices/SelectableDeviceTile.tsx
@@ -32,8 +32,9 @@ const SelectableDeviceTile: React.FC = ({ children, device, isSelected, o
onChange={onClick}
className='mx_SelectableDeviceTile_checkbox'
id={`device-tile-checkbox-${device.device_id}`}
+ data-testid={`device-tile-checkbox-${device.device_id}`}
/>
-
+
{ children }
;
diff --git a/src/components/views/settings/devices/filter.ts b/src/components/views/settings/devices/filter.ts
index ad2bc92152..05ceb9c697 100644
--- a/src/components/views/settings/devices/filter.ts
+++ b/src/components/views/settings/devices/filter.ts
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { DeviceWithVerification, DeviceSecurityVariation } from "./types";
+import { ExtendedDevice, DeviceSecurityVariation } from "./types";
-type DeviceFilterCondition = (device: DeviceWithVerification) => boolean;
+type DeviceFilterCondition = (device: ExtendedDevice) => boolean;
const MS_DAY = 24 * 60 * 60 * 1000;
export const INACTIVE_DEVICE_AGE_MS = 7.776e+9; // 90 days
@@ -32,7 +32,7 @@ const filters: Record = {
};
export const filterDevicesBySecurityRecommendation = (
- devices: DeviceWithVerification[],
+ devices: ExtendedDevice[],
securityVariations: DeviceSecurityVariation[],
) => {
const activeFilters = securityVariations.map(variation => filters[variation]);
diff --git a/src/components/views/settings/devices/types.ts b/src/components/views/settings/devices/types.ts
index 1f3328c09e..3fa125a09f 100644
--- a/src/components/views/settings/devices/types.ts
+++ b/src/components/views/settings/devices/types.ts
@@ -16,8 +16,17 @@ limitations under the License.
import { IMyDevice } from "matrix-js-sdk/src/matrix";
+import { ExtendedDeviceInformation } from "../../../../utils/device/parseUserAgent";
+
export type DeviceWithVerification = IMyDevice & { isVerified: boolean | null };
-export type DevicesDictionary = Record;
+export type ExtendedDeviceAppInfo = {
+ // eg Element Web
+ appName?: string;
+ appVersion?: string;
+ url?: string;
+};
+export type ExtendedDevice = DeviceWithVerification & ExtendedDeviceAppInfo & ExtendedDeviceInformation;
+export type DevicesDictionary = Record;
export enum DeviceSecurityVariation {
Verified = 'Verified',
diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts
index b583d4c080..c3b8cb0212 100644
--- a/src/components/views/settings/devices/useOwnDevices.ts
+++ b/src/components/views/settings/devices/useOwnDevices.ts
@@ -15,15 +15,29 @@ limitations under the License.
*/
import { useCallback, useContext, useEffect, useState } from "react";
-import { IMyDevice, IPusher, MatrixClient, PUSHER_DEVICE_ID, PUSHER_ENABLED } from "matrix-js-sdk/src/matrix";
+import {
+ ClientEvent,
+ IMyDevice,
+ IPusher,
+ LOCAL_NOTIFICATION_SETTINGS_PREFIX,
+ MatrixClient,
+ MatrixEvent,
+ PUSHER_DEVICE_ID,
+ PUSHER_ENABLED,
+ UNSTABLE_MSC3852_LAST_SEEN_UA,
+} from "matrix-js-sdk/src/matrix";
import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { MatrixError } from "matrix-js-sdk/src/http-api";
import { logger } from "matrix-js-sdk/src/logger";
+import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
import { _t } from "../../../../languageHandler";
-import { DevicesDictionary, DeviceWithVerification } from "./types";
+import { getDeviceClientInformation } from "../../../../utils/device/clientInformation";
+import { DevicesDictionary, ExtendedDevice, ExtendedDeviceAppInfo } from "./types";
+import { useEventEmitter } from "../../../../hooks/useEventEmitter";
+import { parseUserAgent } from "../../../../utils/device/parseUserAgent";
const isDeviceVerified = (
matrixClient: MatrixClient,
@@ -51,6 +65,16 @@ const isDeviceVerified = (
}
};
+const parseDeviceExtendedInformation = (matrixClient: MatrixClient, device: IMyDevice): ExtendedDeviceAppInfo => {
+ const { name, version, url } = getDeviceClientInformation(matrixClient, device.device_id);
+
+ return {
+ appName: name,
+ appVersion: version,
+ url,
+ };
+};
+
const fetchDevicesWithVerification = async (
matrixClient: MatrixClient,
userId: string,
@@ -64,6 +88,8 @@ const fetchDevicesWithVerification = async (
[device.device_id]: {
...device,
isVerified: isDeviceVerified(matrixClient, crossSigningInfo, device),
+ ...parseDeviceExtendedInformation(matrixClient, device),
+ ...parseUserAgent(device[UNSTABLE_MSC3852_LAST_SEEN_UA.name]),
},
}), {});
@@ -77,13 +103,14 @@ export enum OwnDevicesError {
export type DevicesState = {
devices: DevicesDictionary;
pushers: IPusher[];
+ localNotificationSettings: Map;
currentDeviceId: string;
isLoadingDeviceList: boolean;
// not provided when current session cannot request verification
- requestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => Promise;
+ requestDeviceVerification?: (deviceId: ExtendedDevice['device_id']) => Promise;
refreshDevices: () => Promise;
- saveDeviceName: (deviceId: DeviceWithVerification['device_id'], deviceName: string) => Promise;
- setPusherEnabled: (deviceId: DeviceWithVerification['device_id'], enabled: boolean) => Promise;
+ saveDeviceName: (deviceId: ExtendedDevice['device_id'], deviceName: string) => Promise;
+ setPushNotifications: (deviceId: ExtendedDevice['device_id'], enabled: boolean) => Promise;
error?: OwnDevicesError;
supportsMSC3881?: boolean | undefined;
};
@@ -95,6 +122,8 @@ export const useOwnDevices = (): DevicesState => {
const [devices, setDevices] = useState({});
const [pushers, setPushers] = useState([]);
+ const [localNotificationSettings, setLocalNotificationSettings]
+ = useState(new Map());
const [isLoadingDeviceList, setIsLoadingDeviceList] = useState(true);
const [supportsMSC3881, setSupportsMSC3881] = useState(true); // optimisticly saying yes!
@@ -120,6 +149,19 @@ export const useOwnDevices = (): DevicesState => {
const { pushers } = await matrixClient.getPushers();
setPushers(pushers);
+ const notificationSettings = new Map();
+ Object.keys(devices).forEach((deviceId) => {
+ const eventType = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
+ const event = matrixClient.getAccountData(eventType);
+ if (event) {
+ notificationSettings.set(
+ deviceId,
+ event.getContent(),
+ );
+ }
+ });
+ setLocalNotificationSettings(notificationSettings);
+
setIsLoadingDeviceList(false);
} catch (error) {
if ((error as MatrixError).httpStatus == 404) {
@@ -137,10 +179,20 @@ export const useOwnDevices = (): DevicesState => {
refreshDevices();
}, [refreshDevices]);
+ useEventEmitter(matrixClient, ClientEvent.AccountData, (event: MatrixEvent): void => {
+ const type = event.getType();
+ if (type.startsWith(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) {
+ const newSettings = new Map(localNotificationSettings);
+ const deviceId = type.slice(type.lastIndexOf(".") + 1);
+ newSettings.set(deviceId, event.getContent());
+ setLocalNotificationSettings(newSettings);
+ }
+ });
+
const isCurrentDeviceVerified = !!devices[currentDeviceId]?.isVerified;
const requestDeviceVerification = isCurrentDeviceVerified && userId
- ? async (deviceId: DeviceWithVerification['device_id']) => {
+ ? async (deviceId: ExtendedDevice['device_id']) => {
return await matrixClient.requestVerification(
userId,
[deviceId],
@@ -149,7 +201,7 @@ export const useOwnDevices = (): DevicesState => {
: undefined;
const saveDeviceName = useCallback(
- async (deviceId: DeviceWithVerification['device_id'], deviceName: string): Promise => {
+ async (deviceId: ExtendedDevice['device_id'], deviceName: string): Promise => {
const device = devices[deviceId];
// no change
@@ -169,32 +221,40 @@ export const useOwnDevices = (): DevicesState => {
}
}, [matrixClient, devices, refreshDevices]);
- const setPusherEnabled = useCallback(
- async (deviceId: DeviceWithVerification['device_id'], enabled: boolean): Promise => {
- const pusher = pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === deviceId);
+ const setPushNotifications = useCallback(
+ async (deviceId: ExtendedDevice['device_id'], enabled: boolean): Promise => {
try {
- await matrixClient.setPusher({
- ...pusher,
- [PUSHER_ENABLED.name]: enabled,
- });
- await refreshDevices();
+ const pusher = pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === deviceId);
+ if (pusher) {
+ await matrixClient.setPusher({
+ ...pusher,
+ [PUSHER_ENABLED.name]: enabled,
+ });
+ } else if (localNotificationSettings.has(deviceId)) {
+ await matrixClient.setLocalNotificationSettings(deviceId, {
+ is_silenced: !enabled,
+ });
+ }
} catch (error) {
logger.error("Error setting pusher state", error);
throw new Error(_t("Failed to set pusher state"));
+ } finally {
+ await refreshDevices();
}
- }, [matrixClient, pushers, refreshDevices],
+ }, [matrixClient, pushers, localNotificationSettings, refreshDevices],
);
return {
devices,
pushers,
+ localNotificationSettings,
currentDeviceId,
isLoadingDeviceList,
error,
requestDeviceVerification,
refreshDevices,
saveDeviceName,
- setPusherEnabled,
+ setPushNotifications,
supportsMSC3881,
};
};
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx
index 643a137306..1519e39a0d 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx
@@ -125,9 +125,12 @@ export default class PreferencesUserSettingsTab extends React.Component {
+ const cli = MatrixClientPeg.get();
+
this.setState({
disablingReadReceiptsSupported: (
- await MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285.stable")
+ (await cli.doesServerSupportUnstableFeature("org.matrix.msc2285.stable"))
+ || (await cli.isVersionSupported("v1.4"))
),
});
}
diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx
index 177c3b4f5e..91b448eb3b 100644
--- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx
@@ -319,6 +319,12 @@ export default class SecurityUserSettingsTab extends React.Component
) }
+