mirror of
https://github.com/element-hq/element-web
synced 2024-11-26 11:15:53 +03:00
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
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
|
@ -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} />);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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} />);
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in a new issue