hooks.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import { useNavigate, useParams } from "react-router-dom";
  2. import { useEffect, useState } from "react";
  3. import subscriptionManager from "../app/SubscriptionManager";
  4. import { disallowedTopic, expandSecureUrl, topicUrl } from "../app/utils";
  5. import notifier from "../app/Notifier";
  6. import routes from "./routes";
  7. import connectionManager from "../app/ConnectionManager";
  8. import poller from "../app/Poller";
  9. import pruner from "../app/Pruner";
  10. import session from "../app/Session";
  11. import accountApi from "../app/AccountApi";
  12. import { UnauthorizedError } from "../app/errors";
  13. /**
  14. * Wire connectionManager and subscriptionManager so that subscriptions are updated when the connection
  15. * state changes. Conversely, when the subscription changes, the connection is refreshed (which may lead
  16. * to the connection being re-established).
  17. */
  18. export const useConnectionListeners = (account, subscriptions, users) => {
  19. const navigate = useNavigate();
  20. // Register listeners for incoming messages, and connection state changes
  21. useEffect(
  22. () => {
  23. const handleInternalMessage = async (message) => {
  24. console.log(`[ConnectionListener] Received message on sync topic`, message.message);
  25. try {
  26. const data = JSON.parse(message.message);
  27. if (data.event === "sync") {
  28. console.log(`[ConnectionListener] Triggering account sync`);
  29. await accountApi.sync();
  30. } else {
  31. console.log(`[ConnectionListener] Unknown message type. Doing nothing.`);
  32. }
  33. } catch (e) {
  34. console.log(`[ConnectionListener] Error parsing sync topic message`, e);
  35. }
  36. };
  37. const handleNotification = async (subscriptionId, notification) => {
  38. const added = await subscriptionManager.addNotification(subscriptionId, notification);
  39. if (added) {
  40. const defaultClickAction = (subscription) => navigate(routes.forSubscription(subscription));
  41. await notifier.notify(subscriptionId, notification, defaultClickAction);
  42. }
  43. };
  44. const handleMessage = async (subscriptionId, message) => {
  45. const subscription = await subscriptionManager.get(subscriptionId);
  46. if (subscription.internal) {
  47. await handleInternalMessage(message);
  48. } else {
  49. await handleNotification(subscriptionId, message);
  50. }
  51. };
  52. connectionManager.registerStateListener(subscriptionManager.updateState);
  53. connectionManager.registerMessageListener(handleMessage);
  54. return () => {
  55. connectionManager.resetStateListener();
  56. connectionManager.resetMessageListener();
  57. };
  58. },
  59. // We have to disable dep checking for "navigate". This is fine, it never changes.
  60. []
  61. );
  62. // Sync topic listener: For accounts with sync_topic, subscribe to an internal topic
  63. useEffect(() => {
  64. if (!account || !account.sync_topic) {
  65. return;
  66. }
  67. subscriptionManager.add(config.base_url, account.sync_topic, true); // Dangle!
  68. }, [account]);
  69. // When subscriptions or users change, refresh the connections
  70. useEffect(() => {
  71. connectionManager.refresh(subscriptions, users); // Dangle
  72. }, [subscriptions, users]);
  73. };
  74. /**
  75. * Automatically adds a subscription if we navigate to a page that has not been subscribed to.
  76. * This will only be run once after the initial page load.
  77. */
  78. export const useAutoSubscribe = (subscriptions, selected) => {
  79. const [hasRun, setHasRun] = useState(false);
  80. const params = useParams();
  81. useEffect(() => {
  82. const loaded = subscriptions !== null && subscriptions !== undefined;
  83. if (!loaded || hasRun) {
  84. return;
  85. }
  86. setHasRun(true);
  87. const eligible = params.topic && !selected && !disallowedTopic(params.topic);
  88. if (eligible) {
  89. const baseUrl = params.baseUrl ? expandSecureUrl(params.baseUrl) : config.base_url;
  90. console.log(`[Hooks] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`);
  91. (async () => {
  92. const subscription = await subscriptionManager.add(baseUrl, params.topic);
  93. if (session.exists()) {
  94. try {
  95. await accountApi.addSubscription(baseUrl, params.topic);
  96. } catch (e) {
  97. console.log(`[Hooks] Auto-subscribing failed`, e);
  98. if (e instanceof UnauthorizedError) {
  99. session.resetAndRedirect(routes.login);
  100. }
  101. }
  102. }
  103. poller.pollInBackground(subscription); // Dangle!
  104. })();
  105. }
  106. }, [params, subscriptions, selected, hasRun]);
  107. };
  108. /**
  109. * Start the poller and the pruner. This is done in a side effect as opposed to just in Pruner.js
  110. * and Poller.js, because side effect imports are not a thing in JS, and "Optimize imports" cleans
  111. * up "unused" imports. See https://github.com/binwiederhier/ntfy/issues/186.
  112. */
  113. export const useBackgroundProcesses = () => {
  114. useEffect(() => {
  115. poller.startWorker();
  116. pruner.startWorker();
  117. accountApi.startWorker();
  118. }, []);
  119. };
  120. export const useAccountListener = (setAccount) => {
  121. useEffect(() => {
  122. accountApi.registerListener(setAccount);
  123. accountApi.sync(); // Dangle
  124. return () => {
  125. accountApi.resetListener();
  126. };
  127. }, []);
  128. };