Device manager - scroll to filtered list from security recommendations (PSG-640) (#9227)

* scroll to filtered list from security recommendations

* test sessionmanager scroll to

* stable snapshot

* fix strict errors

* prtidy

* use smooth scrolling
This commit is contained in:
Kerry 2022-08-31 14:34:22 +02:00 committed by GitHub
parent 0d6a550c33
commit 54a66bd242
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 201 additions and 91 deletions

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, { ForwardedRef, forwardRef } from 'react';
import { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton';
@ -150,70 +150,69 @@ const DeviceListItem: React.FC<{
* Filtered list of devices
* Sorted by latest activity descending
*/
const FilteredDeviceList: React.FC<Props> = ({
devices,
filter,
expandedDeviceIds,
onFilterChange,
onDeviceExpandToggle,
}) => {
const sortedDevices = getFilteredSortedDevices(devices, filter);
export const FilteredDeviceList =
forwardRef(({
devices,
filter,
expandedDeviceIds,
onFilterChange,
onDeviceExpandToggle,
}: Props, ref: ForwardedRef<HTMLDivElement>) => {
const sortedDevices = getFilteredSortedDevices(devices, filter);
const options: FilterDropdownOption<DeviceFilterKey>[] = [
{ id: ALL_FILTER_ID, label: _t('All') },
{
id: DeviceSecurityVariation.Verified,
label: _t('Verified'),
description: _t('Ready for secure messaging'),
},
{
id: DeviceSecurityVariation.Unverified,
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 },
),
},
];
const options: FilterDropdownOption<DeviceFilterKey>[] = [
{ id: ALL_FILTER_ID, label: _t('All') },
{
id: DeviceSecurityVariation.Verified,
label: _t('Verified'),
description: _t('Ready for secure messaging'),
},
{
id: DeviceSecurityVariation.Unverified,
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 },
),
},
];
const onFilterOptionChange = (filterId: DeviceFilterKey) => {
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as DeviceSecurityVariation);
};
const onFilterOptionChange = (filterId: DeviceFilterKey) => {
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as DeviceSecurityVariation);
};
return <div className='mx_FilteredDeviceList'>
<div className='mx_FilteredDeviceList_header'>
<span className='mx_FilteredDeviceList_headerLabel'>
{ _t('Sessions') }
</span>
<FilterDropdown<DeviceFilterKey>
id='device-list-filter'
label={_t('Filter devices')}
value={filter || ALL_FILTER_ID}
onOptionChange={onFilterOptionChange}
options={options}
selectedLabel={_t('Show')}
/>
</div>
{ !!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}
isExpanded={expandedDeviceIds.includes(device.device_id)}
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
/>,
) }
</ol>
</div>
;
};
return <div className='mx_FilteredDeviceList' ref={ref}>
<div className='mx_FilteredDeviceList_header'>
<span className='mx_FilteredDeviceList_headerLabel'>
{ _t('Sessions') }
</span>
<FilterDropdown<DeviceFilterKey>
id='device-list-filter'
label={_t('Filter devices')}
value={filter || ALL_FILTER_ID}
onOptionChange={onFilterOptionChange}
options={options}
selectedLabel={_t('Show')}
/>
</div>
{ !!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}
isExpanded={expandedDeviceIds.includes(device.device_id)}
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
/>,
) }
</ol>
</div>;
});
export default FilteredDeviceList;

View file

@ -29,9 +29,13 @@ import {
interface Props {
devices: DevicesDictionary;
goToFilteredList: (filter: DeviceSecurityVariation) => void;
}
const SecurityRecommendations: React.FC<Props> = ({ devices }) => {
const SecurityRecommendations: React.FC<Props> = ({
devices,
goToFilteredList,
}) => {
const devicesArray = Object.values<DeviceWithVerification>(devices);
const unverifiedDevicesCount = filterDevicesBySecurityRecommendation(
@ -49,9 +53,6 @@ const SecurityRecommendations: React.FC<Props> = ({ devices }) => {
const inactiveAgeDays = INACTIVE_DEVICE_AGE_DAYS;
// TODO(kerrya) stubbed until PSG-640/652
const noop = () => {};
return <SettingsSubsection
heading={_t('Security recommendations')}
description={_t('Improve your account security by following these recommendations')}
@ -69,7 +70,8 @@ const SecurityRecommendations: React.FC<Props> = ({ devices }) => {
>
<AccessibleButton
kind='link_inline'
onClick={noop}
onClick={() => goToFilteredList(DeviceSecurityVariation.Unverified)}
data-testid='unverified-devices-cta'
>
{ _t('View all') + ` (${unverifiedDevicesCount})` }
</AccessibleButton>
@ -90,7 +92,8 @@ const SecurityRecommendations: React.FC<Props> = ({ devices }) => {
>
<AccessibleButton
kind='link_inline'
onClick={noop}
onClick={() => goToFilteredList(DeviceSecurityVariation.Inactive)}
data-testid='inactive-devices-cta'
>
{ _t('View all') + ` (${inactiveDevicesCount})` }
</AccessibleButton>

View file

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { _t } from "../../../../../languageHandler";
import { useOwnDevices } from '../../devices/useOwnDevices';
import SettingsSubsection from '../../shared/SettingsSubsection';
import FilteredDeviceList from '../../devices/FilteredDeviceList';
import { FilteredDeviceList } from '../../devices/FilteredDeviceList';
import CurrentDeviceSection from '../../devices/CurrentDeviceSection';
import SecurityRecommendations from '../../devices/SecurityRecommendations';
import { DeviceSecurityVariation, DeviceWithVerification } from '../../devices/types';
@ -28,7 +28,9 @@ import SettingsTab from '../SettingsTab';
const SessionManagerTab: React.FC = () => {
const { devices, currentDeviceId, isLoading } = useOwnDevices();
const [filter, setFilter] = useState<DeviceSecurityVariation>();
const [expandedDeviceIds, setExpandedDeviceIds] = useState([]);
const [expandedDeviceIds, setExpandedDeviceIds] = useState<DeviceWithVerification['device_id'][]>([]);
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
const scrollIntoViewTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const onDeviceExpandToggle = (deviceId: DeviceWithVerification['device_id']): void => {
if (expandedDeviceIds.includes(deviceId)) {
@ -38,11 +40,29 @@ const SessionManagerTab: React.FC = () => {
}
};
const onGoToFilteredList = (filter: DeviceSecurityVariation) => {
setFilter(filter);
// @TODO(kerrya) clear selection when added in PSG-659
clearTimeout(scrollIntoViewTimeoutRef.current);
// wait a tick for the filtered section to rerender with different height
scrollIntoViewTimeoutRef.current =
window.setTimeout(() => filteredDeviceListRef.current?.scrollIntoView({
// align element to top of scrollbox
block: 'start',
inline: 'nearest',
behavior: 'smooth',
}));
};
const { [currentDeviceId]: currentDevice, ...otherDevices } = devices;
const shouldShowOtherSessions = Object.keys(otherDevices).length > 0;
useEffect(() => () => {
clearTimeout(scrollIntoViewTimeoutRef.current);
}, [scrollIntoViewTimeoutRef]);
return <SettingsTab heading={_t('Sessions')}>
<SecurityRecommendations devices={devices} />
<SecurityRecommendations devices={devices} goToFilteredList={onGoToFilteredList} />
<CurrentDeviceSection
device={currentDevice}
isLoading={isLoading}
@ -63,6 +83,7 @@ const SessionManagerTab: React.FC = () => {
expandedDeviceIds={expandedDeviceIds}
onFilterChange={setFilter}
onDeviceExpandToggle={onDeviceExpandToggle}
ref={filteredDeviceListRef}
/>
</SettingsSubsection>
}