Implement push notification toggle in device detail (#9308)
Co-authored-by: Travis Ralston <travisr@matrix.org>
This commit is contained in:
parent
ace6591f43
commit
641cf28e4c
13 changed files with 269 additions and 3 deletions
|
@ -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 | undefined;
|
||||
isSigningOut: boolean;
|
||||
onVerifyDevice?: () => void;
|
||||
onSignOutDevice: () => void;
|
||||
saveDeviceName: (deviceName: string) => Promise<void>;
|
||||
setPusherEnabled?: (deviceId: string, enabled: boolean) => Promise<void> | undefined;
|
||||
supportsMSC3881?: boolean | undefined;
|
||||
}
|
||||
|
||||
interface MetadataTable {
|
||||
|
@ -39,10 +45,13 @@ interface MetadataTable {
|
|||
|
||||
const DeviceDetails: React.FC<Props> = ({
|
||||
device,
|
||||
pusher,
|
||||
isSigningOut,
|
||||
onVerifyDevice,
|
||||
onSignOutDevice,
|
||||
saveDeviceName,
|
||||
setPusherEnabled,
|
||||
supportsMSC3881,
|
||||
}) => {
|
||||
const metadata: MetadataTable[] = [
|
||||
{
|
||||
|
@ -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}
|
||||
|
|
|
@ -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';
|
||||
|
@ -36,6 +38,7 @@ import { DevicesState } from './useOwnDevices';
|
|||
|
||||
interface Props {
|
||||
devices: DevicesDictionary;
|
||||
pushers: IPusher[];
|
||||
expandedDeviceIds: DeviceWithVerification['device_id'][];
|
||||
signingOutDeviceIds: DeviceWithVerification['device_id'][];
|
||||
filter?: DeviceSecurityVariation;
|
||||
|
@ -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 | undefined;
|
||||
}
|
||||
|
||||
// devices without timestamp metadata should be sorted last
|
||||
|
@ -135,20 +140,26 @@ const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) =>
|
|||
|
||||
const DeviceListItem: React.FC<{
|
||||
device: DeviceWithVerification;
|
||||
pusher?: IPusher | undefined;
|
||||
isExpanded: boolean;
|
||||
isSigningOut: boolean;
|
||||
onDeviceExpandToggle: () => void;
|
||||
onSignOutDevice: () => void;
|
||||
saveDeviceName: (deviceName: string) => Promise<void>;
|
||||
onRequestDeviceVerification?: () => void;
|
||||
setPusherEnabled: (deviceId: string, enabled: boolean) => Promise<void>;
|
||||
supportsMSC3881?: boolean | undefined;
|
||||
}> = ({
|
||||
device,
|
||||
pusher,
|
||||
isExpanded,
|
||||
isSigningOut,
|
||||
onDeviceExpandToggle,
|
||||
onSignOutDevice,
|
||||
saveDeviceName,
|
||||
onRequestDeviceVerification,
|
||||
setPusherEnabled,
|
||||
supportsMSC3881,
|
||||
}) => <li className='mx_FilteredDeviceList_listItem'>
|
||||
<DeviceTile
|
||||
device={device}
|
||||
|
@ -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>;
|
||||
|
@ -177,6 +191,7 @@ const DeviceListItem: React.FC<{
|
|||
export const FilteredDeviceList =
|
||||
forwardRef(({
|
||||
devices,
|
||||
pushers,
|
||||
filter,
|
||||
expandedDeviceIds,
|
||||
signingOutDeviceIds,
|
||||
|
@ -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 | undefined {
|
||||
return pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === device.device_id);
|
||||
}
|
||||
|
||||
const options: FilterDropdownOption<DeviceFilterKey>[] = [
|
||||
{ id: ALL_FILTER_ID, label: _t('All') },
|
||||
{
|
||||
|
@ -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)}
|
||||
|
@ -246,6 +268,8 @@ export const FilteredDeviceList =
|
|||
? () => onRequestDeviceVerification(device.device_id)
|
||||
: undefined
|
||||
}
|
||||
setPusherEnabled={setPusherEnabled}
|
||||
supportsMSC3881={supportsMSC3881}
|
||||
/>,
|
||||
) }
|
||||
</ol>
|
||||
|
|
|
@ -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";
|
||||
|
@ -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 | undefined;
|
||||
};
|
||||
export const useOwnDevices = (): DevicesState => {
|
||||
const matrixClient = useContext(MatrixClientContext);
|
||||
|
@ -91,10 +94,18 @@ 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>();
|
||||
|
||||
useEffect(() => {
|
||||
matrixClient.doesServerSupportUnstableFeature("org.matrix.msc3881").then(hasSupport => {
|
||||
setSupportsMSC3881(hasSupport);
|
||||
});
|
||||
}, [matrixClient]);
|
||||
|
||||
const refreshDevices = useCallback(async () => {
|
||||
setIsLoadingDeviceList(true);
|
||||
try {
|
||||
|
@ -105,6 +116,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) {
|
||||
|
@ -154,13 +169,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 pusher state", error);
|
||||
throw new Error(_t("Failed to set pusher state"));
|
||||
}
|
||||
}, [matrixClient, pushers, refreshDevices],
|
||||
);
|
||||
|
||||
return {
|
||||
devices,
|
||||
pushers,
|
||||
currentDeviceId,
|
||||
isLoadingDeviceList,
|
||||
error,
|
||||
requestDeviceVerification,
|
||||
refreshDevices,
|
||||
saveDeviceName,
|
||||
setPusherEnabled,
|
||||
supportsMSC3881,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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'][]>([]);
|
||||
|
@ -186,6 +189,7 @@ const SessionManagerTab: React.FC = () => {
|
|||
>
|
||||
<FilteredDeviceList
|
||||
devices={otherDevices}
|
||||
pushers={pushers}
|
||||
filter={filter}
|
||||
expandedDeviceIds={expandedDeviceIds}
|
||||
signingOutDeviceIds={signingOutDeviceIds}
|
||||
|
@ -194,7 +198,9 @@ const SessionManagerTab: React.FC = () => {
|
|||
onRequestDeviceVerification={requestDeviceVerification ? onTriggerDeviceVerification : undefined}
|
||||
onSignOutDevices={onSignOutOtherDevices}
|
||||
saveDeviceName={saveDeviceName}
|
||||
setPusherEnabled={setPusherEnabled}
|
||||
ref={filteredDeviceListRef}
|
||||
supportsMSC3881={supportsMSC3881}
|
||||
/>
|
||||
</SettingsSubsection>
|
||||
}
|
||||
|
|
|
@ -1719,6 +1719,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",
|
||||
|
@ -1751,6 +1754,7 @@
|
|||
"Security recommendations": "Security recommendations",
|
||||
"Improve your account security by following these recommendations": "Improve your account security by following these recommendations",
|
||||
"View all": "View all",
|
||||
"Failed to set pusher state": "Failed to set pusher state",
|
||||
"Unable to remove contact information": "Unable to remove contact information",
|
||||
"Remove %(email)s?": "Remove %(email)s?",
|
||||
"Invalid Email Address": "Invalid Email Address",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue