@realtimexsco/live-chat 1.4.8 → 1.4.9

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.
@@ -4,15 +4,9 @@
4
4
  * /firebase-messaging-sw.js:
5
5
  *
6
6
  * cp node_modules/@realtimexsco/live-chat/dist/firebase-messaging-sw.js public/
7
- *
8
- * Do NOT edit the Firebase config into this file — the chat package passes it
9
- * via the registration URL (?config=...), so the copy stays generic.
10
- *
11
- * If your app already has its own service worker at the root scope, merge the
12
- * contents of this file into it instead (two workers cannot share a scope).
13
7
  */
14
8
 
15
- /* global firebase, importScripts */
9
+ /* global firebase, importScripts, BroadcastChannel */
16
10
 
17
11
  importScripts(
18
12
  "https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js",
@@ -21,29 +15,36 @@ importScripts(
21
15
  "https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js",
22
16
  );
23
17
 
18
+ var PUSH_BROADCAST_CHANNEL = "realtimex-push";
19
+ var NOTIFICATION_CLICK_SW_TYPE = "realtimex:notification-click";
20
+
24
21
  var params = new URL(self.location.href).searchParams;
25
22
  var rawConfig = params.get("config");
26
23
 
27
24
  if (rawConfig) {
28
25
  try {
29
26
  var firebaseConfig = JSON.parse(rawConfig);
30
-
31
27
  firebase.initializeApp(firebaseConfig);
32
-
33
- // Background messages that carry a `notification` payload are displayed
34
- // by FCM automatically; initializing messaging here is what enables it.
35
28
  var messaging = firebase.messaging();
36
29
 
37
- // Log full backend payload when a push arrives in the background.
38
- // Check this in DevTools → Application → Service Workers → console,
39
- // or Chrome → chrome://serviceworker-internals
40
30
  messaging.onBackgroundMessage(function (payload) {
41
31
  console.log("[realtimex-push] FCM background payload (raw):", payload);
42
32
  console.log("[realtimex-push] FCM background data:", payload && payload.data);
43
- console.log(
44
- "[realtimex-push] FCM background notification:",
45
- payload && payload.notification,
46
- );
33
+
34
+ var title =
35
+ (payload.notification && payload.notification.title) || "New message";
36
+ var body = (payload.notification && payload.notification.body) || "";
37
+ var data = payload.data || {};
38
+ var tag = "realtimex-" + (data.conversationId || data.messageId || "push");
39
+
40
+ // Show notification ourselves so click always carries `data` for redirect.
41
+ // Same tag replaces FCM auto-notification when both fire.
42
+ return self.registration.showNotification(title, {
43
+ body: body,
44
+ data: data,
45
+ tag: tag,
46
+ renotify: true,
47
+ });
47
48
  });
48
49
  } catch (error) {
49
50
  console.error("[realtimex] Invalid firebase config in SW URL:", error);
@@ -55,13 +56,22 @@ if (rawConfig) {
55
56
  );
56
57
  }
57
58
 
59
+ function parseMaybeJson(value) {
60
+ if (typeof value !== "string") return value;
61
+ try {
62
+ return JSON.parse(value);
63
+ } catch (e) {
64
+ return value;
65
+ }
66
+ }
67
+
58
68
  function extractNotificationData(notification) {
59
69
  var raw = (notification && notification.data) || {};
70
+ var fcmMsg = parseMaybeJson(raw.FCM_MSG);
60
71
  var nested =
61
- raw.FCM_MSG && raw.FCM_MSG.data && typeof raw.FCM_MSG.data === "object"
62
- ? raw.FCM_MSG.data
63
- : null;
72
+ fcmMsg && fcmMsg.data && typeof fcmMsg.data === "object" ? fcmMsg.data : null;
64
73
  var merged = {};
74
+
65
75
  if (nested) {
66
76
  for (var key in nested) {
67
77
  if (Object.prototype.hasOwnProperty.call(nested, key)) {
@@ -69,11 +79,13 @@ function extractNotificationData(notification) {
69
79
  }
70
80
  }
71
81
  }
82
+
72
83
  for (var key2 in raw) {
73
84
  if (Object.prototype.hasOwnProperty.call(raw, key2) && key2 !== "FCM_MSG") {
74
85
  merged[key2] = raw[key2];
75
86
  }
76
87
  }
88
+
77
89
  return merged;
78
90
  }
79
91
 
@@ -84,16 +96,14 @@ function resolveConversationId(data) {
84
96
  data.conversation_id ||
85
97
  data.conversationID ||
86
98
  "",
87
- );
99
+ ).trim();
88
100
  }
89
101
 
90
102
  function buildTargetUrl(data, conversationId) {
91
103
  if (data && data.link) return String(data.link);
92
-
93
104
  if (conversationId) {
94
105
  return "/chat?conversationId=" + encodeURIComponent(conversationId);
95
106
  }
96
-
97
107
  return "/chat";
98
108
  }
99
109
 
@@ -106,7 +116,16 @@ function isChatClient(client) {
106
116
  }
107
117
  }
108
118
 
109
- // Focus (or open) the chat and tell the page which conversation to open.
119
+ function notifyPage(message) {
120
+ try {
121
+ var bc = new BroadcastChannel(PUSH_BROADCAST_CHANNEL);
122
+ bc.postMessage(message);
123
+ bc.close();
124
+ } catch (e) {
125
+ // ignore
126
+ }
127
+ }
128
+
110
129
  self.addEventListener("notificationclick", function (event) {
111
130
  event.notification.close();
112
131
 
@@ -119,8 +138,9 @@ self.addEventListener("notificationclick", function (event) {
119
138
  var conversationId = resolveConversationId(data);
120
139
  var targetUrl = buildTargetUrl(data, conversationId);
121
140
  var message = {
122
- type: "realtimex:notification-click",
141
+ type: NOTIFICATION_CLICK_SW_TYPE,
123
142
  conversationId: conversationId,
143
+ data: data,
124
144
  };
125
145
 
126
146
  console.log("[realtimex-push] notificationclick extracted data:", data);
@@ -143,10 +163,12 @@ self.addEventListener("notificationclick", function (event) {
143
163
  var target = chatClient || anyClient;
144
164
  if (target && "focus" in target) {
145
165
  return target.focus().then(function () {
166
+ notifyPage(message);
146
167
  if (target.postMessage) target.postMessage(message);
147
168
  });
148
169
  }
149
170
 
171
+ notifyPage(message);
150
172
  if (clients.openWindow) return clients.openWindow(targetUrl);
151
173
  return undefined;
152
174
  }),
package/dist/index.cjs CHANGED
@@ -4659,46 +4659,112 @@ init_store_helpers();
4659
4659
  init_store_helpers();
4660
4660
  var OPEN_CONVERSATION_EVENT = "realtimex:open-conversation";
4661
4661
  var NOTIFICATION_CLICK_SW_TYPE = "realtimex:notification-click";
4662
+ var PUSH_BROADCAST_CHANNEL = "realtimex-push";
4662
4663
  var PENDING_STORAGE_KEY = "realtimex:pending-open-conversation";
4663
4664
  var resolveConversationIdFromPushData = (data) => {
4664
4665
  if (!data || typeof data !== "object") return "";
4665
- const nested = data.FCM_MSG && typeof data.FCM_MSG === "object" ? data.FCM_MSG.data : void 0;
4666
+ let fcmMsg = data.FCM_MSG;
4667
+ if (typeof fcmMsg === "string") {
4668
+ try {
4669
+ fcmMsg = JSON.parse(fcmMsg);
4670
+ } catch {
4671
+ fcmMsg = void 0;
4672
+ }
4673
+ }
4674
+ const nested = fcmMsg && typeof fcmMsg === "object" ? fcmMsg.data : void 0;
4666
4675
  const source = { ...nested || {}, ...data };
4667
4676
  return String(
4668
4677
  source.conversationId || source.conversation_id || source.conversationID || ""
4669
- );
4678
+ ).trim();
4679
+ };
4680
+ var normalizePushMeta = (meta) => {
4681
+ if (!meta || typeof meta !== "object") return {};
4682
+ return {
4683
+ senderId: String(
4684
+ meta.senderId || meta.sender_id || ""
4685
+ ).trim() || void 0,
4686
+ kind: String(meta.kind || "").trim() || void 0,
4687
+ messageId: String(
4688
+ meta.messageId || meta.message_id || ""
4689
+ ).trim() || void 0,
4690
+ title: String(meta.title || "").trim() || void 0
4691
+ };
4692
+ };
4693
+ var parsePendingRaw = (raw) => {
4694
+ if (!raw) return null;
4695
+ try {
4696
+ const parsed = JSON.parse(raw);
4697
+ if (parsed?.conversationId) {
4698
+ return {
4699
+ conversationId: String(parsed.conversationId).trim(),
4700
+ ...normalizePushMeta(parsed)
4701
+ };
4702
+ }
4703
+ } catch {
4704
+ }
4705
+ const id = String(raw).trim();
4706
+ return id ? { conversationId: id } : null;
4670
4707
  };
4671
- var peekPendingOpenConversation = () => {
4708
+ var peekPendingOpenConversation = () => peekPendingOpenRequest()?.conversationId ?? null;
4709
+ var peekPendingOpenRequest = () => {
4672
4710
  try {
4673
- return sessionStorage.getItem(PENDING_STORAGE_KEY);
4711
+ return parsePendingRaw(sessionStorage.getItem(PENDING_STORAGE_KEY));
4674
4712
  } catch {
4675
4713
  return null;
4676
4714
  }
4677
4715
  };
4678
- var consumePendingOpenConversation = () => {
4679
- const id = peekPendingOpenConversation();
4716
+ var consumePendingOpenConversation = () => consumePendingOpenRequest()?.conversationId ?? null;
4717
+ var consumePendingOpenRequest = () => {
4718
+ const request = peekPendingOpenRequest();
4680
4719
  try {
4681
4720
  sessionStorage.removeItem(PENDING_STORAGE_KEY);
4682
4721
  } catch {
4683
4722
  }
4684
- return id;
4723
+ return request;
4685
4724
  };
4686
- var requestOpenConversation = (conversationId) => {
4725
+ var requestOpenConversation = (conversationId, meta) => {
4687
4726
  const id = String(conversationId || "").trim();
4688
4727
  if (!id) return;
4728
+ const request = {
4729
+ conversationId: id,
4730
+ ...normalizePushMeta(meta)
4731
+ };
4732
+ console.log("[realtimex-push] requestOpenConversation:", request);
4689
4733
  try {
4690
- sessionStorage.setItem(PENDING_STORAGE_KEY, id);
4734
+ sessionStorage.setItem(PENDING_STORAGE_KEY, JSON.stringify(request));
4691
4735
  } catch {
4692
4736
  }
4693
4737
  try {
4694
4738
  window.dispatchEvent(
4695
- new CustomEvent(OPEN_CONVERSATION_EVENT, {
4696
- detail: { conversationId: id }
4697
- })
4739
+ new CustomEvent(OPEN_CONVERSATION_EVENT, { detail: request })
4698
4740
  );
4699
4741
  } catch {
4700
4742
  }
4701
4743
  };
4744
+ var buildChannelFromPushRequest = (request, loggedUserId, allUsers) => {
4745
+ const conversationId = request.conversationId;
4746
+ const isGroup = request.kind === "group";
4747
+ const senderId = String(request.senderId || "").trim();
4748
+ if (isGroup) {
4749
+ const rawName2 = request.title || "Group Chat";
4750
+ return {
4751
+ id: conversationId,
4752
+ conversationId,
4753
+ name: rawName2.charAt(0).toUpperCase() + rawName2.slice(1),
4754
+ isGroup: true
4755
+ };
4756
+ }
4757
+ const senderUser = senderId ? (allUsers || []).find((user) => String(user._id) === senderId) : null;
4758
+ const rawName = senderUser?.name || request.title || "New message";
4759
+ return {
4760
+ id: senderId || conversationId,
4761
+ conversationId,
4762
+ name: rawName.charAt(0).toUpperCase() + rawName.slice(1),
4763
+ image: senderUser?.image || void 0,
4764
+ email: senderUser?.email,
4765
+ isGroup: false
4766
+ };
4767
+ };
4702
4768
  var mapConversationToChannel = (conversation, loggedUserId, allUsers) => {
4703
4769
  const conversationId = String(conversation?._id || conversation?.id || "");
4704
4770
  const isGroup = isGroupConversation(conversation);
@@ -4743,34 +4809,46 @@ var readConversationIdFromLocation = () => {
4743
4809
  }
4744
4810
  };
4745
4811
  var consumedUrlConversationIds = /* @__PURE__ */ new Set();
4812
+ var handleNotificationOpen = (conversationId, pushData, options) => {
4813
+ const id = String(conversationId || "").trim();
4814
+ if (!id) {
4815
+ console.warn("[realtimex-push] notification open ignored \u2014 no conversationId", pushData);
4816
+ return;
4817
+ }
4818
+ requestOpenConversation(id, pushData);
4819
+ const chatPath = options.chatPath || "/chat";
4820
+ const pathWithQuery = `${chatPath}?conversationId=${encodeURIComponent(id)}`;
4821
+ options.onNavigate?.(id, pathWithQuery);
4822
+ };
4746
4823
  var installNotificationClickBridge = (options = {}) => {
4747
4824
  if (typeof window === "undefined") return () => void 0;
4748
- const chatPath = options.chatPath || "/chat";
4749
- const handleOpen = (conversationId) => {
4750
- const id = String(conversationId || "").trim();
4751
- if (!id) return;
4752
- requestOpenConversation(id);
4753
- const pathWithQuery = `${chatPath}?conversationId=${encodeURIComponent(id)}`;
4754
- options.onNavigate?.(id, pathWithQuery);
4755
- };
4756
4825
  const onSwMessage = (event) => {
4757
4826
  const data = event.data;
4758
4827
  console.log("[realtimex-push] SW \u2192 page message:", data);
4759
4828
  if (!data || data.type !== NOTIFICATION_CLICK_SW_TYPE) return;
4760
- console.log(
4761
- "[realtimex-push] notification click conversationId:",
4762
- data.conversationId
4763
- );
4764
- handleOpen(String(data.conversationId || ""));
4829
+ const pushData = data.data && typeof data.data === "object" ? data.data : void 0;
4830
+ const conversationId = String(data.conversationId || "") || resolveConversationIdFromPushData(pushData);
4831
+ console.log("[realtimex-push] notification click \u2192 open:", {
4832
+ conversationId,
4833
+ pushData
4834
+ });
4835
+ handleNotificationOpen(conversationId, pushData, options);
4765
4836
  };
4766
4837
  navigator.serviceWorker?.addEventListener("message", onSwMessage);
4838
+ let broadcast = null;
4839
+ try {
4840
+ broadcast = new BroadcastChannel(PUSH_BROADCAST_CHANNEL);
4841
+ broadcast.onmessage = onSwMessage;
4842
+ } catch {
4843
+ }
4767
4844
  const fromUrl = readConversationIdFromLocation();
4768
4845
  if (fromUrl && !consumedUrlConversationIds.has(fromUrl)) {
4769
4846
  consumedUrlConversationIds.add(fromUrl);
4770
- handleOpen(fromUrl);
4847
+ handleNotificationOpen(fromUrl, void 0, options);
4771
4848
  }
4772
4849
  return () => {
4773
4850
  navigator.serviceWorker?.removeEventListener("message", onSwMessage);
4851
+ broadcast?.close();
4774
4852
  };
4775
4853
  };
4776
4854
 
@@ -4850,7 +4928,7 @@ var useChatController = () => {
4850
4928
  const allUsers = exports.useChatStore((s) => s.allUsers);
4851
4929
  const isFetchingConversations = exports.useChatStore((s) => s.isFetchingConversations);
4852
4930
  const conversations = exports.useChatStore((s) => s.conversations);
4853
- const [pendingOpenConversationId, setPendingOpenConversationId] = React2.useState(() => peekPendingOpenConversation());
4931
+ const [pendingOpenRequest, setPendingOpenRequest] = React2.useState(() => peekPendingOpenRequest());
4854
4932
  const conversationsLength = exports.useChatStore(
4855
4933
  (s) => state.activeChannel?.conversationId ? 0 : s.conversations.length
4856
4934
  );
@@ -4881,32 +4959,42 @@ var useChatController = () => {
4881
4959
  });
4882
4960
  React2.useEffect(() => {
4883
4961
  const onOpen = (event) => {
4884
- const id = String(
4885
- event.detail?.conversationId || ""
4886
- ).trim();
4887
- if (id) setPendingOpenConversationId(id);
4962
+ const detail = event.detail;
4963
+ if (detail?.conversationId) {
4964
+ console.log("[realtimex-push] OPEN_CONVERSATION_EVENT:", detail);
4965
+ setPendingOpenRequest({
4966
+ conversationId: String(detail.conversationId),
4967
+ senderId: detail.senderId,
4968
+ kind: detail.kind,
4969
+ messageId: detail.messageId,
4970
+ title: detail.title
4971
+ });
4972
+ }
4888
4973
  };
4889
4974
  window.addEventListener(OPEN_CONVERSATION_EVENT, onOpen);
4890
4975
  return () => window.removeEventListener(OPEN_CONVERSATION_EVENT, onOpen);
4891
4976
  }, []);
4892
4977
  React2.useEffect(() => {
4893
- if (!pendingOpenConversationId || !loggedUserDetails) return;
4978
+ if (!pendingOpenRequest?.conversationId || !loggedUserDetails) return;
4894
4979
  let cancelled = false;
4895
4980
  const openPending = async () => {
4896
- const targetId = String(pendingOpenConversationId);
4981
+ const targetId = String(pendingOpenRequest.conversationId);
4982
+ console.log("[realtimex-push] opening conversation:", pendingOpenRequest);
4897
4983
  let conversation = conversations.find(
4898
4984
  (c) => String(c._id || c.id) === targetId
4899
4985
  ) || null;
4900
4986
  if (!conversation) {
4901
- const fromInfo = conversationInfos[targetId];
4902
- if (fromInfo) conversation = fromInfo;
4987
+ conversation = conversationInfos[targetId] || null;
4903
4988
  }
4904
4989
  if (!conversation) {
4905
- if (isFetchingConversations) return;
4990
+ if (isFetchingConversations) {
4991
+ console.log("[realtimex-push] waiting for conversations list\u2026");
4992
+ return;
4993
+ }
4906
4994
  try {
4907
4995
  await fetchConversationInfo2(targetId);
4908
- } catch {
4909
- return;
4996
+ } catch (error) {
4997
+ console.warn("[realtimex-push] fetchConversationInfo failed:", error);
4910
4998
  }
4911
4999
  if (cancelled) return;
4912
5000
  const stateNow = exports.useChatStore.getState();
@@ -4914,12 +5002,17 @@ var useChatController = () => {
4914
5002
  (c) => String(c._id || c.id) === targetId
4915
5003
  ) || stateNow.conversationInfos[targetId] || null;
4916
5004
  }
4917
- if (!conversation || cancelled) return;
4918
- const channel = mapConversationToChannel(
5005
+ const channel = conversation ? mapConversationToChannel(
4919
5006
  conversation,
4920
5007
  loggedUserDetails._id,
4921
5008
  allUsers
5009
+ ) : buildChannelFromPushRequest(
5010
+ pendingOpenRequest,
5011
+ loggedUserDetails._id,
5012
+ allUsers
4922
5013
  );
5014
+ console.log("[realtimex-push] selected channel:", channel);
5015
+ if (cancelled) return;
4923
5016
  setState((prev) => {
4924
5017
  if (prev.activeChannel?.conversationId && String(prev.activeChannel.conversationId) === targetId) {
4925
5018
  return prev;
@@ -4930,15 +5023,16 @@ var useChatController = () => {
4930
5023
  activeConversationId: targetId
4931
5024
  };
4932
5025
  });
4933
- consumePendingOpenConversation();
4934
- setPendingOpenConversationId(null);
5026
+ exports.useChatStore.getState().setActiveConversationId(targetId);
5027
+ consumePendingOpenRequest();
5028
+ setPendingOpenRequest(null);
4935
5029
  };
4936
5030
  void openPending();
4937
5031
  return () => {
4938
5032
  cancelled = true;
4939
5033
  };
4940
5034
  }, [
4941
- pendingOpenConversationId,
5035
+ pendingOpenRequest,
4942
5036
  loggedUserDetails,
4943
5037
  conversations,
4944
5038
  conversationInfos,
@@ -17845,7 +17939,7 @@ var emitToast = (payload, onPush) => {
17845
17939
  type: "button",
17846
17940
  onClick: () => {
17847
17941
  toast2__default.default.dismiss(t.id);
17848
- requestOpenConversation(conversationId);
17942
+ requestOpenConversation(conversationId, payload.data);
17849
17943
  },
17850
17944
  style: {
17851
17945
  display: "block",
@@ -18497,6 +18591,7 @@ exports.MessageItem = MessageItemView;
18497
18591
  exports.MessageList = ActiveChannelMessagesView_default;
18498
18592
  exports.NOTIFICATION_CLICK_SW_TYPE = NOTIFICATION_CLICK_SW_TYPE;
18499
18593
  exports.OPEN_CONVERSATION_EVENT = OPEN_CONVERSATION_EVENT;
18594
+ exports.PUSH_BROADCAST_CHANNEL = PUSH_BROADCAST_CHANNEL;
18500
18595
  exports.PUSH_CHANGED_EVENT = PUSH_CHANGED_EVENT;
18501
18596
  exports.PushNotificationToggle = PushNotificationToggle;
18502
18597
  exports.apiGetPushConfig = apiGetPushConfig;
@@ -18504,9 +18599,11 @@ exports.apiGetPushPreference = apiGetPushPreference;
18504
18599
  exports.apiRegisterPushToken = apiRegisterPushToken;
18505
18600
  exports.apiRemovePushToken = apiRemovePushToken;
18506
18601
  exports.apiUpdatePushPreference = apiUpdatePushPreference;
18602
+ exports.buildChannelFromPushRequest = buildChannelFromPushRequest;
18507
18603
  exports.capitalizeFirst = capitalizeFirst;
18508
18604
  exports.clampJumpDate = clampJumpDate;
18509
18605
  exports.consumePendingOpenConversation = consumePendingOpenConversation;
18606
+ exports.consumePendingOpenRequest = consumePendingOpenRequest;
18510
18607
  exports.default = index_default;
18511
18608
  exports.defaultChatClassNames = defaultChatClassNames;
18512
18609
  exports.defaultChatComponents = defaultChatComponents;
@@ -18527,6 +18624,7 @@ exports.mergeChatLayoutConfig = mergeChatLayoutConfig;
18527
18624
  exports.onForegroundPush = onForegroundPush;
18528
18625
  exports.packageDefaultComponents = packageDefaultComponents;
18529
18626
  exports.peekPendingOpenConversation = peekPendingOpenConversation;
18627
+ exports.peekPendingOpenRequest = peekPendingOpenRequest;
18530
18628
  exports.requestOpenConversation = requestOpenConversation;
18531
18629
  exports.resolveApiUrl = resolveApiUrl;
18532
18630
  exports.resolveChatApiKey = resolveChatApiKey;