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

Commit 9f66082

Browse files
author
Kerry
authored
Notifications: inline error message on notifications saving error (#10288)
* basic sync setup * formatting * get loudest value for synced rules * more types * test synced rules in notifications settings * type fixes * noimplicitany fixes * remove debug * tidying * extract updatePushRuleActions fn to utils * extract update synced rules * just synchronise in one place? * monitor account data changes AND trigger changes sync in notifications form * lint * setup LoggedInView test with enough mocks * test rule syncing in LoggedInView * strict fixes * more comments * one more comment * add error variant for caption component * tests for new error message * tweak styles * noImplicitAny * revert out of date prettier changes to unrelated files * limit inline message to radios only, tests * strict fix
1 parent 72404d7 commit 9f66082

File tree

8 files changed

+233
-33
lines changed

8 files changed

+233
-33
lines changed

res/css/components/views/typography/_Caption.pcss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,8 @@ limitations under the License.
1717
.mx_Caption {
1818
font-size: $font-12px;
1919
color: $secondary-content;
20+
21+
&.mx_Caption_error {
22+
color: $alert;
23+
}
2024
}

res/css/views/settings/_Notifications.pcss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ limitations under the License.
6161
font-size: $font-12px;
6262
font-weight: $font-semi-bold;
6363
}
64+
.mx_UserNotifSettings_gridRowError {
65+
// occupy full row
66+
grid-column: 1/-1;
67+
justify-self: start;
68+
padding-right: 30%;
69+
// collapse half of the grid-gap
70+
margin-top: -$spacing-4;
71+
}
6472

6573
.mx_UserNotifSettings {
6674
color: $primary-content; /* override from default settings page styles */

src/components/views/settings/Notifications.tsx

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
updateExistingPushRulesWithActions,
4848
updatePushRuleActions,
4949
} from "../../../utils/pushRules/updatePushRuleActions";
50+
import { Caption } from "../typography/Caption";
5051

5152
// TODO: this "view" component still has far too much application logic in it,
5253
// which should be factored out to other files.
@@ -55,7 +56,10 @@ enum Phase {
5556
Loading = "loading",
5657
Ready = "ready",
5758
Persisting = "persisting", // technically a meta-state for Ready, but whatever
59+
// unrecoverable error - eg can't load push rules
5860
Error = "error",
61+
// error saving individual rule
62+
SavingError = "savingError",
5963
}
6064

6165
enum RuleClass {
@@ -121,6 +125,8 @@ interface IState {
121125
audioNotifications: boolean;
122126

123127
clearingNotifications: boolean;
128+
129+
ruleIdsWithError: Record<RuleId | string, boolean>;
124130
}
125131
const findInDefaultRules = (
126132
ruleId: RuleId | string,
@@ -194,6 +200,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
194200
desktopShowBody: SettingsStore.getValue("notificationBodyEnabled"),
195201
audioNotifications: SettingsStore.getValue("audioNotificationsEnabled"),
196202
clearingNotifications: false,
203+
ruleIdsWithError: {},
197204
};
198205

199206
this.settingWatchers = [
@@ -243,13 +250,9 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
243250
).reduce((p, c) => Object.assign(c, p), {});
244251

245252
this.setState<
246-
keyof Omit<
253+
keyof Pick<
247254
IState,
248-
| "deviceNotificationsEnabled"
249-
| "desktopNotifications"
250-
| "desktopShowBody"
251-
| "audioNotifications"
252-
| "clearingNotifications"
255+
"phase" | "vectorKeywordRuleInfo" | "vectorPushRules" | "pushers" | "threepids" | "masterPushRule"
253256
>
254257
>({
255258
...newState,
@@ -393,8 +396,8 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
393396
private onMasterRuleChanged = async (checked: boolean): Promise<void> => {
394397
this.setState({ phase: Phase.Persisting });
395398

399+
const masterRule = this.state.masterPushRule!;
396400
try {
397-
const masterRule = this.state.masterPushRule!;
398401
await MatrixClientPeg.get().setPushRuleEnabled("global", masterRule.kind, masterRule.rule_id, !checked);
399402
await this.refreshFromServer();
400403
} catch (e) {
@@ -404,6 +407,13 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
404407
}
405408
};
406409

410+
private setSavingError = (ruleId: RuleId | string): void => {
411+
this.setState(({ ruleIdsWithError }) => ({
412+
phase: Phase.SavingError,
413+
ruleIdsWithError: { ...ruleIdsWithError, [ruleId]: true },
414+
}));
415+
};
416+
407417
private updateDeviceNotifications = async (checked: boolean): Promise<void> => {
408418
await SettingsStore.setValue("deviceNotificationsEnabled", null, SettingLevel.DEVICE, checked);
409419
};
@@ -455,11 +465,18 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
455465
};
456466

457467
private onRadioChecked = async (rule: IVectorPushRule, checkedState: VectorState): Promise<void> => {
458-
this.setState({ phase: Phase.Persisting });
468+
this.setState(({ ruleIdsWithError }) => ({
469+
phase: Phase.Persisting,
470+
ruleIdsWithError: { ...ruleIdsWithError, [rule.ruleId]: false },
471+
}));
459472

460473
try {
461474
const cli = MatrixClientPeg.get();
462475
if (rule.ruleId === KEYWORD_RULE_ID) {
476+
// should not encounter this
477+
if (!this.state.vectorKeywordRuleInfo) {
478+
throw new Error("Notification data is incomplete.");
479+
}
463480
// Update all the keywords
464481
for (const rule of this.state.vectorKeywordRuleInfo.rules) {
465482
let enabled: boolean | undefined;
@@ -505,9 +522,8 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
505522

506523
await this.refreshFromServer();
507524
} catch (e) {
508-
this.setState({ phase: Phase.Error });
525+
this.setSavingError(rule.ruleId);
509526
logger.error("Error updating push rule:", e);
510-
this.showSaveError();
511527
}
512528
};
513529

@@ -618,14 +634,16 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
618634

619635
private renderTopSection(): JSX.Element {
620636
const masterSwitch = (
621-
<LabelledToggleSwitch
622-
data-testid="notif-master-switch"
623-
value={!this.isInhibited}
624-
label={_t("Enable notifications for this account")}
625-
caption={_t("Turn off to disable notifications on all your devices and sessions")}
626-
onChange={this.onMasterRuleChanged}
627-
disabled={this.state.phase === Phase.Persisting}
628-
/>
637+
<>
638+
<LabelledToggleSwitch
639+
data-testid="notif-master-switch"
640+
value={!this.isInhibited}
641+
label={_t("Enable notifications for this account")}
642+
caption={_t("Turn off to disable notifications on all your devices and sessions")}
643+
onChange={this.onMasterRuleChanged}
644+
disabled={this.state.phase === Phase.Persisting}
645+
/>
646+
</>
629647
);
630648

631649
// If all the rules are inhibited, don't show anything.
@@ -639,7 +657,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
639657
<LabelledToggleSwitch
640658
data-testid="notif-email-switch"
641659
key={e.address}
642-
value={this.state.pushers.some((p) => p.kind === "email" && p.pushkey === e.address)}
660+
value={!!this.state.pushers?.some((p) => p.kind === "email" && p.pushkey === e.address)}
643661
label={_t("Enable email notifications for %(email)s", { email: e.address })}
644662
onChange={this.onEmailNotificationsChanged.bind(this, e.address)}
645663
disabled={this.state.phase === Phase.Persisting}
@@ -768,6 +786,15 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
768786
{makeRadio(r, VectorState.Off)}
769787
{makeRadio(r, VectorState.On)}
770788
{makeRadio(r, VectorState.Loud)}
789+
{this.state.ruleIdsWithError[r.ruleId] && (
790+
<div className="mx_UserNotifSettings_gridRowError">
791+
<Caption isError>
792+
{_t(
793+
"An error occurred when updating your notification preferences. Please try to toggle your option again.",
794+
)}
795+
</Caption>
796+
</div>
797+
)}
771798
</fieldset>
772799
));
773800

src/components/views/typography/Caption.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,22 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import classNames from "classnames";
1718
import React, { HTMLAttributes } from "react";
1819

1920
interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "className"> {
2021
children: React.ReactNode;
22+
isError?: boolean;
2123
}
2224

23-
export const Caption: React.FC<Props> = ({ children, ...rest }) => {
25+
export const Caption: React.FC<Props> = ({ children, isError, ...rest }) => {
2426
return (
25-
<span className="mx_Caption" {...rest}>
27+
<span
28+
className={classNames("mx_Caption", {
29+
mx_Caption_error: isError,
30+
})}
31+
{...rest}
32+
>
2633
{children}
2734
</span>
2835
);

src/i18n/strings/en_EN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,6 +1443,7 @@
14431443
"On": "On",
14441444
"Off": "Off",
14451445
"Noisy": "Noisy",
1446+
"An error occurred when updating your notification preferences. Please try to toggle your option again.": "An error occurred when updating your notification preferences. Please try to toggle your option again.",
14461447
"Global": "Global",
14471448
"Mentions & keywords": "Mentions & keywords",
14481449
"Notification targets": "Notification targets",

0 commit comments

Comments
 (0)