@ethora/chat-component 26.5.9 → 26.5.11

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.
@@ -13,6 +13,7 @@ export interface CallState {
13
13
  token: string | null;
14
14
  error: string | null;
15
15
  startedAt: number | null;
16
+ connectedAt: number | null;
16
17
  }
17
18
  export declare const startOutgoingCall: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
18
19
  roomJid: string;
@@ -9,6 +9,7 @@ interface RoomMessagesState {
9
9
  editAction?: EditAction;
10
10
  isLoading: boolean;
11
11
  usersSet: Record<string, RoomMember>;
12
+ presenceByRoom: Record<string, string[]>;
12
13
  reportRoom: {
13
14
  isOpen: boolean;
14
15
  };
@@ -67,7 +68,13 @@ export declare const addRoom: import('@reduxjs/toolkit').ActionCreatorWithOption
67
68
  chatJID?: string;
68
69
  }, "roomMessages/setRoomNoMessages">, setCurrentRoom: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
69
70
  roomJID: string | null;
70
- }, "roomMessages/setCurrentRoom">, setChatUiVisible: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<boolean, "roomMessages/setChatUiVisible">, setRoomRole: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
71
+ }, "roomMessages/setCurrentRoom">, setMemberOnline: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
72
+ roomJID: string;
73
+ xmppUsername: string;
74
+ }, "roomMessages/setMemberOnline">, setMemberOffline: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
75
+ roomJID: string;
76
+ xmppUsername: string;
77
+ }, "roomMessages/setMemberOffline">, setChatUiVisible: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<boolean, "roomMessages/setChatUiVisible">, setRoomRole: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
71
78
  chatJID: string;
72
79
  role: string;
73
80
  }, "roomMessages/setRoomRole">, setReactions: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<ReactionAction, "roomMessages/setReactions">, setLogoutState: import('@reduxjs/toolkit').ActionCreatorWithoutPayload<"roomMessages/setLogoutState">, setActiveMessage: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
@@ -27,6 +27,15 @@ export interface VideoCallsConfig {
27
27
  enabled: boolean;
28
28
  livekitUrl: string;
29
29
  allowedRoomTypes?: Array<'private'>;
30
+ /**
31
+ * Show the audio-only call button next to the video-call button. Off by
32
+ * default: it's a separate opt-in feature. An audio call uses the same call
33
+ * backend as video (no server changes needed); the client just starts the
34
+ * session with `kind: 'audio'` so no camera track is published, and the
35
+ * callee learns the kind from the direct `call-invite` signal. Requires
36
+ * `enabled` and a `livekitUrl` like video calls.
37
+ */
38
+ enableAudioCalls?: boolean;
30
39
  /** Start a video call with the camera already on. Default true. */
31
40
  startWithCameraOn?: boolean;
32
41
  /** Start a call with the microphone already on. Default true. */
@@ -352,6 +361,46 @@ export interface IConfig {
352
361
  iconPath?: string;
353
362
  badgePath?: string;
354
363
  softAsk?: boolean;
364
+ /**
365
+ * In-app banner that nudges the user to turn on browser notifications.
366
+ * Off by default. The banner renders one of two states automatically from
367
+ * the current `Notification.permission`:
368
+ * - "default" (never asked) → an "Enable notifications" card with a button
369
+ * that triggers the browser permission prompt;
370
+ * - "denied" (blocked) → a "Notifications are blocked" card explaining how
371
+ * to re-allow it from the browser's site settings.
372
+ * It hides itself entirely once permission is "granted", when the browser
373
+ * has no Notification API, or when `pushNotifications.enabled` is not true.
374
+ * Every label is overridable so it can be localised.
375
+ */
376
+ permissionBanner?: {
377
+ /** Master switch for the banner. Default false (opt-in). */
378
+ enabled?: boolean;
379
+ /** Show the prompt while permission is "default". Default true. */
380
+ showWhenDefault?: boolean;
381
+ /**
382
+ * Show the notice while permission is "denied". Default false: a blocked
383
+ * state can only be fixed by hand in the browser's site settings, so by
384
+ * default we don't nag. Set true to opt back into the reminder.
385
+ */
386
+ showWhenBlocked?: boolean;
387
+ /** Render a dismiss (×) control. Default true. Dismissal lasts the tab session. */
388
+ dismissible?: boolean;
389
+ /** Where the fixed banner is anchored. Default "bottom-right". */
390
+ position?: 'top-left' | 'top-right' | 'top-center' | 'bottom-left' | 'bottom-right' | 'bottom-center';
391
+ /** Title for the "enable" state. Default "Enable notifications". */
392
+ enableTitle?: string;
393
+ /** Description for the "enable" state. */
394
+ enableDescription?: string;
395
+ /** Button label for the "enable" state. Default "Enable notifications". */
396
+ enableButtonLabel?: string;
397
+ /** Title for the "blocked" state. Default "Notifications are blocked". */
398
+ blockedTitle?: string;
399
+ /** Description for the "blocked" state. */
400
+ blockedDescription?: string;
401
+ /** Inline style overrides merged onto the banner container. */
402
+ style?: React.CSSProperties;
403
+ };
355
404
  onClick?: (params: {
356
405
  roomJID?: string;
357
406
  messageId?: string;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const ETHORA_CHAT_COMPONENT_VERSION = "26.03.28";
1
+ export declare const ETHORA_CHAT_COMPONENT_VERSION = "26.5.11";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethora/chat-component",
3
- "version": "26.5.9",
3
+ "version": "26.5.11",
4
4
  "type": "module",
5
5
  "main": "dist/main.js",
6
6
  "types": "dist/main.d.ts",
@@ -184,6 +184,54 @@ function pickImage(raw = {}, data = {}, notification = {}) {
184
184
  return data.image || notification.image || raw.image || undefined;
185
185
  }
186
186
 
187
+ // ── CALL DETECTION ────────────────────────────────────────────
188
+ // Mirror of helpers/callPush.ts isCallPush(). A backgrounded call push must
189
+ // (a) read as a call (persistent, call-styled title) and (b) carry its FULL
190
+ // data payload through to the click handler — otherwise the app's isCallPush()
191
+ // sees only {url,messageId,...} and can't raise the ring screen.
192
+ function pickField(data, keys) {
193
+ for (let i = 0; i < keys.length; i += 1) {
194
+ const v = data && data[keys[i]];
195
+ if (v !== undefined && v !== null && String(v).trim() !== '') {
196
+ return String(v).trim();
197
+ }
198
+ }
199
+ return '';
200
+ }
201
+
202
+ function isCallPushData(data) {
203
+ if (!data) return false;
204
+ const marker = pickField(data, [
205
+ 'type',
206
+ 'pushType',
207
+ 'category',
208
+ 'messageType',
209
+ 'event',
210
+ ]).toLowerCase();
211
+ if (
212
+ marker === 'call' ||
213
+ marker === 'incoming-call' ||
214
+ marker === 'call-token' ||
215
+ marker === 'call-invite'
216
+ ) {
217
+ return true;
218
+ }
219
+ const callId = pickField(data, ['callId', 'callid', 'callID']);
220
+ const kind = pickField(data, ['kind']).toLowerCase();
221
+ const hasCallField =
222
+ !!pickField(data, ['callToken', 'livekitToken']) ||
223
+ !!pickField(data, ['callKind']) ||
224
+ kind === 'audio' ||
225
+ kind === 'video';
226
+ return !!callId && hasCallField;
227
+ }
228
+
229
+ function callPushKind(data) {
230
+ return pickField(data, ['callKind', 'kind']).toLowerCase() === 'audio'
231
+ ? 'audio'
232
+ : 'video';
233
+ }
234
+
187
235
  function extractMessageId(raw = {}, data = {}) {
188
236
  return (
189
237
  raw.messageId ||
@@ -356,14 +404,40 @@ async function handlePush(raw = {}, source = 'push') {
356
404
  dedupKey,
357
405
  });
358
406
 
407
+ const isCall = isCallPushData(data);
408
+ const callerName = isCall
409
+ ? pickField(data, [
410
+ 'callerName',
411
+ 'callerDisplayName',
412
+ 'senderName',
413
+ 'title',
414
+ ])
415
+ : '';
416
+ const callId = isCall
417
+ ? pickField(data, ['callId', 'callid', 'callID'])
418
+ : '';
419
+
420
+ // Call pushes: a call-styled title, a tag keyed on the callId so repeat
421
+ // call-token pushes replace (not stack), and requireInteraction so the
422
+ // ring notification persists until the user answers/dismisses.
423
+ const notifTitle = isCall
424
+ ? `Incoming ${callPushKind(data)} call${callerName ? ` · ${callerName}` : ''}`
425
+ : title;
426
+ const notifBody = isCall ? 'Tap to answer' : body;
427
+
359
428
  const options = {
360
- body,
429
+ body: notifBody,
361
430
  icon,
362
431
  badge,
363
432
  image,
364
- tag: dedupKey,
365
- renotify: false,
433
+ tag: isCall ? `call:${callId || dedupKey}` : dedupKey,
434
+ renotify: isCall,
435
+ requireInteraction: isCall,
436
+ // Spread the ORIGINAL payload first so the click handler's isCallPush()
437
+ // sees the full call fields (callId/callToken/kind/room); our routing
438
+ // fields win on top.
366
439
  data: {
440
+ ...data,
367
441
  url,
368
442
  messageId,
369
443
  dedupKey,
@@ -375,9 +449,10 @@ async function handlePush(raw = {}, source = 'push') {
375
449
  log('SHOW NOTIFICATION', {
376
450
  reason: 'NO_VISIBLE_CLIENT',
377
451
  tag: options.tag,
452
+ isCall,
378
453
  });
379
454
 
380
- return self.registration.showNotification(title, options);
455
+ return self.registration.showNotification(notifTitle, options);
381
456
  }
382
457
 
383
458
  log('SKIP NOTIFICATION', {