Implement push notification toggle in device detail (#9308)

Co-authored-by: Travis Ralston <travisr@matrix.org>
This commit is contained in:
Germain 2022-09-27 13:35:54 +01:00 committed by GitHub
parent ace6591f43
commit 641cf28e4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 269 additions and 3 deletions

View file

@ -46,6 +46,13 @@ limitations under the License.
.mx_DeviceDetails_sectionHeading { .mx_DeviceDetails_sectionHeading {
margin: 0; margin: 0;
.mx_DeviceDetails_sectionSubheading {
display: block;
font-size: $font-12px;
color: $secondary-content;
line-height: $font-14px;
}
} }
.mx_DeviceDetails_metadataTable { .mx_DeviceDetails_metadataTable {
@ -81,3 +88,10 @@ limitations under the License.
align-items: center; align-items: center;
gap: $spacing-4; gap: $spacing-4;
} }
.mx_DeviceDetails_pushNotifications {
display: block;
.mx_ToggleSwitch {
float: right;
}
}

View file

@ -26,6 +26,10 @@ limitations under the License.
background-color: $togglesw-off-color; background-color: $togglesw-off-color;
opacity: 0.5; opacity: 0.5;
&[aria-disabled="true"] {
cursor: not-allowed;
}
} }
.mx_ToggleSwitch_enabled { .mx_ToggleSwitch_enabled {

View file

@ -15,21 +15,27 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { IPusher } from 'matrix-js-sdk/src/@types/PushRules';
import { PUSHER_ENABLED } from 'matrix-js-sdk/src/@types/event';
import { formatDate } from '../../../../DateUtils'; import { formatDate } from '../../../../DateUtils';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton'; import AccessibleButton from '../../elements/AccessibleButton';
import Spinner from '../../elements/Spinner'; import Spinner from '../../elements/Spinner';
import ToggleSwitch from '../../elements/ToggleSwitch';
import { DeviceDetailHeading } from './DeviceDetailHeading'; import { DeviceDetailHeading } from './DeviceDetailHeading';
import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard'; import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard';
import { DeviceWithVerification } from './types'; import { DeviceWithVerification } from './types';
interface Props { interface Props {
device: DeviceWithVerification; device: DeviceWithVerification;
pusher?: IPusher | undefined;
isSigningOut: boolean; isSigningOut: boolean;
onVerifyDevice?: () => void; onVerifyDevice?: () => void;
onSignOutDevice: () => void; onSignOutDevice: () => void;
saveDeviceName: (deviceName: string) => Promise<void>; saveDeviceName: (deviceName: string) => Promise<void>;
setPusherEnabled?: (deviceId: string, enabled: boolean) => Promise<void> | undefined;
supportsMSC3881?: boolean | undefined;
} }
interface MetadataTable { interface MetadataTable {
@ -39,10 +45,13 @@ interface MetadataTable {
const DeviceDetails: React.FC<Props> = ({ const DeviceDetails: React.FC<Props> = ({
device, device,
pusher,
isSigningOut, isSigningOut,
onVerifyDevice, onVerifyDevice,
onSignOutDevice, onSignOutDevice,
saveDeviceName, saveDeviceName,
setPusherEnabled,
supportsMSC3881,
}) => { }) => {
const metadata: MetadataTable[] = [ const metadata: MetadataTable[] = [
{ {
@ -93,6 +102,28 @@ const DeviceDetails: React.FC<Props> = ({
</table>, </table>,
) } ) }
</section> </section>
{ pusher && (
<section
className='mx_DeviceDetails_section mx_DeviceDetails_pushNotifications'
data-testid='device-detail-push-notification'
>
<ToggleSwitch
// For backwards compatibility, if `enabled` is missing
// default to `true`
checked={pusher?.[PUSHER_ENABLED.name] ?? true}
disabled={!supportsMSC3881}
onChange={(checked) => setPusherEnabled?.(device.device_id, checked)}
aria-label={_t("Toggle push notifications on this session.")}
data-testid='device-detail-push-notification-checkbox'
/>
<p className='mx_DeviceDetails_sectionHeading'>
{ _t('Push notifications') }
<small className='mx_DeviceDetails_sectionSubheading'>
{ _t('Receive push notifications on this session.') }
</small>
</p>
</section>
) }
<section className='mx_DeviceDetails_section'> <section className='mx_DeviceDetails_section'>
<AccessibleButton <AccessibleButton
onClick={onSignOutDevice} onClick={onSignOutDevice}

View file

@ -15,6 +15,8 @@ limitations under the License.
*/ */
import React, { ForwardedRef, forwardRef } from 'react'; 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 { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton'; import AccessibleButton from '../../elements/AccessibleButton';
@ -36,6 +38,7 @@ import { DevicesState } from './useOwnDevices';
interface Props { interface Props {
devices: DevicesDictionary; devices: DevicesDictionary;
pushers: IPusher[];
expandedDeviceIds: DeviceWithVerification['device_id'][]; expandedDeviceIds: DeviceWithVerification['device_id'][];
signingOutDeviceIds: DeviceWithVerification['device_id'][]; signingOutDeviceIds: DeviceWithVerification['device_id'][];
filter?: DeviceSecurityVariation; filter?: DeviceSecurityVariation;
@ -44,6 +47,8 @@ interface Props {
onSignOutDevices: (deviceIds: DeviceWithVerification['device_id'][]) => void; onSignOutDevices: (deviceIds: DeviceWithVerification['device_id'][]) => void;
saveDeviceName: DevicesState['saveDeviceName']; saveDeviceName: DevicesState['saveDeviceName'];
onRequestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => void; onRequestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => void;
setPusherEnabled: (deviceId: string, enabled: boolean) => Promise<void>;
supportsMSC3881?: boolean | undefined;
} }
// devices without timestamp metadata should be sorted last // devices without timestamp metadata should be sorted last
@ -135,20 +140,26 @@ const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) =>
const DeviceListItem: React.FC<{ const DeviceListItem: React.FC<{
device: DeviceWithVerification; device: DeviceWithVerification;
pusher?: IPusher | undefined;
isExpanded: boolean; isExpanded: boolean;
isSigningOut: boolean; isSigningOut: boolean;
onDeviceExpandToggle: () => void; onDeviceExpandToggle: () => void;
onSignOutDevice: () => void; onSignOutDevice: () => void;
saveDeviceName: (deviceName: string) => Promise<void>; saveDeviceName: (deviceName: string) => Promise<void>;
onRequestDeviceVerification?: () => void; onRequestDeviceVerification?: () => void;
setPusherEnabled: (deviceId: string, enabled: boolean) => Promise<void>;
supportsMSC3881?: boolean | undefined;
}> = ({ }> = ({
device, device,
pusher,
isExpanded, isExpanded,
isSigningOut, isSigningOut,
onDeviceExpandToggle, onDeviceExpandToggle,
onSignOutDevice, onSignOutDevice,
saveDeviceName, saveDeviceName,
onRequestDeviceVerification, onRequestDeviceVerification,
setPusherEnabled,
supportsMSC3881,
}) => <li className='mx_FilteredDeviceList_listItem'> }) => <li className='mx_FilteredDeviceList_listItem'>
<DeviceTile <DeviceTile
device={device} device={device}
@ -162,10 +173,13 @@ const DeviceListItem: React.FC<{
isExpanded && isExpanded &&
<DeviceDetails <DeviceDetails
device={device} device={device}
pusher={pusher}
isSigningOut={isSigningOut} isSigningOut={isSigningOut}
onVerifyDevice={onRequestDeviceVerification} onVerifyDevice={onRequestDeviceVerification}
onSignOutDevice={onSignOutDevice} onSignOutDevice={onSignOutDevice}
saveDeviceName={saveDeviceName} saveDeviceName={saveDeviceName}
setPusherEnabled={setPusherEnabled}
supportsMSC3881={supportsMSC3881}
/> />
} }
</li>; </li>;
@ -177,6 +191,7 @@ const DeviceListItem: React.FC<{
export const FilteredDeviceList = export const FilteredDeviceList =
forwardRef(({ forwardRef(({
devices, devices,
pushers,
filter, filter,
expandedDeviceIds, expandedDeviceIds,
signingOutDeviceIds, signingOutDeviceIds,
@ -185,9 +200,15 @@ export const FilteredDeviceList =
saveDeviceName, saveDeviceName,
onSignOutDevices, onSignOutDevices,
onRequestDeviceVerification, onRequestDeviceVerification,
setPusherEnabled,
supportsMSC3881,
}: Props, ref: ForwardedRef<HTMLDivElement>) => { }: Props, ref: ForwardedRef<HTMLDivElement>) => {
const sortedDevices = getFilteredSortedDevices(devices, filter); const sortedDevices = getFilteredSortedDevices(devices, filter);
function getPusherForDevice(device: DeviceWithVerification): IPusher | undefined {
return pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === device.device_id);
}
const options: FilterDropdownOption<DeviceFilterKey>[] = [ const options: FilterDropdownOption<DeviceFilterKey>[] = [
{ id: ALL_FILTER_ID, label: _t('All') }, { id: ALL_FILTER_ID, label: _t('All') },
{ {
@ -236,6 +257,7 @@ export const FilteredDeviceList =
{ sortedDevices.map((device) => <DeviceListItem { sortedDevices.map((device) => <DeviceListItem
key={device.device_id} key={device.device_id}
device={device} device={device}
pusher={getPusherForDevice(device)}
isExpanded={expandedDeviceIds.includes(device.device_id)} isExpanded={expandedDeviceIds.includes(device.device_id)}
isSigningOut={signingOutDeviceIds.includes(device.device_id)} isSigningOut={signingOutDeviceIds.includes(device.device_id)}
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)} onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
@ -246,6 +268,8 @@ export const FilteredDeviceList =
? () => onRequestDeviceVerification(device.device_id) ? () => onRequestDeviceVerification(device.device_id)
: undefined : undefined
} }
setPusherEnabled={setPusherEnabled}
supportsMSC3881={supportsMSC3881}
/>, />,
) } ) }
</ol> </ol>

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import { useCallback, useContext, useEffect, useState } from "react"; import { useCallback, useContext, useEffect, useState } from "react";
import { IMyDevice, MatrixClient } from "matrix-js-sdk/src/matrix"; import { IMyDevice, IPusher, MatrixClient, PUSHER_DEVICE_ID, PUSHER_ENABLED } from "matrix-js-sdk/src/matrix";
import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning"; import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { MatrixError } from "matrix-js-sdk/src/http-api"; import { MatrixError } from "matrix-js-sdk/src/http-api";
@ -76,13 +76,16 @@ export enum OwnDevicesError {
} }
export type DevicesState = { export type DevicesState = {
devices: DevicesDictionary; devices: DevicesDictionary;
pushers: IPusher[];
currentDeviceId: string; currentDeviceId: string;
isLoadingDeviceList: boolean; isLoadingDeviceList: boolean;
// not provided when current session cannot request verification // not provided when current session cannot request verification
requestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => Promise<VerificationRequest>; requestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => Promise<VerificationRequest>;
refreshDevices: () => Promise<void>; refreshDevices: () => Promise<void>;
saveDeviceName: (deviceId: DeviceWithVerification['device_id'], deviceName: string) => Promise<void>; saveDeviceName: (deviceId: DeviceWithVerification['device_id'], deviceName: string) => Promise<void>;
setPusherEnabled: (deviceId: DeviceWithVerification['device_id'], enabled: boolean) => Promise<void>;
error?: OwnDevicesError; error?: OwnDevicesError;
supportsMSC3881?: boolean | undefined;
}; };
export const useOwnDevices = (): DevicesState => { export const useOwnDevices = (): DevicesState => {
const matrixClient = useContext(MatrixClientContext); const matrixClient = useContext(MatrixClientContext);
@ -91,10 +94,18 @@ export const useOwnDevices = (): DevicesState => {
const userId = matrixClient.getUserId(); const userId = matrixClient.getUserId();
const [devices, setDevices] = useState<DevicesState['devices']>({}); const [devices, setDevices] = useState<DevicesState['devices']>({});
const [pushers, setPushers] = useState<DevicesState['pushers']>([]);
const [isLoadingDeviceList, setIsLoadingDeviceList] = useState(true); const [isLoadingDeviceList, setIsLoadingDeviceList] = useState(true);
const [supportsMSC3881, setSupportsMSC3881] = useState(true); // optimisticly saying yes!
const [error, setError] = useState<OwnDevicesError>(); const [error, setError] = useState<OwnDevicesError>();
useEffect(() => {
matrixClient.doesServerSupportUnstableFeature("org.matrix.msc3881").then(hasSupport => {
setSupportsMSC3881(hasSupport);
});
}, [matrixClient]);
const refreshDevices = useCallback(async () => { const refreshDevices = useCallback(async () => {
setIsLoadingDeviceList(true); setIsLoadingDeviceList(true);
try { try {
@ -105,6 +116,10 @@ export const useOwnDevices = (): DevicesState => {
} }
const devices = await fetchDevicesWithVerification(matrixClient, userId); const devices = await fetchDevicesWithVerification(matrixClient, userId);
setDevices(devices); setDevices(devices);
const { pushers } = await matrixClient.getPushers();
setPushers(pushers);
setIsLoadingDeviceList(false); setIsLoadingDeviceList(false);
} catch (error) { } catch (error) {
if ((error as MatrixError).httpStatus == 404) { if ((error as MatrixError).httpStatus == 404) {
@ -154,13 +169,32 @@ export const useOwnDevices = (): DevicesState => {
} }
}, [matrixClient, devices, refreshDevices]); }, [matrixClient, devices, refreshDevices]);
const setPusherEnabled = useCallback(
async (deviceId: DeviceWithVerification['device_id'], enabled: boolean): Promise<void> => {
const pusher = pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === deviceId);
try {
await matrixClient.setPusher({
...pusher,
[PUSHER_ENABLED.name]: enabled,
});
await refreshDevices();
} catch (error) {
logger.error("Error setting pusher state", error);
throw new Error(_t("Failed to set pusher state"));
}
}, [matrixClient, pushers, refreshDevices],
);
return { return {
devices, devices,
pushers,
currentDeviceId, currentDeviceId,
isLoadingDeviceList, isLoadingDeviceList,
error, error,
requestDeviceVerification, requestDeviceVerification,
refreshDevices, refreshDevices,
saveDeviceName, saveDeviceName,
setPusherEnabled,
supportsMSC3881,
}; };
}; };

View file

@ -87,11 +87,14 @@ const useSignOut = (
const SessionManagerTab: React.FC = () => { const SessionManagerTab: React.FC = () => {
const { const {
devices, devices,
pushers,
currentDeviceId, currentDeviceId,
isLoadingDeviceList, isLoadingDeviceList,
requestDeviceVerification, requestDeviceVerification,
refreshDevices, refreshDevices,
saveDeviceName, saveDeviceName,
setPusherEnabled,
supportsMSC3881,
} = useOwnDevices(); } = useOwnDevices();
const [filter, setFilter] = useState<DeviceSecurityVariation>(); const [filter, setFilter] = useState<DeviceSecurityVariation>();
const [expandedDeviceIds, setExpandedDeviceIds] = useState<DeviceWithVerification['device_id'][]>([]); const [expandedDeviceIds, setExpandedDeviceIds] = useState<DeviceWithVerification['device_id'][]>([]);
@ -186,6 +189,7 @@ const SessionManagerTab: React.FC = () => {
> >
<FilteredDeviceList <FilteredDeviceList
devices={otherDevices} devices={otherDevices}
pushers={pushers}
filter={filter} filter={filter}
expandedDeviceIds={expandedDeviceIds} expandedDeviceIds={expandedDeviceIds}
signingOutDeviceIds={signingOutDeviceIds} signingOutDeviceIds={signingOutDeviceIds}
@ -194,7 +198,9 @@ const SessionManagerTab: React.FC = () => {
onRequestDeviceVerification={requestDeviceVerification ? onTriggerDeviceVerification : undefined} onRequestDeviceVerification={requestDeviceVerification ? onTriggerDeviceVerification : undefined}
onSignOutDevices={onSignOutOtherDevices} onSignOutDevices={onSignOutOtherDevices}
saveDeviceName={saveDeviceName} saveDeviceName={saveDeviceName}
setPusherEnabled={setPusherEnabled}
ref={filteredDeviceListRef} ref={filteredDeviceListRef}
supportsMSC3881={supportsMSC3881}
/> />
</SettingsSubsection> </SettingsSubsection>
} }

View file

@ -1719,6 +1719,9 @@
"Device": "Device", "Device": "Device",
"IP address": "IP address", "IP address": "IP address",
"Session details": "Session details", "Session details": "Session details",
"Toggle push notifications on this session.": "Toggle push notifications on this session.",
"Push notifications": "Push notifications",
"Receive push notifications on this session.": "Receive push notifications on this session.",
"Sign out of this session": "Sign out of this session", "Sign out of this session": "Sign out of this session",
"Toggle device details": "Toggle device details", "Toggle device details": "Toggle device details",
"Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days", "Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days",
@ -1751,6 +1754,7 @@
"Security recommendations": "Security recommendations", "Security recommendations": "Security recommendations",
"Improve your account security by following these recommendations": "Improve your account security by following these recommendations", "Improve your account security by following these recommendations": "Improve your account security by following these recommendations",
"View all": "View all", "View all": "View all",
"Failed to set pusher state": "Failed to set pusher state",
"Unable to remove contact information": "Unable to remove contact information", "Unable to remove contact information": "Unable to remove contact information",
"Remove %(email)s?": "Remove %(email)s?", "Remove %(email)s?": "Remove %(email)s?",
"Invalid Email Address": "Invalid Email Address", "Invalid Email Address": "Invalid Email Address",

View file

@ -19,11 +19,13 @@ import { act } from 'react-dom/test-utils';
import { CrossSigningInfo } from 'matrix-js-sdk/src/crypto/CrossSigning'; import { CrossSigningInfo } from 'matrix-js-sdk/src/crypto/CrossSigning';
import { DeviceInfo } from 'matrix-js-sdk/src/crypto/deviceinfo'; import { DeviceInfo } from 'matrix-js-sdk/src/crypto/deviceinfo';
import { sleep } from 'matrix-js-sdk/src/utils'; import { sleep } from 'matrix-js-sdk/src/utils';
import { PUSHER_DEVICE_ID, PUSHER_ENABLED } from 'matrix-js-sdk/src/@types/event';
import DevicesPanel from "../../../../src/components/views/settings/DevicesPanel"; import DevicesPanel from "../../../../src/components/views/settings/DevicesPanel";
import { import {
flushPromises, flushPromises,
getMockClientWithEventEmitter, getMockClientWithEventEmitter,
mkPusher,
mockClientMethodsUser, mockClientMethodsUser,
} from "../../../test-utils"; } from "../../../test-utils";
@ -40,6 +42,8 @@ describe('<DevicesPanel />', () => {
getStoredCrossSigningForUser: jest.fn().mockReturnValue(new CrossSigningInfo(userId, {}, {})), getStoredCrossSigningForUser: jest.fn().mockReturnValue(new CrossSigningInfo(userId, {}, {})),
getStoredDevice: jest.fn().mockReturnValue(new DeviceInfo('id')), getStoredDevice: jest.fn().mockReturnValue(new DeviceInfo('id')),
generateClientSecret: jest.fn(), generateClientSecret: jest.fn(),
getPushers: jest.fn(),
setPusher: jest.fn(),
}); });
const getComponent = () => <DevicesPanel />; const getComponent = () => <DevicesPanel />;
@ -50,6 +54,15 @@ describe('<DevicesPanel />', () => {
mockClient.getDevices mockClient.getDevices
.mockReset() .mockReset()
.mockResolvedValue({ devices: [device1, device2, device3] }); .mockResolvedValue({ devices: [device1, device2, device3] });
mockClient.getPushers
.mockReset()
.mockResolvedValue({
pushers: [mkPusher({
[PUSHER_DEVICE_ID.name]: device1.device_id,
[PUSHER_ENABLED.name]: true,
})],
});
}); });
it('renders device panel with devices', async () => { it('renders device panel with devices', async () => {

View file

@ -40,6 +40,7 @@ describe('<CurrentDeviceSection />', () => {
isLoading: false, isLoading: false,
isSigningOut: false, isSigningOut: false,
}; };
const getComponent = (props = {}): React.ReactElement => const getComponent = (props = {}): React.ReactElement =>
(<CurrentDeviceSection {...defaultProps} {...props} />); (<CurrentDeviceSection {...defaultProps} {...props} />);

View file

@ -15,9 +15,11 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import { PUSHER_ENABLED } from 'matrix-js-sdk/src/@types/event';
import DeviceDetails from '../../../../../src/components/views/settings/devices/DeviceDetails'; import DeviceDetails from '../../../../../src/components/views/settings/devices/DeviceDetails';
import { mkPusher } from '../../../../test-utils/test-utils';
describe('<DeviceDetails />', () => { describe('<DeviceDetails />', () => {
const baseDevice = { const baseDevice = {
@ -26,12 +28,17 @@ describe('<DeviceDetails />', () => {
}; };
const defaultProps = { const defaultProps = {
device: baseDevice, device: baseDevice,
pusher: null,
isSigningOut: false, isSigningOut: false,
isLoading: false, isLoading: false,
onSignOutDevice: jest.fn(), onSignOutDevice: jest.fn(),
saveDeviceName: jest.fn(), saveDeviceName: jest.fn(),
setPusherEnabled: jest.fn(),
supportsMSC3881: true,
}; };
const getComponent = (props = {}) => <DeviceDetails {...defaultProps} {...props} />; const getComponent = (props = {}) => <DeviceDetails {...defaultProps} {...props} />;
// 14.03.2022 16:15 // 14.03.2022 16:15
const now = 1647270879403; const now = 1647270879403;
jest.useFakeTimers(); jest.useFakeTimers();
@ -74,4 +81,82 @@ describe('<DeviceDetails />', () => {
getByTestId('device-detail-sign-out-cta').getAttribute('aria-disabled'), getByTestId('device-detail-sign-out-cta').getAttribute('aria-disabled'),
).toEqual("true"); ).toEqual("true");
}); });
it('renders the push notification section when a pusher exists', () => {
const device = {
...baseDevice,
};
const pusher = mkPusher({
device_id: device.device_id,
});
const { getByTestId } = render(getComponent({
device,
pusher,
isSigningOut: true,
}));
expect(getByTestId('device-detail-push-notification')).toBeTruthy();
});
it('hides the push notification section when no pusher', () => {
const device = {
...baseDevice,
};
const { getByTestId } = render(getComponent({
device,
pusher: null,
isSigningOut: true,
}));
expect(() => getByTestId('device-detail-push-notification')).toThrow();
});
it('disables the checkbox when there is no server support', () => {
const device = {
...baseDevice,
};
const pusher = mkPusher({
device_id: device.device_id,
[PUSHER_ENABLED.name]: false,
});
const { getByTestId } = render(getComponent({
device,
pusher,
isSigningOut: true,
supportsMSC3881: false,
}));
const checkbox = getByTestId('device-detail-push-notification-checkbox');
expect(checkbox.getAttribute('aria-disabled')).toEqual("true");
expect(checkbox.getAttribute('aria-checked')).toEqual("false");
});
it('changes the pusher status when clicked', () => {
const device = {
...baseDevice,
};
const enabled = false;
const pusher = mkPusher({
device_id: device.device_id,
[PUSHER_ENABLED.name]: enabled,
});
const { getByTestId } = render(getComponent({
device,
pusher,
isSigningOut: true,
}));
const checkbox = getByTestId('device-detail-push-notification-checkbox');
fireEvent.click(checkbox);
expect(defaultProps.setPusherEnabled).toHaveBeenCalledWith(device.device_id, !enabled);
});
}); });

View file

@ -45,6 +45,7 @@ describe('<FilteredDeviceList />', () => {
onDeviceExpandToggle: jest.fn(), onDeviceExpandToggle: jest.fn(),
onSignOutDevices: jest.fn(), onSignOutDevices: jest.fn(),
saveDeviceName: jest.fn(), saveDeviceName: jest.fn(),
setPusherEnabled: jest.fn(),
expandedDeviceIds: [], expandedDeviceIds: [],
signingOutDeviceIds: [], signingOutDeviceIds: [],
devices: { devices: {
@ -54,7 +55,10 @@ describe('<FilteredDeviceList />', () => {
[hundredDaysOld.device_id]: hundredDaysOld, [hundredDaysOld.device_id]: hundredDaysOld,
[hundredDaysOldUnverified.device_id]: hundredDaysOldUnverified, [hundredDaysOldUnverified.device_id]: hundredDaysOldUnverified,
}, },
pushers: [],
supportsMSC3881: true,
}; };
const getComponent = (props = {}) => const getComponent = (props = {}) =>
(<FilteredDeviceList {...defaultProps} {...props} />); (<FilteredDeviceList {...defaultProps} {...props} />);

View file

@ -22,13 +22,14 @@ import { logger } from 'matrix-js-sdk/src/logger';
import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
import { VerificationRequest } from 'matrix-js-sdk/src/crypto/verification/request/VerificationRequest'; import { VerificationRequest } from 'matrix-js-sdk/src/crypto/verification/request/VerificationRequest';
import { sleep } from 'matrix-js-sdk/src/utils'; import { sleep } from 'matrix-js-sdk/src/utils';
import { IMyDevice } from 'matrix-js-sdk/src/matrix'; import { IMyDevice, PUSHER_DEVICE_ID, PUSHER_ENABLED } from 'matrix-js-sdk/src/matrix';
import SessionManagerTab from '../../../../../../src/components/views/settings/tabs/user/SessionManagerTab'; import SessionManagerTab from '../../../../../../src/components/views/settings/tabs/user/SessionManagerTab';
import MatrixClientContext from '../../../../../../src/contexts/MatrixClientContext'; import MatrixClientContext from '../../../../../../src/contexts/MatrixClientContext';
import { import {
flushPromisesWithFakeTimers, flushPromisesWithFakeTimers,
getMockClientWithEventEmitter, getMockClientWithEventEmitter,
mkPusher,
mockClientMethodsUser, mockClientMethodsUser,
} from '../../../../../test-utils'; } from '../../../../../test-utils';
import Modal from '../../../../../../src/Modal'; import Modal from '../../../../../../src/Modal';
@ -67,6 +68,9 @@ describe('<SessionManagerTab />', () => {
deleteMultipleDevices: jest.fn(), deleteMultipleDevices: jest.fn(),
generateClientSecret: jest.fn(), generateClientSecret: jest.fn(),
setDeviceDetails: jest.fn(), setDeviceDetails: jest.fn(),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(true),
getPushers: jest.fn(),
setPusher: jest.fn(),
}); });
const defaultProps = {}; const defaultProps = {};
@ -101,6 +105,15 @@ describe('<SessionManagerTab />', () => {
mockClient.getDevices mockClient.getDevices
.mockReset() .mockReset()
.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); .mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] });
mockClient.getPushers
.mockReset()
.mockResolvedValue({
pushers: [mkPusher({
[PUSHER_DEVICE_ID.name]: alicesMobileDevice.device_id,
[PUSHER_ENABLED.name]: true,
})],
});
}); });
it('renders spinner while devices load', () => { it('renders spinner while devices load', () => {
@ -668,4 +681,25 @@ describe('<SessionManagerTab />', () => {
expect(getByTestId('device-rename-error')).toBeTruthy(); expect(getByTestId('device-rename-error')).toBeTruthy();
}); });
}); });
it("lets you change the pusher state", async () => {
const { getByTestId } = render(getComponent());
await act(async () => {
await flushPromisesWithFakeTimers();
});
toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id);
// device details are expanded
expect(getByTestId(`device-detail-${alicesMobileDevice.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.setPusher).toHaveBeenCalled();
});
}); });

View file

@ -30,6 +30,7 @@ import {
EventType, EventType,
IEventRelation, IEventRelation,
IUnsigned, IUnsigned,
IPusher,
} from 'matrix-js-sdk/src/matrix'; } from 'matrix-js-sdk/src/matrix';
import { normalize } from "matrix-js-sdk/src/utils"; import { normalize } from "matrix-js-sdk/src/utils";
import { ReEmitter } from "matrix-js-sdk/src/ReEmitter"; import { ReEmitter } from "matrix-js-sdk/src/ReEmitter";
@ -541,3 +542,14 @@ export const mkSpace = (
))); )));
return space; return space;
}; };
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
app_display_name: "app",
app_id: "123",
data: {},
device_display_name: "name",
kind: "http",
lang: "en",
pushkey: "pushpush",
...extra,
});