Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Device manager - scroll to filtered list from security recommendation…
Browse files Browse the repository at this point in the history
…s (PSG-640) (#9227)

* scroll to filtered list from security recommendations

* test sessionmanager scroll to

* stable snapshot

* fix strict errors

* prtidy

* use smooth scrolling
  • Loading branch information
Kerry authored Aug 31, 2022
1 parent 0d6a550 commit 54a66bd
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 94 deletions.
133 changes: 66 additions & 67 deletions src/components/views/settings/devices/FilteredDeviceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

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);
};

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>
;
};
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 onFilterOptionChange = (filterId: DeviceFilterKey) => {
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as DeviceSecurityVariation);
};

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;
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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')}
Expand All @@ -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>
Expand All @@ -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>
Expand Down
29 changes: 25 additions & 4 deletions src/components/views/settings/tabs/user/SessionManagerTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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)) {
Expand All @@ -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}
Expand All @@ -63,6 +83,7 @@ const SessionManagerTab: React.FC = () => {
expandedDeviceIds={expandedDeviceIds}
onFilterChange={setFilter}
onDeviceExpandToggle={onDeviceExpandToggle}
ref={filteredDeviceListRef}
/>
</SettingsSubsection>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import { act, fireEvent, render } from '@testing-library/react';

import FilteredDeviceList from '../../../../../src/components/views/settings/devices/FilteredDeviceList';
import { FilteredDeviceList } from '../../../../../src/components/views/settings/devices/FilteredDeviceList';
import { DeviceSecurityVariation } from '../../../../../src/components/views/settings/devices/types';
import { flushPromises, mockPlatformPeg } from '../../../../test-utils';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ limitations under the License.
*/

import React from 'react';
import { render } from '@testing-library/react';
import { act, fireEvent, render } from '@testing-library/react';

import SecurityRecommendations from '../../../../../src/components/views/settings/devices/SecurityRecommendations';
import { DeviceSecurityVariation } from '../../../../../src/components/views/settings/devices/types';

const MS_DAY = 24 * 60 * 60 * 1000;
describe('<SecurityRecommendations />', () => {
Expand All @@ -32,6 +33,7 @@ describe('<SecurityRecommendations />', () => {

const defaultProps = {
devices: {},
goToFilteredList: jest.fn(),
};
const getComponent = (props = {}) =>
(<SecurityRecommendations {...defaultProps} {...props} />);
Expand Down Expand Up @@ -69,4 +71,36 @@ describe('<SecurityRecommendations />', () => {
const { container } = render(getComponent({ devices }));
expect(container).toMatchSnapshot();
});

it('clicking view all unverified devices button works', () => {
const goToFilteredList = jest.fn();
const devices = {
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
[hundredDaysOld.device_id]: hundredDaysOld,
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
};
const { getByTestId } = render(getComponent({ devices, goToFilteredList }));

act(() => {
fireEvent.click(getByTestId('unverified-devices-cta'));
});

expect(goToFilteredList).toHaveBeenCalledWith(DeviceSecurityVariation.Unverified);
});

it('clicking view all inactive devices button works', () => {
const goToFilteredList = jest.fn();
const devices = {
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
[hundredDaysOld.device_id]: hundredDaysOld,
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
};
const { getByTestId } = render(getComponent({ devices, goToFilteredList }));

act(() => {
fireEvent.click(getByTestId('inactive-devices-cta'));
});

expect(goToFilteredList).toHaveBeenCalledWith(DeviceSecurityVariation.Inactive);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ exports[`<SecurityRecommendations /> renders both cards when user has both unver
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="unverified-devices-cta"
role="button"
tabindex="0"
>
Expand Down Expand Up @@ -88,6 +89,7 @@ exports[`<SecurityRecommendations /> renders both cards when user has both unver
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="inactive-devices-cta"
role="button"
tabindex="0"
>
Expand Down Expand Up @@ -149,6 +151,7 @@ exports[`<SecurityRecommendations /> renders inactive devices section when user
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="unverified-devices-cta"
role="button"
tabindex="0"
>
Expand Down Expand Up @@ -189,6 +192,7 @@ exports[`<SecurityRecommendations /> renders inactive devices section when user
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="inactive-devices-cta"
role="button"
tabindex="0"
>
Expand Down Expand Up @@ -250,6 +254,7 @@ exports[`<SecurityRecommendations /> renders unverified devices section when use
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="unverified-devices-cta"
role="button"
tabindex="0"
>
Expand Down Expand Up @@ -290,6 +295,7 @@ exports[`<SecurityRecommendations /> renders unverified devices section when use
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="inactive-devices-cta"
role="button"
tabindex="0"
>
Expand Down
Loading

0 comments on commit 54a66bd

Please sign in to comment.