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

Implement push notification toggle in device detail #9308

Merged
merged 19 commits into from
Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions res/css/components/views/settings/devices/_DeviceDetails.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ limitations under the License.

.mx_DeviceDetails_sectionHeading {
margin: 0;

.mx_DeviceDetails_sectionSubheading {
display: block;
font-size: $font-12px;
color: $secondary-content;
line-height: $font-14px;
}
}

.mx_DeviceDetails_metadataTable {
Expand Down Expand Up @@ -81,3 +88,10 @@ limitations under the License.
align-items: center;
gap: $spacing-4;
}

.mx_DeviceDetails_pushNotifications {
display: block;
.mx_ToggleSwitch {
float: right;
}
}
4 changes: 4 additions & 0 deletions res/css/views/elements/_ToggleSwitch.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ limitations under the License.

background-color: $togglesw-off-color;
opacity: 0.5;

&[aria-disabled="true"] {
cursor: not-allowed;
}
}

.mx_ToggleSwitch_enabled {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ const CurrentDeviceSection: React.FC<Props> = ({
onVerifyDevice={onVerifyCurrentDevice}
onSignOutDevice={onSignOutCurrentDevice}
saveDeviceName={saveDeviceName}
// Current device can not have a pusher as Web does not use them
pusher={null}
setPusherEnabled={null}
supportsMSC3881={null}
/>
}
<br />
Expand Down
31 changes: 31 additions & 0 deletions src/components/views/settings/devices/DeviceDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,27 @@ limitations under the License.
*/

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 { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton';
import Spinner from '../../elements/Spinner';
import ToggleSwitch from '../../elements/ToggleSwitch';
import { DeviceDetailHeading } from './DeviceDetailHeading';
import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard';
import { DeviceWithVerification } from './types';

interface Props {
device: DeviceWithVerification;
pusher: IPusher | null;
isSigningOut: boolean;
onVerifyDevice?: () => void;
onSignOutDevice: () => void;
saveDeviceName: (deviceName: string) => Promise<void>;
setPusherEnabled: (deviceId: string, enabled: boolean) => Promise<void> | null;
supportsMSC3881: boolean | null;
}

interface MetadataTable {
Expand All @@ -39,10 +45,13 @@ interface MetadataTable {

const DeviceDetails: React.FC<Props> = ({
device,
pusher,
isSigningOut,
onVerifyDevice,
onSignOutDevice,
saveDeviceName,
setPusherEnabled,
supportsMSC3881,
}) => {
const metadata: MetadataTable[] = [
{
Expand Down Expand Up @@ -93,6 +102,28 @@ const DeviceDetails: React.FC<Props> = ({
</table>,
) }
</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'>
<AccessibleButton
onClick={onSignOutDevice}
Expand Down
24 changes: 24 additions & 0 deletions src/components/views/settings/devices/FilteredDeviceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ 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 { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton';
Expand All @@ -36,6 +38,7 @@ import { DevicesState } from './useOwnDevices';

interface Props {
devices: DevicesDictionary;
pushers: IPusher[];
expandedDeviceIds: DeviceWithVerification['device_id'][];
signingOutDeviceIds: DeviceWithVerification['device_id'][];
filter?: DeviceSecurityVariation;
Expand All @@ -44,6 +47,8 @@ interface Props {
onSignOutDevices: (deviceIds: DeviceWithVerification['device_id'][]) => void;
saveDeviceName: DevicesState['saveDeviceName'];
onRequestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => void;
setPusherEnabled: (deviceId: string, enabled: boolean) => Promise<void>;
supportsMSC3881: boolean | null;
}

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

const DeviceListItem: React.FC<{
device: DeviceWithVerification;
pusher: IPusher | null;
isExpanded: boolean;
isSigningOut: boolean;
onDeviceExpandToggle: () => void;
onSignOutDevice: () => void;
saveDeviceName: (deviceName: string) => Promise<void>;
onRequestDeviceVerification?: () => void;
setPusherEnabled: (deviceId: string, enabled: boolean) => Promise<void>;
supportsMSC3881: boolean | null;
}> = ({
device,
pusher,
isExpanded,
isSigningOut,
onDeviceExpandToggle,
onSignOutDevice,
saveDeviceName,
onRequestDeviceVerification,
setPusherEnabled,
supportsMSC3881,
}) => <li className='mx_FilteredDeviceList_listItem'>
<DeviceTile
device={device}
Expand All @@ -162,10 +173,13 @@ const DeviceListItem: React.FC<{
isExpanded &&
<DeviceDetails
device={device}
pusher={pusher}
isSigningOut={isSigningOut}
onVerifyDevice={onRequestDeviceVerification}
onSignOutDevice={onSignOutDevice}
saveDeviceName={saveDeviceName}
setPusherEnabled={setPusherEnabled}
supportsMSC3881={supportsMSC3881}
/>
}
</li>;
Expand All @@ -177,6 +191,7 @@ const DeviceListItem: React.FC<{
export const FilteredDeviceList =
forwardRef(({
devices,
pushers,
filter,
expandedDeviceIds,
signingOutDeviceIds,
Expand All @@ -185,9 +200,15 @@ export const FilteredDeviceList =
saveDeviceName,
onSignOutDevices,
onRequestDeviceVerification,
setPusherEnabled,
supportsMSC3881,
}: Props, ref: ForwardedRef<HTMLDivElement>) => {
const sortedDevices = getFilteredSortedDevices(devices, filter);

function getPusherForDevice(device: DeviceWithVerification): IPusher | null {
return pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === device.device_id);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function getPusherForDevice(device: DeviceWithVerification): IPusher | null {
return pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === device.device_id);
}
const getPusherForDevice = (device: DeviceWithVerification): IPusher | null => {
return pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === device.device_id);
};

I'm paranoid about scope changing randomly on us

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand the rationale behind this change.
This function, overall declared will be scoped to the function it's declared in. Using const will scope it to the block it's declared in...
It's the whole concept of closures and sounds more like a stylistic preference


const options: FilterDropdownOption<DeviceFilterKey>[] = [
{ id: ALL_FILTER_ID, label: _t('All') },
{
Expand Down Expand Up @@ -236,6 +257,7 @@ export const FilteredDeviceList =
{ sortedDevices.map((device) => <DeviceListItem
key={device.device_id}
device={device}
pusher={getPusherForDevice(device)}
isExpanded={expandedDeviceIds.includes(device.device_id)}
isSigningOut={signingOutDeviceIds.includes(device.device_id)}
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
Expand All @@ -246,6 +268,8 @@ export const FilteredDeviceList =
? () => onRequestDeviceVerification(device.device_id)
: undefined
}
setPusherEnabled={setPusherEnabled}
supportsMSC3881={supportsMSC3881}
/>,
) }
</ol>
Expand Down
34 changes: 33 additions & 1 deletion src/components/views/settings/devices/useOwnDevices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ limitations under the License.
*/

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 { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { MatrixError } from "matrix-js-sdk/src/http-api";
Expand Down Expand Up @@ -76,13 +76,16 @@ export enum OwnDevicesError {
}
export type DevicesState = {
devices: DevicesDictionary;
pushers: IPusher[];
currentDeviceId: string;
isLoadingDeviceList: boolean;
// not provided when current session cannot request verification
requestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => Promise<VerificationRequest>;
refreshDevices: () => Promise<void>;
saveDeviceName: (deviceId: DeviceWithVerification['device_id'], deviceName: string) => Promise<void>;
setPusherEnabled: (deviceId: DeviceWithVerification['device_id'], enabled: boolean) => Promise<void>;
error?: OwnDevicesError;
supportsMSC3881: boolean;
};
export const useOwnDevices = (): DevicesState => {
const matrixClient = useContext(MatrixClientContext);
Expand All @@ -91,10 +94,16 @@ export const useOwnDevices = (): DevicesState => {
const userId = matrixClient.getUserId();

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

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

matrixClient.doesServerSupportUnstableFeature("org.matrix.msc3881").then(hasSupport => {
setSupportsMSC3881(hasSupport);
});

const refreshDevices = useCallback(async () => {
setIsLoadingDeviceList(true);
try {
Expand All @@ -105,6 +114,10 @@ export const useOwnDevices = (): DevicesState => {
}
const devices = await fetchDevicesWithVerification(matrixClient, userId);
setDevices(devices);

const { pushers } = await matrixClient.getPushers();
setPushers(pushers);

setIsLoadingDeviceList(false);
} catch (error) {
if ((error as MatrixError).httpStatus == 404) {
Expand Down Expand Up @@ -154,13 +167,32 @@ export const useOwnDevices = (): DevicesState => {
}
}, [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 session display name", error);
throw new Error(_t("Failed to set display name"));
}
}, [matrixClient, pushers, refreshDevices],
);

return {
devices,
pushers,
currentDeviceId,
isLoadingDeviceList,
error,
requestDeviceVerification,
refreshDevices,
saveDeviceName,
setPusherEnabled,
supportsMSC3881,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,14 @@ const useSignOut = (
const SessionManagerTab: React.FC = () => {
const {
devices,
pushers,
currentDeviceId,
isLoadingDeviceList,
requestDeviceVerification,
refreshDevices,
saveDeviceName,
setPusherEnabled,
supportsMSC3881,
} = useOwnDevices();
const [filter, setFilter] = useState<DeviceSecurityVariation>();
const [expandedDeviceIds, setExpandedDeviceIds] = useState<DeviceWithVerification['device_id'][]>([]);
Expand Down Expand Up @@ -186,6 +189,7 @@ const SessionManagerTab: React.FC = () => {
>
<FilteredDeviceList
devices={otherDevices}
pushers={pushers}
filter={filter}
expandedDeviceIds={expandedDeviceIds}
signingOutDeviceIds={signingOutDeviceIds}
Expand All @@ -194,7 +198,9 @@ const SessionManagerTab: React.FC = () => {
onRequestDeviceVerification={requestDeviceVerification ? onTriggerDeviceVerification : undefined}
onSignOutDevices={onSignOutOtherDevices}
saveDeviceName={saveDeviceName}
setPusherEnabled={setPusherEnabled}
ref={filteredDeviceListRef}
supportsMSC3881={supportsMSC3881}
/>
</SettingsSubsection>
}
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1717,6 +1717,9 @@
"Device": "Device",
"IP address": "IP address",
"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",
"Toggle device details": "Toggle device details",
"Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe('<CurrentDeviceSection />', () => {
isLoading: false,
isSigningOut: false,
};

const getComponent = (props = {}): React.ReactElement =>
(<CurrentDeviceSection {...defaultProps} {...props} />);

Expand Down
Loading