@in-the-loop-labs/pair-review 3.1.3 → 3.2.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.
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/pr.css +980 -3
- package/public/js/components/AIPanel.js +7 -4
- package/public/js/components/ChatPanel.js +34 -4
- package/public/js/components/CouncilProgressModal.js +11 -0
- package/public/js/components/NotificationDropdown.js +257 -0
- package/public/js/components/StackAnalysisDialog.js +313 -0
- package/public/js/components/StackProgressModal.js +475 -0
- package/public/js/components/StatusIndicator.js +1 -0
- package/public/js/components/SuggestionNavigator.js +2 -0
- package/public/js/modules/comment-manager.js +7 -0
- package/public/js/modules/comment-minimizer.js +151 -4
- package/public/js/modules/file-comment-manager.js +66 -2
- package/public/js/modules/suggestion-manager.js +2 -1
- package/public/js/pr.js +433 -2
- package/public/js/utils/notification-sounds.js +62 -0
- package/public/local.html +10 -0
- package/public/pr.html +12 -0
- package/public/setup.html +4 -0
- package/src/ai/claude-provider.js +1 -11
- package/src/ai/codex-provider.js +18 -16
- package/src/ai/copilot-provider.js +21 -21
- package/src/ai/gemini-provider.js +10 -0
- package/src/ai/pi-provider.js +22 -25
- package/src/ai/provider.js +26 -3
- package/src/chat/pi-bridge.js +8 -0
- package/src/chat/session-manager.js +1 -0
- package/src/git/base-branch.js +1 -51
- package/src/git/worktree-lock.js +88 -0
- package/src/git/worktree.js +64 -0
- package/src/github/stack-walker.js +196 -0
- package/src/routes/local.js +12 -8
- package/src/routes/pr.js +139 -26
- package/src/routes/sound.js +49 -0
- package/src/routes/stack-analysis.js +886 -0
- package/src/server.js +4 -0
- package/src/setup/stack-setup.js +77 -0
|
@@ -807,8 +807,9 @@ class AIPanel {
|
|
|
807
807
|
const finding = this.findings.find(f => f.id === findingId);
|
|
808
808
|
if (finding) {
|
|
809
809
|
suggestionContext = {
|
|
810
|
+
suggestionId: findingId ? String(findingId) : null,
|
|
810
811
|
title: finding.title || title,
|
|
811
|
-
body: finding.body || '',
|
|
812
|
+
body: finding.formattedBody || finding.body || '',
|
|
812
813
|
type: finding.type || '',
|
|
813
814
|
file: finding.file || file,
|
|
814
815
|
line_start: finding.line_start || null,
|
|
@@ -1002,6 +1003,8 @@ class AIPanel {
|
|
|
1002
1003
|
if (targetSuggestion) {
|
|
1003
1004
|
const minimizer = window.prManager?.commentMinimizer;
|
|
1004
1005
|
if (minimizer?.active) {
|
|
1006
|
+
// Expand file-level comments so the target becomes visible
|
|
1007
|
+
minimizer.expandForElement(targetSuggestion);
|
|
1005
1008
|
// Comments are minimized — scroll to the parent diff line instead
|
|
1006
1009
|
const diffRow = minimizer.findDiffRowFor(targetSuggestion);
|
|
1007
1010
|
(diffRow || targetSuggestion).scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
@@ -1069,9 +1072,9 @@ class AIPanel {
|
|
|
1069
1072
|
|
|
1070
1073
|
if (targetElement) {
|
|
1071
1074
|
const minimizer = window.prManager?.commentMinimizer;
|
|
1072
|
-
if (minimizer?.active
|
|
1073
|
-
|
|
1074
|
-
const diffRow = minimizer.findDiffRowFor(targetElement);
|
|
1075
|
+
if (minimizer?.active) {
|
|
1076
|
+
minimizer.expandForElement(targetElement);
|
|
1077
|
+
const diffRow = isFileLevel ? null : minimizer.findDiffRowFor(targetElement);
|
|
1075
1078
|
(diffRow || targetElement).scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1076
1079
|
} else {
|
|
1077
1080
|
targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
@@ -32,6 +32,7 @@ class ChatPanel {
|
|
|
32
32
|
this._pendingContext = [];
|
|
33
33
|
this._pendingContextData = [];
|
|
34
34
|
this._pendingDiffStateNotifications = [];
|
|
35
|
+
this._pendingUserActionHints = [];
|
|
35
36
|
this._contextSource = null; // 'suggestion' or 'user' — set when opened with context
|
|
36
37
|
this._contextItemId = null; // suggestion ID or comment ID from context
|
|
37
38
|
this._contextLineMeta = null; // { file, line_start, line_end } — set when opened with line context
|
|
@@ -667,6 +668,7 @@ class ChatPanel {
|
|
|
667
668
|
this._pendingContext = [];
|
|
668
669
|
this._pendingContextData = [];
|
|
669
670
|
this._pendingDiffStateNotifications = [];
|
|
671
|
+
this._pendingUserActionHints = [];
|
|
670
672
|
this._contextSource = null;
|
|
671
673
|
this._contextItemId = null;
|
|
672
674
|
this._contextLineMeta = null;
|
|
@@ -1030,6 +1032,7 @@ class ChatPanel {
|
|
|
1030
1032
|
this._pendingContext = [];
|
|
1031
1033
|
this._pendingContextData = [];
|
|
1032
1034
|
this._pendingDiffStateNotifications = [];
|
|
1035
|
+
this._pendingUserActionHints = [];
|
|
1033
1036
|
this._contextSource = null;
|
|
1034
1037
|
this._contextItemId = null;
|
|
1035
1038
|
this._contextLineMeta = null;
|
|
@@ -1269,12 +1272,28 @@ class ChatPanel {
|
|
|
1269
1272
|
this._pendingDiffStateNotifications = [];
|
|
1270
1273
|
}
|
|
1271
1274
|
|
|
1275
|
+
// Snapshot user-action-hints queue for error recovery (invisible to user, no UI cards)
|
|
1276
|
+
const savedUserActionHints = this._pendingUserActionHints.slice();
|
|
1277
|
+
let userActionPrefix = '';
|
|
1278
|
+
if (this._pendingUserActionHints.length > 0) {
|
|
1279
|
+
userActionPrefix = '[User Action Hints]\n' + this._pendingUserActionHints.join('\n');
|
|
1280
|
+
this._pendingUserActionHints = [];
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Combine invisible prefixes (diff state + user action hints)
|
|
1284
|
+
let invisiblePrefix = '';
|
|
1285
|
+
if (diffStatePrefix && userActionPrefix) {
|
|
1286
|
+
invisiblePrefix = diffStatePrefix + '\n\n' + userActionPrefix;
|
|
1287
|
+
} else {
|
|
1288
|
+
invisiblePrefix = diffStatePrefix || userActionPrefix;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1272
1291
|
const savedContext = this._pendingContext;
|
|
1273
1292
|
const savedContextData = this._pendingContextData;
|
|
1274
1293
|
if (this._pendingContext.length > 0) {
|
|
1275
1294
|
const userContext = this._pendingContext.join('\n\n');
|
|
1276
|
-
payload.context =
|
|
1277
|
-
?
|
|
1295
|
+
payload.context = invisiblePrefix
|
|
1296
|
+
? invisiblePrefix + '\n\n' + userContext
|
|
1278
1297
|
: userContext;
|
|
1279
1298
|
payload.contextData = this._pendingContextData;
|
|
1280
1299
|
this._pendingContext = [];
|
|
@@ -1287,8 +1306,8 @@ class ChatPanel {
|
|
|
1287
1306
|
if (btn) btn.remove();
|
|
1288
1307
|
delete card.dataset.contextIndex;
|
|
1289
1308
|
});
|
|
1290
|
-
} else if (
|
|
1291
|
-
payload.context =
|
|
1309
|
+
} else if (invisiblePrefix) {
|
|
1310
|
+
payload.context = invisiblePrefix;
|
|
1292
1311
|
}
|
|
1293
1312
|
|
|
1294
1313
|
// Lock analysis context card (not indexed, handled separately from pending context)
|
|
@@ -1353,6 +1372,7 @@ class ChatPanel {
|
|
|
1353
1372
|
this._pendingContext = savedContext;
|
|
1354
1373
|
this._pendingContextData = savedContextData;
|
|
1355
1374
|
this._pendingDiffStateNotifications = [...savedDiffState, ...this._pendingDiffStateNotifications];
|
|
1375
|
+
this._pendingUserActionHints = [...savedUserActionHints, ...this._pendingUserActionHints];
|
|
1356
1376
|
// Restore removability on context cards that were locked before the failed send
|
|
1357
1377
|
this._restoreRemovableCards();
|
|
1358
1378
|
console.error('[ChatPanel] Error sending message:', error);
|
|
@@ -1371,6 +1391,16 @@ class ChatPanel {
|
|
|
1371
1391
|
this._pendingDiffStateNotifications.push(message);
|
|
1372
1392
|
}
|
|
1373
1393
|
|
|
1394
|
+
/**
|
|
1395
|
+
* Queue an invisible user-action hint for the chat agent.
|
|
1396
|
+
* Like diff-state notifications, these do NOT render UI cards and survive panel close.
|
|
1397
|
+
* Drained into the context parameter on the next sendMessage() call.
|
|
1398
|
+
* @param {string} message - Description of the user action (e.g., "[User Action: adopted suggestion 42]")
|
|
1399
|
+
*/
|
|
1400
|
+
queueUserActionHint(message) {
|
|
1401
|
+
this._pendingUserActionHints.push(message);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1374
1404
|
/**
|
|
1375
1405
|
* Store pending context and render a compact context card in the UI.
|
|
1376
1406
|
* Called when the user clicks "Ask about this" on a suggestion.
|
|
@@ -55,6 +55,7 @@ class CouncilProgressModal {
|
|
|
55
55
|
this.councilConfig = councilConfig;
|
|
56
56
|
this.isVisible = true;
|
|
57
57
|
this._voiceStates = {};
|
|
58
|
+
this._completionHandled = false;
|
|
58
59
|
|
|
59
60
|
// Detect rendering mode
|
|
60
61
|
const configType = options.configType || (councilConfig ? 'advanced' : 'single');
|
|
@@ -903,6 +904,13 @@ class CouncilProgressModal {
|
|
|
903
904
|
// ---------------------------------------------------------------------------
|
|
904
905
|
|
|
905
906
|
_handleCompletion(status) {
|
|
907
|
+
// Guard against duplicate invocations (e.g. WebSocket retry + reconnect fetch race)
|
|
908
|
+
if (this._completionHandled) return;
|
|
909
|
+
this._completionHandled = true;
|
|
910
|
+
|
|
911
|
+
// Play notification sound before any async work so it fires promptly
|
|
912
|
+
if (window.notificationSounds) window.notificationSounds.playIfEnabled('analysis');
|
|
913
|
+
|
|
906
914
|
if (this._renderMode === 'council') {
|
|
907
915
|
// Voice-centric: mark all voice-level children as complete
|
|
908
916
|
const vcEls = this.modal.querySelectorAll('[data-vc-voice][data-vc-level]');
|
|
@@ -1312,6 +1320,9 @@ class CouncilProgressModal {
|
|
|
1312
1320
|
<span class="council-voice-icon pending">\u25CB</span>
|
|
1313
1321
|
<span class="council-voice-label">Consolidation</span>
|
|
1314
1322
|
<span class="council-voice-status pending">Pending</span>
|
|
1323
|
+
<div class="council-voice-detail">
|
|
1324
|
+
<div class="council-voice-snippet" style="display: none;"></div>
|
|
1325
|
+
</div>
|
|
1315
1326
|
</div>
|
|
1316
1327
|
`;
|
|
1317
1328
|
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* NotificationDropdown - Bell-icon popover for notification sound preferences.
|
|
4
|
+
*
|
|
5
|
+
* Anchors a small dropdown below the bell button with checkbox toggles that
|
|
6
|
+
* control which events trigger a notification chime. Supports configurable
|
|
7
|
+
* event types (e.g. 'analysis', 'setup') and a "Test sound" link.
|
|
8
|
+
*
|
|
9
|
+
* Follows the same popover pattern as DiffOptionsDropdown (fixed positioning
|
|
10
|
+
* via getBoundingClientRect, click-outside and Escape to dismiss,
|
|
11
|
+
* opacity+transform animation).
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const dropdown = new NotificationDropdown(
|
|
15
|
+
* document.getElementById('notification-btn'),
|
|
16
|
+
* { events: ['analysis', 'setup'] }
|
|
17
|
+
* );
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const EVENT_LABELS = {
|
|
21
|
+
'analysis': 'Analysis complete',
|
|
22
|
+
'setup': 'Setup complete'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
class NotificationDropdown {
|
|
26
|
+
/**
|
|
27
|
+
* @param {HTMLElement} buttonElement - The bell icon button already in the DOM
|
|
28
|
+
* @param {Object} options
|
|
29
|
+
* @param {string[]} options.events - Event type strings to show toggles for
|
|
30
|
+
*/
|
|
31
|
+
constructor(buttonElement, { events }) {
|
|
32
|
+
this._btn = buttonElement;
|
|
33
|
+
this._events = events || [];
|
|
34
|
+
|
|
35
|
+
this._popoverEl = null;
|
|
36
|
+
this._checkboxes = {};
|
|
37
|
+
this._visible = false;
|
|
38
|
+
this._outsideClickHandler = null;
|
|
39
|
+
this._escapeHandler = null;
|
|
40
|
+
|
|
41
|
+
this._renderPopover();
|
|
42
|
+
this._syncButtonActive();
|
|
43
|
+
|
|
44
|
+
// Toggle popover on button click
|
|
45
|
+
this._btnClickHandler = (e) => {
|
|
46
|
+
e.stopPropagation();
|
|
47
|
+
if (this._visible) {
|
|
48
|
+
this._hide();
|
|
49
|
+
} else {
|
|
50
|
+
this._show();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
this._btn.addEventListener('click', this._btnClickHandler);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Public API
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/** Remove all DOM elements and event listeners. Safe to call multiple times. */
|
|
61
|
+
destroy() {
|
|
62
|
+
this._hide();
|
|
63
|
+
if (this._popoverEl) {
|
|
64
|
+
this._popoverEl.remove();
|
|
65
|
+
this._popoverEl = null;
|
|
66
|
+
}
|
|
67
|
+
if (this._btn && this._btnClickHandler) {
|
|
68
|
+
this._btn.removeEventListener('click', this._btnClickHandler);
|
|
69
|
+
this._btnClickHandler = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// DOM construction
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
_renderPopover() {
|
|
78
|
+
const popover = document.createElement('div');
|
|
79
|
+
popover.className = 'notification-popover';
|
|
80
|
+
// Start hidden (opacity 0, shifted up)
|
|
81
|
+
popover.style.opacity = '0';
|
|
82
|
+
popover.style.transform = 'translateY(-4px)';
|
|
83
|
+
popover.style.pointerEvents = 'none';
|
|
84
|
+
popover.style.position = 'fixed';
|
|
85
|
+
popover.style.zIndex = '1100';
|
|
86
|
+
popover.style.transition = 'opacity 0.15s ease, transform 0.15s ease';
|
|
87
|
+
|
|
88
|
+
// --- Event checkboxes ---
|
|
89
|
+
this._events.forEach((eventType) => {
|
|
90
|
+
const labelText = EVENT_LABELS[eventType] || eventType;
|
|
91
|
+
const enabled = window.notificationSounds
|
|
92
|
+
? window.notificationSounds.isEnabled(eventType)
|
|
93
|
+
: false;
|
|
94
|
+
|
|
95
|
+
const label = this._createCheckboxLabel(labelText, enabled);
|
|
96
|
+
const checkbox = label.querySelector('input');
|
|
97
|
+
popover.appendChild(label);
|
|
98
|
+
|
|
99
|
+
this._checkboxes[eventType] = checkbox;
|
|
100
|
+
|
|
101
|
+
checkbox.addEventListener('change', () => {
|
|
102
|
+
if (window.notificationSounds) {
|
|
103
|
+
window.notificationSounds.setEnabled(eventType, checkbox.checked);
|
|
104
|
+
}
|
|
105
|
+
this._syncButtonActive();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// --- Divider ---
|
|
110
|
+
const divider = document.createElement('div');
|
|
111
|
+
divider.style.height = '1px';
|
|
112
|
+
divider.style.background = 'var(--color-border-primary, #d0d7de)';
|
|
113
|
+
divider.style.margin = '0';
|
|
114
|
+
popover.appendChild(divider);
|
|
115
|
+
|
|
116
|
+
// --- Test sound link ---
|
|
117
|
+
const testLink = document.createElement('div');
|
|
118
|
+
testLink.textContent = 'Test sound';
|
|
119
|
+
testLink.style.padding = '8px 12px';
|
|
120
|
+
testLink.style.fontSize = '0.8125rem';
|
|
121
|
+
testLink.style.color = 'var(--color-accent-primary)';
|
|
122
|
+
testLink.style.cursor = 'pointer';
|
|
123
|
+
testLink.style.textDecoration = 'none';
|
|
124
|
+
testLink.style.userSelect = 'none';
|
|
125
|
+
|
|
126
|
+
testLink.addEventListener('mouseenter', () => {
|
|
127
|
+
testLink.style.textDecoration = 'underline';
|
|
128
|
+
});
|
|
129
|
+
testLink.addEventListener('mouseleave', () => {
|
|
130
|
+
testLink.style.textDecoration = 'none';
|
|
131
|
+
});
|
|
132
|
+
testLink.addEventListener('click', (e) => {
|
|
133
|
+
e.stopPropagation();
|
|
134
|
+
if (window.notificationSounds) {
|
|
135
|
+
window.notificationSounds.playChime();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
popover.appendChild(testLink);
|
|
140
|
+
|
|
141
|
+
document.body.appendChild(popover);
|
|
142
|
+
this._popoverEl = popover;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create a label element wrapping a checkbox.
|
|
147
|
+
* @param {string} text - Label text
|
|
148
|
+
* @param {boolean} checked - Initial checked state
|
|
149
|
+
* @returns {HTMLLabelElement}
|
|
150
|
+
*/
|
|
151
|
+
_createCheckboxLabel(text, checked) {
|
|
152
|
+
const label = document.createElement('label');
|
|
153
|
+
label.style.display = 'flex';
|
|
154
|
+
label.style.alignItems = 'center';
|
|
155
|
+
label.style.gap = '8px';
|
|
156
|
+
label.style.cursor = 'pointer';
|
|
157
|
+
label.style.fontSize = '0.8125rem';
|
|
158
|
+
label.style.whiteSpace = 'nowrap';
|
|
159
|
+
label.style.padding = '8px 12px';
|
|
160
|
+
label.style.color = 'var(--color-text-primary, #24292f)';
|
|
161
|
+
label.style.userSelect = 'none';
|
|
162
|
+
|
|
163
|
+
const checkbox = document.createElement('input');
|
|
164
|
+
checkbox.type = 'checkbox';
|
|
165
|
+
checkbox.checked = checked;
|
|
166
|
+
checkbox.style.margin = '0';
|
|
167
|
+
checkbox.style.cursor = 'pointer';
|
|
168
|
+
|
|
169
|
+
label.appendChild(checkbox);
|
|
170
|
+
label.appendChild(document.createTextNode(text));
|
|
171
|
+
return label;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Show / Hide (mirrors DiffOptionsDropdown pattern)
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
_show() {
|
|
179
|
+
if (!this._popoverEl || !this._btn) return;
|
|
180
|
+
|
|
181
|
+
// Position below the button
|
|
182
|
+
const rect = this._btn.getBoundingClientRect();
|
|
183
|
+
this._popoverEl.style.top = `${rect.bottom + 4}px`;
|
|
184
|
+
this._popoverEl.style.left = `${rect.left + rect.width / 2}px`;
|
|
185
|
+
this._popoverEl.style.transform = 'translateX(-50%) translateY(-4px)';
|
|
186
|
+
|
|
187
|
+
// Make visible
|
|
188
|
+
this._popoverEl.style.opacity = '1';
|
|
189
|
+
this._popoverEl.style.pointerEvents = 'auto';
|
|
190
|
+
this._visible = true;
|
|
191
|
+
|
|
192
|
+
// Animate into final position
|
|
193
|
+
requestAnimationFrame(() => {
|
|
194
|
+
if (this._popoverEl) {
|
|
195
|
+
this._popoverEl.style.transform = 'translateX(-50%) translateY(0)';
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Click-outside-to-close
|
|
200
|
+
this._outsideClickHandler = (e) => {
|
|
201
|
+
if (!this._popoverEl.contains(e.target) && !this._btn.contains(e.target)) {
|
|
202
|
+
this._hide();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
document.addEventListener('click', this._outsideClickHandler, true);
|
|
206
|
+
|
|
207
|
+
// Escape to dismiss
|
|
208
|
+
this._escapeHandler = (e) => {
|
|
209
|
+
if (e.key === 'Escape') {
|
|
210
|
+
this._hide();
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
document.addEventListener('keydown', this._escapeHandler, true);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
_hide() {
|
|
217
|
+
if (!this._popoverEl) return;
|
|
218
|
+
|
|
219
|
+
this._popoverEl.style.opacity = '0';
|
|
220
|
+
this._popoverEl.style.transform = 'translateX(-50%) translateY(-4px)';
|
|
221
|
+
this._popoverEl.style.pointerEvents = 'none';
|
|
222
|
+
this._visible = false;
|
|
223
|
+
|
|
224
|
+
if (this._outsideClickHandler) {
|
|
225
|
+
document.removeEventListener('click', this._outsideClickHandler, true);
|
|
226
|
+
this._outsideClickHandler = null;
|
|
227
|
+
}
|
|
228
|
+
if (this._escapeHandler) {
|
|
229
|
+
document.removeEventListener('keydown', this._escapeHandler, true);
|
|
230
|
+
this._escapeHandler = null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Helpers
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
/** Swap bell icon visibility when any notification is enabled. */
|
|
239
|
+
_syncButtonActive() {
|
|
240
|
+
if (!this._btn) return;
|
|
241
|
+
const anyEnabled = this._events.some((eventType) => {
|
|
242
|
+
return window.notificationSounds
|
|
243
|
+
? window.notificationSounds.isEnabled(eventType)
|
|
244
|
+
: false;
|
|
245
|
+
});
|
|
246
|
+
const onIcon = this._btn.querySelector('.bell-icon-on');
|
|
247
|
+
const offIcon = this._btn.querySelector('.bell-icon-off');
|
|
248
|
+
if (onIcon) onIcon.style.display = anyEnabled ? '' : 'none';
|
|
249
|
+
if (offIcon) offIcon.style.display = anyEnabled ? 'none' : '';
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
window.NotificationDropdown = NotificationDropdown;
|
|
254
|
+
|
|
255
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
256
|
+
module.exports = { NotificationDropdown };
|
|
257
|
+
}
|