@genesislcap/pbc-notify-ui 15.8.3 → 15.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +39 -0
  2. package/custom-elements-manifest.config.js +5 -0
  3. package/dist/custom-elements.json +1771 -321
  4. package/dist/dts/components/components.d.ts.map +1 -1
  5. package/dist/dts/components/foundation-inbox/inbox-base/inbox-base.d.ts +2 -0
  6. package/dist/dts/components/foundation-inbox/inbox-base/inbox-base.d.ts.map +1 -1
  7. package/dist/dts/components/foundation-inbox/inbox.styles.d.ts.map +1 -1
  8. package/dist/dts/components/foundation-inbox/inbox.template.d.ts.map +1 -1
  9. package/dist/dts/components/foundation-inbox/inbox.types.d.ts +4 -1
  10. package/dist/dts/components/foundation-inbox/inbox.types.d.ts.map +1 -1
  11. package/dist/dts/components/foundation-notification-dashboard/alert-sound-attachment.d.ts +86 -0
  12. package/dist/dts/components/foundation-notification-dashboard/alert-sound-attachment.d.ts.map +1 -0
  13. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.d.ts +39 -1
  14. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.d.ts.map +1 -1
  15. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.template.d.ts.map +1 -1
  16. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.types.d.ts +8 -0
  17. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.types.d.ts.map +1 -1
  18. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-management.d.ts.map +1 -1
  19. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.d.ts +41 -1
  20. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.d.ts.map +1 -1
  21. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.template.d.ts.map +1 -1
  22. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.types.d.ts +2 -1
  23. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.types.d.ts.map +1 -1
  24. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-management.d.ts.map +1 -1
  25. package/dist/dts/components/foundation-notification-dashboard/styles/dynamic-rule.styles.d.ts.map +1 -1
  26. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.d.ts +73 -0
  27. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.d.ts.map +1 -0
  28. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.styles.d.ts +2 -0
  29. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.styles.d.ts.map +1 -0
  30. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.template.d.ts +3 -0
  31. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.template.d.ts.map +1 -0
  32. package/dist/dts/components/foundation-sounds/foundation-sounds.d.ts +34 -0
  33. package/dist/dts/components/foundation-sounds/foundation-sounds.d.ts.map +1 -0
  34. package/dist/dts/components/foundation-sounds/foundation-sounds.styles.d.ts +3 -0
  35. package/dist/dts/components/foundation-sounds/foundation-sounds.styles.d.ts.map +1 -0
  36. package/dist/dts/index.d.ts +4 -0
  37. package/dist/dts/index.d.ts.map +1 -1
  38. package/dist/dts/react.d.ts +31 -11
  39. package/dist/dts/services/inbox.service.d.ts +6 -0
  40. package/dist/dts/services/inbox.service.d.ts.map +1 -1
  41. package/dist/dts/services/rule.service.d.ts +2 -1
  42. package/dist/dts/services/rule.service.d.ts.map +1 -1
  43. package/dist/dts/utils/alert-sound.d.ts +177 -0
  44. package/dist/dts/utils/alert-sound.d.ts.map +1 -0
  45. package/dist/dts/utils/alert-sound.test.d.ts +2 -0
  46. package/dist/dts/utils/alert-sound.test.d.ts.map +1 -0
  47. package/dist/esm/components/components.js +4 -0
  48. package/dist/esm/components/foundation-inbox/inbox-base/inbox-base.js +6 -0
  49. package/dist/esm/components/foundation-inbox/inbox.styles.js +15 -0
  50. package/dist/esm/components/foundation-inbox/inbox.template.js +16 -0
  51. package/dist/esm/components/foundation-inbox/inbox.types.js +1 -0
  52. package/dist/esm/components/foundation-notification-dashboard/alert-sound-attachment.js +218 -0
  53. package/dist/esm/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.js +17 -2
  54. package/dist/esm/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.template.js +3 -0
  55. package/dist/esm/components/foundation-notification-dashboard/components/rules/rule-management.js +5 -1
  56. package/dist/esm/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.js +23 -2
  57. package/dist/esm/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.template.js +3 -0
  58. package/dist/esm/components/foundation-notification-dashboard/components/templates/template-management.js +8 -1
  59. package/dist/esm/components/foundation-notification-dashboard/styles/dynamic-rule.styles.js +31 -1
  60. package/dist/esm/components/foundation-sounds/foundation-sounds-panel.js +283 -0
  61. package/dist/esm/components/foundation-sounds/foundation-sounds-panel.styles.js +200 -0
  62. package/dist/esm/components/foundation-sounds/foundation-sounds-panel.template.js +179 -0
  63. package/dist/esm/components/foundation-sounds/foundation-sounds.js +206 -0
  64. package/dist/esm/components/foundation-sounds/foundation-sounds.styles.js +7 -0
  65. package/dist/esm/index.js +3 -0
  66. package/dist/esm/services/inbox.service.js +6 -2
  67. package/dist/esm/services/rule.service.js +35 -0
  68. package/dist/esm/utils/alert-sound.js +955 -0
  69. package/dist/esm/utils/alert-sound.test.js +25 -0
  70. package/dist/esm/utils/index.js +3 -0
  71. package/dist/pbc-notify-ui.api.json +2859 -332
  72. package/dist/pbc-notify-ui.d.ts +281 -3
  73. package/dist/react.cjs +19 -5
  74. package/dist/react.mjs +16 -4
  75. package/dist/tsconfig.tsbuildinfo +1 -1
  76. package/package.json +23 -23
  77. package/src/components/components.ts +4 -0
  78. package/src/components/foundation-inbox/inbox-base/inbox-base.ts +7 -0
  79. package/src/components/foundation-inbox/inbox.styles.ts +15 -0
  80. package/src/components/foundation-inbox/inbox.template.ts +16 -0
  81. package/src/components/foundation-inbox/inbox.types.ts +3 -0
  82. package/src/components/foundation-notification-dashboard/alert-sound-attachment.ts +283 -0
  83. package/src/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.template.ts +3 -0
  84. package/src/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.ts +21 -2
  85. package/src/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.types.ts +9 -0
  86. package/src/components/foundation-notification-dashboard/components/rules/rule-management.ts +5 -1
  87. package/src/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.template.ts +3 -0
  88. package/src/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.ts +25 -2
  89. package/src/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.types.ts +2 -1
  90. package/src/components/foundation-notification-dashboard/components/templates/template-management.ts +8 -1
  91. package/src/components/foundation-notification-dashboard/styles/dynamic-rule.styles.ts +31 -1
  92. package/src/components/foundation-sounds/foundation-sounds-panel.styles.ts +201 -0
  93. package/src/components/foundation-sounds/foundation-sounds-panel.template.ts +188 -0
  94. package/src/components/foundation-sounds/foundation-sounds-panel.ts +280 -0
  95. package/src/components/foundation-sounds/foundation-sounds.styles.ts +8 -0
  96. package/src/components/foundation-sounds/foundation-sounds.ts +222 -0
  97. package/src/index.ts +8 -0
  98. package/src/services/inbox.service.ts +12 -2
  99. package/src/services/rule.service.ts +37 -1
  100. package/src/utils/alert-sound.test.ts +32 -0
  101. package/src/utils/alert-sound.ts +1135 -0
  102. package/src/utils/index.ts +3 -0
@@ -0,0 +1,200 @@
1
+ import { css } from '@genesislcap/web-core';
2
+ import { scrollbarStyles } from '../../styles/scrollbar.styles';
3
+ export const FoundationSoundsPanelStyles = css `
4
+ :host {
5
+ display: flex;
6
+ flex-direction: column;
7
+ height: 100%;
8
+ min-height: 0;
9
+ width: 100%;
10
+ }
11
+
12
+ .sounds-controls {
13
+ display: flex;
14
+ flex-direction: column;
15
+ flex: 1;
16
+ min-height: 0;
17
+ gap: 12px;
18
+ padding: calc(var(--design-unit) * 3px);
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ .sounds-help-disclosure {
23
+ flex: 0 0 auto;
24
+ }
25
+
26
+ .sounds-help-toggle {
27
+ margin-inline-start: calc(var(--design-unit) * 1px);
28
+ }
29
+
30
+ .sounds-help-toggle::before {
31
+ content: '+';
32
+ }
33
+
34
+ .sounds-help-disclosure[expanded] .sounds-help-toggle::before {
35
+ content: '−';
36
+ }
37
+
38
+ .sounds-help {
39
+ margin: 0;
40
+ font-size: var(--type-ramp-minus-1-font-size);
41
+ color: var(--neutral-foreground-hint);
42
+ text-align: left;
43
+ }
44
+
45
+ .volume-row {
46
+ display: flex;
47
+ align-items: center;
48
+ flex: 0 0 auto;
49
+ gap: calc(var(--design-unit) * 2px);
50
+ min-width: 0;
51
+ }
52
+
53
+ .volume-icon {
54
+ flex: 0 0 auto;
55
+ }
56
+
57
+ .volume-label {
58
+ flex: 0 0 auto;
59
+ font-size: var(--type-ramp-minus-1-font-size);
60
+ color: var(--neutral-foreground-hint);
61
+ }
62
+
63
+ .volume-slider {
64
+ flex: 1 1 auto;
65
+ min-width: 0;
66
+ }
67
+
68
+ .volume-percent {
69
+ flex: 0 0 auto;
70
+ min-width: 3ch;
71
+ text-align: end;
72
+ font-size: var(--type-ramp-minus-1-font-size);
73
+ color: var(--neutral-foreground-hint);
74
+ }
75
+
76
+ .mute-all {
77
+ flex: 0 0 auto;
78
+ margin: 0;
79
+ }
80
+
81
+ .sound-rules-panel {
82
+ flex: 1 1 auto;
83
+ min-height: 0;
84
+ overflow: auto;
85
+ }
86
+
87
+ .sound-rules {
88
+ display: flex;
89
+ flex-direction: column;
90
+ gap: 12px;
91
+ }
92
+
93
+ .sound-rule {
94
+ display: flex;
95
+ flex-direction: column;
96
+ gap: 8px;
97
+ padding: 12px;
98
+ border: calc(var(--stroke-width) * 1px) solid var(--neutral-stroke-divider-rest);
99
+ border-radius: calc(var(--control-corner-radius) * 1px);
100
+
101
+ /* Keep absolute listboxes from being clipped by the card itself. */
102
+ overflow: visible;
103
+ }
104
+
105
+ .sound-rule-header {
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: space-between;
109
+ font-weight: 700;
110
+ font-size: var(--type-ramp-minus-1-font-size);
111
+ }
112
+
113
+ .sound-rule-actions {
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 2px;
117
+ }
118
+
119
+ .sound-rule-label {
120
+ display: flex;
121
+ flex-direction: column;
122
+ gap: 4px;
123
+ font-size: var(--type-ramp-minus-1-font-size);
124
+ color: var(--neutral-foreground-hint);
125
+ }
126
+
127
+ .pattern-input,
128
+ .match-mode-select,
129
+ .rule-select,
130
+ .sound-file-input {
131
+ width: 100%;
132
+ }
133
+
134
+ /*
135
+ * Anchor the listbox to the select host and use position:fixed so it escapes
136
+ * .sound-rules-panel overflow:auto (and the rule card). Each select sets a
137
+ * unique --sounds-select-anchor / anchor-name via inline style.
138
+ * !important beats FAST's internal absolute top/bottom for [open] selects.
139
+ */
140
+ .match-mode-select[open]::part(listbox),
141
+ .rule-select[open]::part(listbox) {
142
+ position: fixed !important;
143
+ /* stylelint-disable-next-line property-no-unknown -- CSS anchor positioning */
144
+ position-anchor: var(--sounds-select-anchor);
145
+ inset: auto !important;
146
+ top: anchor(bottom) !important;
147
+ left: anchor(left) !important;
148
+ right: anchor(right) !important;
149
+ bottom: auto !important;
150
+ width: auto !important;
151
+ margin-top: calc(var(--design-unit) * 1px);
152
+ z-index: 10000;
153
+ max-height: min(240px, 40vh);
154
+ overflow-x: hidden;
155
+ overflow-y: auto;
156
+ box-shadow: 0 4px 16px rgb(0 0 0 / 24%);
157
+ }
158
+
159
+ .match-mode-select[open][position='above']::part(listbox),
160
+ .rule-select[open][position='above']::part(listbox) {
161
+ top: auto !important;
162
+ bottom: anchor(top) !important;
163
+ margin-top: 0;
164
+ margin-bottom: calc(var(--design-unit) * 1px);
165
+ }
166
+
167
+ .sound-file-row {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: 4px;
171
+ }
172
+
173
+ .sound-file-row .sound-file-input {
174
+ flex: 1;
175
+ min-width: 0;
176
+ }
177
+
178
+ .choose-sound {
179
+ cursor: pointer;
180
+ }
181
+
182
+ .sound-rules-footer {
183
+ display: flex;
184
+ align-items: center;
185
+ justify-content: space-between;
186
+ flex: 0 0 auto;
187
+ gap: 8px;
188
+ }
189
+
190
+ .rule-count {
191
+ font-size: var(--type-ramp-minus-1-font-size);
192
+ color: var(--neutral-foreground-hint);
193
+ }
194
+
195
+ .file-input {
196
+ display: none;
197
+ }
198
+
199
+ ${scrollbarStyles}
200
+ `;
@@ -0,0 +1,179 @@
1
+ import { html, ref, repeat } from '@genesislcap/web-core';
2
+ export const FoundationSoundsPanelTemplate = html `
3
+ <div class="sounds-controls" part="controls">
4
+ <input
5
+ ${ref('fileInput')}
6
+ class="file-input"
7
+ type="file"
8
+ accept="audio/*"
9
+ @change=${(x, c) => x.onFileSelected(c.event)}
10
+ />
11
+ <rapid-disclosure class="sounds-help-disclosure" appearance="lightweight">
12
+ <span slot="title">
13
+ Sound Alerts Information
14
+ <span class="sounds-help-toggle" aria-hidden="true"></span>
15
+ </span>
16
+ <p class="sounds-help">
17
+ Add up to ${(x) => x.maxRules} sounds.
18
+ <br />
19
+ Match text in MESSAGE or HEADER (a word or phrase), or a notification rule (including rules
20
+ created when you subscribe to a template).
21
+ <br />
22
+ The first matching rule is played.
23
+ </p>
24
+ </rapid-disclosure>
25
+ <div class="volume-row">
26
+ <rapid-icon
27
+ class="volume-icon"
28
+ name=${(x) => (x.muted || x.volumePercent === 0 ? 'volume-xmark' : 'volume-high')}
29
+ ></rapid-icon>
30
+ <span class="volume-label">Volume</span>
31
+ <rapid-slider
32
+ class="volume-slider"
33
+ min="0"
34
+ max="100"
35
+ step="1"
36
+ :value=${(x) => String(x.volumePercent)}
37
+ @change=${(x, c) => x.onVolumeChange(c.event)}
38
+ ></rapid-slider>
39
+ <span class="volume-percent">${(x) => `${x.volumePercent}%`}</span>
40
+ <rapid-button
41
+ class="mute-all"
42
+ appearance="lightweight"
43
+ title=${(x) => (x.muted ? 'Unmute alert sounds' : 'Mute all alert sounds')}
44
+ @click=${(x) => x.toggleMute()}
45
+ >
46
+ ${(x) => (x.muted ? 'Unmute' : 'Mute all')}
47
+ </rapid-button>
48
+ </div>
49
+ <div class="sound-rules-panel">
50
+ <div class="sound-rules">
51
+ ${repeat((x) => x.rules, html `
52
+ <div class="sound-rule" data-rule-index=${(x) => x.index}>
53
+ <div class="sound-rule-header">
54
+ <span>Rule ${(x) => x.index + 1}</span>
55
+ <div class="sound-rule-actions">
56
+ <rapid-button
57
+ class="mute-rule"
58
+ appearance="stealth"
59
+ title=${(x) => (x.muted ? 'Unmute this rule' : 'Mute this rule')}
60
+ @click=${(x, c) => c.parent.toggleRuleMute(x.index)}
61
+ >
62
+ <rapid-icon
63
+ name=${(x) => (x.muted ? 'volume-xmark' : 'volume-high')}
64
+ ></rapid-icon>
65
+ </rapid-button>
66
+ <rapid-button
67
+ class="remove-rule"
68
+ appearance="stealth"
69
+ title=${(_, c) => (c.parent.canRemoveRule ? 'Remove rule' : 'Reset rule')}
70
+ @click=${(x, c) => c.parent.removeRule(x.index)}
71
+ >
72
+ <rapid-icon name="trash"></rapid-icon>
73
+ </rapid-button>
74
+ </div>
75
+ </div>
76
+ <label class="sound-rule-label">
77
+ Match by
78
+ <rapid-select
79
+ class="match-mode-select"
80
+ style=${(x) => `anchor-name: --sound-match-${x.index}; --sounds-select-anchor: --sound-match-${x.index}`}
81
+ :value=${(x) => x.matchMode}
82
+ @change=${(x, c) => c.parent.onMatchModeChange(x.index, c.event)}
83
+ >
84
+ <rapid-option value="message">MESSAGE text</rapid-option>
85
+ <rapid-option value="header">HEADER text</rapid-option>
86
+ <rapid-option value="rule">Notification rule</rapid-option>
87
+ </rapid-select>
88
+ </label>
89
+ <label
90
+ class="sound-rule-label"
91
+ style=${(x) => (x.matchMode === 'rule' ? 'display: none;' : '')}
92
+ >
93
+ ${(x) => (x.matchMode === 'header' ? 'HEADER text' : 'MESSAGE text')}
94
+ <rapid-text-field
95
+ class="pattern-input"
96
+ placeholder=${(x) => x.matchMode === 'header' ? 'e.g. Price alert' : 'e.g. limit breached'}
97
+ :value=${(x) => x.pattern}
98
+ @input=${(x, c) => c.parent.onPatternInput(x.index, c.event)}
99
+ ></rapid-text-field>
100
+ </label>
101
+ <label
102
+ class="sound-rule-label"
103
+ style=${(x) => (x.matchMode === 'rule' ? '' : 'display: none;')}
104
+ >
105
+ Notification rule
106
+ <rapid-select
107
+ class="rule-select"
108
+ style=${(x) => `anchor-name: --sound-rule-${x.index}; --sounds-select-anchor: --sound-rule-${x.index}`}
109
+ :value=${(x) => { var _a; return (_a = x.templateId) !== null && _a !== void 0 ? _a : ''; }}
110
+ @change=${(x, c) => c.parent.onRuleChange(x.index, c.event)}
111
+ >
112
+ <options-datasource
113
+ value-field="id"
114
+ label-field="name"
115
+ option-element="rapid-option"
116
+ :data=${(_, c) => c.parent.ruleSelectOptions}
117
+ ></options-datasource>
118
+ </rapid-select>
119
+ </label>
120
+ <label class="sound-rule-label">
121
+ Sound file
122
+ <div class="sound-file-row">
123
+ <rapid-text-field
124
+ class="sound-file-input"
125
+ readonly
126
+ placeholder="No sound selected"
127
+ :value=${(x) => { var _a; return (_a = x.fileName) !== null && _a !== void 0 ? _a : ''; }}
128
+ title=${(x) => { var _a; return (_a = x.fileName) !== null && _a !== void 0 ? _a : 'No sound selected'; }}
129
+ >
130
+ <rapid-icon
131
+ slot="end"
132
+ class="choose-sound"
133
+ name="folder-open"
134
+ title="Choose alert sound"
135
+ @click=${(x, c) => {
136
+ c.event.stopPropagation();
137
+ return c.parent.openFilePicker(x.index);
138
+ }}
139
+ ></rapid-icon>
140
+ </rapid-text-field>
141
+ <rapid-button
142
+ class="test-sound"
143
+ appearance="stealth"
144
+ title="Test play this sound"
145
+ ?disabled=${(x) => !x.hasSound}
146
+ @click=${(x, c) => c.parent.testSound(x.index)}
147
+ >
148
+ <rapid-icon name="play"></rapid-icon>
149
+ </rapid-button>
150
+ <rapid-button
151
+ class="clear-sound"
152
+ appearance="stealth"
153
+ title="Clear sound file"
154
+ ?disabled=${(x) => !x.hasSound}
155
+ @click=${(x, c) => c.parent.clearSound(x.index)}
156
+ >
157
+ <rapid-icon name="times"></rapid-icon>
158
+ </rapid-button>
159
+ </div>
160
+ </label>
161
+ </div>
162
+ `)}
163
+ </div>
164
+ </div>
165
+ <div class="sound-rules-footer">
166
+ <rapid-button
167
+ class="add-rule"
168
+ appearance="lightweight"
169
+ title="Add sound rule"
170
+ ?disabled=${(x) => !x.canAddRule}
171
+ @click=${(x) => x.addRule()}
172
+ >
173
+ <rapid-icon name="plus"></rapid-icon>
174
+ Add rule
175
+ </rapid-button>
176
+ <span class="rule-count">${(x) => `${x.rules.length} / ${x.maxRules}`}</span>
177
+ </div>
178
+ </div>
179
+ `;
@@ -0,0 +1,206 @@
1
+ import { __awaiter, __decorate } from "tslib";
2
+ import { Connect } from '@genesislcap/foundation-comms';
3
+ import { attr, customElement, GenesisElement, html } from '@genesislcap/web-core';
4
+ import { FoundationInboxService } from '../../services/inbox.service';
5
+ import { RuleService } from '../../services/rule.service';
6
+ import { AlertSoundPlayer, base64ToBlob, getAlertDynamicRuleId, getAlertHeader, getAlertId, getAlertMessage, } from '../../utils/alert-sound';
7
+ import { logger } from '../../utils/logger';
8
+ import { AlertStatus } from '../foundation-inbox/inbox.types';
9
+ import { FoundationSoundsStyles } from './foundation-sounds.styles';
10
+ /**
11
+ * Caps the NEW-alert snapshot used to prime known IDs. Realtime inserts are
12
+ * still prepended by the inbox service after this ceiling.
13
+ */
14
+ const SOUNDS_MAX_ROWS = 100;
15
+ /**
16
+ * Headless listener for inbox alert inserts. Plays configured sounds via the
17
+ * shared AlertSoundPlayer. Mount once in the app layout.
18
+ *
19
+ * Configure rules in the inbox Sounds tab with {@link FoundationSoundsPanel}.
20
+ *
21
+ * @public
22
+ */
23
+ let FoundationSounds = class FoundationSounds extends GenesisElement {
24
+ constructor() {
25
+ super(...arguments);
26
+ /**
27
+ * When false, new audible alerts are tracked but do not play.
28
+ */
29
+ this.soundEnabled = true;
30
+ this.player = AlertSoundPlayer.shared;
31
+ this.knownAlertIds = new Set();
32
+ /** In-flight server sound fetches keyed by DYNAMIC_RULE_ID. */
33
+ this.serverSoundFetches = new Map();
34
+ this.subscriptionHandler = ({ store }) => {
35
+ var _a;
36
+ const items = (store === null || store === void 0 ? void 0 : store.items) || [];
37
+ const current = [];
38
+ for (const item of items) {
39
+ const id = getAlertId(item);
40
+ if (!id) {
41
+ continue;
42
+ }
43
+ current.push({
44
+ id,
45
+ message: getAlertMessage(item),
46
+ header: getAlertHeader(item),
47
+ dynamicRuleId: getAlertDynamicRuleId(item),
48
+ });
49
+ }
50
+ // SEQUENCE_ID <= 1 is (part of) the initial snapshot rather than a
51
+ // real-time update — same boundary as `notification-listener`. A
52
+ // first-callback latch instead would misfire on a rowless bookkeeping
53
+ // message (SOURCE_REF/MORE_ROWS only), or if the snapshot itself arrives
54
+ // split across more than one message.
55
+ if (((_a = store === null || store === void 0 ? void 0 : store.sequenceId) !== null && _a !== void 0 ? _a : 0) <= 1) {
56
+ for (const row of current) {
57
+ this.knownAlertIds.add(row.id);
58
+ }
59
+ return;
60
+ }
61
+ if (!this.soundEnabled) {
62
+ for (const row of current) {
63
+ this.knownAlertIds.add(row.id);
64
+ }
65
+ return;
66
+ }
67
+ // Collect new rows first so known IDs stay in sync even while async
68
+ // server-sound fetches run. Play at most once per batch: user Sounds-tab
69
+ // match first, else server sound on DYNAMIC_RULE_ID — only latch after a
70
+ // successful play so a failed/in-flight fetch can fall through to later rows.
71
+ const newRows = [];
72
+ for (const row of current) {
73
+ if (this.knownAlertIds.has(row.id)) {
74
+ continue;
75
+ }
76
+ this.knownAlertIds.add(row.id);
77
+ newRows.push(row);
78
+ }
79
+ if (newRows.length) {
80
+ void this.playForNewAlerts(newRows);
81
+ }
82
+ };
83
+ }
84
+ connectedCallback() {
85
+ const _super = Object.create(null, {
86
+ connectedCallback: { get: () => super.connectedCallback }
87
+ });
88
+ return __awaiter(this, void 0, void 0, function* () {
89
+ _super.connectedCallback.call(this);
90
+ yield this.player.restore();
91
+ // The await above can outlive this element (mount → unmount → mount, e.g.
92
+ // via PBCElementsRenderer re-rendering its layout target). Without this
93
+ // guard we'd wire up a subscription nothing will ever tear down, and a
94
+ // later mount adds a second one — two overlapping sounds per alert.
95
+ if (!this.isConnected) {
96
+ return;
97
+ }
98
+ this.connectionSub = this.connect.isConnected$.subscribe((isConnected) => {
99
+ if (isConnected) {
100
+ this.subscribeToAlerts();
101
+ }
102
+ });
103
+ });
104
+ }
105
+ disconnectedCallback() {
106
+ var _a;
107
+ super.disconnectedCallback();
108
+ this.inboxService.unsubscribe(this.subscriptionHandler);
109
+ (_a = this.connectionSub) === null || _a === void 0 ? void 0 : _a.unsubscribe();
110
+ this.connectionSub = undefined;
111
+ }
112
+ subscribeToAlerts() {
113
+ this.inboxService.unsubscribe(this.subscriptionHandler);
114
+ this.knownAlertIds = new Set();
115
+ this.inboxService.subscribe(this.subscriptionHandler, false, `ALERT_STATUS=='${AlertStatus.NEW}'`, SOUNDS_MAX_ROWS, 'ALERT_ID,MESSAGE,HEADER,DYNAMIC_RULE_ID');
116
+ }
117
+ playForNewAlerts(rows) {
118
+ return __awaiter(this, void 0, void 0, function* () {
119
+ for (const row of rows) {
120
+ if (this.player.playForAlert(row)) {
121
+ return;
122
+ }
123
+ if (row.dynamicRuleId && !this.player.isMuted) {
124
+ // Sequential: stop on first successful play; do not race later rows.
125
+ // eslint-disable-next-line no-await-in-loop -- try each alert until one sound plays
126
+ if (yield this.tryPlayServerSound(row.dynamicRuleId)) {
127
+ return;
128
+ }
129
+ }
130
+ }
131
+ });
132
+ }
133
+ tryPlayServerSound(dynamicRuleId) {
134
+ return __awaiter(this, void 0, void 0, function* () {
135
+ const id = dynamicRuleId === null || dynamicRuleId === void 0 ? void 0 : dynamicRuleId.trim();
136
+ if (!id || this.player.isMuted) {
137
+ return false;
138
+ }
139
+ if (this.player.hasServerSound(id)) {
140
+ return this.player.playServerSound(id);
141
+ }
142
+ let fetchPromise = this.serverSoundFetches.get(id);
143
+ if (!fetchPromise) {
144
+ fetchPromise = this.fetchAndCacheServerSound(id);
145
+ this.serverSoundFetches.set(id, fetchPromise);
146
+ try {
147
+ return yield fetchPromise;
148
+ }
149
+ finally {
150
+ this.serverSoundFetches.delete(id);
151
+ }
152
+ }
153
+ return fetchPromise;
154
+ });
155
+ }
156
+ fetchAndCacheServerSound(dynamicRuleId) {
157
+ return __awaiter(this, void 0, void 0, function* () {
158
+ const sound = yield this.ruleService.getAlertSound(dynamicRuleId);
159
+ if (!(sound === null || sound === void 0 ? void 0 : sound.SOUND_DATA)) {
160
+ // Rule may have cleared its sound since we last played — drop stale cache.
161
+ this.player.clearServerSound(dynamicRuleId);
162
+ return false;
163
+ }
164
+ const mimeType = sound.ALERT_SOUND_MIME_TYPE || 'audio/mpeg';
165
+ let blob;
166
+ try {
167
+ blob = base64ToBlob(sound.SOUND_DATA, mimeType);
168
+ }
169
+ catch (error) {
170
+ // Malformed base64 must not reject playForNewAlerts — that aborts the
171
+ // rest of the batch as an unhandled rejection.
172
+ logger.error(`Invalid alert sound data for rule ${dynamicRuleId}`, error);
173
+ this.player.clearServerSound(dynamicRuleId);
174
+ return false;
175
+ }
176
+ this.player.setServerSound(dynamicRuleId, {
177
+ blob,
178
+ fileName: sound.ALERT_SOUND_FILE_NAME,
179
+ mimeType,
180
+ });
181
+ return this.player.playServerSound(dynamicRuleId);
182
+ });
183
+ }
184
+ };
185
+ __decorate([
186
+ Connect
187
+ ], FoundationSounds.prototype, "connect", void 0);
188
+ __decorate([
189
+ FoundationInboxService
190
+ ], FoundationSounds.prototype, "inboxService", void 0);
191
+ __decorate([
192
+ RuleService
193
+ ], FoundationSounds.prototype, "ruleService", void 0);
194
+ __decorate([
195
+ attr({ attribute: 'sound-enabled', mode: 'boolean' })
196
+ ], FoundationSounds.prototype, "soundEnabled", void 0);
197
+ FoundationSounds = __decorate([
198
+ customElement({
199
+ name: 'foundation-sounds',
200
+ template: html `
201
+ <slot></slot>
202
+ `,
203
+ styles: FoundationSoundsStyles,
204
+ })
205
+ ], FoundationSounds);
206
+ export { FoundationSounds };
@@ -0,0 +1,7 @@
1
+ import { css } from '@genesislcap/web-core';
2
+ /** Headless layout listener — take no space in the layout chrome. */
3
+ export const FoundationSoundsStyles = css `
4
+ :host {
5
+ display: none;
6
+ }
7
+ `;
package/dist/esm/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  export * from './components/foundation-inbox/inbox';
2
2
  export * from './components/foundation-inbox/components/foundation-inbox-flyout/foundation-inbox-flyout';
3
3
  export * from './components/foundation-inbox/components/foundation-inbox-counter/foundation-inbox-counter';
4
+ export * from './components/foundation-sounds/foundation-sounds';
5
+ export * from './components/foundation-sounds/foundation-sounds-panel';
4
6
  export * from './components/foundation-notification-dashboard';
7
+ export { AlertSoundPlayer, MAX_SOUND_RULES, MAX_SOUND_FILE_BYTES } from './utils/alert-sound';
5
8
  export { AlertStatus } from './components/foundation-inbox/inbox.types';
@@ -15,6 +15,7 @@ class FoundationInboxServiceImpl {
15
15
  this.store = {
16
16
  items: [],
17
17
  moreRows: false,
18
+ sequenceId: 0,
18
19
  };
19
20
  }
20
21
  getStream(reverse, criteriaMatch, maxRows, fields) {
@@ -27,6 +28,9 @@ class FoundationInboxServiceImpl {
27
28
  if (result.SOURCE_REF) {
28
29
  this.sourceRef = result.SOURCE_REF;
29
30
  }
31
+ if (typeof result.SEQUENCE_ID === 'number') {
32
+ this.store.sequenceId = result.SEQUENCE_ID;
33
+ }
30
34
  this.store.moreRows = Boolean(result.MORE_ROWS);
31
35
  // getMoreRows batches continue the existing order, so append them
32
36
  // rather than prepending like real-time inserts.
@@ -61,13 +65,13 @@ class FoundationInboxServiceImpl {
61
65
  const found = this.subscriptions.splice(index, 1);
62
66
  found.forEach((subscription) => {
63
67
  subscription.socketSubscription.unsubscribe();
64
- subscription.callback({ store: { items: [], moreRows: false } });
68
+ subscription.callback({ store: { items: [], moreRows: false, sequenceId: 0 } });
65
69
  });
66
70
  }
67
71
  this.stream = undefined;
68
72
  this.sourceRef = null;
69
73
  this.fetchingMore = false;
70
- this.store = { items: [], moreRows: false };
74
+ this.store = { items: [], moreRows: false, sequenceId: 0 };
71
75
  }
72
76
  }
73
77
  __decorate([
@@ -21,6 +21,41 @@ class RuleServiceImpl {
21
21
  return response.REPLY[0];
22
22
  });
23
23
  }
24
+ getAlertSound(dynamicRuleId) {
25
+ return __awaiter(this, void 0, void 0, function* () {
26
+ var _a, _b, _c, _d, _e, _f;
27
+ if (!dynamicRuleId) {
28
+ return null;
29
+ }
30
+ try {
31
+ const response = yield this.connect.request('REQ_NOTIFY_ALERT_SOUND', {
32
+ REQUEST: {
33
+ DYNAMIC_RULE_ID: dynamicRuleId,
34
+ },
35
+ });
36
+ const reply = response === null || response === void 0 ? void 0 : response.REPLY;
37
+ if (!Array.isArray(reply) || !reply.length) {
38
+ return null;
39
+ }
40
+ const row = reply[0];
41
+ const soundData = (_a = row.SOUND_DATA) !== null && _a !== void 0 ? _a : row.soundData;
42
+ if (!soundData) {
43
+ return null;
44
+ }
45
+ return {
46
+ ALERT_SOUND_ID: (_b = row.ALERT_SOUND_ID) !== null && _b !== void 0 ? _b : row.alertSoundId,
47
+ DYNAMIC_RULE_ID: (_d = (_c = row.DYNAMIC_RULE_ID) !== null && _c !== void 0 ? _c : row.dynamicRuleId) !== null && _d !== void 0 ? _d : dynamicRuleId,
48
+ ALERT_SOUND_FILE_NAME: (_e = row.ALERT_SOUND_FILE_NAME) !== null && _e !== void 0 ? _e : row.alertSoundFileName,
49
+ ALERT_SOUND_MIME_TYPE: (_f = row.ALERT_SOUND_MIME_TYPE) !== null && _f !== void 0 ? _f : row.alertSoundMimeType,
50
+ SOUND_DATA: soundData,
51
+ };
52
+ }
53
+ catch (error) {
54
+ console.error('Failed to fetch alert sound for rule', dynamicRuleId, error);
55
+ return null;
56
+ }
57
+ });
58
+ }
24
59
  createRule(rule) {
25
60
  return __awaiter(this, void 0, void 0, function* () {
26
61
  return this.connect.commitEvent('EVENT_NOTIFICATION_RULE_CREATE', {