jupyterlab_notifications_extension 1.2.20 → 1.2.22

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/lib/index.js CHANGED
@@ -136,12 +136,67 @@ function injectTimeAgoIntoCenter(center) {
136
136
  }
137
137
  }, 10000);
138
138
  }
139
+ /**
140
+ * Inject time-ago into a single toast element using Notification.manager
141
+ * timestamps. Used by the MutationObserver for toasts NOT created by our
142
+ * extension (e.g. JupyterLab culler, file upload, kernel notifications).
143
+ */
144
+ function injectTimeAgoIntoToast(toast) {
145
+ const msgEl = toast.querySelector('.jp-toast-message');
146
+ if (!msgEl) {
147
+ return;
148
+ }
149
+ const parent = msgEl.parentElement;
150
+ if (msgEl.querySelector('.jp-toast-time-ago') ||
151
+ (parent && parent.querySelector('.jp-toast-time-ago'))) {
152
+ return;
153
+ }
154
+ const msg = normalizeMsg(msgEl.innerText || '');
155
+ // Check server-side map first, then fall back to manager timestamps
156
+ const serverTs = serverCreatedAtMap.get(msg);
157
+ let createdAt = null;
158
+ if (serverTs && serverTs.length > 0) {
159
+ createdAt = serverTs[0];
160
+ }
161
+ else {
162
+ for (const n of Notification.manager.notifications) {
163
+ if (normalizeMsg(n.message) === msg) {
164
+ createdAt = n.createdAt;
165
+ break;
166
+ }
167
+ }
168
+ }
169
+ if (createdAt === null) {
170
+ return;
171
+ }
172
+ const timeEl = createTimeAgoElement(createdAt);
173
+ const bar = parent ? parent.querySelector('.jp-toast-buttonBar') : null;
174
+ if (bar) {
175
+ timeEl.style.marginTop = '0';
176
+ bar.insertBefore(timeEl, bar.firstChild);
177
+ }
178
+ else {
179
+ msgEl.appendChild(timeEl);
180
+ }
181
+ // Refresh while the toast is in the DOM
182
+ const refreshInterval = setInterval(() => {
183
+ if (!document.body.contains(timeEl)) {
184
+ clearInterval(refreshInterval);
185
+ return;
186
+ }
187
+ const ts = Number(timeEl.dataset.createdAt);
188
+ if (ts) {
189
+ timeEl.textContent = formatTimeAgo(ts);
190
+ }
191
+ }, 10000);
192
+ }
139
193
  /**
140
194
  * Set up a MutationObserver to watch for the Notification Center
141
- * opening and inject time-ago indicators into its list items.
195
+ * opening and for any toast popup appearing, injecting time-ago
196
+ * indicators into both.
142
197
  *
143
- * Uses subtree observation to catch both the center being added
144
- * and its list items being populated after the initial render.
198
+ * Uses subtree observation to catch the center being added,
199
+ * its list items being populated, and individual toast popups.
145
200
  */
146
201
  function observeNotificationCenter() {
147
202
  const observer = new MutationObserver(mutations => {
@@ -163,6 +218,14 @@ function observeNotificationCenter() {
163
218
  const existingCenter = node.closest('.jp-Notification-Center');
164
219
  if (existingCenter) {
165
220
  setTimeout(() => injectTimeAgoIntoCenter(existingCenter), 100);
221
+ continue;
222
+ }
223
+ // Catch toast popups (from any source, not just our extension)
224
+ const toast = node.classList.contains('Toastify__toast')
225
+ ? node
226
+ : node.querySelector('.Toastify__toast');
227
+ if (toast instanceof HTMLElement) {
228
+ setTimeout(() => injectTimeAgoIntoToast(toast), 100);
166
229
  }
167
230
  }
168
231
  }
package/lib/utils.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Format a Unix timestamp (ms) as a relative time string.
3
3
  *
4
- * Returns a compact label such as "just now", "30s ago", "5m ago",
5
- * "2h ago", or "3d ago".
4
+ * Returns a compact label such as "just now", "5m ago", "2h ago",
5
+ * or "3d ago". Anything under 60 seconds is shown as "just now".
6
6
  */
7
7
  export declare function formatTimeAgo(createdAt: number): string;
8
8
  /**
package/lib/utils.js CHANGED
@@ -1,17 +1,14 @@
1
1
  /**
2
2
  * Format a Unix timestamp (ms) as a relative time string.
3
3
  *
4
- * Returns a compact label such as "just now", "30s ago", "5m ago",
5
- * "2h ago", or "3d ago".
4
+ * Returns a compact label such as "just now", "5m ago", "2h ago",
5
+ * or "3d ago". Anything under 60 seconds is shown as "just now".
6
6
  */
7
7
  export function formatTimeAgo(createdAt) {
8
8
  const delta = Math.max(0, Date.now() - createdAt);
9
9
  const seconds = Math.floor(delta / 1000);
10
- if (seconds < 5) {
11
- return 'just now';
12
- }
13
10
  if (seconds < 60) {
14
- return `${seconds}s ago`;
11
+ return 'just now';
15
12
  }
16
13
  const minutes = Math.floor(seconds / 60);
17
14
  if (minutes < 60) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_notifications_extension",
3
- "version": "1.2.20",
3
+ "version": "1.2.22",
4
4
  "description": "Jupyterlab extension to receive and display notifications in the main panel. Those can be from the jupyterjub administrator or from other places.",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -1,15 +1,11 @@
1
1
  import { formatTimeAgo, appendTimeAgo } from '../utils';
2
2
 
3
3
  describe('formatTimeAgo', () => {
4
- it('returns "just now" for timestamps less than 5 seconds old', () => {
4
+ it('returns "just now" for timestamps less than 60 seconds old', () => {
5
5
  expect(formatTimeAgo(Date.now())).toBe('just now');
6
- expect(formatTimeAgo(Date.now() - 4999)).toBe('just now');
7
- });
8
-
9
- it('returns seconds for 5-59 seconds', () => {
10
- expect(formatTimeAgo(Date.now() - 5000)).toBe('5s ago');
11
- expect(formatTimeAgo(Date.now() - 30000)).toBe('30s ago');
12
- expect(formatTimeAgo(Date.now() - 59000)).toBe('59s ago');
6
+ expect(formatTimeAgo(Date.now() - 5000)).toBe('just now');
7
+ expect(formatTimeAgo(Date.now() - 30000)).toBe('just now');
8
+ expect(formatTimeAgo(Date.now() - 59000)).toBe('just now');
13
9
  });
14
10
 
15
11
  it('returns minutes for 1-59 minutes', () => {
package/src/index.ts CHANGED
@@ -192,12 +192,71 @@ function injectTimeAgoIntoCenter(center: Element): void {
192
192
  }, 10000);
193
193
  }
194
194
 
195
+ /**
196
+ * Inject time-ago into a single toast element using Notification.manager
197
+ * timestamps. Used by the MutationObserver for toasts NOT created by our
198
+ * extension (e.g. JupyterLab culler, file upload, kernel notifications).
199
+ */
200
+ function injectTimeAgoIntoToast(toast: HTMLElement): void {
201
+ const msgEl = toast.querySelector('.jp-toast-message') as HTMLElement | null;
202
+ if (!msgEl) {
203
+ return;
204
+ }
205
+ const parent = msgEl.parentElement;
206
+ if (
207
+ msgEl.querySelector('.jp-toast-time-ago') ||
208
+ (parent && parent.querySelector('.jp-toast-time-ago'))
209
+ ) {
210
+ return;
211
+ }
212
+ const msg = normalizeMsg(msgEl.innerText || '');
213
+
214
+ // Check server-side map first, then fall back to manager timestamps
215
+ const serverTs = serverCreatedAtMap.get(msg);
216
+ let createdAt: number | null = null;
217
+ if (serverTs && serverTs.length > 0) {
218
+ createdAt = serverTs[0];
219
+ } else {
220
+ for (const n of Notification.manager.notifications) {
221
+ if (normalizeMsg(n.message) === msg) {
222
+ createdAt = n.createdAt;
223
+ break;
224
+ }
225
+ }
226
+ }
227
+ if (createdAt === null) {
228
+ return;
229
+ }
230
+
231
+ const timeEl = createTimeAgoElement(createdAt);
232
+ const bar = parent ? parent.querySelector('.jp-toast-buttonBar') : null;
233
+ if (bar) {
234
+ timeEl.style.marginTop = '0';
235
+ bar.insertBefore(timeEl, bar.firstChild);
236
+ } else {
237
+ msgEl.appendChild(timeEl);
238
+ }
239
+
240
+ // Refresh while the toast is in the DOM
241
+ const refreshInterval = setInterval(() => {
242
+ if (!document.body.contains(timeEl)) {
243
+ clearInterval(refreshInterval);
244
+ return;
245
+ }
246
+ const ts = Number(timeEl.dataset.createdAt);
247
+ if (ts) {
248
+ timeEl.textContent = formatTimeAgo(ts);
249
+ }
250
+ }, 10000);
251
+ }
252
+
195
253
  /**
196
254
  * Set up a MutationObserver to watch for the Notification Center
197
- * opening and inject time-ago indicators into its list items.
255
+ * opening and for any toast popup appearing, injecting time-ago
256
+ * indicators into both.
198
257
  *
199
- * Uses subtree observation to catch both the center being added
200
- * and its list items being populated after the initial render.
258
+ * Uses subtree observation to catch the center being added,
259
+ * its list items being populated, and individual toast popups.
201
260
  */
202
261
  function observeNotificationCenter(): void {
203
262
  const observer = new MutationObserver(mutations => {
@@ -219,6 +278,14 @@ function observeNotificationCenter(): void {
219
278
  const existingCenter = node.closest('.jp-Notification-Center');
220
279
  if (existingCenter) {
221
280
  setTimeout(() => injectTimeAgoIntoCenter(existingCenter), 100);
281
+ continue;
282
+ }
283
+ // Catch toast popups (from any source, not just our extension)
284
+ const toast = node.classList.contains('Toastify__toast')
285
+ ? node
286
+ : node.querySelector('.Toastify__toast');
287
+ if (toast instanceof HTMLElement) {
288
+ setTimeout(() => injectTimeAgoIntoToast(toast), 100);
222
289
  }
223
290
  }
224
291
  }
package/src/utils.ts CHANGED
@@ -1,18 +1,15 @@
1
1
  /**
2
2
  * Format a Unix timestamp (ms) as a relative time string.
3
3
  *
4
- * Returns a compact label such as "just now", "30s ago", "5m ago",
5
- * "2h ago", or "3d ago".
4
+ * Returns a compact label such as "just now", "5m ago", "2h ago",
5
+ * or "3d ago". Anything under 60 seconds is shown as "just now".
6
6
  */
7
7
  export function formatTimeAgo(createdAt: number): string {
8
8
  const delta = Math.max(0, Date.now() - createdAt);
9
9
  const seconds = Math.floor(delta / 1000);
10
10
 
11
- if (seconds < 5) {
12
- return 'just now';
13
- }
14
11
  if (seconds < 60) {
15
- return `${seconds}s ago`;
12
+ return 'just now';
16
13
  }
17
14
  const minutes = Math.floor(seconds / 60);
18
15
  if (minutes < 60) {