notificationUtils.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. // This is a separate file since the other utils import `config.js`, which depends on `window`
  2. // and cannot be used in the service worker
  3. import emojisMapped from "./emojisMapped";
  4. import { ACTION_HTTP, ACTION_VIEW } from "./actions";
  5. const toEmojis = (tags) => {
  6. if (!tags) return [];
  7. return tags.filter((tag) => tag in emojisMapped).map((tag) => emojisMapped[tag]);
  8. };
  9. export const formatTitle = (m) => {
  10. const emojiList = toEmojis(m.tags);
  11. if (emojiList.length > 0) {
  12. return `${emojiList.join(" ")} ${m.title}`;
  13. }
  14. return m.title;
  15. };
  16. const formatTitleWithDefault = (m, fallback) => {
  17. if (m.title) {
  18. return formatTitle(m);
  19. }
  20. return fallback;
  21. };
  22. export const formatMessage = (m) => {
  23. if (m.title) {
  24. return m.message || "";
  25. }
  26. const emojiList = toEmojis(m.tags);
  27. if (emojiList.length > 0) {
  28. return `${emojiList.join(" ")} ${m.message || ""}`;
  29. }
  30. return m.message || "";
  31. };
  32. const imageRegex = /\.(png|jpe?g|gif|webp)$/i;
  33. export const isImage = (attachment) => {
  34. if (!attachment) return false;
  35. // if there's a type, only take that into account
  36. if (attachment.type) {
  37. return attachment.type.startsWith("image/");
  38. }
  39. // otherwise, check the extension
  40. return attachment.name?.match(imageRegex) || attachment.url?.match(imageRegex);
  41. };
  42. export const icon = "/static/images/ntfy.png";
  43. export const badge = "/static/images/mask-icon.svg";
  44. /**
  45. * Computes a unique notification tag scoped by baseUrl, topic, and sequence ID.
  46. * This ensures notifications from different topics with the same sequence ID don't collide.
  47. */
  48. export const notificationTag = (baseUrl, topic, sequenceId) => `${baseUrl}/${topic}/${sequenceId}`;
  49. export const toNotificationParams = ({ message, defaultTitle, topicRoute, baseUrl, topic }) => {
  50. const image = isImage(message.attachment) ? message.attachment.url : undefined;
  51. const sequenceId = message.sequence_id || message.id;
  52. const tag = notificationTag(baseUrl, topic, sequenceId);
  53. const subscriptionId = `${baseUrl}/${topic}`;
  54. // https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API
  55. return [
  56. formatTitleWithDefault(message, defaultTitle),
  57. {
  58. body: formatMessage(message),
  59. badge,
  60. icon,
  61. image,
  62. timestamp: message.time * 1000,
  63. tag, // Scoped by baseUrl/topic/sequenceId to avoid cross-topic collisions
  64. renotify: true,
  65. silent: false,
  66. // This is used by the notification onclick event
  67. data: {
  68. subscriptionId,
  69. message,
  70. topicRoute,
  71. },
  72. actions: message.actions
  73. ?.filter(({ action }) => action === ACTION_VIEW || action === ACTION_HTTP)
  74. .map(({ label }) => ({
  75. action: label,
  76. title: label,
  77. })),
  78. },
  79. ];
  80. };
  81. export const messageWithSequenceId = (message) => {
  82. if (message.sequenceId) {
  83. return message;
  84. }
  85. return { ...message, sequenceId: message.sequence_id || message.id };
  86. };