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

Commit 641cf28

Browse files
Germainturt2live
andauthored
Implement push notification toggle in device detail (#9308)
Co-authored-by: Travis Ralston <[email protected]>
1 parent ace6591 commit 641cf28

File tree

13 files changed

+269
-3
lines changed

13 files changed

+269
-3
lines changed

res/css/components/views/settings/devices/_DeviceDetails.pcss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ limitations under the License.
4646

4747
.mx_DeviceDetails_sectionHeading {
4848
margin: 0;
49+
50+
.mx_DeviceDetails_sectionSubheading {
51+
display: block;
52+
font-size: $font-12px;
53+
color: $secondary-content;
54+
line-height: $font-14px;
55+
}
4956
}
5057

5158
.mx_DeviceDetails_metadataTable {
@@ -81,3 +88,10 @@ limitations under the License.
8188
align-items: center;
8289
gap: $spacing-4;
8390
}
91+
92+
.mx_DeviceDetails_pushNotifications {
93+
display: block;
94+
.mx_ToggleSwitch {
95+
float: right;
96+
}
97+
}

res/css/views/elements/_ToggleSwitch.pcss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ limitations under the License.
2626

2727
background-color: $togglesw-off-color;
2828
opacity: 0.5;
29+
30+
&[aria-disabled="true"] {
31+
cursor: not-allowed;
32+
}
2933
}
3034

3135
.mx_ToggleSwitch_enabled {

src/components/views/settings/devices/DeviceDetails.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,27 @@ limitations under the License.
1515
*/
1616

1717
import React from 'react';
18+
import { IPusher } from 'matrix-js-sdk/src/@types/PushRules';
19+
import { PUSHER_ENABLED } from 'matrix-js-sdk/src/@types/event';
1820

1921
import { formatDate } from '../../../../DateUtils';
2022
import { _t } from '../../../../languageHandler';
2123
import AccessibleButton from '../../elements/AccessibleButton';
2224
import Spinner from '../../elements/Spinner';
25+
import ToggleSwitch from '../../elements/ToggleSwitch';
2326
import { DeviceDetailHeading } from './DeviceDetailHeading';
2427
import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard';
2528
import { DeviceWithVerification } from './types';
2629

2730
interface Props {
2831
device: DeviceWithVerification;
32+
pusher?: IPusher | undefined;
2933
isSigningOut: boolean;
3034
onVerifyDevice?: () => void;
3135
onSignOutDevice: () => void;
3236
saveDeviceName: (deviceName: string) => Promise<void>;
37+
setPusherEnabled?: (deviceId: string, enabled: boolean) => Promise<void> | undefined;
38+
supportsMSC3881?: boolean | undefined;
3339
}
3440

3541
interface MetadataTable {
@@ -39,10 +45,13 @@ interface MetadataTable {
3945

4046
const DeviceDetails: React.FC<Props> = ({
4147
device,
48+
pusher,
4249
isSigningOut,
4350
onVerifyDevice,
4451
onSignOutDevice,
4552
saveDeviceName,
53+
setPusherEnabled,
54+
supportsMSC3881,
4655
}) => {
4756
const metadata: MetadataTable[] = [
4857
{
@@ -93,6 +102,28 @@ const DeviceDetails: React.FC<Props> = ({
93102
</table>,
94103
) }
95104
</section>
105+
{ pusher && (
106+
<section
107+
className='mx_DeviceDetails_section mx_DeviceDetails_pushNotifications'
108+
data-testid='device-detail-push-notification'
109+
>
110+
<ToggleSwitch
111+
// For backwards compatibility, if `enabled` is missing
112+
// default to `true`
113+
checked={pusher?.[PUSHER_ENABLED.name] ?? true}
114+
disabled={!supportsMSC3881}
115+
onChange={(checked) => setPusherEnabled?.(device.device_id, checked)}
116+
aria-label={_t("Toggle push notifications on this session.")}
117+
data-testid='device-detail-push-notification-checkbox'
118+
/>
119+
<p className='mx_DeviceDetails_sectionHeading'>
120+
{ _t('Push notifications') }
121+
<small className='mx_DeviceDetails_sectionSubheading'>
122+
{ _t('Receive push notifications on this session.') }
123+
</small>
124+
</p>
125+
</section>
126+
) }
96127
<section className='mx_DeviceDetails_section'>
97128
<AccessibleButton
98129
onClick={onSignOutDevice}

src/components/views/settings/devices/FilteredDeviceList.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ limitations under the License.
1515
*/
1616

1717
import React, { ForwardedRef, forwardRef } from 'react';
18+
import { IPusher } from 'matrix-js-sdk/src/@types/PushRules';
19+
import { PUSHER_DEVICE_ID } from 'matrix-js-sdk/src/@types/event';
1820

1921
import { _t } from '../../../../languageHandler';
2022
import AccessibleButton from '../../elements/AccessibleButton';
@@ -36,6 +38,7 @@ import { DevicesState } from './useOwnDevices';
3638

3739
interface Props {
3840
devices: DevicesDictionary;
41+
pushers: IPusher[];
3942
expandedDeviceIds: DeviceWithVerification['device_id'][];
4043
signingOutDeviceIds: DeviceWithVerification['device_id'][];
4144
filter?: DeviceSecurityVariation;
@@ -44,6 +47,8 @@ interface Props {
4447
onSignOutDevices: (deviceIds: DeviceWithVerification['device_id'][]) => void;
4548
saveDeviceName: DevicesState['saveDeviceName'];
4649
onRequestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => void;
50+
setPusherEnabled: (deviceId: string, enabled: boolean) => Promise<void>;
51+
supportsMSC3881?: boolean | undefined;
4752
}
4853

4954
// devices without timestamp metadata should be sorted last
@@ -135,20 +140,26 @@ const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) =>
135140

136141
const DeviceListItem: React.FC<{
137142
device: DeviceWithVerification;
143+
pusher?: IPusher | undefined;
138144
isExpanded: boolean;
139145
isSigningOut: boolean;
140146
onDeviceExpandToggle: () => void;
141147
onSignOutDevice: () => void;
142148
saveDeviceName: (deviceName: string) => Promise<void>;
143149
onRequestDeviceVerification?: () => void;
150+
setPusherEnabled: (deviceId: string, enabled: boolean) => Promise<void>;
151+
supportsMSC3881?: boolean | undefined;
144152
}> = ({
145153
device,
154+
pusher,
146155
isExpanded,
147156
isSigningOut,
148157
onDeviceExpandToggle,
149158
onSignOutDevice,
150159
saveDeviceName,
151160
onRequestDeviceVerification,
161+
setPusherEnabled,
162+
supportsMSC3881,
152163
}) => <li className='mx_FilteredDeviceList_listItem'>
153164
<DeviceTile
154165
device={device}
@@ -162,10 +173,13 @@ const DeviceListItem: React.FC<{
162173
isExpanded &&
163174
<DeviceDetails
164175
device={device}
176+
pusher={pusher}
165177
isSigningOut={isSigningOut}
166178
onVerifyDevice={onRequestDeviceVerification}
167179
onSignOutDevice={onSignOutDevice}
168180
saveDeviceName={saveDeviceName}
181+
setPusherEnabled={setPusherEnabled}
182+
supportsMSC3881={supportsMSC3881}
169183
/>
170184
}
171185
</li>;
@@ -177,6 +191,7 @@ const DeviceListItem: React.FC<{
177191
export const FilteredDeviceList =
178192
forwardRef(({
179193
devices,
194+
pushers,
180195
filter,
181196
expandedDeviceIds,
182197
signingOutDeviceIds,
@@ -185,9 +200,15 @@ export const FilteredDeviceList =
185200
saveDeviceName,
186201
onSignOutDevices,
187202
onRequestDeviceVerification,
203+
setPusherEnabled,
204+
supportsMSC3881,
188205
}: Props, ref: ForwardedRef<HTMLDivElement>) => {
189206
const sortedDevices = getFilteredSortedDevices(devices, filter);
190207

208+
function getPusherForDevice(device: DeviceWithVerification): IPusher | undefined {
209+
return pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === device.device_id);
210+
}
211+
191212
const options: FilterDropdownOption<DeviceFilterKey>[] = [
192213
{ id: ALL_FILTER_ID, label: _t('All') },
193214
{
@@ -236,6 +257,7 @@ export const FilteredDeviceList =
236257
{ sortedDevices.map((device) => <DeviceListItem
237258
key={device.device_id}
238259
device={device}
260+
pusher={getPusherForDevice(device)}
239261
isExpanded={expandedDeviceIds.includes(device.device_id)}
240262
isSigningOut={signingOutDeviceIds.includes(device.device_id)}
241263
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
@@ -246,6 +268,8 @@ export const FilteredDeviceList =
246268
? () => onRequestDeviceVerification(device.device_id)
247269
: undefined
248270
}
271+
setPusherEnabled={setPusherEnabled}
272+
supportsMSC3881={supportsMSC3881}
249273
/>,
250274
) }
251275
</ol>

src/components/views/settings/devices/useOwnDevices.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ limitations under the License.
1515
*/
1616

1717
import { useCallback, useContext, useEffect, useState } from "react";
18-
import { IMyDevice, MatrixClient } from "matrix-js-sdk/src/matrix";
18+
import { IMyDevice, IPusher, MatrixClient, PUSHER_DEVICE_ID, PUSHER_ENABLED } from "matrix-js-sdk/src/matrix";
1919
import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning";
2020
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
2121
import { MatrixError } from "matrix-js-sdk/src/http-api";
@@ -76,13 +76,16 @@ export enum OwnDevicesError {
7676
}
7777
export type DevicesState = {
7878
devices: DevicesDictionary;
79+
pushers: IPusher[];
7980
currentDeviceId: string;
8081
isLoadingDeviceList: boolean;
8182
// not provided when current session cannot request verification
8283
requestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => Promise<VerificationRequest>;
8384
refreshDevices: () => Promise<void>;
8485
saveDeviceName: (deviceId: DeviceWithVerification['device_id'], deviceName: string) => Promise<void>;
86+
setPusherEnabled: (deviceId: DeviceWithVerification['device_id'], enabled: boolean) => Promise<void>;
8587
error?: OwnDevicesError;
88+
supportsMSC3881?: boolean | undefined;
8689
};
8790
export const useOwnDevices = (): DevicesState => {
8891
const matrixClient = useContext(MatrixClientContext);
@@ -91,10 +94,18 @@ export const useOwnDevices = (): DevicesState => {
9194
const userId = matrixClient.getUserId();
9295

9396
const [devices, setDevices] = useState<DevicesState['devices']>({});
97+
const [pushers, setPushers] = useState<DevicesState['pushers']>([]);
9498
const [isLoadingDeviceList, setIsLoadingDeviceList] = useState(true);
99+
const [supportsMSC3881, setSupportsMSC3881] = useState(true); // optimisticly saying yes!
95100

96101
const [error, setError] = useState<OwnDevicesError>();
97102

103+
useEffect(() => {
104+
matrixClient.doesServerSupportUnstableFeature("org.matrix.msc3881").then(hasSupport => {
105+
setSupportsMSC3881(hasSupport);
106+
});
107+
}, [matrixClient]);
108+
98109
const refreshDevices = useCallback(async () => {
99110
setIsLoadingDeviceList(true);
100111
try {
@@ -105,6 +116,10 @@ export const useOwnDevices = (): DevicesState => {
105116
}
106117
const devices = await fetchDevicesWithVerification(matrixClient, userId);
107118
setDevices(devices);
119+
120+
const { pushers } = await matrixClient.getPushers();
121+
setPushers(pushers);
122+
108123
setIsLoadingDeviceList(false);
109124
} catch (error) {
110125
if ((error as MatrixError).httpStatus == 404) {
@@ -154,13 +169,32 @@ export const useOwnDevices = (): DevicesState => {
154169
}
155170
}, [matrixClient, devices, refreshDevices]);
156171

172+
const setPusherEnabled = useCallback(
173+
async (deviceId: DeviceWithVerification['device_id'], enabled: boolean): Promise<void> => {
174+
const pusher = pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === deviceId);
175+
try {
176+
await matrixClient.setPusher({
177+
...pusher,
178+
[PUSHER_ENABLED.name]: enabled,
179+
});
180+
await refreshDevices();
181+
} catch (error) {
182+
logger.error("Error setting pusher state", error);
183+
throw new Error(_t("Failed to set pusher state"));
184+
}
185+
}, [matrixClient, pushers, refreshDevices],
186+
);
187+
157188
return {
158189
devices,
190+
pushers,
159191
currentDeviceId,
160192
isLoadingDeviceList,
161193
error,
162194
requestDeviceVerification,
163195
refreshDevices,
164196
saveDeviceName,
197+
setPusherEnabled,
198+
supportsMSC3881,
165199
};
166200
};

src/components/views/settings/tabs/user/SessionManagerTab.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,14 @@ const useSignOut = (
8787
const SessionManagerTab: React.FC = () => {
8888
const {
8989
devices,
90+
pushers,
9091
currentDeviceId,
9192
isLoadingDeviceList,
9293
requestDeviceVerification,
9394
refreshDevices,
9495
saveDeviceName,
96+
setPusherEnabled,
97+
supportsMSC3881,
9598
} = useOwnDevices();
9699
const [filter, setFilter] = useState<DeviceSecurityVariation>();
97100
const [expandedDeviceIds, setExpandedDeviceIds] = useState<DeviceWithVerification['device_id'][]>([]);
@@ -186,6 +189,7 @@ const SessionManagerTab: React.FC = () => {
186189
>
187190
<FilteredDeviceList
188191
devices={otherDevices}
192+
pushers={pushers}
189193
filter={filter}
190194
expandedDeviceIds={expandedDeviceIds}
191195
signingOutDeviceIds={signingOutDeviceIds}
@@ -194,7 +198,9 @@ const SessionManagerTab: React.FC = () => {
194198
onRequestDeviceVerification={requestDeviceVerification ? onTriggerDeviceVerification : undefined}
195199
onSignOutDevices={onSignOutOtherDevices}
196200
saveDeviceName={saveDeviceName}
201+
setPusherEnabled={setPusherEnabled}
197202
ref={filteredDeviceListRef}
203+
supportsMSC3881={supportsMSC3881}
198204
/>
199205
</SettingsSubsection>
200206
}

src/i18n/strings/en_EN.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,6 +1719,9 @@
17191719
"Device": "Device",
17201720
"IP address": "IP address",
17211721
"Session details": "Session details",
1722+
"Toggle push notifications on this session.": "Toggle push notifications on this session.",
1723+
"Push notifications": "Push notifications",
1724+
"Receive push notifications on this session.": "Receive push notifications on this session.",
17221725
"Sign out of this session": "Sign out of this session",
17231726
"Toggle device details": "Toggle device details",
17241727
"Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days",
@@ -1751,6 +1754,7 @@
17511754
"Security recommendations": "Security recommendations",
17521755
"Improve your account security by following these recommendations": "Improve your account security by following these recommendations",
17531756
"View all": "View all",
1757+
"Failed to set pusher state": "Failed to set pusher state",
17541758
"Unable to remove contact information": "Unable to remove contact information",
17551759
"Remove %(email)s?": "Remove %(email)s?",
17561760
"Invalid Email Address": "Invalid Email Address",

test/components/views/settings/DevicesPanel-test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ import { act } from 'react-dom/test-utils';
1919
import { CrossSigningInfo } from 'matrix-js-sdk/src/crypto/CrossSigning';
2020
import { DeviceInfo } from 'matrix-js-sdk/src/crypto/deviceinfo';
2121
import { sleep } from 'matrix-js-sdk/src/utils';
22+
import { PUSHER_DEVICE_ID, PUSHER_ENABLED } from 'matrix-js-sdk/src/@types/event';
2223

2324
import DevicesPanel from "../../../../src/components/views/settings/DevicesPanel";
2425
import {
2526
flushPromises,
2627
getMockClientWithEventEmitter,
28+
mkPusher,
2729
mockClientMethodsUser,
2830
} from "../../../test-utils";
2931

@@ -40,6 +42,8 @@ describe('<DevicesPanel />', () => {
4042
getStoredCrossSigningForUser: jest.fn().mockReturnValue(new CrossSigningInfo(userId, {}, {})),
4143
getStoredDevice: jest.fn().mockReturnValue(new DeviceInfo('id')),
4244
generateClientSecret: jest.fn(),
45+
getPushers: jest.fn(),
46+
setPusher: jest.fn(),
4347
});
4448

4549
const getComponent = () => <DevicesPanel />;
@@ -50,6 +54,15 @@ describe('<DevicesPanel />', () => {
5054
mockClient.getDevices
5155
.mockReset()
5256
.mockResolvedValue({ devices: [device1, device2, device3] });
57+
58+
mockClient.getPushers
59+
.mockReset()
60+
.mockResolvedValue({
61+
pushers: [mkPusher({
62+
[PUSHER_DEVICE_ID.name]: device1.device_id,
63+
[PUSHER_ENABLED.name]: true,
64+
})],
65+
});
5366
});
5467

5568
it('renders device panel with devices', async () => {

0 commit comments

Comments
 (0)