@@ -15,10 +15,18 @@ limitations under the License.
1515*/
1616
1717import React , { ReactNode } from "react" ;
18- import { IAnnotatedPushRule , IPusher , PushRuleAction , PushRuleKind , RuleId } from "matrix-js-sdk/src/@types/PushRules" ;
18+ import {
19+ IAnnotatedPushRule ,
20+ IPusher ,
21+ PushRuleAction ,
22+ IPushRule ,
23+ PushRuleKind ,
24+ RuleId ,
25+ } from "matrix-js-sdk/src/@types/PushRules" ;
1926import { IThreepid , ThreepidMedium } from "matrix-js-sdk/src/@types/threepids" ;
2027import { logger } from "matrix-js-sdk/src/logger" ;
2128import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications" ;
29+ import { PushProcessor } from "matrix-js-sdk/src/pushprocessor" ;
2230
2331import Spinner from "../elements/Spinner" ;
2432import { MatrixClientPeg } from "../../../MatrixClientPeg" ;
@@ -92,6 +100,9 @@ interface IVectorPushRule {
92100 rule ?: IAnnotatedPushRule ;
93101 description : TranslatedString | string ;
94102 vectorState : VectorState ;
103+ // loudest vectorState of a rule and its synced rules
104+ // undefined when rule has no synced rules
105+ syncedVectorState ?: VectorState ;
95106}
96107
97108interface IProps { }
@@ -115,9 +126,68 @@ interface IState {
115126
116127 clearingNotifications : boolean ;
117128}
129+ const findInDefaultRules = (
130+ ruleId : RuleId | string ,
131+ defaultRules : {
132+ [ k in RuleClass ] : IAnnotatedPushRule [ ] ;
133+ } ,
134+ ) : IAnnotatedPushRule | undefined => {
135+ for ( const category in defaultRules ) {
136+ const rule : IAnnotatedPushRule | undefined = defaultRules [ category as RuleClass ] . find (
137+ ( rule ) => rule . rule_id === ruleId ,
138+ ) ;
139+ if ( rule ) {
140+ return rule ;
141+ }
142+ }
143+ } ;
144+
145+ // Vector notification states ordered by loudness in ascending order
146+ const OrderedVectorStates = [ VectorState . Off , VectorState . On , VectorState . Loud ] ;
147+
148+ /**
149+ * Find the 'loudest' vector state assigned to a rule
150+ * and it's synced rules
151+ * If rules have fallen out of sync,
152+ * the loudest rule can determine the display value
153+ * @param defaultRules
154+ * @param rule - parent rule
155+ * @param definition - definition of parent rule
156+ * @returns VectorState - the maximum/loudest state for the parent and synced rules
157+ */
158+ const maximumVectorState = (
159+ defaultRules : {
160+ [ k in RuleClass ] : IAnnotatedPushRule [ ] ;
161+ } ,
162+ rule : IAnnotatedPushRule ,
163+ definition : VectorPushRuleDefinition ,
164+ ) : VectorState | undefined => {
165+ if ( ! definition . syncedRuleIds ?. length ) {
166+ return undefined ;
167+ }
168+ const vectorState = definition . syncedRuleIds . reduce < VectorState > ( ( maxVectorState , ruleId ) => {
169+ // already set to maximum
170+ if ( maxVectorState === VectorState . Loud ) {
171+ return maxVectorState ;
172+ }
173+ const syncedRule = findInDefaultRules ( ruleId , defaultRules ) ;
174+ if ( syncedRule ) {
175+ const syncedRuleVectorState = definition . ruleToVectorState ( syncedRule ) ;
176+ // if syncedRule is 'louder' than current maximum
177+ // set maximum to louder vectorState
178+ if ( OrderedVectorStates . indexOf ( syncedRuleVectorState ) > OrderedVectorStates . indexOf ( maxVectorState ) ) {
179+ return syncedRuleVectorState ;
180+ }
181+ }
182+ return maxVectorState ;
183+ } , definition . ruleToVectorState ( rule ) ) ;
184+
185+ return vectorState ;
186+ } ;
118187
119188export default class Notifications extends React . PureComponent < IProps , IState > {
120189 private settingWatchers : string [ ] ;
190+ private pushProcessor : PushProcessor ;
121191
122192 public constructor ( props : IProps ) {
123193 super ( props ) ;
@@ -145,6 +215,8 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
145215 this . setState ( { audioNotifications : value as boolean } ) ,
146216 ) ,
147217 ] ;
218+
219+ this . pushProcessor = new PushProcessor ( MatrixClientPeg . get ( ) ) ;
148220 }
149221
150222 private get isInhibited ( ) : boolean {
@@ -281,6 +353,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
281353 ruleId : rule . rule_id ,
282354 rule,
283355 vectorState,
356+ syncedVectorState : maximumVectorState ( defaultRules , rule , definition ) ,
284357 description : _t ( definition . description ) ,
285358 } ) ;
286359 }
@@ -388,6 +461,43 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
388461 await SettingsStore . setValue ( "audioNotificationsEnabled" , null , SettingLevel . DEVICE , checked ) ;
389462 } ;
390463
464+ private setPushRuleActions = async (
465+ ruleId : IPushRule [ "rule_id" ] ,
466+ kind : PushRuleKind ,
467+ actions ?: PushRuleAction [ ] ,
468+ ) : Promise < void > => {
469+ const cli = MatrixClientPeg . get ( ) ;
470+ if ( ! actions ) {
471+ await cli . setPushRuleEnabled ( "global" , kind , ruleId , false ) ;
472+ } else {
473+ await cli . setPushRuleActions ( "global" , kind , ruleId , actions ) ;
474+ await cli . setPushRuleEnabled ( "global" , kind , ruleId , true ) ;
475+ }
476+ } ;
477+
478+ /**
479+ * Updated syncedRuleIds from rule definition
480+ * If a rule does not exist it is ignored
481+ * Synced rules are updated sequentially
482+ * and stop at first error
483+ */
484+ private updateSyncedRules = async (
485+ syncedRuleIds : VectorPushRuleDefinition [ "syncedRuleIds" ] ,
486+ actions ?: PushRuleAction [ ] ,
487+ ) : Promise < void > => {
488+ // get synced rules that exist for user
489+ const syncedRules : ReturnType < PushProcessor [ "getPushRuleAndKindById" ] > [ ] = syncedRuleIds
490+ ?. map ( ( ruleId ) => this . pushProcessor . getPushRuleAndKindById ( ruleId ) )
491+ . filter ( Boolean ) ;
492+
493+ if ( ! syncedRules ?. length ) {
494+ return ;
495+ }
496+ for ( const { kind, rule : syncedRule } of syncedRules ) {
497+ await this . setPushRuleActions ( syncedRule . rule_id , kind , actions ) ;
498+ }
499+ } ;
500+
391501 private onRadioChecked = async ( rule : IVectorPushRule , checkedState : VectorState ) : Promise < void > => {
392502 this . setState ( { phase : Phase . Persisting } ) ;
393503
@@ -428,12 +538,8 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
428538 } else {
429539 const definition : VectorPushRuleDefinition = VectorPushRulesDefinitions [ rule . ruleId ] ;
430540 const actions = definition . vectorStateToActions [ checkedState ] ;
431- if ( ! actions ) {
432- await cli . setPushRuleEnabled ( "global" , rule . rule . kind , rule . rule . rule_id , false ) ;
433- } else {
434- await cli . setPushRuleActions ( "global" , rule . rule . kind , rule . rule . rule_id , actions ) ;
435- await cli . setPushRuleEnabled ( "global" , rule . rule . kind , rule . rule . rule_id , true ) ;
436- }
541+ await this . setPushRuleActions ( rule . rule . rule_id , rule . rule . kind , actions ) ;
542+ await this . updateSyncedRules ( definition . syncedRuleIds , actions ) ;
437543 }
438544
439545 await this . refreshFromServer ( ) ;
@@ -684,7 +790,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
684790 < StyledRadioButton
685791 key = { r . ruleId + s }
686792 name = { r . ruleId }
687- checked = { r . vectorState === s }
793+ checked = { ( r . syncedVectorState ?? r . vectorState ) === s }
688794 onChange = { this . onRadioChecked . bind ( this , r , s ) }
689795 disabled = { this . state . phase === Phase . Persisting }
690796 aria-label = { VectorStateToLabel [ s ] }
0 commit comments