@ethora/chat-component 26.10.6 → 26.10.8
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/README.md +102 -3
- package/dist/ChatProfileModal-83t5BRkI.js +2298 -0
- package/dist/{FilePreviewModal-DwNUEHei.js → FilePreviewModal-DxUGjSHe.js} +135 -135
- package/dist/{ManageDataModal-CaQ2VBPn.js → ManageDataModal-DFE_Wegj.js} +24 -24
- package/dist/{MarkdownBody-BeoG2cpo.js → MarkdownBody-uFITrPjT.js} +1 -1
- package/dist/{MessageSearchModal-DMw9o49-.js → MessageSearchModal-CsbTWgNm.js} +46 -46
- package/dist/{PublicChatsModal-CWdYILv_.js → PublicChatsModal-bDOp3IL9.js} +18 -18
- package/dist/{SideDrawer-Cl-vbuIG.js → SideDrawer-A_1ECqD2.js} +29 -29
- package/dist/UserProfileModal-Ca5wHYvr.js +639 -0
- package/dist/{UserSettingsModal-DgCAHmNh.js → UserSettingsModal-CAyyQGV6.js} +4 -4
- package/dist/{VideoCallOverlay-Cq2cECWj.js → VideoCallOverlay-BMiQh8kI.js} +6605 -4726
- package/dist/{VisibilityModal-D7vqGbrA.js → VisibilityModal-BXyIy7_J.js} +12 -12
- package/dist/assets/icons.d.ts +4 -0
- package/dist/calls/callDisplay.d.ts +22 -0
- package/dist/calls/callMode.d.ts +105 -0
- package/dist/calls/openScheduledCall.d.ts +20 -0
- package/dist/calls/p2p/P2PCallController.d.ts +160 -0
- package/dist/calls/p2p/icePolicy.d.ts +36 -0
- package/dist/calls/p2p/negotiator.d.ts +56 -0
- package/dist/calls/p2p/p2pRuntime.d.ts +35 -0
- package/dist/calls/p2p/signalInbox.d.ts +34 -0
- package/dist/calls/p2p/signalStanza.d.ts +51 -0
- package/dist/calls/placeCall.d.ts +20 -0
- package/dist/calls/scheduledCall.d.ts +101 -0
- package/dist/calls/scheduledCallActions.d.ts +12 -0
- package/dist/calls/scheduledCallFormat.d.ts +13 -0
- package/dist/calls/scheduledCallsConfig.d.ts +27 -0
- package/dist/calls/senderTrust.d.ts +7 -0
- package/dist/calls/startCall.d.ts +16 -0
- package/dist/components/MainComponents/ChatWrapper.d.ts +1 -0
- package/dist/components/MainComponents/LoginWrapper.d.ts +1 -0
- package/dist/components/MainComponents/ProfileImagePlaceholder.d.ts +1 -0
- package/dist/components/MainComponents/ReduxWrapper.d.ts +6 -0
- package/dist/components/MenuRoom/MenuRoom.d.ts +1 -0
- package/dist/components/Modals/NewChatModal/styledNewChatComponents.d.ts +27 -0
- package/dist/components/Modals/styledModalComponents.d.ts +25 -4
- package/dist/components/ScheduledCalls/ScheduleCallModal.d.ts +3 -0
- package/dist/components/ScheduledCalls/ScheduledCallCancelConfirm.d.ts +8 -0
- package/dist/components/ScheduledCalls/ScheduledCallCard.d.ts +7 -0
- package/dist/components/ScheduledCalls/ScheduledCallLayer.d.ts +15 -0
- package/dist/components/ScheduledCalls/ScheduledCallPreJoin.d.ts +3 -0
- package/dist/components/VideoCalls/P2PCallRunner.d.ts +1 -0
- package/dist/components/VideoCalls/P2PCallSession.d.ts +14 -0
- package/dist/components/VideoCalls/VideoCallSession.d.ts +39 -0
- package/dist/firebase-messaging-sw.js +918 -96
- package/dist/{handleCopyClick-Crn2jEEu.js → handleCopyClick-BX1fcFGN.js} +1 -1
- package/dist/helpers/callClaim.d.ts +17 -0
- package/dist/helpers/callOutcomeSync.d.ts +23 -0
- package/dist/helpers/callPush.d.ts +25 -0
- package/dist/helpers/canLeaveRoom.d.ts +3 -0
- package/dist/helpers/chatComponentConfigEvents.d.ts +16 -0
- package/dist/helpers/incomingCallPush.d.ts +89 -0
- package/dist/helpers/liveConfig.d.ts +46 -0
- package/dist/helpers/pushRoomHandoff.d.ts +13 -0
- package/dist/helpers/reportRoom.d.ts +18 -0
- package/dist/helpers/scheduledCallMessage.d.ts +23 -0
- package/dist/helpers/storeConsole.d.ts +1 -0
- package/dist/hooks/useChatSettingState.d.ts +1 -0
- package/dist/hooks/useNow.d.ts +6 -0
- package/dist/hooks/usePushNotifications.d.ts +2 -0
- package/dist/hooks/useRemoteAppearance.d.ts +21 -0
- package/dist/hooks/useTypography.d.ts +4 -0
- package/dist/{main-36gqdbJ8.js → main-B25iFKAC.js} +16623 -13399
- package/dist/main.d.ts +3 -1
- package/dist/main.js +25 -23
- package/dist/networking/api-requests/callState.api.d.ts +24 -0
- package/dist/networking/api-requests/remoteAppearance.api.d.ts +20 -0
- package/dist/networking/api-requests/rooms.api.d.ts +11 -1
- package/dist/networking/api-requests/scheduledCalls.api.d.ts +19 -0
- package/dist/networking/ethoraEvents.d.ts +14 -1
- package/dist/networking/xmpp/notifyXmppEvent.d.ts +6 -0
- package/dist/networking/xmpp/sendMessageReaction.xmpp.d.ts +13 -1
- package/dist/networking/xmppClient.d.ts +2 -1
- package/dist/roomStore/Middleware/blockedHydrationMiddleware.d.ts +2 -0
- package/dist/roomStore/callSlice.d.ts +23 -1
- package/dist/roomStore/chatSettingsSlice.d.ts +11 -2
- package/dist/roomStore/index.d.ts +4 -0
- package/dist/roomStore/scheduledCallSlice.d.ts +47 -0
- package/dist/types/models/config.model.d.ts +78 -1
- package/dist/types/models/message.model.d.ts +2 -0
- package/dist/types/models/room.model.d.ts +2 -0
- package/dist/types/models/scheduledCall.model.d.ts +116 -0
- package/dist/types/models/xmpp.model.d.ts +2 -1
- package/dist/utils/callAuthMirror.d.ts +1 -0
- package/dist/utils/callBroadcast.d.ts +12 -0
- package/dist/utils/callNotifications.d.ts +67 -0
- package/dist/utils/firebasePushNotifications.d.ts +14 -0
- package/dist/utils/handledCalls.d.ts +5 -0
- package/dist/utils/pushServiceWorkerBridge.d.ts +19 -0
- package/dist/utils/webLock.d.ts +13 -0
- package/package.json +1 -1
- package/public/firebase-messaging-sw.js +918 -96
- package/dist/ChatProfileModal-OJCKwdNp.js +0 -2168
- package/dist/UserProfileModal-B8KtGnwZ.js +0 -594
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ Use it as a standalone chat page, as an embedded widget in your existing app, or
|
|
|
20
20
|
- [Chat Props Reference](#chat-props-reference)
|
|
21
21
|
- [Full Config Reference (`IConfig`)](#full-config-reference-iconfig)
|
|
22
22
|
- [Custom Widgets and Overrides](#custom-widgets-and-overrides)
|
|
23
|
+
- [Scheduled Calls](#scheduled-calls)
|
|
23
24
|
- [Push Notifications](#push-notifications)
|
|
24
25
|
- [Auth Strategies](#auth-strategies)
|
|
25
26
|
- [Hooks and API Exports](#hooks-and-api-exports)
|
|
@@ -325,6 +326,7 @@ These are the top-level props accepted by `Chat` (exported from `ReduxWrapper`).
|
|
|
325
326
|
| --- | --- | --- | --- |
|
|
326
327
|
| `config` | `IConfig` | No | Main behavior/configuration object. |
|
|
327
328
|
| `roomJID` | `string` | No | Force specific room JID on load. |
|
|
329
|
+
| `scheduledCallSlug` | `string` | No | Open a scheduled call: once signed in, selects its room and shows the pre-join panel. The slug is the last part of a `/call/<slug>` join link. See [Scheduled Calls](#scheduled-calls). |
|
|
328
330
|
| `user` | `{ email: string; password: string }` | No | Credentials for email/password login helper path. |
|
|
329
331
|
| `loginData` | `{ email: string; password: string }` | No | Optional login payload. |
|
|
330
332
|
| `MainComponentStyles` | `React.CSSProperties` | No | Outer container style override. |
|
|
@@ -351,7 +353,8 @@ Below is a grouped reference for all `config` options.
|
|
|
351
353
|
| `newArch` | `boolean` | Defaults to `true`; set `false` to explicitly force legacy/old architecture paths. |
|
|
352
354
|
| `useStoreConsoleEnabled` | `boolean` | Enable verbose internal logging in console. |
|
|
353
355
|
| `userLookupRoute` | `'auto' \| 'v1' \| 'v2'` | Route used to look up one sender that is not in the room member list. Default `'auto'`: `GET /v1/apps/users/<xmppUsername>` first; if the backend refuses the user token there (400/401, missing route) the chat uses `GET /v2/chats/users?xmppUsername=` for 10 minutes, then tries v1 again. A 403 from v1 is treated as a per-user "not allowed" and shows the name carried on the message, or "Unknown user". `'v1'` / `'v2'` pin one route. |
|
|
354
|
-
| `trustedEventSenders` | `string[]` | Who may push server `ethora-event` headlines (see Server events). Default (unset): any bare JID without a resource on the account's own XMPP domain, which is how the server sends them. Set it to pin the exact sender(s) as full bare JIDs (`admin@xmpp.example.com`) or local parts (`admin`). |
|
|
356
|
+
| `trustedEventSenders` | `string[]` | Who may push server `ethora-event` headlines (see Server events). Default (unset): any bare JID without a resource on the account's own XMPP domain, which is how the server sends them. Set it to pin the exact sender(s) as full bare JIDs (`admin@xmpp.example.com`) or local parts (`admin`). The `chat-component-config-updated` event comes from the bare server domain (see [Server events](#server-events-urnethoraevents1)), so a pinned list must name the domain (`xmpp.example.com`) for it to be heard. |
|
|
357
|
+
| `remoteAppearance` | `{ appId: string; baseUrl?: string; initialWaitMs?: number }` | Read the App's look from the server and keep it current: the colours, fonts, layout switches and AI display options saved in the admin's Chat component tab. Opt in by naming the App. Read at start (the first paint waits for it, at most `initialWaitMs`, default 2500) and again on every `chat-component-config-updated` server event, applied to the open chat without reconnecting. Only the keys in [Applying config live](#applying-config-live-and-remoteappearance) are accepted from the server. Off by default. |
|
|
355
358
|
| `historyPreload` | `{ mode?: 'staged' \| 'all' \| 'off'; topRooms?: number; concurrency?: number }` | Background message-history preload after connect, for accounts with many rooms. Default `{ mode: 'staged', topRooms: 8, concurrency: 3 }`. `staged`: the `topRooms` most recently active rooms get a full page loaded, `concurrency` at a time, and rooms the backend gave no `lastMessage` for get a one-message preview (up to 4x `topRooms` of them); every other room loads the moment it is opened. A room the user opens jumps the queue, and a reconnect only re-queues rooms that are not already preloaded. `all`: preload every room (the old behaviour; fine for a handful of rooms). `off`: no background preload at all. The room list's last message, time and unread badge come from `GET /v1/chats/my` (`lastMessage`, `unreadCount`) when the backend sends them, so they render without waiting for any history; a backend without those fields falls back to the history preload. The older `historyQoS.preloadTopKRooms` / `stagedPreloadConcurrency` are still honoured when the matching field here is not set; pinning `historyQoS.stagedPreloadEnabled: false` keeps the legacy startup catch-up path. |
|
|
356
359
|
| `historyQoS.joinHistoryStanzas` | `number` | Messages the MUC service replays on every room join (sent as `<history maxstanzas="N"/>`). Default `0`: history comes from the message archive only, so connecting an account with many rooms no longer pulls the server default (up to 20 messages) per room. Raise it only if a room of yours has no archiving and relies on the join replay. |
|
|
357
360
|
| `historyQoS.joinConcurrency` | `number` | Rooms joined in parallel by the background join sweep. The room list is shown as soon as `/chats/my` returns; every room is then joined in the background (the open room first, then most recent activity; a room you open while it is queued jumps the queue). Default `5`. |
|
|
@@ -420,7 +423,7 @@ Below is a grouped reference for all `config` options.
|
|
|
420
423
|
| `disableSentLogic` | `boolean` | Disable default sent-state logic when needed. |
|
|
421
424
|
| `secondarySendButton` | `{ enabled: boolean; messageEdit: string; label?: React.ReactNode; buttonStyles?: React.CSSProperties; hideInputSendButton?: boolean; overwriteEnterClick?: true }` | Extra send action/button config. |
|
|
422
425
|
| `messageTextFilter` | `{ enabled: boolean; filterFunction: (text: string) => string }` | Transform/filter outgoing message text. |
|
|
423
|
-
| `eventHandlers` | `{ onMessageSent?; onMessageFailed?; onMessageEdited?; onError? }` | Lifecycle callbacks for message operations
|
|
426
|
+
| `eventHandlers` | `{ onXmppEvent?; onMessageSent?; onMessageFailed?; onMessageEdited?; onError?; onChatComponentConfigUpdated? }` | Lifecycle callbacks for incoming XMPP messages and message operations. `onXmppEvent` receives every incoming `<message/>` stanza as raw XML plus its `from`, `to`, `id`, and `type`, including archive and protocol messages. On end-to-end encrypted messages the XML may still be encrypted. `onError` reports uncaught render errors caught by the SDK's error boundary, see [Error handling](#error-handling-and-crash-containment); `onChatComponentConfigUpdated({ appId, version })` runs when the server says the App's chat component config changed, see [Applying config live](#applying-config-live-and-remoteappearance). |
|
|
424
427
|
| `translates` | `{ enabled: boolean; translations?: Iso639_1Codes; mode?: 'auto' \| 'manual'; forceType?: boolean; readerLocale?: string; showLanguageSelector?: boolean; showLanguageList?: boolean; onTranslate?: (text, ctx) => Promise<string>; showTranslateForMessage?: (message) => boolean; endpoint?: string }` | Message translation-related options. In manual mode, an on-demand translate click shows exactly what `mode: 'auto'` would have shown automatically - the same `message.translations` lookup, no network call - unless `onTranslate` is provided, in which case it wins and may do its own HTTP. The built-in translate-service fetch is strictly opt-in: it only runs when the host sets `endpoint` explicitly (there is no default derived from `config.baseUrl`), and the result is cached onto the message so a second click, or switching to 'auto', sees it for free. The call is made directly from the browser, so `endpoint` must respond with `Access-Control-Allow-Origin` for that origin (a self-hosted translate service needs its `CORS_ORIGIN` env var set) or the fetch is blocked with no error shown beyond the retry link; if you can't set that on the endpoint, use `onTranslate` and proxy the call through your own backend instead. When there is genuinely nothing to reveal (no attached translation, no `onTranslate`, no `endpoint`), no "Translate" link is shown at all. |
|
|
425
428
|
| `whitelistSystemMessage` | `string[]` | Restrict/render only selected system message types. |
|
|
426
429
|
| `customSystemMessage` | `React.ComponentType<MessageProps>` | Replace system message component renderer. |
|
|
@@ -450,6 +453,7 @@ Below is a grouped reference for all `config` options.
|
|
|
450
453
|
| `pushNotifications.serviceWorkerScope` | `string` | Service worker scope, default `/`. |
|
|
451
454
|
| `pushNotifications.iconPath` | `string` | Custom icon URL/path for OS push notifications. |
|
|
452
455
|
| `pushNotifications.badgePath` | `string` | Custom badge URL/path for OS push notifications. Falls back to `iconPath`. |
|
|
456
|
+
| `pushNotifications.clickPath` | `string` | Same-origin path a push tap opens when no window is open yet (for example `/app/chat`). The tapped room and message are appended as `fromPush=1&chatId=...&messageId=...` (ids only). Defaults to the origin root. |
|
|
453
457
|
| `pushNotifications.softAsk` | `boolean` | Do not immediately trigger browser permission prompt. |
|
|
454
458
|
| `pushNotifications.onClick` | `(params) => void \| Promise<void>` | Callback invoked when the user clicks an OS push notification (including cold start via URL marker). |
|
|
455
459
|
|
|
@@ -488,6 +492,7 @@ with React's component stack, regardless of `useStoreConsoleEnabled`.
|
|
|
488
492
|
|
|
489
493
|
| Option | Type | Description |
|
|
490
494
|
| --- | --- | --- |
|
|
495
|
+
| `eventHandlers.onXmppEvent` | `(event: XmppMessageEvent) => void \| Promise<void>` | Called for every incoming XMPP `<message/>` before chat/protocol/MAM routing. The event contains the raw XML `stanza` and optional `from`, `to`, `id`, and `type`. Encrypted message XML may still be encrypted. Callback errors are caught and logged. |
|
|
491
496
|
| `fallbackScreens.error` | `React.ReactNode \| ((ctx) => React.ReactNode)` | Replaces the built-in crash screen. A plain string renders as centered text, a React node renders as-is, a function receives `{ error, componentStack, scope, reset }`. |
|
|
492
497
|
| `eventHandlers.onError` | `(event: ChatErrorInfo) => void` | Called with `{ error, componentStack, scope }` when the boundary catches a crash. Wire it to your crash reporter. A throw inside your handler is caught and logged, never rethrown. |
|
|
493
498
|
|
|
@@ -534,8 +539,37 @@ The server tells a user that something they display changed by sending a `type="
|
|
|
534
539
|
| --- | --- | --- |
|
|
535
540
|
| `user-profile-updated` | `xmppUsername` (`<appId>_<userId>`), `uuid`, `appId`, `ts` | Re-fetches that user, bypassing the cache and any "not found" memory, through the same lookup route as unknown senders (`userLookupRoute`). Existing bubbles are re-named and re-pictured. If it is the signed-in user, their own profile data is updated too. Bursts within 500 ms collapse into one request. |
|
|
536
541
|
| `chat-meta-updated` | `chatName` (`<appId>_<roomId>`), `appId`, `ts` | Re-fetches `GET /v1/chats/my/<chatName>` and applies only title, description, picture, type and user count to the room. Members, messages, last message and unread counters are never replaced, and the user count is never lowered. Events for rooms the chat does not hold are ignored. Debounced per room (500 ms), one request in flight, backoff after errors. |
|
|
542
|
+
| `chat-component-config-updated` | `appId`, `version`, `ts` | The admin saved a change in the Chat component tab. Calls `config.eventHandlers.onChatComponentConfigUpdated({ appId, version })` and, when `config.remoteAppearance` is set, reads the config again and applies it live. Ignored unless `appId` is this session's own App (`config.appId` or `remoteAppearance.appId`). Once per `version`, a few hundred ms after the last of a burst. No values travel in the event. |
|
|
537
543
|
|
|
538
|
-
Unknown event types and malformed attributes are ignored. Trust rule: the stanza must come from a bare JID (no resource) on the session's own XMPP domain, so room occupants, other users, the `conference.` domain and other servers are ignored; `trustedEventSenders` narrows this to named senders. `xmppUsername` and `chatName` are validated (charset, length, `<appId>_` prefix) before use. The older `<user-update>` / `<chat-update>` headlines (namespace `your:custom:ns`) are still handled as before.
|
|
544
|
+
Unknown event types and malformed attributes are ignored. Trust rule: the stanza must come from a bare JID (no resource) on the session's own XMPP domain, so room occupants, other users, the `conference.` domain and other servers are ignored; `trustedEventSenders` narrows this to named senders. `chat-component-config-updated` is the one event that is also accepted from the bare server domain itself (`xmpp.example.com`, no user part): the server delivers it to every online session of the host in a single announcement (ejabberd `announce/online`), so it also arrives for other Apps on the host and is dropped unless it names this session's App. A client cannot send as the server domain. `xmppUsername` and `chatName` are validated (charset, length, `<appId>_` prefix) before use. The older `<user-update>` / `<chat-update>` headlines (namespace `your:custom:ns`) are still handled as before.
|
|
545
|
+
|
|
546
|
+
### Applying config live, and `remoteAppearance`
|
|
547
|
+
|
|
548
|
+
Changing the `config` prop of a mounted `<Chat>` restyles it in place. Nothing reconnects, no room or message is reloaded, no draft is lost, and the signed-in session is left alone: the new config replaces the one in the store, the components that read it re-render, and the CSS variables, fonts and icon tint are re-published (a font or colour you remove goes back to the default). The connection and login are keyed on identity fields, not on the config object, so a host that rebuilds `config` on every render is already safe.
|
|
549
|
+
|
|
550
|
+
| Applied live (change them any time) | Read when the chat starts (a new value is used by the next mount) |
|
|
551
|
+
| --- | --- |
|
|
552
|
+
| `colorScheme`; `colors.*` (`primary`, `secondary`, `icons`, `iconsBg`, `ownMessageBackground`, `otherMessageBackground`, `inputBackground`); `typography.*`; `backgroundChat.*` | `appId`, `baseUrl`, `customAppToken`, `xmppSettings.*` (these also pick the cache scope, a different one clears the local cache) |
|
|
553
|
+
| `disableHeader`, `disableMedia`, `disableRooms`, `disableRoomMenu`, `disableNewChatButton`, `disableUserCount`, `disableTypingIndicator`, `disableProfilesInteractions`, `chatHeaderBurgerMenu`, `chatHeaderSettings.hideSearch` | `userLogin`, `jwtLogin`, `customLogin`, `refreshTokens`, `initBeforeLoad`, `initBeforeLoadAuth` |
|
|
554
|
+
| `blockMessageSendingWhenProcessing`, `customTypingIndicator.enabled` / `.text` / `.position`, `translates.enabled`, `secondarySendButton.enabled` | `newArch`, `historyQoS`, `historyPreload`, `disableLastRead`, `receipts`, `e2ee`, `videoCalls`, `pushNotifications` |
|
|
555
|
+
|
|
556
|
+
Anything not in the left column is not applied from the server by `remoteAppearance` either.
|
|
557
|
+
|
|
558
|
+
**`remoteAppearance`.** The Ethora admin saves these options per App (`PUT /v2/apps/<appId>/chat-component/config`), and the server tells every online chat of that App with the `chat-component-config-updated` event. Opt in and the chat does the rest:
|
|
559
|
+
|
|
560
|
+
```tsx
|
|
561
|
+
<Chat config={{ appId, baseUrl, remoteAppearance: { appId } /* baseUrl defaults to config.baseUrl */ }} />
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
It reads `GET /v2/chat-component/config?appId=` (public, no token) at start and holds the first paint for it, at most `initialWaitMs` (2500), so the chat starts once with its final look; the saved values win over the same keys in your `config`. On every event it reads again with `&v=<version>` (which gets past the endpoint's 30 s cache) and applies the answer live. A failed read keeps the look you have. Only the left-column keys are taken from the answer, each checked (colours, font names and sizes, a few enums and booleans), so a wrong answer cannot do more than restyle. The record is kept in the store next to `config`, so a second writer of `config` (for example `<XmppProvider config>`) does not undo it.
|
|
565
|
+
|
|
566
|
+
Prefer to fetch yourself? Leave `remoteAppearance` out, read the endpoint in the callback and pass the merged result as `config`:
|
|
567
|
+
|
|
568
|
+
```tsx
|
|
569
|
+
<Chat config={{ ...base, ...saved, eventHandlers: { onChatComponentConfigUpdated: ({ appId, version }) => reload(appId, version) } }} />
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Delivery is best effort, like any push: a client that was offline misses the event and picks the change up at its next start.
|
|
539
573
|
|
|
540
574
|
## Custom Widgets and Overrides
|
|
541
575
|
|
|
@@ -583,6 +617,56 @@ Reference example components in repository:
|
|
|
583
617
|
- `src/examples/customComponents/CustomDaySeparator.tsx`
|
|
584
618
|
- `src/examples/customComponents/CustomNewMessageLabel.tsx`
|
|
585
619
|
|
|
620
|
+
## Scheduled Calls
|
|
621
|
+
|
|
622
|
+
Scheduled calls let a member book a call in a chat. Everyone in the room gets an invite card with "Google Calendar", "Outlook" and "Download .ics" links, and a Join button that opens 15 minutes before the start. Booking only decides when: the call itself runs on the existing call path (1:1 rooms over P2P WebRTC, group rooms over LiveKit), and no camera or microphone is touched until the user taps Join on the pre-join panel. Calendar delivery is `.ics` and template links only; there is no calendar OAuth.
|
|
623
|
+
|
|
624
|
+
It needs a backend that serves the scheduled-call endpoints (`/v1/chats/calls/scheduled`) and is on by default whenever `config.videoCalls.enabled` is on:
|
|
625
|
+
|
|
626
|
+
```tsx
|
|
627
|
+
<Chat
|
|
628
|
+
config={{
|
|
629
|
+
videoCalls: { enabled: true, livekitUrl: 'wss://livekit.example.com', enableAudioCalls: true },
|
|
630
|
+
// scheduledCalls is optional; these are the defaults.
|
|
631
|
+
scheduledCalls: { enabled: true },
|
|
632
|
+
}}
|
|
633
|
+
/>
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
What the user sees:
|
|
637
|
+
|
|
638
|
+
- A "Schedule call" button next to the call buttons in the chat header (same room types and gating as the call buttons). The form takes a title, date and time in the local time zone, a duration (15 minutes to 2 hours, or the call's own when editing) and video or audio (audio only when `videoCalls.enableAudioCalls` is on).
|
|
639
|
+
- An invite card in the room for each scheduled call. An edit or a cancel from the backend replaces the card (one card per call, the newest status wins): upcoming, open to join, ended (greyed) or cancelled (struck through). The person who scheduled the call (the message names them in `scheduledCallCreatedBy`, so no request is needed) and the room owner also get Edit and Cancel.
|
|
640
|
+
- Join opens the pre-join panel (title, time, duration, room, calendar links) and a "Join call" button, enabled from 15 minutes before the start until the call ends.
|
|
641
|
+
|
|
642
|
+
Clients that do not know the message type show its plain-text body, so older apps stay readable.
|
|
643
|
+
|
|
644
|
+
### Opening a call from a link
|
|
645
|
+
|
|
646
|
+
A join link looks like `https://<web app host>/call/<slug>`. Pass the slug to `<Chat>` and, once the user is signed in and the room list has loaded, the chat selects that room and shows the pre-join panel. Nothing starts until the user taps Join.
|
|
647
|
+
|
|
648
|
+
```tsx
|
|
649
|
+
import { Chat, parseScheduledCallLink, openScheduledCall } from '@ethora/chat-component';
|
|
650
|
+
|
|
651
|
+
// Declarative: the route /call/:slug renders the chat with the slug.
|
|
652
|
+
<Chat config={config} scheduledCallSlug={slug} />
|
|
653
|
+
|
|
654
|
+
// Imperative: from a deep-link handler, any time after import.
|
|
655
|
+
const slug = parseScheduledCallLink(url); // null when `url` is not a join link
|
|
656
|
+
if (slug) openScheduledCall(slug);
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
`parseScheduledCallLink(url, options?)` accepts `https://<host>/call/<slug>` and `<scheme>://call/<slug>` (never plain `http`), on `app.chat.ethora.com`, the page's own host and `options.hosts` (or, when omitted, `config.scheduledCalls.hosts`); schemes follow `options.schemes` or the configured ones. A request made before sign-in or before `<Chat>` is mounted is held and opened once both are true. A slug that cannot be opened (unknown, not a member, cancelled, offline) shows a toast and calls `config.scheduledCalls.onOpenError({ slug, reason })` with `reason` one of `'not-found' | 'not-a-member' | 'cancelled' | 'network'`. A host that wants its own error screens can call `GET /v1/chats/calls/scheduled/:slug` itself first (as `ethora-app-reactjs` does) and only mount the chat for a call that can be opened.
|
|
660
|
+
|
|
661
|
+
| Option | Type | Description |
|
|
662
|
+
| --- | --- | --- |
|
|
663
|
+
| `scheduledCalls.enabled` | `boolean` | Default `true` when `videoCalls.enabled` is on. `false` hides the "Schedule call" button and the Join button on cards; cards still render with their calendar links. |
|
|
664
|
+
| `scheduledCalls.hosts` | `string[]` | Extra hosts whose `https://<host>/call/<slug>` links `parseScheduledCallLink` accepts, on top of `app.chat.ethora.com` and the page's own host. Same option and defaults as the React Native SDK. |
|
|
665
|
+
| `scheduledCalls.schemes` | `string[]` | App URL schemes (without `://`) accepted for `<scheme>://call/<slug>`. Unset accepts any scheme. |
|
|
666
|
+
| `scheduledCalls.onOpenError` | `(e: { slug: string; reason: ScheduledCallOpenError }) => void` | Called when `scheduledCallSlug` / `openScheduledCall` could not open a call. |
|
|
667
|
+
|
|
668
|
+
Notes: the invite message is sent by the backend in the clear, also in `e2ee` rooms, like other system messages. Calendar links on a card are accepted only when they are plausible (https calendar provider hosts, and an `.ics` link on the API's own origin), because any member could forge the message attributes.
|
|
669
|
+
|
|
586
670
|
## Push Notifications
|
|
587
671
|
|
|
588
672
|
### Prerequisites
|
|
@@ -662,6 +746,19 @@ The badge has no per-push override: it is `pushNotifications.badgePath`, then th
|
|
|
662
746
|
|
|
663
747
|
Clicking a push opens `<origin>/chat?chatId=<room>&messageId=<message>` (or the `url` carried by the push) and calls `pushNotifications.onClick`; see the deep link section above.
|
|
664
748
|
|
|
749
|
+
#### Incoming call pushes (web)
|
|
750
|
+
|
|
751
|
+
With `videoCalls.enabled: true` and push enabled, a data-only call push (`type: "call"`, `callId`, `callToken`, `room`, `kind`, `callerName`, `callerXmppUsername`) rings the same way as on mobile:
|
|
752
|
+
|
|
753
|
+
- The worker asks every open window to ring (`CALL_PUSH`). A visible page that shows the ring answers (`CALL_PUSH_ACK`) and nothing else is shown. With no ack within 1.5 s (no window, a hidden tab, a dead socket) the worker shows a sticky notification "Incoming video call" with **Answer** and **Decline** actions, tag `call:<callId>` (`call:room:<room>` when the push has no `callId`). A page that already has that call connecting or in progress acks it, hidden or not, so a duplicate push never rings again; a page busy with a different call acks it as busy and the worker shows a plain "Missed call" instead of a ring. A push without `callToken` cannot be answered: it shows Decline only, and a tap opens the room. A tap or Answer with no window opens `pushNotifications.clickPath` (the page that mounts the chat); the call itself travels in the pending click, never in the url.
|
|
754
|
+
- The push has no TTL, so freshness is read from the `exp` of `callToken`. With less than 3 s left nothing rings and a plain "Missed video call" notification (tag `missed-call:<room>`, tap opens the room) is shown instead. A token with no readable `exp` is treated as fresh. A ring the worker shows is never sticky for longer than 60 s (the backend's TTL for a call): at `exp`, or 60 s after the push when `exp` is unreadable or further away, it is replaced by the "Missed call" notification. A page that times a ring out in a hidden tab shows the same "Missed call" notification itself (same tag, shown once), so the result does not depend on whether the page or the worker gets there first.
|
|
755
|
+
- Answer focuses or opens the app, raises the call and accepts it at once, so LiveKit connects with the push token (the browser may ask for microphone or camera). Tapping the notification body shows the ring screen. The click goes to every open window that announced it takes calls (the page has the call push listener mounted with `videoCalls.enabled`; `CALL_CLIENT_READY`), and the one that took it answers `CALL_PUSH_ANSWER_ACK` and is focused; a host page with no `<Chat>` is never picked just because it was focused last. With no ready window the worker does not wait: it keeps the click as the pending click and focuses and navigates an open tab to `pushNotifications.clickPath` (or opens it), and the new page accepts the call. The same happens when a ready window does not ack within 1.5 s, except that a refused `openWindow` (the click's user activation is short-lived) leaves the pending click for the next page load (2 min). The focus and the no-ready fallback run while the click is fresh; only the ack wait can outlast it, which is why a tab that was ready but silent is never the one navigated.
|
|
756
|
+
- Decline goes to every open window the same way. The page that takes it (XMPP `call-state` plus `POST /v1/chats/call/state/decline/:callId`) answers `CALL_PUSH_DECLINE_ACK`; if no ack comes within 1.5 s (a host page with no `<Chat>`, the config not loaded yet) the worker declines over REST itself. With no page open the worker calls that endpoint directly, using the user token the page mirrors into Cache Storage (the token the SDK already keeps in this origin's localStorage; deleted on logout). Without that token Decline only closes the notification and the caller times out.
|
|
757
|
+
- Several tabs of one origin can ring for the same call. Accepting or declining takes one per-call claim (a Web Lock), whether it comes from the notification's Answer and Decline or from the Accept and Decline buttons on the ring screen, so two tabs never both accept. A tab that does not get the claim does nothing. The tab that accepts, declines or sees the caller cancel broadcasts the outcome to the other tabs of the origin (on a `BroadcastChannel` named `ethora-call` and, for tabs without `BroadcastChannel`, as a `localStorage` write picked up by the `storage` event; a tab acts on whichever arrives first), and a tab still ringing for that call ends its ring quietly: no "Missed call", nothing sent to the caller, its notification closed. A ring that merely runs out is each tab's own. The outcome is also remembered for about 2 minutes (call keys and timestamps only, in `localStorage`), so a late push, a stale Answer or Decline, or a replay of the call token never raises, accepts or hangs up a call another tab already handled. A call with no `callId` is keyed by its room and is not remembered. Without Web Locks the tabs behave as before, and only the remembered outcome protects against a second accept.
|
|
758
|
+
- Set `pushNotifications.clickPath` to the route where `<Chat>` is mounted (for example `/chat`), even though it is optional. It decides where Answer, a tap or a refused `openWindow` goes when no window that can take calls is open. It defaults to the origin root, which in most apps is a landing page or a login screen that does not mount `<Chat>`: the worker then opens a page that never hears the call push listener, so Answer lands on that page, nothing accepts, the pending click waits for the next page load (2 min) and the call is lost for the user. With `clickPath` pointing at the chat route, the new page mounts the chat, picks up the pending click and accepts the call. The same applies to a tab that is open on a page without `<Chat>`: it is not offered the click, the worker navigates it to `clickPath` instead.
|
|
759
|
+
- A ring a page shows through the service worker registration (a hidden tab with a live socket) is persistent, so the page tells the worker (`CALL_RING_SHOWN`, with the call's `exp` and no token) and the worker ends it like a ring of its own, even if the tab is closed in the meantime. A "Missed call" shown by the page and by the worker at the same moment is shown once (both check and show under one Web Lock).
|
|
760
|
+
- The call notification is closed whenever the ring ends (answered, declined, cancelled, timed out). A push that arrives before the chat config and the user are in Redux is held until they are, or until its token expires. On logout (or a switch to another account) the held call, the tap the worker keeps for the next page load and every call notification on screen are cleared.
|
|
761
|
+
|
|
665
762
|
## Auth Strategies
|
|
666
763
|
|
|
667
764
|
| Strategy | Config shape | Best for |
|
|
@@ -731,6 +828,8 @@ Clicking a push opens `<origin>/chat?chatId=<room>&messageId=<message>` (or the
|
|
|
731
828
|
| `useInAppNotifications` | hook | Enables and handles in-app notifications. |
|
|
732
829
|
| `usePushNotifications` | hook | Push subscription + foreground handling workflow. |
|
|
733
830
|
| `resendMessage` | function | Retry sending failed/pending messages. |
|
|
831
|
+
| `parseScheduledCallLink` | function | `parseScheduledCallLink(url)` returns the slug of a scheduled-call join link (`https://<host>/call/<slug>` or `<scheme>://call/<slug>`), or `null`. |
|
|
832
|
+
| `openScheduledCall` | function | `openScheduledCall(slug)` asks the mounted `<Chat>` to select that call's room and show its pre-join panel; held until the user is signed in. Returns `false` for an implausible slug. |
|
|
734
833
|
| `useRoomMute` | hook | `useRoomMute(roomJid)` returns `{ muted, isSupported, isPending, toggleMute, setMuted }` for a room's mute preference - optimistic with rollback on failure. `isSupported` is only `true` once the backend has reported a `muted` boolean for that room (it hasn't shipped to prod yet), so hosts building their own UI should hide a mute control until then. |
|
|
735
834
|
|
|
736
835
|
Basic hook usage:
|