@ethora/chat-component 26.5.8 → 26.5.10
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.
- package/dist/assets/icons.d.ts +12 -0
- package/dist/components/VideoCalls/VideoCallOverlay.d.ts +2 -0
- package/dist/components/VideoCalls/VideoCallSession.d.ts +25 -0
- package/dist/firebase-messaging-sw.js +79 -4
- package/dist/helpers/callLogMessage.d.ts +11 -0
- package/dist/helpers/callPush.d.ts +22 -0
- package/dist/helpers/storeConsole.d.ts +1 -0
- package/dist/helpers/useDraggable.d.ts +18 -0
- package/dist/main.js +47563 -23462
- package/dist/main.js.map +1 -1
- package/dist/networking/api-requests/rooms.api.d.ts +3 -0
- package/dist/networking/callTokenStanza.d.ts +13 -0
- package/dist/roomStore/callSlice.d.ts +37 -0
- package/dist/roomStore/index.d.ts +4 -0
- package/dist/types/models/config.model.d.ts +41 -0
- package/dist/types/models/message.model.d.ts +7 -0
- package/dist/version.d.ts +1 -1
- package/package.json +4 -1
- package/public/firebase-messaging-sw.js +79 -4
|
@@ -10,3 +10,6 @@ export declare function postReportRoom(data: PostReportRoom): Promise<any>;
|
|
|
10
10
|
export declare function postAddRoomMember(data: PostAddRoomMember): Promise<RoomMember[]>;
|
|
11
11
|
export declare function deleteRoomMember(data: DeleteRoomMember): Promise<any>;
|
|
12
12
|
export declare function deleteRoom(name: string): Promise<any>;
|
|
13
|
+
export declare function createChatCall(chatName: string, options?: {
|
|
14
|
+
kind?: 'audio' | 'video';
|
|
15
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Element } from 'ltx';
|
|
2
|
+
export declare const onCallTokenMessage: (stanza: Element) => boolean;
|
|
3
|
+
export type CallSignalState = 'ended' | 'declined' | 'cancelled' | 'rejected';
|
|
4
|
+
export declare const sendCallStateSignal: (state: CallSignalState, options?: {
|
|
5
|
+
peerXmppUsername?: string | null;
|
|
6
|
+
callId?: string | null;
|
|
7
|
+
roomBareName?: string | null;
|
|
8
|
+
}) => void;
|
|
9
|
+
export declare const sendCallInviteSignal: (kind: "audio" | "video", options: {
|
|
10
|
+
peerXmppUsername: string;
|
|
11
|
+
roomBareName: string;
|
|
12
|
+
callId?: string | null;
|
|
13
|
+
}) => void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type CallPhase = 'idle' | 'requesting' | 'ringing-incoming' | 'connecting' | 'in-call' | 'error';
|
|
2
|
+
export type CallDirection = 'outgoing' | 'incoming' | null;
|
|
3
|
+
export type CallKind = 'audio' | 'video';
|
|
4
|
+
export interface CallState {
|
|
5
|
+
phase: CallPhase;
|
|
6
|
+
direction: CallDirection;
|
|
7
|
+
kind: CallKind;
|
|
8
|
+
roomJid: string | null;
|
|
9
|
+
roomName: string | null;
|
|
10
|
+
roomBareName: string | null;
|
|
11
|
+
callId: string | null;
|
|
12
|
+
peerXmppUsername: string | null;
|
|
13
|
+
token: string | null;
|
|
14
|
+
error: string | null;
|
|
15
|
+
startedAt: number | null;
|
|
16
|
+
}
|
|
17
|
+
export declare const startOutgoingCall: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
|
|
18
|
+
roomJid: string;
|
|
19
|
+
roomName?: string | null;
|
|
20
|
+
roomBareName?: string | null;
|
|
21
|
+
kind?: CallKind;
|
|
22
|
+
peerXmppUsername?: string | null;
|
|
23
|
+
}, "call/startOutgoingCall">, setIncomingCallToken: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
|
|
24
|
+
roomJid: string;
|
|
25
|
+
roomName?: string | null;
|
|
26
|
+
roomBareName?: string | null;
|
|
27
|
+
token: string;
|
|
28
|
+
kind?: CallKind;
|
|
29
|
+
callId?: string | null;
|
|
30
|
+
peerXmppUsername?: string | null;
|
|
31
|
+
}, "call/setIncomingCallToken">, setOutgoingCallToken: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
|
|
32
|
+
roomJid: string;
|
|
33
|
+
token: string;
|
|
34
|
+
callId?: string | null;
|
|
35
|
+
}, "call/setOutgoingCallToken">, acceptIncomingCall: import('@reduxjs/toolkit').ActionCreatorWithoutPayload<"call/acceptIncomingCall">, declineIncomingCall: import('@reduxjs/toolkit').ActionCreatorWithoutPayload<"call/declineIncomingCall">, endCall: import('@reduxjs/toolkit').ActionCreatorWithoutPayload<"call/endCall">, setCallError: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<string, "call/setCallError">, setCallPhase: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<CallPhase, "call/setCallPhase">, setCallKind: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<CallKind, "call/setCallKind">, resetCall: import('@reduxjs/toolkit').ActionCreatorWithoutPayload<"call/resetCall">;
|
|
36
|
+
declare const _default: import('redux').Reducer<import('immer').WritableDraft<CallState>>;
|
|
37
|
+
export default _default;
|
|
@@ -4,10 +4,12 @@ declare const rootReducer: Reducer<{
|
|
|
4
4
|
chatSettingStore: any;
|
|
5
5
|
rooms: any;
|
|
6
6
|
roomHeapSlice: import('./roomHeapSlice').roomHeapSliceState;
|
|
7
|
+
call: import('immer').WritableDraft<import('./callSlice').CallState>;
|
|
7
8
|
}, import('redux').UnknownAction, Partial<{
|
|
8
9
|
chatSettingStore: any;
|
|
9
10
|
rooms: any;
|
|
10
11
|
roomHeapSlice: import('./roomHeapSlice').roomHeapSliceState;
|
|
12
|
+
call: import('immer').WritableDraft<import('./callSlice').CallState>;
|
|
11
13
|
}>>;
|
|
12
14
|
export type RootState = ReturnType<typeof rootReducer>;
|
|
13
15
|
export declare const getActiveRoom: (state: RootState) => IRoom | null;
|
|
@@ -15,11 +17,13 @@ export declare const store: import('@reduxjs/toolkit').EnhancedStore<{
|
|
|
15
17
|
chatSettingStore: any;
|
|
16
18
|
rooms: any;
|
|
17
19
|
roomHeapSlice: import('./roomHeapSlice').roomHeapSliceState;
|
|
20
|
+
call: import('immer').WritableDraft<import('./callSlice').CallState>;
|
|
18
21
|
}, AnyAction, import('@reduxjs/toolkit').Tuple<[import('redux').StoreEnhancer<{
|
|
19
22
|
dispatch: import('redux-thunk').ThunkDispatch<{
|
|
20
23
|
chatSettingStore: any;
|
|
21
24
|
rooms: any;
|
|
22
25
|
roomHeapSlice: import('./roomHeapSlice').roomHeapSliceState;
|
|
26
|
+
call: import('immer').WritableDraft<import('./callSlice').CallState>;
|
|
23
27
|
}, undefined, import('redux').UnknownAction>;
|
|
24
28
|
}>, import('redux').StoreEnhancer]>>;
|
|
25
29
|
export type AppDispatch = typeof store.dispatch;
|
|
@@ -5,6 +5,46 @@ import { MessageBubble, MessageProps, IMessage } from './message.model';
|
|
|
5
5
|
import { Iso639_1Codes } from './language.model';
|
|
6
6
|
import { default as React } from 'react';
|
|
7
7
|
import { MessageNotificationToastProps } from '../../components/MessageNotification/MessageNotificationToast';
|
|
8
|
+
/**
|
|
9
|
+
* Override the icons used by the in-call control bar and the incoming-call
|
|
10
|
+
* ring screen. Each value is any React node (an <svg>, an <img>, an icon
|
|
11
|
+
* component element). When omitted, the chat's built-in icon is used. The
|
|
12
|
+
* mic/camera/screen-share toggles take separate on/off icons so the host can
|
|
13
|
+
* express the active vs muted state however they like.
|
|
14
|
+
*/
|
|
15
|
+
export interface VideoCallIcons {
|
|
16
|
+
micOn?: React.ReactNode;
|
|
17
|
+
micOff?: React.ReactNode;
|
|
18
|
+
cameraOn?: React.ReactNode;
|
|
19
|
+
cameraOff?: React.ReactNode;
|
|
20
|
+
screenShareOn?: React.ReactNode;
|
|
21
|
+
screenShareOff?: React.ReactNode;
|
|
22
|
+
hangup?: React.ReactNode;
|
|
23
|
+
accept?: React.ReactNode;
|
|
24
|
+
decline?: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
export interface VideoCallsConfig {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
livekitUrl: string;
|
|
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;
|
|
39
|
+
/** Start a video call with the camera already on. Default true. */
|
|
40
|
+
startWithCameraOn?: boolean;
|
|
41
|
+
/** Start a call with the microphone already on. Default true. */
|
|
42
|
+
startWithMicOn?: boolean;
|
|
43
|
+
/** Show the screen-share control. Default true. */
|
|
44
|
+
showScreenShare?: boolean;
|
|
45
|
+
/** Custom icons for the call control bar / ring screen. See VideoCallIcons. */
|
|
46
|
+
icons?: VideoCallIcons;
|
|
47
|
+
}
|
|
8
48
|
export interface FBConfig {
|
|
9
49
|
apiKey: string;
|
|
10
50
|
authDomain: string;
|
|
@@ -333,4 +373,5 @@ export interface IConfig {
|
|
|
333
373
|
source?: 'service_worker' | 'foreground';
|
|
334
374
|
}) => void | Promise<void>;
|
|
335
375
|
};
|
|
376
|
+
videoCalls?: VideoCallsConfig;
|
|
336
377
|
}
|
|
@@ -32,6 +32,13 @@ export interface IMessage {
|
|
|
32
32
|
size?: string;
|
|
33
33
|
xmppId?: string;
|
|
34
34
|
xmppFrom?: string;
|
|
35
|
+
callLog?: {
|
|
36
|
+
callId: string;
|
|
37
|
+
direction: 'outgoing' | 'incoming';
|
|
38
|
+
durationMs: number;
|
|
39
|
+
missed: boolean;
|
|
40
|
+
kind: 'audio' | 'video';
|
|
41
|
+
};
|
|
35
42
|
}
|
|
36
43
|
export interface ReactionMessage {
|
|
37
44
|
emoji: string[];
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const ETHORA_CHAT_COMPONENT_VERSION = "26.
|
|
1
|
+
export declare const ETHORA_CHAT_COMPONENT_VERSION = "26.03.28";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethora/chat-component",
|
|
3
|
-
"version": "26.5.
|
|
3
|
+
"version": "26.5.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"types": "dist/main.d.ts",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@emoji-mart/data": "^1.2.1",
|
|
31
31
|
"@emoji-mart/react": "^1.1.1",
|
|
32
|
+
"@livekit/components-react": "^2.9.20",
|
|
33
|
+
"@livekit/components-styles": "^1.2.0",
|
|
32
34
|
"@reduxjs/toolkit": "^2.8.2",
|
|
33
35
|
"@types/styled-components": "^5.1.34",
|
|
34
36
|
"@xmpp/client": "^0.13.6",
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
"emoji-mart": "^5.6.0",
|
|
37
39
|
"firebase": "^11.9.1",
|
|
38
40
|
"js-sha256": "^0.11.1",
|
|
41
|
+
"livekit-client": "^2.18.3",
|
|
39
42
|
"ltx": "^3.1.2",
|
|
40
43
|
"react": "^18.3.1",
|
|
41
44
|
"react-dom": "^18.3.1",
|
|
@@ -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:
|
|
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(
|
|
455
|
+
return self.registration.showNotification(notifTitle, options);
|
|
381
456
|
}
|
|
382
457
|
|
|
383
458
|
log('SKIP NOTIFICATION', {
|