quickblox 2.23.1-beta.5 → 2.24.0-beta.1

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.
@@ -6,7 +6,8 @@
6
6
  var chatUtils = require('./qbChatHelpers'),
7
7
  config = require('../../qbConfig'),
8
8
  Utils = require('../../qbUtils'),
9
- StreamManagement = require('../../plugins/streamManagement');
9
+ StreamManagement = require('../../plugins/streamManagement'),
10
+ noticeConsts = require('./qbNoticeConsts');
10
11
 
11
12
  var unsupportedError = 'This function isn\'t supported outside of the browser (...yet)';
12
13
 
@@ -26,6 +27,508 @@ if (Utils.getEnv().browser) {
26
27
  XMPP = require('node-xmpp-client');
27
28
  }
28
29
 
30
+ // ============================================================================
31
+ // Notice Feature parsing helpers (CROS-1055, SDK 2.24.0)
32
+ // ----------------------------------------------------------------------------
33
+ // Module-private. Used by ChatProxy._onSystemMessageListener to detect/parse
34
+ // urn:xmpp:notice:0 stanzas. Pure functions — no instance state, easy to test
35
+ // indirectly via chatProxy._onSystemMessageListener(stanza) call paths.
36
+ //
37
+ // Server contract reference: QuickBlox Confluence "Notice Feature" page
38
+ // (https://quickblox.atlassian.net/wiki/spaces/CHAT/pages/4126081033) and
39
+ // Android tests in android-reference-2026-05-07/test/.
40
+ // ============================================================================
41
+
42
+ var NOTICE_KNOWN_MODULE_IDS = {};
43
+ NOTICE_KNOWN_MODULE_IDS[noticeConsts.NOTICE_MODULE_IDENTIFIER.UPDATED_MESSAGE] = true;
44
+ NOTICE_KNOWN_MODULE_IDS[noticeConsts.NOTICE_MODULE_IDENTIFIER.DELETED_MESSAGE] = true;
45
+ NOTICE_KNOWN_MODULE_IDS[noticeConsts.NOTICE_MODULE_IDENTIFIER.UPDATED_DIALOG] = true;
46
+ NOTICE_KNOWN_MODULE_IDS[noticeConsts.NOTICE_MODULE_IDENTIFIER.DELETED_DIALOG] = true;
47
+
48
+ /**
49
+ * Cross-env: get all direct child elements of `parent` whose tag name equals
50
+ * `name`. Works for browser DOM, xmldom, and ltx (node-xmpp-client).
51
+ *
52
+ * @param {Element|Object} parent
53
+ * @param {String} name
54
+ * @return {Array<Element|Object>}
55
+ */
56
+ function _getChildElements(parent, name) {
57
+ if (!parent) {
58
+ return [];
59
+ }
60
+ var out = [];
61
+
62
+ // Browser DOM / xmldom: walk childNodes filtering by nodeType and tagName.
63
+ if (parent.childNodes && parent.childNodes.length !== undefined && typeof parent.childNodes !== 'function') {
64
+ for (var i = 0; i < parent.childNodes.length; i++) {
65
+ var c = parent.childNodes[i];
66
+ if (!c || c.nodeType !== 1 /* ELEMENT_NODE */) {
67
+ continue;
68
+ }
69
+ var tag = c.tagName || c.nodeName || c.localName;
70
+ if (tag === name) {
71
+ out.push(c);
72
+ }
73
+ }
74
+ if (out.length > 0 || parent.childNodes.length > 0) {
75
+ // Used DOM path (even if no matches) — return what we have.
76
+ return out;
77
+ }
78
+ }
79
+
80
+ // ltx: `parent.children` is a mixed array of strings (text) and Element objects with `.name`.
81
+ if (parent.children && parent.children.length !== undefined) {
82
+ for (var j = 0; j < parent.children.length; j++) {
83
+ var ch = parent.children[j];
84
+ if (ch && typeof ch === 'object' && ch.name === name) {
85
+ out.push(ch);
86
+ }
87
+ }
88
+ }
89
+
90
+ return out;
91
+ }
92
+
93
+ /**
94
+ * Cross-env text reader for an Element node. In browser / xmldom uses
95
+ * `textContent`; in node-xmpp-client (ltx) uses `getText()` or descends
96
+ * into `.children` (mixed string/element children). Empty string treated
97
+ * as no text, returns ''. Returns '' if element has no text at all.
98
+ *
99
+ * This is needed because the existing `chatUtils.getElementText(parent, name)`
100
+ * resolves the child by name and reads its text, but we sometimes already have
101
+ * the element reference (from a previous lookup or namespace check) and need
102
+ * to read text from THAT element, not search again from a parent.
103
+ *
104
+ * @param {Element|Object} el
105
+ * @return {String}
106
+ */
107
+ function _readElementText(el) {
108
+ if (!el) {
109
+ return '';
110
+ }
111
+ // Browser DOM and xmldom both expose textContent.
112
+ if (typeof el.textContent === 'string') {
113
+ return el.textContent;
114
+ }
115
+ // ltx / node-xmpp-client Element: getText() returns the concatenated
116
+ // text of all string children.
117
+ if (typeof el.getText === 'function') {
118
+ return el.getText();
119
+ }
120
+ // Last resort: walk .children if present.
121
+ if (el.children && el.children.length !== undefined) {
122
+ var out = '';
123
+ for (var i = 0; i < el.children.length; i++) {
124
+ var c = el.children[i];
125
+ if (typeof c === 'string') {
126
+ out += c;
127
+ }
128
+ }
129
+ return out;
130
+ }
131
+ return '';
132
+ }
133
+
134
+ /**
135
+ * Returns the moduleIdentifier value if and only if its xmlns attribute equals
136
+ * urn:xmpp:notice:0. Matching only by text without namespace check is unsafe
137
+ * (existing SystemNotifications stanzas use the same element name).
138
+ * @param {Element} extraParams
139
+ * @return {String|null} known notice moduleIdentifier value or null.
140
+ */
141
+ function _getNoticeModuleIdentifier(extraParams) {
142
+ if (!extraParams) {
143
+ return null;
144
+ }
145
+
146
+ var moduleIdEl = chatUtils.getElement(extraParams, 'moduleIdentifier');
147
+ if (!moduleIdEl) {
148
+ return null;
149
+ }
150
+
151
+ // Namespace check: stanza is a Notice only when xmlns equals NOTICE_NAMESPACE.
152
+ var xmlns = chatUtils.getAttr(moduleIdEl, 'xmlns');
153
+ if (xmlns !== noticeConsts.NOTICE_NAMESPACE) {
154
+ return null;
155
+ }
156
+
157
+ var text = (_readElementText(moduleIdEl) || '').trim();
158
+ if (!NOTICE_KNOWN_MODULE_IDS[text]) {
159
+ return null;
160
+ }
161
+
162
+ return text;
163
+ }
164
+
165
+ /**
166
+ * Convert a CSV string of numeric ids ("10,11,13007") to Array<Number>.
167
+ * Empty or null input → empty array. Non-numeric items are dropped silently.
168
+ */
169
+ function _parseNumericIdsCsv(value) {
170
+ if (value === null || value === undefined || value === '') {
171
+ return [];
172
+ }
173
+ var parts = String(value).split(',');
174
+ var out = [];
175
+ for (var i = 0; i < parts.length; i++) {
176
+ var n = parseInt(parts[i], 10);
177
+ if (!isNaN(n)) {
178
+ out.push(n);
179
+ }
180
+ }
181
+ return out;
182
+ }
183
+
184
+ /**
185
+ * Parse <reactions><reaction>{name,count,<user_ids>...}...</reaction></reactions>
186
+ * aggregate snapshot used in NoticeUpdatedMessage text-update stanzas.
187
+ * Reference: Android QBReactionsPropertyParser.java.
188
+ *
189
+ * @param {Element} extraParams
190
+ * @return {Array<{name, count, user_ids: number[]}>|null} or null if absent.
191
+ */
192
+ function _parseAggregateReactions(extraParams) {
193
+ var reactionsEl = chatUtils.getElement(extraParams, 'reactions');
194
+ if (!reactionsEl) {
195
+ return null;
196
+ }
197
+
198
+ var out = [];
199
+ var reactionChildren = _getChildElements(reactionsEl, 'reaction');
200
+ for (var i = 0; i < reactionChildren.length; i++) {
201
+ var child = reactionChildren[i];
202
+
203
+ try {
204
+ var name = chatUtils.getElementText(child, 'name');
205
+ var countText = chatUtils.getElementText(child, 'count');
206
+ var count = parseInt(countText, 10);
207
+ if (isNaN(count)) {
208
+ count = 0;
209
+ }
210
+
211
+ var userIds = [];
212
+ var userIdsEl = chatUtils.getElement(child, 'user_ids');
213
+ if (userIdsEl) {
214
+ var uidEls = _getChildElements(userIdsEl, 'user_id');
215
+ for (var j = 0; j < uidEls.length; j++) {
216
+ var uid = parseInt((_readElementText(uidEls[j]) || '').trim(), 10);
217
+ if (!isNaN(uid)) {
218
+ userIds.push(uid);
219
+ }
220
+ }
221
+ }
222
+
223
+ out.push({ name: name, count: count, user_ids: userIds });
224
+ } catch (err) {
225
+ // Malformed reaction child — skip silently per AC#9 (no exceptions).
226
+ }
227
+ }
228
+
229
+ return out;
230
+ }
231
+
232
+ /**
233
+ * Parse singular <reaction>{name,user_id,action}</reaction> incremental
234
+ * add/remove event used in NoticeUpdatedMessage stanzas. Reference: Android
235
+ * QBReactionPropertyParser.java.
236
+ *
237
+ * @param {Element} extraParams
238
+ * @return {Object|null} singular reaction descriptor or null if absent.
239
+ */
240
+ function _parseSingularReaction(extraParams) {
241
+ var reactionEl = chatUtils.getElement(extraParams, 'reaction');
242
+ if (!reactionEl) {
243
+ return null;
244
+ }
245
+
246
+ try {
247
+ var name = chatUtils.getElementText(reactionEl, 'name');
248
+ var userIdText = chatUtils.getElementText(reactionEl, 'user_id');
249
+ var action = chatUtils.getElementText(reactionEl, 'action');
250
+
251
+ if (!action || (action !== 'add' && action !== 'remove')) {
252
+ return null;
253
+ }
254
+
255
+ var userId = parseInt(userIdText, 10);
256
+ if (isNaN(userId)) {
257
+ userId = userIdText;
258
+ }
259
+
260
+ return {
261
+ name: name,
262
+ userId: userId,
263
+ action: action
264
+ };
265
+ } catch (err) {
266
+ return null;
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Parse custom_data element. Two server formats supported:
272
+ * 1) Nested XML: <custom_data><class_name>...</class_name>...</custom_data>
273
+ * 2) JSON string: <custom_data>{"class_name":"...",...}</custom_data>
274
+ * Returns plain object or undefined if custom_data is absent.
275
+ * Reference: Android QBDialogCustomDataNoticeParser.java.
276
+ */
277
+ function _parseCustomData(extraParams) {
278
+ var customDataEl = chatUtils.getElement(extraParams, 'custom_data');
279
+ if (!customDataEl) {
280
+ return undefined;
281
+ }
282
+
283
+ var rawText = (_readElementText(customDataEl) || '').trim();
284
+ if (rawText && rawText.charAt(0) === '{') {
285
+ // JSON form.
286
+ try {
287
+ return JSON.parse(rawText);
288
+ } catch (err) {
289
+ // Fall through to nested-XML parsing.
290
+ }
291
+ }
292
+
293
+ // Nested XML: walk element children, build flat map of name → text.
294
+ // Cross-env: collect all element children regardless of name, then read
295
+ // their tag and text. _getChildElements expects a name filter, but here
296
+ // we want every child element, so do a one-off walk.
297
+ var out = {};
298
+ var hasChildEl = false;
299
+
300
+ // Browser DOM / xmldom path.
301
+ if (customDataEl.childNodes && customDataEl.childNodes.length !== undefined && typeof customDataEl.childNodes !== 'function') {
302
+ for (var i = 0; i < customDataEl.childNodes.length; i++) {
303
+ var child = customDataEl.childNodes[i];
304
+ if (!child || child.nodeType !== 1) {
305
+ continue;
306
+ }
307
+ hasChildEl = true;
308
+ var key = child.tagName || child.nodeName || child.localName;
309
+ out[key] = (_readElementText(child) || '').trim();
310
+ }
311
+ }
312
+
313
+ // ltx fallback: walk .children for Element-like objects.
314
+ if (!hasChildEl && customDataEl.children && customDataEl.children.length !== undefined) {
315
+ for (var k = 0; k < customDataEl.children.length; k++) {
316
+ var ch = customDataEl.children[k];
317
+ if (ch && typeof ch === 'object' && ch.name) {
318
+ hasChildEl = true;
319
+ out[ch.name] = (_readElementText(ch) || '').trim();
320
+ }
321
+ }
322
+ }
323
+
324
+ if (!hasChildEl) {
325
+ return undefined;
326
+ }
327
+ return out;
328
+ }
329
+
330
+ /**
331
+ * Parse a Notice headline stanza into {type, payload} ready for routing.
332
+ * Returns null if stanza is not a Notice (no urn:xmpp:notice:0 namespace, or
333
+ * unknown moduleIdentifier value).
334
+ *
335
+ * @param {Element} stanza - <message type="headline"> element from Strophe / xmpp-client.
336
+ * @return {{type: String, payload: Object}|null}
337
+ */
338
+ function _parseNoticeStanza(stanza) {
339
+ if (!stanza) {
340
+ return null;
341
+ }
342
+ var extraParams = chatUtils.getElement(stanza, 'extraParams');
343
+ if (!extraParams) {
344
+ return null;
345
+ }
346
+
347
+ var type = _getNoticeModuleIdentifier(extraParams);
348
+ if (!type) {
349
+ return null;
350
+ }
351
+
352
+ // Common flat fields used across most notice variants.
353
+ var dialogId = chatUtils.getElementText(extraParams, 'dialog_id');
354
+ var messageId = chatUtils.getElementText(extraParams, 'message_id');
355
+ var dateSentText = chatUtils.getElementText(extraParams, 'date_sent');
356
+ var dateSent = parseInt(dateSentText, 10);
357
+ if (isNaN(dateSent)) {
358
+ dateSent = undefined;
359
+ }
360
+
361
+ var payload;
362
+ var IDS = noticeConsts.NOTICE_MODULE_IDENTIFIER;
363
+
364
+ if (type === IDS.DELETED_MESSAGE) {
365
+ payload = {
366
+ dialogId: dialogId,
367
+ messageId: messageId,
368
+ dateSent: dateSent
369
+ };
370
+ } else if (type === IDS.DELETED_DIALOG) {
371
+ payload = {
372
+ dialogId: dialogId,
373
+ dateSent: dateSent
374
+ };
375
+ } else if (type === IDS.UPDATED_MESSAGE) {
376
+ // Two variants: singular <reaction> (incremental) vs aggregate <reactions> (text update with snapshot).
377
+ var singular = _parseSingularReaction(extraParams);
378
+ if (singular) {
379
+ payload = {
380
+ kind: 'reaction',
381
+ event: {
382
+ dialogId: dialogId,
383
+ messageId: messageId,
384
+ reactionName: singular.name,
385
+ userId: singular.userId,
386
+ action: singular.action,
387
+ dateSent: dateSent
388
+ }
389
+ };
390
+ } else {
391
+ // Build a partial QBChatMessage object from extraParams.
392
+ // We read the well-known fields explicitly (no dependency on parseExtraParams,
393
+ // which has env-specific branches and can throw on non-standard stanza shapes).
394
+ var msg = {};
395
+ msg._id = messageId;
396
+ msg.chat_dialog_id = dialogId;
397
+ if (dateSent !== undefined) {
398
+ msg.date_sent = dateSent;
399
+ }
400
+ var msgText = chatUtils.getElementText(extraParams, 'message');
401
+ if (msgText) {
402
+ msg.message = msgText;
403
+ }
404
+ var senderIdText = chatUtils.getElementText(extraParams, 'sender_id');
405
+ var senderId = parseInt(senderIdText, 10);
406
+ if (!isNaN(senderId)) { msg.sender_id = senderId; }
407
+ var recipientIdText = chatUtils.getElementText(extraParams, 'recipient_id');
408
+ var recipientId = parseInt(recipientIdText, 10);
409
+ if (!isNaN(recipientId)) { msg.recipient_id = recipientId; }
410
+ var readIds = _parseNumericIdsCsv(chatUtils.getElementText(extraParams, 'read_ids'));
411
+ if (readIds.length > 0) { msg.read_ids = readIds; }
412
+ var deliveredIds = _parseNumericIdsCsv(chatUtils.getElementText(extraParams, 'delivered_ids'));
413
+ if (deliveredIds.length > 0) { msg.delivered_ids = deliveredIds; }
414
+ // Aggregate reactions (if any).
415
+ var aggregate = _parseAggregateReactions(extraParams);
416
+ if (aggregate !== null && aggregate.length > 0) {
417
+ msg.reactions = aggregate;
418
+ }
419
+ payload = {
420
+ kind: 'message',
421
+ dialogId: dialogId,
422
+ message: msg
423
+ };
424
+ }
425
+ } else if (type === IDS.UPDATED_DIALOG) {
426
+ // Build a partial QBChatDialog object from extraParams (full server snapshot).
427
+ var dlg = {};
428
+ dlg._id = dialogId;
429
+ var name = chatUtils.getElementText(extraParams, 'name');
430
+ if (name !== undefined && name !== null && name !== '') {
431
+ dlg.name = name;
432
+ }
433
+ var photo = chatUtils.getElementText(extraParams, 'photo');
434
+ if (photo !== undefined && photo !== null && photo !== '') {
435
+ dlg.photo = photo;
436
+ }
437
+ var typeText = chatUtils.getElementText(extraParams, 'type');
438
+ var typeNum = parseInt(typeText, 10);
439
+ if (!isNaN(typeNum)) {
440
+ dlg.type = typeNum;
441
+ }
442
+ var occupants = _parseNumericIdsCsv(chatUtils.getElementText(extraParams, 'occupants_ids'));
443
+ if (occupants.length > 0) {
444
+ dlg.occupants_ids = occupants;
445
+ }
446
+ var admins = _parseNumericIdsCsv(chatUtils.getElementText(extraParams, 'admin_ids'));
447
+ if (admins.length > 0) {
448
+ dlg.admin_ids = admins;
449
+ }
450
+ var isJoinReqText = chatUtils.getElementText(extraParams, 'is_join_required');
451
+ if (isJoinReqText !== undefined && isJoinReqText !== null && isJoinReqText !== '') {
452
+ var isJoinReq = parseInt(isJoinReqText, 10);
453
+ dlg.is_join_required = isNaN(isJoinReq) ? isJoinReqText : isJoinReq;
454
+ }
455
+ var roomJid = chatUtils.getElementText(extraParams, 'xmpp_room_jid');
456
+ if (roomJid !== undefined && roomJid !== null && roomJid !== '') {
457
+ dlg.xmpp_room_jid = roomJid;
458
+ }
459
+ var customData = _parseCustomData(extraParams);
460
+ if (customData !== undefined) {
461
+ dlg.custom_data = customData;
462
+ }
463
+ // Last message fields.
464
+ var lmText = chatUtils.getElementText(extraParams, 'last_message');
465
+ if (lmText) { dlg.last_message = lmText; }
466
+ var lmId = chatUtils.getElementText(extraParams, 'last_message_id');
467
+ if (lmId) { dlg.last_message_id = lmId; }
468
+ var lmDsText = chatUtils.getElementText(extraParams, 'last_message_date_sent');
469
+ var lmDs = parseInt(lmDsText, 10);
470
+ if (!isNaN(lmDs)) { dlg.last_message_date_sent = lmDs; }
471
+ var lmUidText = chatUtils.getElementText(extraParams, 'last_message_user_id');
472
+ var lmUid = parseInt(lmUidText, 10);
473
+ if (!isNaN(lmUid)) { dlg.last_message_user_id = lmUid; }
474
+
475
+ payload = {
476
+ kind: 'dialog',
477
+ dialog: dlg
478
+ };
479
+ } else {
480
+ return null;
481
+ }
482
+
483
+ return { type: type, payload: payload };
484
+ }
485
+
486
+ /**
487
+ * Route a parsed Notice event to the matching listener-property on chatProxy.
488
+ * No-op if the listener is null/undefined. Listener exceptions are caught by
489
+ * Utils.safeCallbackCall (existing SDK convention).
490
+ *
491
+ * @param {ChatProxy} chatProxy
492
+ * @param {{type: String, payload: Object}} parsed
493
+ */
494
+ function _routeNoticeEvent(chatProxy, parsed) {
495
+ if (!chatProxy || !parsed) {
496
+ return;
497
+ }
498
+ var IDS = noticeConsts.NOTICE_MODULE_IDENTIFIER;
499
+ var type = parsed.type;
500
+ var p = parsed.payload || {};
501
+
502
+ if (type === IDS.DELETED_MESSAGE) {
503
+ if (typeof chatProxy.onMessageDeletedListener === 'function') {
504
+ Utils.safeCallbackCall(chatProxy.onMessageDeletedListener, p.dialogId, p.messageId, p.dateSent);
505
+ }
506
+ } else if (type === IDS.UPDATED_MESSAGE) {
507
+ if (p.kind === 'reaction') {
508
+ if (typeof chatProxy.onMessageReactionChangedListener === 'function') {
509
+ Utils.safeCallbackCall(chatProxy.onMessageReactionChangedListener, p.event);
510
+ }
511
+ } else if (p.kind === 'message') {
512
+ if (typeof chatProxy.onMessageUpdatedListener === 'function') {
513
+ Utils.safeCallbackCall(chatProxy.onMessageUpdatedListener, p.dialogId, p.message);
514
+ }
515
+ }
516
+ } else if (type === IDS.DELETED_DIALOG) {
517
+ if (typeof chatProxy.onDialogDeletedListener === 'function') {
518
+ Utils.safeCallbackCall(chatProxy.onDialogDeletedListener, p.dialogId, p.dateSent);
519
+ }
520
+ } else if (type === IDS.UPDATED_DIALOG) {
521
+ if (typeof chatProxy.onDialogUpdatedListener === 'function') {
522
+ Utils.safeCallbackCall(chatProxy.onDialogUpdatedListener, p.dialog);
523
+ }
524
+ }
525
+ }
526
+
527
+ // ============================================================================
528
+ // End of Notice Feature parsing helpers
529
+ // ============================================================================
530
+
531
+
29
532
 
30
533
  function ChatProxy(service) {
31
534
  var self = this;
@@ -102,6 +605,23 @@ function ChatProxy(service) {
102
605
  this._sessionHasExpired = false;
103
606
  this._pings = {};
104
607
 
608
+ /**
609
+ * Notice Feature local state (CROS-1055, SDK 2.24.0).
610
+ * Reflects the current XMPP session subscription only — reset to false on disconnect.
611
+ * Consumers must call enableNotices() again after reconnect to re-subscribe.
612
+ */
613
+ this._isNoticesEnabled = false;
614
+
615
+ /**
616
+ * Notice listener properties (CROS-1055). All initialized to null.
617
+ * Single-listener pattern matching existing onMessageListener / onSystemMessageListener style.
618
+ */
619
+ this.onMessageDeletedListener = null;
620
+ this.onMessageUpdatedListener = null;
621
+ this.onMessageReactionChangedListener = null;
622
+ this.onDialogDeletedListener = null;
623
+ this.onDialogUpdatedListener = null;
624
+
105
625
  // [QC-1550] XMPP connection is considered "verified" only after the first
106
626
  // successful pong response. Strophe emits Status.CONNECTED at the transport
107
627
  // layer (TCP/WebSocket handshake completed) before XMPP-level traffic is
@@ -683,9 +1203,23 @@ function ChatProxy(service) {
683
1203
  delay = chatUtils.getElement(stanza, 'delay'),
684
1204
  moduleIdentifier = chatUtils.getElementText(extraParams, 'moduleIdentifier'),
685
1205
  bodyContent = chatUtils.getElementText(stanza, 'body'),
686
- extraParamsParsed = chatUtils.parseExtraParams(extraParams),
1206
+ extraParamsParsed,
687
1207
  message;
688
1208
 
1209
+ // CROS-1055: parseExtraParams may throw on edge-case stanza shapes
1210
+ // (e.g. test fixtures using xmldom which lacks .children). Catch
1211
+ // defensively so a parsing failure doesn't kill the whole headline
1212
+ // handler — Notice routing must still run.
1213
+ try {
1214
+ extraParamsParsed = chatUtils.parseExtraParams(extraParams);
1215
+ } catch (parseErr) {
1216
+ extraParamsParsed = {};
1217
+ Utils.QBLog('[QBChat]', '[parseExtraParams] failed: ' + (parseErr && parseErr.message ? parseErr.message : parseErr));
1218
+ }
1219
+ if (!extraParamsParsed) {
1220
+ extraParamsParsed = {};
1221
+ }
1222
+
689
1223
  if (moduleIdentifier === 'SystemNotifications' && typeof self.onSystemMessageListener === 'function') {
690
1224
  message = {
691
1225
  id: messageId,
@@ -697,6 +1231,20 @@ function ChatProxy(service) {
697
1231
  Utils.safeCallbackCall(self.onSystemMessageListener, message);
698
1232
  } else if (self.webrtcSignalingProcessor && !delay && moduleIdentifier === 'WebRTCVideoChat') {
699
1233
  self.webrtcSignalingProcessor._onMessage(from, extraParams, delay, userId, extraParamsParsed.extension);
1234
+ } else {
1235
+ // Notice Feature (CROS-1055, SDK 2.24.0). Sibling branch — only triggers
1236
+ // when extraParams contains <moduleIdentifier xmlns="urn:xmpp:notice:0">.
1237
+ // Non-notice headline stanzas (e.g. legacy SystemNotifications without
1238
+ // matching xmlns) fall through to the parser which returns null.
1239
+ try {
1240
+ var noticeParsed = _parseNoticeStanza(stanza);
1241
+ if (noticeParsed) {
1242
+ _routeNoticeEvent(self, noticeParsed);
1243
+ }
1244
+ } catch (err) {
1245
+ // Per AC#9: never throw from headline handler.
1246
+ Utils.QBLog('[QBChat]', '[Notice] handler error: ' + (err && err.message ? err.message : err));
1247
+ }
700
1248
  }
701
1249
 
702
1250
  /**
@@ -1262,6 +1810,7 @@ ChatProxy.prototype = {
1262
1810
  self._isConnectionVerified = true;
1263
1811
  self._isReconnectListenerPending = false;
1264
1812
 
1813
+ // TODO(2.25.0): auto-enable Notice Feature here (see qbChat.js enableNotices JSDoc).
1265
1814
  self.roster.get(function (contacts) {
1266
1815
  xmppClient.send(presence);
1267
1816
 
@@ -1895,6 +2444,10 @@ ChatProxy.prototype = {
1895
2444
  this._isLogout = true;
1896
2445
  this.helpers.setUserCurrentJid('');
1897
2446
 
2447
+ // Notice Feature (CROS-1055): reset local subscription flag on disconnect.
2448
+ // Consumers must call enableNotices() again after a new connection.
2449
+ this._isNoticesEnabled = false;
2450
+
1898
2451
  if (Utils.getEnv().browser) {
1899
2452
  this.connection.flush();
1900
2453
  this.connection.disconnect('call QB.chat.disconnect');//artik should add reason = 'disconnect from SDK'
@@ -1908,6 +2461,132 @@ ChatProxy.prototype = {
1908
2461
  }
1909
2462
  },
1910
2463
 
2464
+ /**
2465
+ * Subscribe the current XMPP session to Notice Feature stanzas
2466
+ * (urn:xmpp:notice:0). After a successful IQ result the server starts
2467
+ * delivering NoticeUpdatedMessage / NoticeDeletedMessage /
2468
+ * NoticeUpdatedDialog / NoticeDeletedDialog headline stanzas.
2469
+ *
2470
+ * The local enabled flag is reset to false on chat disconnect — consumers
2471
+ * must call enableNotices() again after a reconnect (intentional divergence
2472
+ * from the Android SDK, which auto-restores the subscription).
2473
+ *
2474
+ * TODO(2.25.0): make auto-enable SDK-internal — fire on initial connect and
2475
+ * on relogin after session-expired, skip on Stream-Management reconnect.
2476
+ * Spec & three-state matrix:
2477
+ * __ai-work-log/sdk-js/tasks/ad-hoc-2026-05-05-notice-reaction-admin-role/
2478
+ * backlog-2.25.0-auto-enable-notices.md
2479
+ * jira-stories/_jira-src-06-auto-enable-notices.md
2480
+ *
2481
+ * Do not call enableNotices again before the previous callback fires —
2482
+ * concurrent enable calls are not specified.
2483
+ *
2484
+ * @memberof QB.chat
2485
+ * @param {enableNoticesCallback} callback - Called with (error, result) on IQ result.
2486
+ * @since 2.24.0
2487
+ */
2488
+ enableNotices: function (callback) {
2489
+ /**
2490
+ * Callback for QB.chat.enableNotices().
2491
+ * @callback enableNoticesCallback
2492
+ * @param {Object|null} error - Error object on failure, null on success.
2493
+ * @param {Object} [result] - IQ result element on success.
2494
+ */
2495
+ this._sendNoticeIQ('enable', callback);
2496
+ },
2497
+
2498
+ /**
2499
+ * Unsubscribe the current XMPP session from Notice Feature stanzas.
2500
+ * Sends a <disable xmlns="urn:xmpp:notice:0"/> IQ. On success the local
2501
+ * enabled flag becomes false; the server stops delivering notice stanzas.
2502
+ *
2503
+ * @memberof QB.chat
2504
+ * @param {disableNoticesCallback} callback - Called with (error, result) on IQ result.
2505
+ * @since 2.24.0
2506
+ */
2507
+ disableNotices: function (callback) {
2508
+ /**
2509
+ * Callback for QB.chat.disableNotices().
2510
+ * @callback disableNoticesCallback
2511
+ * @param {Object|null} error - Error object on failure, null on success.
2512
+ * @param {Object} [result] - IQ result element on success.
2513
+ */
2514
+ this._sendNoticeIQ('disable', callback);
2515
+ },
2516
+
2517
+ /**
2518
+ * Returns the local Notice Feature subscription flag.
2519
+ * @memberof QB.chat
2520
+ * @return {Boolean} true after a successful enableNotices(), false otherwise.
2521
+ * Reset to false on chat disconnect; not synchronized with the server.
2522
+ * @since 2.24.0
2523
+ */
2524
+ isNoticesEnabled: function () {
2525
+ return this._isNoticesEnabled === true;
2526
+ },
2527
+
2528
+ /**
2529
+ * @private
2530
+ * Internal IQ helper used by enableNotices/disableNotices. Implements the
2531
+ * dual-env IQ pattern (browser/Strophe vs Node/NativeScript stanza-builder)
2532
+ * used elsewhere in the SDK (see RosterProxy.get for the canonical reference).
2533
+ *
2534
+ * @param {String} action - 'enable' or 'disable'.
2535
+ * @param {Function} callback
2536
+ */
2537
+ _sendNoticeIQ: function (action, callback) {
2538
+ var self = this;
2539
+ var iqParams = {
2540
+ type: 'set',
2541
+ from: self.helpers.getUserCurrentJid(),
2542
+ id: chatUtils.getUniqueId('notice_' + action)
2543
+ };
2544
+ var builder = Utils.getEnv().browser ? $iq : XMPP.Stanza;
2545
+ var iq = chatUtils.createStanza(builder, iqParams, 'iq');
2546
+
2547
+ // Append <enable xmlns="urn:xmpp:notice:0"/> or <disable xmlns="..."/>
2548
+ iq.c(action, { xmlns: noticeConsts.NOTICE_NAMESPACE }).up();
2549
+
2550
+ function _onSuccess(stanza) {
2551
+ self._isNoticesEnabled = (action === 'enable');
2552
+ if (typeof callback === 'function') {
2553
+ Utils.safeCallbackCall(callback, null, stanza);
2554
+ }
2555
+ }
2556
+
2557
+ function _onError(stanza) {
2558
+ // Per AC#3/AC#4: on error keep the flag unchanged (do NOT flip to true on enable error).
2559
+ if (typeof callback === 'function') {
2560
+ var err;
2561
+ try {
2562
+ err = stanza ? chatUtils.getErrorFromXMLNode(stanza) : null;
2563
+ } catch (e) {
2564
+ err = null;
2565
+ }
2566
+ if (!err) {
2567
+ err = { type: 'cancel', message: 'Notice IQ error' };
2568
+ }
2569
+ Utils.safeCallbackCall(callback, err);
2570
+ }
2571
+ }
2572
+
2573
+ if (Utils.getEnv().browser) {
2574
+ self.connection.sendIQ(iq, _onSuccess, _onError);
2575
+ } else {
2576
+ // Node/NativeScript path: register callback in nodeStanzasCallbacks map.
2577
+ self.nodeStanzasCallbacks[iqParams.id] = function (stanza) {
2578
+ // The map handler receives the result/error stanza. Inspect type to dispatch.
2579
+ var iqType = chatUtils.getAttr(stanza, 'type');
2580
+ if (iqType === 'result') {
2581
+ _onSuccess(stanza);
2582
+ } else {
2583
+ _onError(stanza);
2584
+ }
2585
+ };
2586
+ self.Client.send(iq);
2587
+ }
2588
+ },
2589
+
1911
2590
  addListener: function (params, callback) {
1912
2591
  Utils.QBLog('[Deprecated!]', 'Avoid using it, this feature will be removed in future version.');
1913
2592