From 96e138a861c321e6a1eeec7a6a917cdb356bffef Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Wed, 14 Sep 2022 17:39:27 +0200 Subject: [PATCH] add loading state --- .../devices/_DeviceDetailHeading.pcss | 7 +++ .../settings/devices/CurrentDeviceSection.tsx | 7 ++- .../settings/devices/DeviceDetailHeading.tsx | 25 +++++++--- .../views/settings/devices/DeviceDetails.tsx | 4 +- .../views/settings/devices/useOwnDevices.ts | 46 ++++++++++++++++--- .../settings/tabs/user/SessionManagerTab.tsx | 7 ++- .../DeviceDetailHeading-test.tsx.snap | 2 + 7 files changed, 80 insertions(+), 18 deletions(-) diff --git a/res/css/components/views/settings/devices/_DeviceDetailHeading.pcss b/res/css/components/views/settings/devices/_DeviceDetailHeading.pcss index 06c2f3882bbc..107eefdb6e53 100644 --- a/res/css/components/views/settings/devices/_DeviceDetailHeading.pcss +++ b/res/css/components/views/settings/devices/_DeviceDetailHeading.pcss @@ -33,7 +33,14 @@ limitations under the License. } .mx_DeviceDetailHeading_renameFormButtons { + display: flex; + flex-direction: row; gap: $spacing-8; + + .mx_Spinner { + width: auto; + flex-grow: 0; + } } .mx_DeviceDetailHeading_renameFormInput { diff --git a/src/components/views/settings/devices/CurrentDeviceSection.tsx b/src/components/views/settings/devices/CurrentDeviceSection.tsx index e720b47ede97..b4cc39a8e3c6 100644 --- a/src/components/views/settings/devices/CurrentDeviceSection.tsx +++ b/src/components/views/settings/devices/CurrentDeviceSection.tsx @@ -31,6 +31,7 @@ interface Props { isSigningOut: boolean; onVerifyCurrentDevice: () => void; onSignOutCurrentDevice: () => void; + onSaveDeviceName: (deviceName: string) => Promise; } const CurrentDeviceSection: React.FC = ({ @@ -39,6 +40,7 @@ const CurrentDeviceSection: React.FC = ({ isSigningOut, onVerifyCurrentDevice, onSignOutCurrentDevice, + onSaveDeviceName, }) => { const [isExpanded, setIsExpanded] = useState(false); @@ -46,7 +48,8 @@ const CurrentDeviceSection: React.FC = ({ heading={_t('Current session')} data-testid='current-session-section' > - { isLoading && } + { /* only show big spinner on first load */ } + { isLoading && !device && } { !!device && <> = ({ }
diff --git a/src/components/views/settings/devices/DeviceDetailHeading.tsx b/src/components/views/settings/devices/DeviceDetailHeading.tsx index c1c959ad552c..2e6c88bb68e7 100644 --- a/src/components/views/settings/devices/DeviceDetailHeading.tsx +++ b/src/components/views/settings/devices/DeviceDetailHeading.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useEffect, useState } from 'react'; +import React, { FormEvent, useEffect, useState } from 'react'; import { _t } from '../../../../languageHandler'; import AccessibleButton from '../../elements/AccessibleButton'; @@ -27,7 +27,7 @@ import { DeviceWithVerification } from './types'; interface Props { device: DeviceWithVerification; isLoading: boolean; - saveDeviceName: (deviceName: string) => void; + saveDeviceName: (deviceName: string) => Promise; } const DeviceNameEditor: React.FC void }> = ({ @@ -37,19 +37,26 @@ const DeviceNameEditor: React.FC void }> = ({ useEffect(() => { setDeviceName(device.display_name); - }, [device]); + }, [device.display_name]); const onInputChange = (event: React.ChangeEvent): void => setDeviceName(event.target.value); - const onSubmit = () => saveDeviceName(deviceName); + const onSubmit = async (event: FormEvent) => { + event.preventDefault(); + await saveDeviceName(deviceName); + stopEditing(); + }; + const headingId = `device-rename-${device.device_id}`; const descriptionId = `device-rename-description-${device.device_id}`; return
+ onSubmit={onSubmit} + method="post" + >

void }> = ({ aria-labelledby={headingId} aria-describedby={descriptionId} className="mx_DeviceDetailHeading_renameFormInput" + maxLength={100} /> void }> = ({ > { _t('Save') } - { isLoading && } { _t('Cancel') } + > + { _t('Cancel') } + + { isLoading && } ; }; @@ -99,6 +109,7 @@ export const DeviceDetailHeading: React.FC = ({ device, isLoading, saveDeviceName, }) => { const [isEditing, setIsEditing] = useState(false); + return isEditing ? void; onSignOutDevice: () => void; onSetDeviceName: (deviceName: string) => void; @@ -40,6 +41,7 @@ interface MetadataTable { const DeviceDetails: React.FC = ({ device, isSigningOut, + isLoading, onVerifyDevice, onSignOutDevice, onSaveDeviceName, @@ -65,7 +67,7 @@ const DeviceDetails: React.FC = ({

Promise; refreshDevices: () => Promise; + saveDeviceName: (deviceId: DeviceWithVerification['device_id'], deviceName: string) => Promise; error?: OwnDevicesError; }; export const useOwnDevices = (): DevicesState => { @@ -89,11 +93,14 @@ export const useOwnDevices = (): DevicesState => { const userId = matrixClient.getUserId(); const [devices, setDevices] = useState({}); - const [isLoading, setIsLoading] = useState(true); + const [isLoadingDeviceList, setIsLoadingDeviceList] = useState(true); + + // device ids with pending requests + const [pendingDeviceIds, setPendingDeviceIds] = useState([]); const [error, setError] = useState(); const refreshDevices = useCallback(async () => { - setIsLoading(true); + setIsLoadingDeviceList(true); try { // realistically we should never hit this // but it satisfies types @@ -102,7 +109,7 @@ export const useOwnDevices = (): DevicesState => { } const devices = await fetchDevicesWithVerification(matrixClient, userId); setDevices(devices); - setIsLoading(false); + setIsLoadingDeviceList(false); } catch (error) { if ((error as MatrixError).httpStatus == 404) { // 404 probably means the HS doesn't yet support the API. @@ -111,7 +118,7 @@ export const useOwnDevices = (): DevicesState => { logger.error("Error loading sessions:", error); setError(OwnDevicesError.Default); } - setIsLoading(false); + setIsLoadingDeviceList(false); } }, [matrixClient, userId]); @@ -130,12 +137,37 @@ export const useOwnDevices = (): DevicesState => { } : undefined; + const saveDeviceName = useCallback( + async (deviceId: DeviceWithVerification['device_id'], deviceName: string): Promise => { + const device = devices[deviceId]; + + // no change + if (deviceName === device?.display_name) { + return; + } + + try { + setPendingDeviceIds(p => ([...p, deviceId])); + 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")); + } + setPendingDeviceIds(p => p.filter(id => id !== deviceId)); + }, [matrixClient, devices, refreshDevices, setPendingDeviceIds]); + return { devices, currentDeviceId, + isLoadingDeviceList, + error, + pendingDeviceIds, requestDeviceVerification, refreshDevices, - isLoading, - error, + saveDeviceName, }; }; diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 0b2056b63dcc..81684b9da6fd 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -88,9 +88,11 @@ const SessionManagerTab: React.FC = () => { const { devices, currentDeviceId, - isLoading, + isLoadingDeviceList, + pendingDeviceIds, requestDeviceVerification, refreshDevices, + saveDeviceName, } = useOwnDevices(); const [filter, setFilter] = useState(); const [expandedDeviceIds, setExpandedDeviceIds] = useState([]); @@ -167,8 +169,9 @@ const SessionManagerTab: React.FC = () => { /> saveDeviceName(currentDevice.device_id, deviceName)} onVerifyCurrentDevice={onVerifyCurrentDevice} onSignOutCurrentDevice={onSignOutCurrentDevice} /> diff --git a/test/components/views/settings/devices/__snapshots__/DeviceDetailHeading-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/DeviceDetailHeading-test.tsx.snap index 9f52f02f4c19..cae06d101a7a 100644 --- a/test/components/views/settings/devices/__snapshots__/DeviceDetailHeading-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/DeviceDetailHeading-test.tsx.snap @@ -6,6 +6,7 @@ Object {