diff --git a/res/css/components/views/settings/devices/_DeviceDetails.pcss b/res/css/components/views/settings/devices/_DeviceDetails.pcss index 76cacfa1c95..3017935bb7b 100644 --- a/res/css/components/views/settings/devices/_DeviceDetails.pcss +++ b/res/css/components/views/settings/devices/_DeviceDetails.pcss @@ -32,6 +32,9 @@ limitations under the License. margin-bottom: $spacing-16; border-bottom: 1px solid $quinary-content; + display: grid; + grid-gap: $spacing-16; + &:last-child { padding-bottom: 0; border-bottom: 0; @@ -48,7 +51,6 @@ limitations under the License. color: $secondary-content; width: 100%; - margin-top: $spacing-20; border-spacing: 0; diff --git a/res/css/components/views/settings/devices/_FilteredDeviceList.pcss b/res/css/components/views/settings/devices/_FilteredDeviceList.pcss index e0deb5546d5..01c8df787ef 100644 --- a/res/css/components/views/settings/devices/_FilteredDeviceList.pcss +++ b/res/css/components/views/settings/devices/_FilteredDeviceList.pcss @@ -48,6 +48,11 @@ limitations under the License. padding: 0 $spacing-8; } +.mx_FilteredDeviceList_listItem { + display: flex; + flex-direction: column; +} + .mx_FilteredDeviceList_securityCard { margin-bottom: $spacing-32; } diff --git a/src/components/views/settings/devices/DeviceDetails.tsx b/src/components/views/settings/devices/DeviceDetails.tsx index e18c9ee24f8..5a58efaa887 100644 --- a/src/components/views/settings/devices/DeviceDetails.tsx +++ b/src/components/views/settings/devices/DeviceDetails.tsx @@ -49,7 +49,7 @@ const DeviceDetails: React.FC = ({ device }) => { ], }, ]; - return
+ return
{ device.display_name ?? device.device_id } diff --git a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx index 57a8c35e940..a0293fec64f 100644 --- a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx +++ b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx @@ -18,6 +18,7 @@ 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 AccessibleButton from '../../elements/AccessibleButton'; interface Props { @@ -28,6 +29,7 @@ interface Props { const DeviceExpandDetailsButton: React.FC = ({ isExpanded, onClick, ...rest }) => { return void; + onDeviceExpandToggle: (deviceId: DeviceWithVerification['device_id']) => void; } // devices without timestamp metadata should be sorted last @@ -123,11 +127,35 @@ const NoResults: React.FC = ({ filter, clearFilter }) => }
; +const DeviceListItem: React.FC<{ + device: DeviceWithVerification; + isExpanded: boolean; + onDeviceExpandToggle: () => void; +}> = ({ + device, isExpanded, onDeviceExpandToggle, +}) =>
  • + + + + { isExpanded && } +
  • ; + /** * Filtered list of devices * Sorted by latest activity descending */ -const FilteredDeviceList: React.FC = ({ devices, filter, onFilterChange }) => { +const FilteredDeviceList: React.FC = ({ + devices, + filter, + expandedDeviceIds, + onFilterChange, + onDeviceExpandToggle, +}) => { const sortedDevices = getFilteredSortedDevices(devices, filter); const options = [ @@ -177,13 +205,12 @@ const FilteredDeviceList: React.FC = ({ devices, filter, onFilterChange } : onFilterChange(undefined)} /> }
      - { sortedDevices.map((device) => -
    1. - -
    2. , - + { sortedDevices.map((device) => onDeviceExpandToggle(device.device_id)} + />, ) }
    diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index ddd2293254f..c4878dbb372 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -22,12 +22,21 @@ import SettingsSubsection from '../../shared/SettingsSubsection'; import FilteredDeviceList from '../../devices/FilteredDeviceList'; import CurrentDeviceSection from '../../devices/CurrentDeviceSection'; import SecurityRecommendations from '../../devices/SecurityRecommendations'; -import { DeviceSecurityVariation } from '../../devices/types'; +import { DeviceSecurityVariation, DeviceWithVerification } from '../../devices/types'; import SettingsTab from '../SettingsTab'; const SessionManagerTab: React.FC = () => { const { devices, currentDeviceId, isLoading } = useOwnDevices(); const [filter, setFilter] = useState(); + const [expandedDeviceIds, setExpandedDeviceIds] = useState([]); + + const onDeviceExpandToggle = (deviceId: DeviceWithVerification['device_id']): void => { + if (expandedDeviceIds.includes(deviceId)) { + setExpandedDeviceIds(expandedDeviceIds.filter(id => id !== deviceId)); + } else { + setExpandedDeviceIds([...expandedDeviceIds, deviceId]); + } + }; const { [currentDeviceId]: currentDevice, ...otherDevices } = devices; const shouldShowOtherSessions = Object.keys(otherDevices).length > 0; @@ -51,7 +60,9 @@ const SessionManagerTab: React.FC = () => { } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index efc10360399..b417c0e6833 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1701,6 +1701,7 @@ "Device": "Device", "IP address": "IP address", "Session details": "Session details", + "Toggle device details": "Toggle device details", "Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days", "Verified": "Verified", "Unverified": "Unverified", diff --git a/test/components/views/settings/devices/FilteredDeviceList-test.tsx b/test/components/views/settings/devices/FilteredDeviceList-test.tsx index f814a6d7038..ecfbc0489d3 100644 --- a/test/components/views/settings/devices/FilteredDeviceList-test.tsx +++ b/test/components/views/settings/devices/FilteredDeviceList-test.tsx @@ -42,6 +42,8 @@ describe('', () => { }; const defaultProps = { onFilterChange: jest.fn(), + onDeviceExpandToggle: jest.fn(), + expandedDeviceIds: [], devices: { [unverifiedNoMetadata.device_id]: unverifiedNoMetadata, [verifiedNoMetadata.device_id]: verifiedNoMetadata, @@ -179,4 +181,27 @@ describe('', () => { expect(onFilterChange).toHaveBeenCalledWith(undefined); }); }); + + describe('device details', () => { + it('renders expanded devices with device details', () => { + const expandedDeviceIds = [newDevice.device_id, hundredDaysOld.device_id]; + const { container, getByTestId } = render(getComponent({ expandedDeviceIds })); + expect(container.getElementsByClassName('mx_DeviceDetails').length).toBeTruthy(); + expect(getByTestId(`device-detail-${newDevice.device_id}`)).toBeTruthy(); + expect(getByTestId(`device-detail-${hundredDaysOld.device_id}`)).toBeTruthy(); + }); + + it('clicking toggle calls onDeviceExpandToggle', () => { + const onDeviceExpandToggle = jest.fn(); + const { getByTestId } = render(getComponent({ onDeviceExpandToggle })); + + act(() => { + const tile = getByTestId(`device-tile-${hundredDaysOld.device_id}`); + const toggle = tile.querySelector('[aria-label="Toggle device details"]'); + fireEvent.click(toggle); + }); + + expect(onDeviceExpandToggle).toHaveBeenCalledWith(hundredDaysOld.device_id); + }); + }); }); diff --git a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap index 0d07f29e9f8..20c72fc9d38 100644 --- a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap @@ -4,6 +4,7 @@ exports[` displays device details on toggle click 1`] = HTMLCollection [
    renders device and correct security card when class="mx_DeviceTile_actions" >
    renders device and correct security card when class="mx_DeviceTile_actions" >
    renders a verified device 1`] = `
    renders device with metadata 1`] = `
    renders device without metadata 1`] = `
    renders when expanded 1`] = ` Object { "container":
    renders when not expanded 1`] = ` Object { "container":
    ', () => { expect(getByTestId('other-sessions-section')).toBeTruthy(); }); + + describe('device detail expansion', () => { + it('renders no devices expanded by default', async () => { + mockClient.getDevices.mockResolvedValue({ + devices: [alicesDevice, alicesOlderMobileDevice, alicesMobileDevice], + }); + const { getByTestId } = render(getComponent()); + + await act(async () => { + await flushPromisesWithFakeTimers(); + }); + + const otherSessionsSection = getByTestId('other-sessions-section'); + + // no expanded device details + expect(otherSessionsSection.getElementsByClassName('mx_DeviceDetails').length).toBeFalsy(); + }); + + it('toggles device expansion on click', async () => { + mockClient.getDevices.mockResolvedValue({ + devices: [alicesDevice, alicesOlderMobileDevice, alicesMobileDevice], + }); + const { getByTestId, queryByTestId } = render(getComponent()); + + await act(async () => { + await flushPromisesWithFakeTimers(); + }); + + act(() => { + const tile = getByTestId(`device-tile-${alicesOlderMobileDevice.device_id}`); + const toggle = tile.querySelector('[aria-label="Toggle device details"]'); + fireEvent.click(toggle); + }); + + // device details are expanded + expect(getByTestId(`device-detail-${alicesOlderMobileDevice.device_id}`)).toBeTruthy(); + + act(() => { + const tile = getByTestId(`device-tile-${alicesMobileDevice.device_id}`); + const toggle = tile.querySelector('[aria-label="Toggle device details"]'); + fireEvent.click(toggle); + }); + + // both device details are expanded + expect(getByTestId(`device-detail-${alicesOlderMobileDevice.device_id}`)).toBeTruthy(); + expect(getByTestId(`device-detail-${alicesMobileDevice.device_id}`)).toBeTruthy(); + + act(() => { + const tile = getByTestId(`device-tile-${alicesMobileDevice.device_id}`); + const toggle = tile.querySelector('[aria-label="Toggle device details"]'); + fireEvent.click(toggle); + }); + + // alicesMobileDevice was toggled off + expect(queryByTestId(`device-detail-${alicesMobileDevice.device_id}`)).toBeFalsy(); + // alicesOlderMobileDevice stayed open + expect(getByTestId(`device-detail-${alicesOlderMobileDevice.device_id}`)).toBeTruthy(); + }); + }); }); diff --git a/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap index bb2094aa53d..899b02e4159 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap @@ -41,6 +41,7 @@ exports[` renders current session section with a verified s class="mx_DeviceTile_actions" >
    renders current session section with an unverifie class="mx_DeviceTile_actions" >
    sets device verification status correctly 1`] = ` class="mx_DeviceTile_actions" >