diff --git a/src/components/views/settings/devices/CurrentDeviceSection.tsx b/src/components/views/settings/devices/CurrentDeviceSection.tsx index 023d33b083..615c9c69f0 100644 --- a/src/components/views/settings/devices/CurrentDeviceSection.tsx +++ b/src/components/views/settings/devices/CurrentDeviceSection.tsx @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { LocalNotificationSettings } from 'matrix-js-sdk/src/@types/local_notifications'; import React, { useState } from 'react'; import { _t } from '../../../../languageHandler'; @@ -29,6 +30,8 @@ interface Props { device?: DeviceWithVerification; isLoading: boolean; isSigningOut: boolean; + localNotificationSettings?: LocalNotificationSettings | undefined; + setPushNotifications?: (deviceId: string, enabled: boolean) => Promise | undefined; onVerifyCurrentDevice: () => void; onSignOutCurrentDevice: () => void; saveDeviceName: (deviceName: string) => Promise; @@ -38,6 +41,8 @@ const CurrentDeviceSection: React.FC = ({ device, isLoading, isSigningOut, + localNotificationSettings, + setPushNotifications, onVerifyCurrentDevice, onSignOutCurrentDevice, saveDeviceName, @@ -63,6 +68,8 @@ const CurrentDeviceSection: React.FC = ({ { isExpanded && void; onSignOutDevice: () => void; saveDeviceName: (deviceName: string) => Promise; - setPusherEnabled?: (deviceId: string, enabled: boolean) => Promise | undefined; + setPushNotifications?: (deviceId: string, enabled: boolean) => Promise | undefined; supportsMSC3881?: boolean | undefined; } @@ -46,11 +48,12 @@ interface MetadataTable { const DeviceDetails: React.FC = ({ device, pusher, + localNotificationSettings, isSigningOut, onVerifyDevice, onSignOutDevice, saveDeviceName, - setPusherEnabled, + setPushNotifications, supportsMSC3881, }) => { const metadata: MetadataTable[] = [ @@ -70,6 +73,21 @@ const DeviceDetails: React.FC = ({ ], }, ]; + + const showPushNotificationSection = !!pusher || !!localNotificationSettings; + + function isPushNotificationsEnabled(pusher: IPusher, notificationSettings: LocalNotificationSettings): boolean { + if (pusher) return pusher[PUSHER_ENABLED.name]; + if (localNotificationSettings) return !localNotificationSettings.is_silenced; + return true; + } + + function isCheckboxDisabled(pusher: IPusher, notificationSettings: LocalNotificationSettings): boolean { + if (localNotificationSettings) return false; + if (pusher && !supportsMSC3881) return true; + return false; + } + return
= ({ , ) }
- { pusher && ( + { showPushNotificationSection && (
= ({ setPusherEnabled?.(device.device_id, checked)} + checked={isPushNotificationsEnabled(pusher, localNotificationSettings)} + disabled={isCheckboxDisabled(pusher, localNotificationSettings)} + onChange={checked => setPushNotifications?.(device.device_id, checked)} aria-label={_t("Toggle push notifications on this session.")} data-testid='device-detail-push-notification-checkbox' /> diff --git a/src/components/views/settings/devices/FilteredDeviceList.tsx b/src/components/views/settings/devices/FilteredDeviceList.tsx index baee4e1d0e..7affee684c 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'; @@ -39,6 +40,7 @@ import { DevicesState } from './useOwnDevices'; interface Props { devices: DevicesDictionary; pushers: IPusher[]; + localNotificationSettings: Map; expandedDeviceIds: DeviceWithVerification['device_id'][]; signingOutDeviceIds: DeviceWithVerification['device_id'][]; filter?: DeviceSecurityVariation; @@ -47,7 +49,7 @@ interface Props { onSignOutDevices: (deviceIds: DeviceWithVerification['device_id'][]) => void; saveDeviceName: DevicesState['saveDeviceName']; onRequestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => void; - setPusherEnabled: (deviceId: string, enabled: boolean) => Promise; + setPushNotifications: (deviceId: string, enabled: boolean) => Promise; supportsMSC3881?: boolean | undefined; } @@ -141,24 +143,26 @@ const NoResults: React.FC = ({ filter, clearFilter }) => const DeviceListItem: React.FC<{ device: DeviceWithVerification; pusher?: IPusher | undefined; + localNotificationSettings?: LocalNotificationSettings | undefined; isExpanded: boolean; isSigningOut: boolean; onDeviceExpandToggle: () => void; onSignOutDevice: () => void; saveDeviceName: (deviceName: string) => Promise; onRequestDeviceVerification?: () => void; - setPusherEnabled: (deviceId: string, enabled: boolean) => Promise; + setPushNotifications: (deviceId: string, enabled: boolean) => Promise; supportsMSC3881?: boolean | undefined; }> = ({ device, pusher, + localNotificationSettings, isExpanded, isSigningOut, onDeviceExpandToggle, onSignOutDevice, saveDeviceName, onRequestDeviceVerification, - setPusherEnabled, + setPushNotifications, supportsMSC3881, }) =>
  • } @@ -192,6 +197,7 @@ export const FilteredDeviceList = forwardRef(({ devices, pushers, + localNotificationSettings, filter, expandedDeviceIds, signingOutDeviceIds, @@ -200,7 +206,7 @@ export const FilteredDeviceList = saveDeviceName, onSignOutDevices, onRequestDeviceVerification, - setPusherEnabled, + setPushNotifications, supportsMSC3881, }: Props, ref: ForwardedRef) => { const sortedDevices = getFilteredSortedDevices(devices, filter); @@ -258,6 +264,7 @@ 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)} onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)} @@ -268,7 +275,7 @@ export const FilteredDeviceList = ? () => onRequestDeviceVerification(device.device_id) : undefined } - setPusherEnabled={setPusherEnabled} + setPushNotifications={setPushNotifications} supportsMSC3881={supportsMSC3881} />, ) } diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts index b583d4c080..255bcc2d53 100644 --- a/src/components/views/settings/devices/useOwnDevices.ts +++ b/src/components/views/settings/devices/useOwnDevices.ts @@ -15,11 +15,19 @@ 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 { + IMyDevice, + IPusher, + LOCAL_NOTIFICATION_SETTINGS_PREFIX, + MatrixClient, + PUSHER_DEVICE_ID, + PUSHER_ENABLED, +} 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"; @@ -77,13 +85,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; refreshDevices: () => Promise; saveDeviceName: (deviceId: DeviceWithVerification['device_id'], deviceName: string) => Promise; - setPusherEnabled: (deviceId: DeviceWithVerification['device_id'], enabled: boolean) => Promise; + setPushNotifications: (deviceId: DeviceWithVerification['device_id'], enabled: boolean) => Promise; error?: OwnDevicesError; supportsMSC3881?: boolean | undefined; }; @@ -95,6 +104,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 +131,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) { @@ -169,32 +193,40 @@ export const useOwnDevices = (): DevicesState => { } }, [matrixClient, devices, refreshDevices]); - const setPusherEnabled = useCallback( + const setPushNotifications = useCallback( async (deviceId: DeviceWithVerification['device_id'], enabled: boolean): Promise => { - const pusher = pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === deviceId); 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/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index ec2b7e8a61..e87d548d57 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -88,12 +88,13 @@ const SessionManagerTab: React.FC = () => { const { devices, pushers, + localNotificationSettings, currentDeviceId, isLoadingDeviceList, requestDeviceVerification, refreshDevices, saveDeviceName, - setPusherEnabled, + setPushNotifications, supportsMSC3881, } = useOwnDevices(); const [filter, setFilter] = useState(); @@ -171,9 +172,11 @@ const SessionManagerTab: React.FC = () => { /> saveDeviceName(currentDevice?.device_id, deviceName)} + saveDeviceName={(deviceName) => saveDeviceName(currentDeviceId, deviceName)} onVerifyCurrentDevice={onVerifyCurrentDevice} onSignOutCurrentDevice={onSignOutCurrentDevice} /> @@ -190,6 +193,7 @@ const SessionManagerTab: React.FC = () => { { onRequestDeviceVerification={requestDeviceVerification ? onTriggerDeviceVerification : undefined} onSignOutDevices={onSignOutOtherDevices} saveDeviceName={saveDeviceName} - setPusherEnabled={setPusherEnabled} + setPushNotifications={setPushNotifications} ref={filteredDeviceListRef} supportsMSC3881={supportsMSC3881} /> diff --git a/test/components/views/settings/devices/DeviceDetails-test.tsx b/test/components/views/settings/devices/DeviceDetails-test.tsx index 0cec7f387b..bb088e6000 100644 --- a/test/components/views/settings/devices/DeviceDetails-test.tsx +++ b/test/components/views/settings/devices/DeviceDetails-test.tsx @@ -33,7 +33,7 @@ describe('', () => { isLoading: false, onSignOutDevice: jest.fn(), saveDeviceName: jest.fn(), - setPusherEnabled: jest.fn(), + setPushNotifications: jest.fn(), supportsMSC3881: true, }; @@ -157,6 +157,27 @@ describe('', () => { fireEvent.click(checkbox); - expect(defaultProps.setPusherEnabled).toHaveBeenCalledWith(device.device_id, !enabled); + expect(defaultProps.setPushNotifications).toHaveBeenCalledWith(device.device_id, !enabled); + }); + + it('changes the local notifications settings status when clicked', () => { + const device = { + ...baseDevice, + }; + + const enabled = false; + + const { getByTestId } = render(getComponent({ + device, + localNotificationSettings: { + is_silenced: !enabled, + }, + isSigningOut: true, + })); + + const checkbox = getByTestId('device-detail-push-notification-checkbox'); + fireEvent.click(checkbox); + + expect(defaultProps.setPushNotifications).toHaveBeenCalledWith(device.device_id, !enabled); }); }); diff --git a/test/components/views/settings/devices/FilteredDeviceList-test.tsx b/test/components/views/settings/devices/FilteredDeviceList-test.tsx index 181c435b60..4a57565e19 100644 --- a/test/components/views/settings/devices/FilteredDeviceList-test.tsx +++ b/test/components/views/settings/devices/FilteredDeviceList-test.tsx @@ -45,9 +45,10 @@ describe('', () => { onDeviceExpandToggle: jest.fn(), onSignOutDevices: jest.fn(), saveDeviceName: jest.fn(), - setPusherEnabled: jest.fn(), + setPushNotifications: jest.fn(), expandedDeviceIds: [], signingOutDeviceIds: [], + localNotificationSettings: new Map(), devices: { [unverifiedNoMetadata.device_id]: unverifiedNoMetadata, [verifiedNoMetadata.device_id]: verifiedNoMetadata, diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index 12af8a18e0..5c1d5586aa 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -22,7 +22,13 @@ import { logger } from 'matrix-js-sdk/src/logger'; import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; import { VerificationRequest } from 'matrix-js-sdk/src/crypto/verification/request/VerificationRequest'; import { sleep } from 'matrix-js-sdk/src/utils'; -import { IMyDevice, PUSHER_DEVICE_ID, PUSHER_ENABLED } from 'matrix-js-sdk/src/matrix'; +import { + IMyDevice, + LOCAL_NOTIFICATION_SETTINGS_PREFIX, + MatrixEvent, + PUSHER_DEVICE_ID, + PUSHER_ENABLED, +} from 'matrix-js-sdk/src/matrix'; import SessionManagerTab from '../../../../../../src/components/views/settings/tabs/user/SessionManagerTab'; import MatrixClientContext from '../../../../../../src/contexts/MatrixClientContext'; @@ -71,6 +77,8 @@ describe('', () => { doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(true), getPushers: jest.fn(), setPusher: jest.fn(), + getAccountData: jest.fn(), + setLocalNotificationSettings: jest.fn(), }); const defaultProps = {}; @@ -114,6 +122,19 @@ describe('', () => { [PUSHER_ENABLED.name]: true, })], }); + + mockClient.getAccountData + .mockReset() + .mockImplementation(eventType => { + if (eventType.startsWith(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) { + return new MatrixEvent({ + type: eventType, + content: { + is_silenced: false, + }, + }); + } + }); }); it('renders spinner while devices load', () => { @@ -333,7 +354,6 @@ describe('', () => { mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); mockCrossSigningInfo.checkDeviceTrust .mockImplementation((_userId, { deviceId }) => { - console.log('hhh', deviceId); if (deviceId === alicesDevice.device_id) { return new DeviceTrustLevel(true, true, false, false); } @@ -363,7 +383,6 @@ describe('', () => { mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId)); mockCrossSigningInfo.checkDeviceTrust .mockImplementation((_userId, { deviceId }) => { - console.log('hhh', deviceId); if (deviceId === alicesDevice.device_id) { return new DeviceTrustLevel(true, true, false, false); } @@ -702,4 +721,28 @@ describe('', () => { expect(mockClient.setPusher).toHaveBeenCalled(); }); + + it("lets you change the local notification settings state", async () => { + const { getByTestId } = render(getComponent()); + + await act(async () => { + await flushPromisesWithFakeTimers(); + }); + + toggleDeviceDetails(getByTestId, alicesDevice.device_id); + + // device details are expanded + expect(getByTestId(`device-detail-${alicesDevice.device_id}`)).toBeTruthy(); + expect(getByTestId('device-detail-push-notification')).toBeTruthy(); + + const checkbox = getByTestId('device-detail-push-notification-checkbox'); + + expect(checkbox).toBeTruthy(); + fireEvent.click(checkbox); + + expect(mockClient.setLocalNotificationSettings).toHaveBeenCalledWith( + alicesDevice.device_id, + { is_silenced: true }, + ); + }); });