@adatechnology/notification-ui 0.4.1 → 0.6.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.
@@ -16,6 +16,7 @@ var pt_BR_default = {
16
16
  "item.markAsRead": "Marcar como lida",
17
17
  "item.delete": "Excluir",
18
18
  "item.unread": "N\xE3o lida",
19
+ "item.addToCalendar": "Adicionar \xE0 agenda",
19
20
  "preferences.title": "Prefer\xEAncias de notifica\xE7\xE3o",
20
21
  "preferences.description": "Escolha por onde voc\xEA quer ser avisado em cada assunto.",
21
22
  "preferences.save": "Salvar prefer\xEAncias",
@@ -111,7 +112,23 @@ var pt_BR_default = {
111
112
  "toolbar.italic": "Italico",
112
113
  "toolbar.strikethrough": "Tachado",
113
114
  "toolbar.monospace": "Monoespacado",
114
- "toolbar.emoji": "Emoji"
115
+ "toolbar.emoji": "Emoji",
116
+ "banner.title": "Ativar notifica\xE7\xF5es",
117
+ "banner.description": "Receba lembretes de atividades, mensagens e avisos importantes diretamente no seu navegador.",
118
+ "banner.allow": "Permitir notifica\xE7\xF5es",
119
+ "banner.dismiss": "Agora n\xE3o",
120
+ "banner.activeTitle": "Notifica\xE7\xF5es ativas",
121
+ "banner.activeDescription": "Voc\xEA est\xE1 recebendo notifica\xE7\xF5es neste navegador.",
122
+ "banner.disable": "Desativar notifica\xE7\xF5es",
123
+ "banner.disabling": "Desativando\u2026",
124
+ "banner.deniedTitle": "Notifica\xE7\xF5es bloqueadas",
125
+ "banner.deniedDescription": "As notifica\xE7\xF5es est\xE3o bloqueadas no navegador. Para receber alertas, permita nas configura\xE7\xF5es do site.",
126
+ "settings.pager": "Pagina\xE7\xE3o das mensagens",
127
+ "settings.pagerRange": "{from}\u2013{to} de {total}",
128
+ "settings.pageSize": "Por p\xE1gina",
129
+ "settings.pagePrevious": "Anterior",
130
+ "settings.pageNext": "Pr\xF3xima",
131
+ "settings.pageOf": "P\xE1gina {page} de {pages}"
115
132
  };
116
133
 
117
134
  // src/locales/en.json
@@ -129,6 +146,7 @@ var en_default = {
129
146
  "item.markAsRead": "Mark as read",
130
147
  "item.delete": "Delete",
131
148
  "item.unread": "Unread",
149
+ "item.addToCalendar": "Add to calendar",
132
150
  "preferences.title": "Notification preferences",
133
151
  "preferences.description": "Choose how you want to be notified about each topic.",
134
152
  "preferences.save": "Save preferences",
@@ -224,7 +242,23 @@ var en_default = {
224
242
  "toolbar.italic": "Italic",
225
243
  "toolbar.strikethrough": "Strikethrough",
226
244
  "toolbar.monospace": "Monospace",
227
- "toolbar.emoji": "Emoji"
245
+ "toolbar.emoji": "Emoji",
246
+ "banner.title": "Enable notifications",
247
+ "banner.description": "Receive activity reminders, messages, and important alerts directly in your browser.",
248
+ "banner.allow": "Allow notifications",
249
+ "banner.dismiss": "Not now",
250
+ "banner.activeTitle": "Notifications enabled",
251
+ "banner.activeDescription": "You are receiving notifications in this browser.",
252
+ "banner.disable": "Disable notifications",
253
+ "banner.disabling": "Disabling\u2026",
254
+ "banner.deniedTitle": "Notifications blocked",
255
+ "banner.deniedDescription": "Notifications are blocked in your browser. To receive alerts, allow them in your site settings.",
256
+ "settings.pager": "Message pagination",
257
+ "settings.pagerRange": "{from}\u2013{to} of {total}",
258
+ "settings.pageSize": "Per page",
259
+ "settings.pagePrevious": "Previous",
260
+ "settings.pageNext": "Next",
261
+ "settings.pageOf": "Page {page} of {pages}"
228
262
  };
229
263
 
230
264
  // src/locales/index.ts
@@ -265,6 +299,187 @@ function useNotificationContext() {
265
299
  if (!context) throw new Error("notification-ui: componente usado fora de <NotificationProvider>");
266
300
  return context;
267
301
  }
302
+ function useOptionalNotificationContext() {
303
+ return useContext(NotificationContext);
304
+ }
305
+
306
+ // src/hooks/useNotificationPermission.ts
307
+ import { useCallback, useEffect, useState } from "react";
308
+ import {
309
+ getWebPushSubscription,
310
+ isWebPushSupported,
311
+ subscribeWebPush,
312
+ unsubscribeWebPush
313
+ } from "@adatechnology/notification-client";
314
+ var DEFAULT_STORAGE_KEY = "adatechnology:notification:permission-dismissed";
315
+ var DEFAULT_DEVICE_KEY = "adatechnology:notification:device-id";
316
+ var DEFAULT_DISMISS_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
317
+ function readCurrentNotificationPermission() {
318
+ if (typeof window === "undefined" || !("Notification" in window)) {
319
+ return "unsupported";
320
+ }
321
+ return Notification.permission;
322
+ }
323
+ function checkIsNotificationDismissed(storageKey, dismissTtlMs) {
324
+ if (typeof window === "undefined" || typeof window.localStorage === "undefined") {
325
+ return false;
326
+ }
327
+ try {
328
+ const raw = window.localStorage.getItem(storageKey);
329
+ if (!raw) return false;
330
+ const parsed = JSON.parse(raw);
331
+ if (!parsed?.dismissedAt) return false;
332
+ if (dismissTtlMs > 0 && Date.now() - parsed.dismissedAt > dismissTtlMs) {
333
+ window.localStorage.removeItem(storageKey);
334
+ return false;
335
+ }
336
+ return true;
337
+ } catch {
338
+ return false;
339
+ }
340
+ }
341
+ function useNotificationPermission(options) {
342
+ const context = useOptionalNotificationContext();
343
+ const storageKey = options?.storageKey ?? DEFAULT_STORAGE_KEY;
344
+ const deviceStorageKey = options?.deviceStorageKey ?? DEFAULT_DEVICE_KEY;
345
+ const dismissTtlMs = options?.dismissTtlMs ?? DEFAULT_DISMISS_TTL_MS;
346
+ const isSupported = isWebPushSupported();
347
+ const [permission, setPermission] = useState(readCurrentNotificationPermission);
348
+ const [isSubscribed, setIsSubscribed] = useState(false);
349
+ const [isDismissed, setIsDismissed] = useState(() => checkIsNotificationDismissed(storageKey, dismissTtlMs));
350
+ const [isPending, setIsPending] = useState(false);
351
+ const [error, setError] = useState(null);
352
+ useEffect(() => {
353
+ setPermission(readCurrentNotificationPermission());
354
+ setIsDismissed(checkIsNotificationDismissed(storageKey, dismissTtlMs));
355
+ if (!isSupported) return;
356
+ let isMounted = true;
357
+ getWebPushSubscription(options?.serviceWorkerRegistration).then((sub) => {
358
+ if (isMounted && sub) {
359
+ setIsSubscribed(true);
360
+ }
361
+ }).catch(() => {
362
+ });
363
+ return () => {
364
+ isMounted = false;
365
+ };
366
+ }, [isSupported, storageKey, dismissTtlMs, options?.serviceWorkerRegistration]);
367
+ const requestPermission = useCallback(async () => {
368
+ if (typeof window === "undefined" || !("Notification" in window)) {
369
+ return false;
370
+ }
371
+ setIsPending(true);
372
+ setError(null);
373
+ try {
374
+ const result = await Notification.requestPermission();
375
+ const updatedStatus = result;
376
+ setPermission(updatedStatus);
377
+ if (updatedStatus !== "granted") {
378
+ setIsPending(false);
379
+ return false;
380
+ }
381
+ let vapidKey = options?.vapidPublicKey;
382
+ if (!vapidKey && context?.client) {
383
+ try {
384
+ vapidKey = await context.client.getVapidPublicKey();
385
+ } catch {
386
+ }
387
+ }
388
+ if (vapidKey) {
389
+ const subscription = await subscribeWebPush({
390
+ vapidPublicKey: vapidKey,
391
+ serviceWorkerRegistration: options?.serviceWorkerRegistration
392
+ });
393
+ if (subscription) {
394
+ setIsSubscribed(true);
395
+ if (context?.client) {
396
+ try {
397
+ const device = await context.client.registerWebPush(subscription);
398
+ if (typeof window !== "undefined" && window.localStorage) {
399
+ window.localStorage.setItem(deviceStorageKey, device.id);
400
+ }
401
+ } catch {
402
+ }
403
+ }
404
+ if (options?.onSubscribe) {
405
+ await options.onSubscribe(subscription);
406
+ }
407
+ }
408
+ }
409
+ if (typeof window !== "undefined" && window.localStorage) {
410
+ window.localStorage.removeItem(storageKey);
411
+ }
412
+ setIsDismissed(false);
413
+ setIsPending(false);
414
+ return true;
415
+ } catch (err) {
416
+ setError(err instanceof Error ? err : new Error(String(err)));
417
+ setIsPending(false);
418
+ return false;
419
+ }
420
+ }, [
421
+ context?.client,
422
+ deviceStorageKey,
423
+ options?.onSubscribe,
424
+ options?.serviceWorkerRegistration,
425
+ options?.vapidPublicKey,
426
+ storageKey
427
+ ]);
428
+ const disableNotifications = useCallback(async () => {
429
+ setIsPending(true);
430
+ setError(null);
431
+ try {
432
+ await unsubscribeWebPush(options?.serviceWorkerRegistration);
433
+ if (context?.client && typeof window !== "undefined" && window.localStorage) {
434
+ const deviceId = window.localStorage.getItem(deviceStorageKey);
435
+ if (deviceId) {
436
+ await context.client.unregisterDevice(deviceId).catch(() => void 0);
437
+ window.localStorage.removeItem(deviceStorageKey);
438
+ }
439
+ }
440
+ if (options?.onUnsubscribe) {
441
+ await options.onUnsubscribe();
442
+ }
443
+ setIsSubscribed(false);
444
+ setIsPending(false);
445
+ return true;
446
+ } catch (err) {
447
+ setError(err instanceof Error ? err : new Error(String(err)));
448
+ setIsPending(false);
449
+ return false;
450
+ }
451
+ }, [context?.client, deviceStorageKey, options?.onUnsubscribe, options?.serviceWorkerRegistration]);
452
+ const dismiss = useCallback(() => {
453
+ if (typeof window !== "undefined" && window.localStorage) {
454
+ try {
455
+ window.localStorage.setItem(storageKey, JSON.stringify({ dismissedAt: Date.now() }));
456
+ } catch {
457
+ }
458
+ }
459
+ setIsDismissed(true);
460
+ }, [storageKey]);
461
+ const resetDismissed = useCallback(() => {
462
+ if (typeof window !== "undefined" && window.localStorage) {
463
+ try {
464
+ window.localStorage.removeItem(storageKey);
465
+ } catch {
466
+ }
467
+ }
468
+ setIsDismissed(false);
469
+ }, [storageKey]);
470
+ return {
471
+ permission,
472
+ isSupported,
473
+ isSubscribed,
474
+ isDismissed,
475
+ isPending,
476
+ error,
477
+ requestPermission,
478
+ disableNotifications,
479
+ dismiss,
480
+ resetDismissed
481
+ };
482
+ }
268
483
 
269
484
  // src/hooks/useNotifications.ts
270
485
  import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -354,7 +569,7 @@ function flattenNotificationPages(pages) {
354
569
  }
355
570
 
356
571
  // src/hooks/useNotificationStream.ts
357
- import { useEffect } from "react";
572
+ import { useEffect as useEffect2 } from "react";
358
573
  import { useQueryClient as useQueryClient2 } from "@tanstack/react-query";
359
574
  import { subscribeToNotificationStream } from "@adatechnology/notification-client";
360
575
  function useNotificationStream(options = {}) {
@@ -362,7 +577,7 @@ function useNotificationStream(options = {}) {
362
577
  const queryClient = useQueryClient2();
363
578
  const enabled = options.enabled ?? false;
364
579
  const onEvent = options.onEvent;
365
- useEffect(() => {
580
+ useEffect2(() => {
366
581
  if (!enabled) return;
367
582
  let subscription;
368
583
  let cancelled = false;
@@ -385,7 +600,7 @@ function useNotificationStream(options = {}) {
385
600
  }
386
601
 
387
602
  // src/hooks/useCategoryPolicies.ts
388
- import { useMemo as useMemo2, useState } from "react";
603
+ import { useMemo as useMemo2, useState as useState2 } from "react";
389
604
  import { useMutation as useMutation2, useQuery as useQuery2, useQueryClient as useQueryClient3 } from "@tanstack/react-query";
390
605
  function identityOf(params) {
391
606
  return `${params.category}:${params.channel}`;
@@ -403,7 +618,7 @@ function useCategoryPolicies() {
403
618
  void queryClient.invalidateQueries({ queryKey: NOTIFICATION_QUERY_KEYS.categoryPolicies() });
404
619
  }
405
620
  });
406
- const [touched, setTouched] = useState({});
621
+ const [touched, setTouched] = useState2({});
407
622
  const saved = useMemo2(() => {
408
623
  const byIdentity = /* @__PURE__ */ new Map();
409
624
  for (const policy of policiesQuery.data ?? []) byIdentity.set(identityOf(policy), policy.enabled);
@@ -543,7 +758,7 @@ function formatNotificationTimestamp(isoTimestamp, locale) {
543
758
  }
544
759
 
545
760
  // src/hooks/useTemplateEditor.ts
546
- import { useMemo as useMemo3, useState as useState2 } from "react";
761
+ import { useMemo as useMemo3, useState as useState3 } from "react";
547
762
 
548
763
  // src/templateList.constant.ts
549
764
  var TEMPLATE_VIEWS = {
@@ -566,6 +781,12 @@ var TEMPLATE_COLUMNS = [
566
781
  { labelKey: "settings.column.version", field: TEMPLATE_SORT_FIELDS.VERSION },
567
782
  { labelKey: "settings.column.actions", headerHidden: true }
568
783
  ];
784
+ var TEMPLATE_PAGE_SIZES = [10, 25, 50];
785
+ var DEFAULT_TEMPLATE_PAGE_SIZE = 25;
786
+ function summarizeTemplateBody(body, maxLength = 140) {
787
+ const text = body.replace(/<!--[\s\S]*?-->/g, " ").replace(/<(style|script|head)[\s\S]*?<\/\1>/gi, " ").replace(/<[^>]*>/g, " ").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
788
+ return text.length > maxLength ? `${text.slice(0, maxLength - 1).trimEnd()}\u2026` : text;
789
+ }
569
790
 
570
791
  // src/hooks/useTemplateEditor.ts
571
792
  import {
@@ -638,14 +859,16 @@ function useTemplateEditor(params = {}) {
638
859
  const upsert = useUpsertTemplate();
639
860
  const deactivateMutation = useDeactivateTemplate();
640
861
  const restoreMutation = useRestoreTemplateDefault();
641
- const [search, setSearch] = useState2("");
642
- const [channelFilter, setChannelFilter] = useState2([]);
643
- const [categoryFilter, setCategoryFilter] = useState2([]);
644
- const [view, setView] = useState2(TEMPLATE_VIEWS.TABLE);
645
- const [sort, setSort] = useState2(void 0);
646
- const [selectedId, setSelectedId] = useState2(void 0);
647
- const [draft, setDraft] = useState2(void 0);
648
- const [isNew, setIsNew] = useState2(false);
862
+ const [search, setSearch] = useState3("");
863
+ const [channelFilter, setChannelFilter] = useState3([]);
864
+ const [categoryFilter, setCategoryFilter] = useState3([]);
865
+ const [view, setView] = useState3(TEMPLATE_VIEWS.TABLE);
866
+ const [requestedPage, setRequestedPage] = useState3(1);
867
+ const [pageSize, setPageSizeState] = useState3(DEFAULT_TEMPLATE_PAGE_SIZE);
868
+ const [sort, setSort] = useState3(void 0);
869
+ const [selectedId, setSelectedId] = useState3(void 0);
870
+ const [draft, setDraft] = useState3(void 0);
871
+ const [isNew, setIsNew] = useState3(false);
649
872
  const templates = useMemo3(() => {
650
873
  const activeByIdentity = /* @__PURE__ */ new Map();
651
874
  for (const template of templatesQuery.data ?? []) {
@@ -680,16 +903,19 @@ function useTemplateEditor(params = {}) {
680
903
  return left[campo].localeCompare(right[campo]) * sinal;
681
904
  });
682
905
  }, [filtered, sort]);
906
+ const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize));
907
+ const page = Math.min(requestedPage, pageCount);
908
+ const paged = useMemo3(() => sorted.slice((page - 1) * pageSize, page * pageSize), [sorted, page, pageSize]);
683
909
  const groups = useMemo3(() => {
684
910
  const porCategoria = /* @__PURE__ */ new Map();
685
- for (const template of sorted) {
911
+ for (const template of paged) {
686
912
  const categoria = categoryOf(template.key);
687
913
  const atual = porCategoria.get(categoria);
688
914
  if (atual) atual.push(template);
689
915
  else porCategoria.set(categoria, [template]);
690
916
  }
691
917
  return [...porCategoria.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([category, list]) => ({ category, templates: list }));
692
- }, [sorted]);
918
+ }, [paged]);
693
919
  const selected = useMemo3(() => templates.find((template) => template.id === selectedId), [templates, selectedId]);
694
920
  const variables = useMemo3(() => draft ? variablesQuery.data?.[draft.key] ?? [] : [], [variablesQuery.data, draft]);
695
921
  const variableDiff = useMemo3(
@@ -738,7 +964,16 @@ function useTemplateEditor(params = {}) {
738
964
  return Object.keys(original).some((field) => original[field] !== atual[field]);
739
965
  }, [draft, selected, isNew]);
740
966
  return {
741
- templates: sorted,
967
+ templates: paged,
968
+ filteredCount: sorted.length,
969
+ page,
970
+ pageCount,
971
+ pageSize,
972
+ setPage: (next) => setRequestedPage(Math.min(Math.max(1, next), pageCount)),
973
+ setPageSize: (size) => {
974
+ setPageSizeState(size);
975
+ setRequestedPage(1);
976
+ },
742
977
  groups,
743
978
  view,
744
979
  sort,
@@ -770,13 +1005,18 @@ function useTemplateEditor(params = {}) {
770
1005
  /** Todo canal marcado precisa do proprio texto — senao a gravacao criaria template vazio. */
771
1006
  canSave: Boolean(draft?.key) && (draft?.channels.length ?? 0) > 0 && (draft?.channels.every((channel) => (draft.byChannel[channel]?.body ?? "").length > 0) ?? false) && variableDiff.unknown.length === 0 && // O modelo padrão inutilizável nunca é gravado: o servidor recusa (400) e o envio o ignoraria.
772
1007
  (standardAssessment?.usable ?? true),
773
- setSearch,
1008
+ setSearch(value) {
1009
+ setSearch(value);
1010
+ setRequestedPage(1);
1011
+ },
774
1012
  toggleChannelFilter(channel) {
1013
+ setRequestedPage(1);
775
1014
  setChannelFilter(
776
1015
  (current) => current.includes(channel) ? current.filter((each) => each !== channel) : [...current, channel]
777
1016
  );
778
1017
  },
779
1018
  toggleCategoryFilter(category) {
1019
+ setRequestedPage(1);
780
1020
  setCategoryFilter(
781
1021
  (current) => current.includes(category) ? current.filter((each) => each !== category) : [...current, category]
782
1022
  );
@@ -790,6 +1030,7 @@ function useTemplateEditor(params = {}) {
790
1030
  });
791
1031
  },
792
1032
  clearFilters() {
1033
+ setRequestedPage(1);
793
1034
  setSearch("");
794
1035
  setChannelFilter([]);
795
1036
  setCategoryFilter([]);
@@ -898,6 +1139,8 @@ export {
898
1139
  resolveMessages,
899
1140
  NotificationProvider,
900
1141
  useNotificationContext,
1142
+ useOptionalNotificationContext,
1143
+ useNotificationPermission,
901
1144
  NOTIFICATION_QUERY_KEYS,
902
1145
  useNotifications,
903
1146
  useUnreadCount,
@@ -920,7 +1163,9 @@ export {
920
1163
  TEMPLATE_SORT_FIELDS,
921
1164
  SORT_DIRECTIONS,
922
1165
  TEMPLATE_COLUMNS,
1166
+ TEMPLATE_PAGE_SIZES,
1167
+ summarizeTemplateBody,
923
1168
  isStandardEmailKey,
924
1169
  useTemplateEditor
925
1170
  };
926
- //# sourceMappingURL=chunk-SFMOFCHV.js.map
1171
+ //# sourceMappingURL=chunk-Y65PYMEH.js.map