@acorex-platform/module-conversation-client 0.0.28 → 0.0.32

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.
Files changed (38) hide show
  1. package/fesm2022/{acorex-platform-module-conversation-client-acorex-platform-module-conversation-client-DsjrXRZ6.mjs → acorex-platform-module-conversation-client-acorex-platform-module-conversation-client-BP8QEicz.mjs} +7655 -6569
  2. package/fesm2022/acorex-platform-module-conversation-client-acorex-platform-module-conversation-client-BP8QEicz.mjs.map +1 -0
  3. package/fesm2022/{acorex-platform-module-conversation-client-after-message-sent.command-D-DZ9ylV.mjs → acorex-platform-module-conversation-client-after-message-sent.command-CpMGuSU1.mjs} +2 -2
  4. package/fesm2022/{acorex-platform-module-conversation-client-after-message-sent.command-D-DZ9ylV.mjs.map → acorex-platform-module-conversation-client-after-message-sent.command-CpMGuSU1.mjs.map} +1 -1
  5. package/fesm2022/{acorex-platform-module-conversation-client-assist-delegated-agent-detail-popup.component-BfjJCQ-e.mjs → acorex-platform-module-conversation-client-assist-delegated-agent-detail-popup.component-Dgs6fpN3.mjs} +6 -6
  6. package/fesm2022/{acorex-platform-module-conversation-client-assist-delegated-agent-detail-popup.component-BfjJCQ-e.mjs.map → acorex-platform-module-conversation-client-assist-delegated-agent-detail-popup.component-Dgs6fpN3.mjs.map} +1 -1
  7. package/fesm2022/{acorex-platform-module-conversation-client-assist-tool-detail-popup.component-GC58B_n0.mjs → acorex-platform-module-conversation-client-assist-tool-detail-popup.component-BRW1cHN6.mjs} +3 -3
  8. package/fesm2022/{acorex-platform-module-conversation-client-assist-tool-detail-popup.component-GC58B_n0.mjs.map → acorex-platform-module-conversation-client-assist-tool-detail-popup.component-BRW1cHN6.mjs.map} +1 -1
  9. package/fesm2022/acorex-platform-module-conversation-client-chat-call-session-page.component-BStKnruT.mjs +1379 -0
  10. package/fesm2022/acorex-platform-module-conversation-client-chat-call-session-page.component-BStKnruT.mjs.map +1 -0
  11. package/fesm2022/{acorex-platform-module-conversation-client-chat-call-start-popup.component-D4go5xbr.mjs → acorex-platform-module-conversation-client-chat-call-start-popup.component-eKYhjy91.mjs} +4 -4
  12. package/fesm2022/{acorex-platform-module-conversation-client-chat-call-start-popup.component-D4go5xbr.mjs.map → acorex-platform-module-conversation-client-chat-call-start-popup.component-eKYhjy91.mjs.map} +1 -1
  13. package/fesm2022/{acorex-platform-module-conversation-client-chat-conversation-info-media-view.component-BVJdVPke.mjs → acorex-platform-module-conversation-client-chat-conversation-info-media-view.component-DLYtqFLi.mjs} +3 -3
  14. package/fesm2022/{acorex-platform-module-conversation-client-chat-conversation-info-media-view.component-BVJdVPke.mjs.map → acorex-platform-module-conversation-client-chat-conversation-info-media-view.component-DLYtqFLi.mjs.map} +1 -1
  15. package/fesm2022/acorex-platform-module-conversation-client-chat-conversation-info-panel.component-CE40xbl9.mjs +472 -0
  16. package/fesm2022/acorex-platform-module-conversation-client-chat-conversation-info-panel.component-CE40xbl9.mjs.map +1 -0
  17. package/fesm2022/{acorex-platform-module-conversation-client-comments-page.component-Bq4SdK0_.mjs → acorex-platform-module-conversation-client-comments-page.component-VOuN3y_8.mjs} +2 -2
  18. package/fesm2022/{acorex-platform-module-conversation-client-comments-page.component-Bq4SdK0_.mjs.map → acorex-platform-module-conversation-client-comments-page.component-VOuN3y_8.mjs.map} +1 -1
  19. package/fesm2022/acorex-platform-module-conversation-client-contracts.mjs +39 -6
  20. package/fesm2022/acorex-platform-module-conversation-client-contracts.mjs.map +1 -1
  21. package/fesm2022/{acorex-platform-module-conversation-client-mark-conversation-unread.command-BlYDwGVe.mjs → acorex-platform-module-conversation-client-mark-conversation-unread.command-9tbWXMsa.mjs} +2 -2
  22. package/fesm2022/{acorex-platform-module-conversation-client-mark-conversation-unread.command-BlYDwGVe.mjs.map → acorex-platform-module-conversation-client-mark-conversation-unread.command-9tbWXMsa.mjs.map} +1 -1
  23. package/fesm2022/{acorex-platform-module-conversation-client-open-chat.command-B03q60Qx.mjs → acorex-platform-module-conversation-client-open-chat.command-CdMsYHwG.mjs} +16 -1
  24. package/fesm2022/acorex-platform-module-conversation-client-open-chat.command-CdMsYHwG.mjs.map +1 -0
  25. package/fesm2022/{acorex-platform-module-conversation-client-retry-assist-response.command-BQLK6F69.mjs → acorex-platform-module-conversation-client-retry-assist-response.command-DvOeezMD.mjs} +2 -2
  26. package/fesm2022/{acorex-platform-module-conversation-client-retry-assist-response.command-BQLK6F69.mjs.map → acorex-platform-module-conversation-client-retry-assist-response.command-DvOeezMD.mjs.map} +1 -1
  27. package/fesm2022/acorex-platform-module-conversation-client-sync-read-state.command-CmJAjrZb.mjs +50 -0
  28. package/fesm2022/acorex-platform-module-conversation-client-sync-read-state.command-CmJAjrZb.mjs.map +1 -0
  29. package/fesm2022/acorex-platform-module-conversation-client.mjs +1 -1
  30. package/package.json +2 -2
  31. package/types/acorex-platform-module-conversation-client-contracts.d.ts +28 -18
  32. package/types/acorex-platform-module-conversation-client.d.ts +150 -76
  33. package/fesm2022/acorex-platform-module-conversation-client-acorex-platform-module-conversation-client-DsjrXRZ6.mjs.map +0 -1
  34. package/fesm2022/acorex-platform-module-conversation-client-chat-call-session-page.component-DkX4JSbN.mjs +0 -871
  35. package/fesm2022/acorex-platform-module-conversation-client-chat-call-session-page.component-DkX4JSbN.mjs.map +0 -1
  36. package/fesm2022/acorex-platform-module-conversation-client-open-chat.command-B03q60Qx.mjs.map +0 -1
  37. package/fesm2022/acorex-platform-module-conversation-client-sync-read-state.command-B1Z_gZ6O.mjs +0 -142
  38. package/fesm2022/acorex-platform-module-conversation-client-sync-read-state.command-B1Z_gZ6O.mjs.map +0 -1
@@ -1,871 +0,0 @@
1
- import { AXPAvatarComponent } from '@acorex-platform/framework-client/layout/components';
2
- import { AXBasePageComponent } from '@acorex/components/page';
3
- import { AXToastService } from '@acorex/components/toast';
4
- import * as i1 from '@acorex/core/translation';
5
- import { AXTranslationModule, AXTranslationService } from '@acorex/core/translation';
6
- import { AsyncPipe } from '@angular/common';
7
- import * as i0 from '@angular/core';
8
- import { input, output, signal, computed, ChangeDetectionStrategy, Component, Injectable, inject, ElementRef, effect, Directive, ViewEncapsulation } from '@angular/core';
9
- import { ConnectionState } from 'livekit-client';
10
- import { i as AXMChatUserApi, j as AXMChatCallRtcService, k as AXMChatCallRtcSignalInboxService, l as AXMChatCallEventsService, m as AXMConversationRealtimeBus, f as axmResolveConversationListAvatar, n as axmReadChatCallLog, o as AXM_CHAT_CALL_RTC_PROVIDERS } from './acorex-platform-module-conversation-client-acorex-platform-module-conversation-client-DsjrXRZ6.mjs';
11
- import { AXP_ROOT_CONFIG_TOKEN } from '@acorex-platform/framework-client/common';
12
- import { HttpClient } from '@angular/common/http';
13
- import { firstValueFrom } from 'rxjs';
14
-
15
- //#region ----- Imports -----
16
- //#endregion
17
- //#region ----- Component -----
18
- class AXMChatCallControlsComponent {
19
- constructor() {
20
- //#region ----- Inputs / Outputs -----
21
- this.phase = input('outgoing-waiting', /* @ts-ignore */
22
- ...(ngDevMode ? [{ debugName: "phase" }] : /* istanbul ignore next */ []));
23
- this.videoEnabled = input(false, /* @ts-ignore */
24
- ...(ngDevMode ? [{ debugName: "videoEnabled" }] : /* istanbul ignore next */ []));
25
- this.micMuted = input(false, /* @ts-ignore */
26
- ...(ngDevMode ? [{ debugName: "micMuted" }] : /* istanbul ignore next */ []));
27
- this.showAddPeople = input(false, /* @ts-ignore */
28
- ...(ngDevMode ? [{ debugName: "showAddPeople" }] : /* istanbul ignore next */ []));
29
- this.showScreencast = input(true, /* @ts-ignore */
30
- ...(ngDevMode ? [{ debugName: "showScreencast" }] : /* istanbul ignore next */ []));
31
- this.audioDevices = input([], /* @ts-ignore */
32
- ...(ngDevMode ? [{ debugName: "audioDevices" }] : /* istanbul ignore next */ []));
33
- this.videoDevices = input([], /* @ts-ignore */
34
- ...(ngDevMode ? [{ debugName: "videoDevices" }] : /* istanbul ignore next */ []));
35
- this.selectedAudioDeviceId = input(null, /* @ts-ignore */
36
- ...(ngDevMode ? [{ debugName: "selectedAudioDeviceId" }] : /* istanbul ignore next */ []));
37
- this.selectedVideoDeviceId = input(null, /* @ts-ignore */
38
- ...(ngDevMode ? [{ debugName: "selectedVideoDeviceId" }] : /* istanbul ignore next */ []));
39
- this.screencast = output();
40
- this.toggleVideo = output();
41
- this.toggleMute = output();
42
- this.endCall = output();
43
- this.accept = output();
44
- this.decline = output();
45
- this.addPeople = output();
46
- this.pickDevice = output();
47
- this.requestDevices = output();
48
- //#endregion
49
- //#region ----- State Signals -----
50
- this.openMenu = signal(null, /* @ts-ignore */
51
- ...(ngDevMode ? [{ debugName: "openMenu" }] : /* istanbul ignore next */ []));
52
- //#endregion
53
- //#region ----- Computed Properties -----
54
- this.isIncoming = computed(() => this.phase() === 'incoming', /* @ts-ignore */
55
- ...(ngDevMode ? [{ debugName: "isIncoming" }] : /* istanbul ignore next */ []));
56
- this.hangupLabelKey = computed(() => this.phase() === 'active'
57
- ? '@conversation:chat.call.end-call'
58
- : '@conversation:chat.call.cancel', /* @ts-ignore */
59
- ...(ngDevMode ? [{ debugName: "hangupLabelKey" }] : /* istanbul ignore next */ []));
60
- this.videoLabelKey = computed(() => this.videoEnabled()
61
- ? '@conversation:chat.call.stop-video'
62
- : '@conversation:chat.call.start-video', /* @ts-ignore */
63
- ...(ngDevMode ? [{ debugName: "videoLabelKey" }] : /* istanbul ignore next */ []));
64
- this.muteLabelKey = computed(() => this.micMuted() ? '@conversation:chat.call.unmute' : '@conversation:chat.call.mute', /* @ts-ignore */
65
- ...(ngDevMode ? [{ debugName: "muteLabelKey" }] : /* istanbul ignore next */ []));
66
- this.menuDevices = computed(() => {
67
- const kind = this.openMenu();
68
- if (kind === 'audioinput') {
69
- return this.audioDevices();
70
- }
71
- if (kind === 'videoinput') {
72
- return this.videoDevices();
73
- }
74
- return [];
75
- }, /* @ts-ignore */
76
- ...(ngDevMode ? [{ debugName: "menuDevices" }] : /* istanbul ignore next */ []));
77
- }
78
- //#endregion
79
- //#region ----- Event Handlers -----
80
- onOpenDeviceMenu(event, kind) {
81
- event.preventDefault();
82
- event.stopPropagation();
83
- this.openMenu.set(this.openMenu() === kind ? null : kind);
84
- this.requestDevices.emit(kind);
85
- }
86
- onCloseDeviceMenu() {
87
- this.openMenu.set(null);
88
- }
89
- onSelectDevice(kind, deviceId) {
90
- this.pickDevice.emit({ kind, deviceId });
91
- this.openMenu.set(null);
92
- }
93
- selectedDeviceId(kind) {
94
- return kind === 'audioinput' ? this.selectedAudioDeviceId() : this.selectedVideoDeviceId();
95
- }
96
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatCallControlsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
97
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: AXMChatCallControlsComponent, isStandalone: true, selector: "axm-chat-call-controls", inputs: { phase: { classPropertyName: "phase", publicName: "phase", isSignal: true, isRequired: false, transformFunction: null }, videoEnabled: { classPropertyName: "videoEnabled", publicName: "videoEnabled", isSignal: true, isRequired: false, transformFunction: null }, micMuted: { classPropertyName: "micMuted", publicName: "micMuted", isSignal: true, isRequired: false, transformFunction: null }, showAddPeople: { classPropertyName: "showAddPeople", publicName: "showAddPeople", isSignal: true, isRequired: false, transformFunction: null }, showScreencast: { classPropertyName: "showScreencast", publicName: "showScreencast", isSignal: true, isRequired: false, transformFunction: null }, audioDevices: { classPropertyName: "audioDevices", publicName: "audioDevices", isSignal: true, isRequired: false, transformFunction: null }, videoDevices: { classPropertyName: "videoDevices", publicName: "videoDevices", isSignal: true, isRequired: false, transformFunction: null }, selectedAudioDeviceId: { classPropertyName: "selectedAudioDeviceId", publicName: "selectedAudioDeviceId", isSignal: true, isRequired: false, transformFunction: null }, selectedVideoDeviceId: { classPropertyName: "selectedVideoDeviceId", publicName: "selectedVideoDeviceId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { screencast: "screencast", toggleVideo: "toggleVideo", toggleMute: "toggleMute", endCall: "endCall", accept: "accept", decline: "decline", addPeople: "addPeople", pickDevice: "pickDevice", requestDevices: "requestDevices" }, ngImport: i0, template: "<div class=\"axm-chat-call-controls\">\n <!-- ----- Incoming: camera before decline/accept ----- -->\n @if (isIncoming()) {\n <div class=\"axm-chat-call-controls__item\">\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"toggleVideo.emit()\">\n <span class=\"axm-chat-call-controls__circle\" [class.axm-chat-call-controls__circle--light]=\"videoEnabled()\">\n <i class=\"fa-solid\" [class.fa-video]=\"videoEnabled()\" [class.fa-video-slash]=\"!videoEnabled()\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ videoLabelKey() | translate | async }}</span>\n </button>\n <button\n type=\"button\"\n class=\"axm-chat-call-controls__chevron\"\n (click)=\"onOpenDeviceMenu($event, 'videoinput')\"\n [attr.aria-label]=\"'@conversation:chat.call.devices.camera' | translate | async\"\n >\n <i class=\"fa-solid fa-chevron-up\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"decline.emit()\">\n <span class=\"axm-chat-call-controls__circle axm-chat-call-controls__circle--danger\">\n <i class=\"fa-solid fa-phone axm-chat-call-controls__hangup\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ '@conversation:chat.call.decline' | translate | async }}</span>\n </button>\n\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"accept.emit()\">\n <span class=\"axm-chat-call-controls__circle axm-chat-call-controls__circle--success\">\n <i class=\"fa-solid fa-phone\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ '@conversation:chat.call.accept' | translate | async }}</span>\n </button>\n\n <div class=\"axm-chat-call-controls__item\">\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"toggleMute.emit()\">\n <span class=\"axm-chat-call-controls__circle\" [class.axm-chat-call-controls__circle--light]=\"!micMuted()\">\n <i class=\"fa-solid\" [class.fa-microphone]=\"!micMuted()\" [class.fa-microphone-slash]=\"micMuted()\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ muteLabelKey() | translate | async }}</span>\n </button>\n <button\n type=\"button\"\n class=\"axm-chat-call-controls__chevron\"\n (click)=\"onOpenDeviceMenu($event, 'audioinput')\"\n [attr.aria-label]=\"'@conversation:chat.call.devices.microphone' | translate | async\"\n >\n <i class=\"fa-solid fa-chevron-up\" aria-hidden=\"true\"></i>\n </button>\n </div>\n }\n <!-- ----- Outgoing / active controls ----- -->\n @else {\n <!-- ----- Screencast only during an active call ----- -->\n @if (showScreencast()) {\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"screencast.emit()\">\n <span class=\"axm-chat-call-controls__circle axm-chat-call-controls__circle--light\">\n <i class=\"fa-solid fa-desktop\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ '@conversation:chat.call.screencast' | translate | async }}</span>\n </button>\n }\n\n <div class=\"axm-chat-call-controls__item\">\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"toggleVideo.emit()\">\n <span class=\"axm-chat-call-controls__circle\" [class.axm-chat-call-controls__circle--light]=\"videoEnabled()\">\n <i class=\"fa-solid\" [class.fa-video]=\"videoEnabled()\" [class.fa-video-slash]=\"!videoEnabled()\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ videoLabelKey() | translate | async }}</span>\n </button>\n <button\n type=\"button\"\n class=\"axm-chat-call-controls__chevron\"\n (click)=\"onOpenDeviceMenu($event, 'videoinput')\"\n [attr.aria-label]=\"'@conversation:chat.call.devices.camera' | translate | async\"\n >\n <i class=\"fa-solid fa-chevron-up\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"endCall.emit()\">\n <span class=\"axm-chat-call-controls__circle axm-chat-call-controls__circle--danger\">\n <i class=\"fa-solid fa-phone axm-chat-call-controls__hangup\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ hangupLabelKey() | translate | async }}</span>\n </button>\n\n <div class=\"axm-chat-call-controls__item\">\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"toggleMute.emit()\">\n <span class=\"axm-chat-call-controls__circle\" [class.axm-chat-call-controls__circle--light]=\"!micMuted()\">\n <i class=\"fa-solid\" [class.fa-microphone]=\"!micMuted()\" [class.fa-microphone-slash]=\"micMuted()\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ muteLabelKey() | translate | async }}</span>\n </button>\n <button\n type=\"button\"\n class=\"axm-chat-call-controls__chevron\"\n (click)=\"onOpenDeviceMenu($event, 'audioinput')\"\n [attr.aria-label]=\"'@conversation:chat.call.devices.microphone' | translate | async\"\n >\n <i class=\"fa-solid fa-chevron-up\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <!-- ----- Add people on an active group call ----- -->\n @if (showAddPeople()) {\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"addPeople.emit()\">\n <span class=\"axm-chat-call-controls__circle axm-chat-call-controls__circle--light\">\n <i class=\"fa-solid fa-user-plus\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ '@conversation:chat.call.add-people' | translate | async }}</span>\n </button>\n }\n }\n\n <!-- ----- Device switcher ----- -->\n @if (openMenu(); as kind) {\n <button type=\"button\" class=\"axm-chat-call-controls__menu-backdrop\" (click)=\"onCloseDeviceMenu()\"></button>\n <ul class=\"axm-chat-call-controls__menu\" role=\"listbox\">\n @if (menuDevices().length === 0) {\n <li class=\"axm-chat-call-controls__menu-empty\">\n {{ '@conversation:chat.call.devices.empty' | translate | async }}\n </li>\n }\n @for (device of menuDevices(); track device.deviceId) {\n <li>\n <button\n type=\"button\"\n class=\"axm-chat-call-controls__menu-item\"\n [class.axm-chat-call-controls__menu-item--active]=\"device.deviceId === selectedDeviceId(kind)\"\n (click)=\"onSelectDevice(kind, device.deviceId)\"\n >\n {{ device.label || device.deviceId }}\n </button>\n </li>\n }\n </ul>\n }\n</div>\n", styles: [":host{display:block;width:100%}.axm-chat-call-controls{position:relative;display:flex;align-items:flex-start;justify-content:center;flex-wrap:wrap;gap:1.25rem;padding:.75rem 1rem 1.25rem}.axm-chat-call-controls__item{position:relative;display:flex;flex-direction:column;align-items:center}.axm-chat-call-controls__action{position:relative;display:flex;flex-direction:column;align-items:center;gap:.5rem;min-width:4.5rem;padding:0;border:0;background:transparent;color:#fff;cursor:pointer}.axm-chat-call-controls__action:focus-visible{outline:2px solid rgba(255,255,255,.45);outline-offset:4px;border-radius:.5rem}.axm-chat-call-controls__circle{position:relative;display:inline-flex;align-items:center;justify-content:center;width:3.25rem;height:3.25rem;border-radius:9999px;background:#3a3d41;color:#fff;font-size:1.125rem}.axm-chat-call-controls__circle--danger{background:#e53935}.axm-chat-call-controls__circle--success{background:#4caf50}.axm-chat-call-controls__circle--light{background:#f2f2f2;color:#1a1c1e}.axm-chat-call-controls__chevron{position:absolute;top:-.15rem;inset-inline-end:.35rem;z-index:2;display:inline-flex;align-items:center;justify-content:center;width:1.15rem;height:1.15rem;padding:0;border-radius:9999px;background:#2b2e32;border:1px solid rgba(255,255,255,.2);font-size:.55rem;color:#fff;cursor:pointer}.axm-chat-call-controls__label{font-size:.75rem;line-height:1.2;color:#fff;white-space:nowrap}.axm-chat-call-controls__hangup{transform:rotate(135deg)}.axm-chat-call-controls__menu-backdrop{position:fixed;inset:0;z-index:20;border:0;background:transparent;cursor:default}.axm-chat-call-controls__menu{position:absolute;bottom:calc(100% + .5rem);inset-inline-start:50%;z-index:21;min-width:12rem;max-width:18rem;margin:0;padding:.35rem;list-style:none;border-radius:.75rem;background:#2b2e32;border:1px solid rgba(255,255,255,.12);box-shadow:0 10px 28px #0006;transform:translate(-50%)}.axm-chat-call-controls__menu-empty,.axm-chat-call-controls__menu-item{display:block;width:100%;padding:.5rem .75rem;border:0;border-radius:.5rem;background:transparent;color:#fff;font-size:.8125rem;text-align:start}.axm-chat-call-controls__menu-item{cursor:pointer}.axm-chat-call-controls__menu-item:hover,.axm-chat-call-controls__menu-item--active{background:#ffffff1a}\n"], dependencies: [{ kind: "ngmodule", type: AXTranslationModule }, { kind: "pipe", type: i1.AXTranslatorPipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
98
- }
99
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatCallControlsComponent, decorators: [{
100
- type: Component,
101
- args: [{ selector: 'axm-chat-call-controls', changeDetection: ChangeDetectionStrategy.OnPush, imports: [AXTranslationModule, AsyncPipe], template: "<div class=\"axm-chat-call-controls\">\n <!-- ----- Incoming: camera before decline/accept ----- -->\n @if (isIncoming()) {\n <div class=\"axm-chat-call-controls__item\">\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"toggleVideo.emit()\">\n <span class=\"axm-chat-call-controls__circle\" [class.axm-chat-call-controls__circle--light]=\"videoEnabled()\">\n <i class=\"fa-solid\" [class.fa-video]=\"videoEnabled()\" [class.fa-video-slash]=\"!videoEnabled()\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ videoLabelKey() | translate | async }}</span>\n </button>\n <button\n type=\"button\"\n class=\"axm-chat-call-controls__chevron\"\n (click)=\"onOpenDeviceMenu($event, 'videoinput')\"\n [attr.aria-label]=\"'@conversation:chat.call.devices.camera' | translate | async\"\n >\n <i class=\"fa-solid fa-chevron-up\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"decline.emit()\">\n <span class=\"axm-chat-call-controls__circle axm-chat-call-controls__circle--danger\">\n <i class=\"fa-solid fa-phone axm-chat-call-controls__hangup\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ '@conversation:chat.call.decline' | translate | async }}</span>\n </button>\n\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"accept.emit()\">\n <span class=\"axm-chat-call-controls__circle axm-chat-call-controls__circle--success\">\n <i class=\"fa-solid fa-phone\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ '@conversation:chat.call.accept' | translate | async }}</span>\n </button>\n\n <div class=\"axm-chat-call-controls__item\">\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"toggleMute.emit()\">\n <span class=\"axm-chat-call-controls__circle\" [class.axm-chat-call-controls__circle--light]=\"!micMuted()\">\n <i class=\"fa-solid\" [class.fa-microphone]=\"!micMuted()\" [class.fa-microphone-slash]=\"micMuted()\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ muteLabelKey() | translate | async }}</span>\n </button>\n <button\n type=\"button\"\n class=\"axm-chat-call-controls__chevron\"\n (click)=\"onOpenDeviceMenu($event, 'audioinput')\"\n [attr.aria-label]=\"'@conversation:chat.call.devices.microphone' | translate | async\"\n >\n <i class=\"fa-solid fa-chevron-up\" aria-hidden=\"true\"></i>\n </button>\n </div>\n }\n <!-- ----- Outgoing / active controls ----- -->\n @else {\n <!-- ----- Screencast only during an active call ----- -->\n @if (showScreencast()) {\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"screencast.emit()\">\n <span class=\"axm-chat-call-controls__circle axm-chat-call-controls__circle--light\">\n <i class=\"fa-solid fa-desktop\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ '@conversation:chat.call.screencast' | translate | async }}</span>\n </button>\n }\n\n <div class=\"axm-chat-call-controls__item\">\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"toggleVideo.emit()\">\n <span class=\"axm-chat-call-controls__circle\" [class.axm-chat-call-controls__circle--light]=\"videoEnabled()\">\n <i class=\"fa-solid\" [class.fa-video]=\"videoEnabled()\" [class.fa-video-slash]=\"!videoEnabled()\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ videoLabelKey() | translate | async }}</span>\n </button>\n <button\n type=\"button\"\n class=\"axm-chat-call-controls__chevron\"\n (click)=\"onOpenDeviceMenu($event, 'videoinput')\"\n [attr.aria-label]=\"'@conversation:chat.call.devices.camera' | translate | async\"\n >\n <i class=\"fa-solid fa-chevron-up\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"endCall.emit()\">\n <span class=\"axm-chat-call-controls__circle axm-chat-call-controls__circle--danger\">\n <i class=\"fa-solid fa-phone axm-chat-call-controls__hangup\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ hangupLabelKey() | translate | async }}</span>\n </button>\n\n <div class=\"axm-chat-call-controls__item\">\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"toggleMute.emit()\">\n <span class=\"axm-chat-call-controls__circle\" [class.axm-chat-call-controls__circle--light]=\"!micMuted()\">\n <i class=\"fa-solid\" [class.fa-microphone]=\"!micMuted()\" [class.fa-microphone-slash]=\"micMuted()\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ muteLabelKey() | translate | async }}</span>\n </button>\n <button\n type=\"button\"\n class=\"axm-chat-call-controls__chevron\"\n (click)=\"onOpenDeviceMenu($event, 'audioinput')\"\n [attr.aria-label]=\"'@conversation:chat.call.devices.microphone' | translate | async\"\n >\n <i class=\"fa-solid fa-chevron-up\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <!-- ----- Add people on an active group call ----- -->\n @if (showAddPeople()) {\n <button type=\"button\" class=\"axm-chat-call-controls__action\" (click)=\"addPeople.emit()\">\n <span class=\"axm-chat-call-controls__circle axm-chat-call-controls__circle--light\">\n <i class=\"fa-solid fa-user-plus\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-chat-call-controls__label\">{{ '@conversation:chat.call.add-people' | translate | async }}</span>\n </button>\n }\n }\n\n <!-- ----- Device switcher ----- -->\n @if (openMenu(); as kind) {\n <button type=\"button\" class=\"axm-chat-call-controls__menu-backdrop\" (click)=\"onCloseDeviceMenu()\"></button>\n <ul class=\"axm-chat-call-controls__menu\" role=\"listbox\">\n @if (menuDevices().length === 0) {\n <li class=\"axm-chat-call-controls__menu-empty\">\n {{ '@conversation:chat.call.devices.empty' | translate | async }}\n </li>\n }\n @for (device of menuDevices(); track device.deviceId) {\n <li>\n <button\n type=\"button\"\n class=\"axm-chat-call-controls__menu-item\"\n [class.axm-chat-call-controls__menu-item--active]=\"device.deviceId === selectedDeviceId(kind)\"\n (click)=\"onSelectDevice(kind, device.deviceId)\"\n >\n {{ device.label || device.deviceId }}\n </button>\n </li>\n }\n </ul>\n }\n</div>\n", styles: [":host{display:block;width:100%}.axm-chat-call-controls{position:relative;display:flex;align-items:flex-start;justify-content:center;flex-wrap:wrap;gap:1.25rem;padding:.75rem 1rem 1.25rem}.axm-chat-call-controls__item{position:relative;display:flex;flex-direction:column;align-items:center}.axm-chat-call-controls__action{position:relative;display:flex;flex-direction:column;align-items:center;gap:.5rem;min-width:4.5rem;padding:0;border:0;background:transparent;color:#fff;cursor:pointer}.axm-chat-call-controls__action:focus-visible{outline:2px solid rgba(255,255,255,.45);outline-offset:4px;border-radius:.5rem}.axm-chat-call-controls__circle{position:relative;display:inline-flex;align-items:center;justify-content:center;width:3.25rem;height:3.25rem;border-radius:9999px;background:#3a3d41;color:#fff;font-size:1.125rem}.axm-chat-call-controls__circle--danger{background:#e53935}.axm-chat-call-controls__circle--success{background:#4caf50}.axm-chat-call-controls__circle--light{background:#f2f2f2;color:#1a1c1e}.axm-chat-call-controls__chevron{position:absolute;top:-.15rem;inset-inline-end:.35rem;z-index:2;display:inline-flex;align-items:center;justify-content:center;width:1.15rem;height:1.15rem;padding:0;border-radius:9999px;background:#2b2e32;border:1px solid rgba(255,255,255,.2);font-size:.55rem;color:#fff;cursor:pointer}.axm-chat-call-controls__label{font-size:.75rem;line-height:1.2;color:#fff;white-space:nowrap}.axm-chat-call-controls__hangup{transform:rotate(135deg)}.axm-chat-call-controls__menu-backdrop{position:fixed;inset:0;z-index:20;border:0;background:transparent;cursor:default}.axm-chat-call-controls__menu{position:absolute;bottom:calc(100% + .5rem);inset-inline-start:50%;z-index:21;min-width:12rem;max-width:18rem;margin:0;padding:.35rem;list-style:none;border-radius:.75rem;background:#2b2e32;border:1px solid rgba(255,255,255,.12);box-shadow:0 10px 28px #0006;transform:translate(-50%)}.axm-chat-call-controls__menu-empty,.axm-chat-call-controls__menu-item{display:block;width:100%;padding:.5rem .75rem;border:0;border-radius:.5rem;background:transparent;color:#fff;font-size:.8125rem;text-align:start}.axm-chat-call-controls__menu-item{cursor:pointer}.axm-chat-call-controls__menu-item:hover,.axm-chat-call-controls__menu-item--active{background:#ffffff1a}\n"] }]
102
- }], propDecorators: { phase: [{ type: i0.Input, args: [{ isSignal: true, alias: "phase", required: false }] }], videoEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoEnabled", required: false }] }], micMuted: [{ type: i0.Input, args: [{ isSignal: true, alias: "micMuted", required: false }] }], showAddPeople: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAddPeople", required: false }] }], showScreencast: [{ type: i0.Input, args: [{ isSignal: true, alias: "showScreencast", required: false }] }], audioDevices: [{ type: i0.Input, args: [{ isSignal: true, alias: "audioDevices", required: false }] }], videoDevices: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoDevices", required: false }] }], selectedAudioDeviceId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedAudioDeviceId", required: false }] }], selectedVideoDeviceId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedVideoDeviceId", required: false }] }], screencast: [{ type: i0.Output, args: ["screencast"] }], toggleVideo: [{ type: i0.Output, args: ["toggleVideo"] }], toggleMute: [{ type: i0.Output, args: ["toggleMute"] }], endCall: [{ type: i0.Output, args: ["endCall"] }], accept: [{ type: i0.Output, args: ["accept"] }], decline: [{ type: i0.Output, args: ["decline"] }], addPeople: [{ type: i0.Output, args: ["addPeople"] }], pickDevice: [{ type: i0.Output, args: ["pickDevice"] }], requestDevices: [{ type: i0.Output, args: ["requestDevices"] }] } });
103
-
104
- //#region ----- Imports -----
105
- //#endregion
106
- //#region ----- Constants -----
107
- const CHAT_CALL_INCOMING_RING_SRC = 'assets/sounds/ring.mp3';
108
- const CHAT_CALL_OUTGOING_RING_SRC = 'assets/sounds/phone-call.mp3';
109
- const CHAT_CALL_OUTGOING_GAP_MS = 2500;
110
- //#endregion
111
- //#region ----- Service -----
112
- /**
113
- * Plays call audio: looping ringtone for the callee, spaced ringback for the caller.
114
- */
115
- class AXMChatCallRingerService {
116
- constructor() {
117
- //#region ----- Private State -----
118
- this.audio = null;
119
- this.gapTimer = null;
120
- this.playing = false;
121
- }
122
- //#endregion
123
- //#region ----- Playback -----
124
- playIncoming() {
125
- this.#playLooped(CHAT_CALL_INCOMING_RING_SRC);
126
- }
127
- playOutgoing() {
128
- this.#playWithGap(CHAT_CALL_OUTGOING_RING_SRC, CHAT_CALL_OUTGOING_GAP_MS);
129
- }
130
- stop() {
131
- this.playing = false;
132
- this.#clearGapTimer();
133
- const audio = this.audio;
134
- this.audio = null;
135
- if (!audio) {
136
- return;
137
- }
138
- audio.pause();
139
- audio.removeAttribute('src');
140
- audio.load();
141
- }
142
- #playLooped(src) {
143
- this.stop();
144
- if (typeof Audio === 'undefined') {
145
- return;
146
- }
147
- this.playing = true;
148
- const audio = new Audio(src);
149
- audio.loop = true;
150
- void audio.play().catch(() => {
151
- // Autoplay may be blocked until a user gesture; the call UI still shows.
152
- });
153
- this.audio = audio;
154
- }
155
- #playWithGap(src, gapMs) {
156
- this.stop();
157
- if (typeof Audio === 'undefined') {
158
- return;
159
- }
160
- this.playing = true;
161
- this.#playOutgoingClip(src, gapMs);
162
- }
163
- #playOutgoingClip(src, gapMs) {
164
- if (!this.playing) {
165
- return;
166
- }
167
- const previous = this.audio;
168
- this.audio = null;
169
- if (previous) {
170
- previous.pause();
171
- previous.removeAttribute('src');
172
- previous.load();
173
- }
174
- const audio = new Audio(src);
175
- audio.loop = false;
176
- audio.addEventListener('ended', () => {
177
- if (!this.playing) {
178
- return;
179
- }
180
- this.#clearGapTimer();
181
- this.gapTimer = setTimeout(() => {
182
- this.gapTimer = null;
183
- this.#playOutgoingClip(src, gapMs);
184
- }, gapMs);
185
- }, { once: true });
186
- void audio.play().catch(() => {
187
- // Autoplay may be blocked until a user gesture; the call UI still shows.
188
- });
189
- this.audio = audio;
190
- }
191
- #clearGapTimer() {
192
- if (this.gapTimer == null) {
193
- return;
194
- }
195
- clearTimeout(this.gapTimer);
196
- this.gapTimer = null;
197
- }
198
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatCallRingerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
199
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatCallRingerService, providedIn: 'root' }); }
200
- }
201
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatCallRingerService, decorators: [{
202
- type: Injectable,
203
- args: [{ providedIn: 'root' }]
204
- }] });
205
-
206
- //#region ----- Imports -----
207
- //#endregion
208
- //#region ----- Client -----
209
- /**
210
- * Posts call signaling to the conversation API (built-in realtime fan-out on server).
211
- */
212
- class AXMChatCallSignalClient {
213
- constructor() {
214
- this.http = inject(HttpClient);
215
- this.rootConfig = inject(AXP_ROOT_CONFIG_TOKEN);
216
- }
217
- async signal(request) {
218
- const conversationId = request.conversationId.trim();
219
- const url = `${this.rootConfig.baseUrl}/v1/global/conversation/conversations/${encodeURIComponent(conversationId)}/calls/signal`;
220
- return firstValueFrom(this.http.post(url, {
221
- action: request.action,
222
- media: request.media,
223
- callId: request.callId,
224
- targetUserIds: request.targetUserIds,
225
- }, { withCredentials: true }));
226
- }
227
- async log(request) {
228
- const conversationId = request.conversationId.trim();
229
- const url = `${this.rootConfig.baseUrl}/v1/global/conversation/conversations/${encodeURIComponent(conversationId)}/calls/log`;
230
- return firstValueFrom(this.http.post(url, {
231
- outcome: request.outcome,
232
- media: request.media,
233
- callId: request.callId,
234
- }, { withCredentials: true }));
235
- }
236
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatCallSignalClient, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
237
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatCallSignalClient, providedIn: 'root' }); }
238
- }
239
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatCallSignalClient, decorators: [{
240
- type: Injectable,
241
- args: [{ providedIn: 'root' }]
242
- }] });
243
-
244
- //#region ----- Imports -----
245
- //#endregion
246
- //#region ----- Component -----
247
- class AXMChatCallSidebarComponent {
248
- constructor() {
249
- //#region ----- Inputs / Outputs -----
250
- this.participants = input([], /* @ts-ignore */
251
- ...(ngDevMode ? [{ debugName: "participants" }] : /* istanbul ignore next */ []));
252
- this.addPeople = output();
253
- this.shareLink = output();
254
- }
255
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatCallSidebarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
256
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: AXMChatCallSidebarComponent, isStandalone: true, selector: "axm-chat-call-sidebar", inputs: { participants: { classPropertyName: "participants", publicName: "participants", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { addPeople: "addPeople", shareLink: "shareLink" }, ngImport: i0, template: "<div class=\"axm-chat-call-sidebar__header\">\n {{ '@conversation:chat.call.encrypted' | translate | async }}\n</div>\n\n<div class=\"axm-chat-call-sidebar__list\">\n @for (participant of participants(); track participant.id) {\n <div class=\"axm-chat-call-sidebar__row\">\n <axp-avatar [size]=\"36\" [fullName]=\"participant.name\" [src]=\"participant.avatar ?? ''\" />\n <div class=\"axm-chat-call-sidebar__meta\">\n <span class=\"axm-chat-call-sidebar__name\">{{ participant.name }}</span>\n @if (participant.speaking) {\n <span class=\"axm-chat-call-sidebar__status\">\n <i class=\"fa-solid fa-microphone\" aria-hidden=\"true\"></i>\n <i class=\"fa-solid fa-video\" aria-hidden=\"true\"></i>\n {{ '@conversation:chat.call.speaking' | translate | async }}\n </span>\n }\n </div>\n </div>\n }\n</div>\n\n<div class=\"axm-chat-call-sidebar__actions\">\n <button type=\"button\" class=\"axm-chat-call-sidebar__action\" (click)=\"addPeople.emit()\">\n <i class=\"fa-solid fa-user-plus\" aria-hidden=\"true\"></i>\n <span>{{ '@conversation:chat.call.add-people' | translate | async }}</span>\n </button>\n <button type=\"button\" class=\"axm-chat-call-sidebar__action\" (click)=\"shareLink.emit()\">\n <i class=\"fa-solid fa-link\" aria-hidden=\"true\"></i>\n <span>{{ '@conversation:chat.call.share-link' | translate | async }}</span>\n </button>\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:16.5rem;min-width:16.5rem;height:100%;background:#141517;border-inline-start:1px solid rgba(255,255,255,.06);color:#fff}.axm-chat-call-sidebar__header{padding:1rem 1rem .75rem;font-size:.75rem;color:#ffffff8c}.axm-chat-call-sidebar__list{display:flex;flex-direction:column;gap:.75rem;flex:1;min-height:0;overflow:auto;padding:.5rem 1rem 1rem}.axm-chat-call-sidebar__row{display:flex;align-items:center;gap:.75rem}.axm-chat-call-sidebar__meta{display:flex;flex-direction:column;gap:.2rem;min-width:0}.axm-chat-call-sidebar__name{font-size:.875rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.axm-chat-call-sidebar__status{display:inline-flex;align-items:center;gap:.35rem;font-size:.75rem;color:#7dd87d}.axm-chat-call-sidebar__status i{font-size:.7rem}.axm-chat-call-sidebar__actions{display:flex;flex-direction:column;gap:.25rem;padding:.75rem .5rem 1rem;border-top:1px solid rgba(255,255,255,.06)}.axm-chat-call-sidebar__action{display:flex;align-items:center;gap:.65rem;width:100%;padding:.65rem .75rem;border:0;border-radius:.5rem;background:transparent;color:#fff;font-size:.875rem;cursor:pointer;text-align:start}.axm-chat-call-sidebar__action:hover{background:#ffffff0f}\n"], dependencies: [{ kind: "component", type: AXPAvatarComponent, selector: "axp-avatar", inputs: ["size", "src", "fullName", "fallbackText", "placeholder"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "pipe", type: i1.AXTranslatorPipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
257
- }
258
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatCallSidebarComponent, decorators: [{
259
- type: Component,
260
- args: [{ selector: 'axm-chat-call-sidebar', changeDetection: ChangeDetectionStrategy.OnPush, imports: [AXPAvatarComponent, AXTranslationModule, AsyncPipe], template: "<div class=\"axm-chat-call-sidebar__header\">\n {{ '@conversation:chat.call.encrypted' | translate | async }}\n</div>\n\n<div class=\"axm-chat-call-sidebar__list\">\n @for (participant of participants(); track participant.id) {\n <div class=\"axm-chat-call-sidebar__row\">\n <axp-avatar [size]=\"36\" [fullName]=\"participant.name\" [src]=\"participant.avatar ?? ''\" />\n <div class=\"axm-chat-call-sidebar__meta\">\n <span class=\"axm-chat-call-sidebar__name\">{{ participant.name }}</span>\n @if (participant.speaking) {\n <span class=\"axm-chat-call-sidebar__status\">\n <i class=\"fa-solid fa-microphone\" aria-hidden=\"true\"></i>\n <i class=\"fa-solid fa-video\" aria-hidden=\"true\"></i>\n {{ '@conversation:chat.call.speaking' | translate | async }}\n </span>\n }\n </div>\n </div>\n }\n</div>\n\n<div class=\"axm-chat-call-sidebar__actions\">\n <button type=\"button\" class=\"axm-chat-call-sidebar__action\" (click)=\"addPeople.emit()\">\n <i class=\"fa-solid fa-user-plus\" aria-hidden=\"true\"></i>\n <span>{{ '@conversation:chat.call.add-people' | translate | async }}</span>\n </button>\n <button type=\"button\" class=\"axm-chat-call-sidebar__action\" (click)=\"shareLink.emit()\">\n <i class=\"fa-solid fa-link\" aria-hidden=\"true\"></i>\n <span>{{ '@conversation:chat.call.share-link' | translate | async }}</span>\n </button>\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:16.5rem;min-width:16.5rem;height:100%;background:#141517;border-inline-start:1px solid rgba(255,255,255,.06);color:#fff}.axm-chat-call-sidebar__header{padding:1rem 1rem .75rem;font-size:.75rem;color:#ffffff8c}.axm-chat-call-sidebar__list{display:flex;flex-direction:column;gap:.75rem;flex:1;min-height:0;overflow:auto;padding:.5rem 1rem 1rem}.axm-chat-call-sidebar__row{display:flex;align-items:center;gap:.75rem}.axm-chat-call-sidebar__meta{display:flex;flex-direction:column;gap:.2rem;min-width:0}.axm-chat-call-sidebar__name{font-size:.875rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.axm-chat-call-sidebar__status{display:inline-flex;align-items:center;gap:.35rem;font-size:.75rem;color:#7dd87d}.axm-chat-call-sidebar__status i{font-size:.7rem}.axm-chat-call-sidebar__actions{display:flex;flex-direction:column;gap:.25rem;padding:.75rem .5rem 1rem;border-top:1px solid rgba(255,255,255,.06)}.axm-chat-call-sidebar__action{display:flex;align-items:center;gap:.65rem;width:100%;padding:.65rem .75rem;border:0;border-radius:.5rem;background:transparent;color:#fff;font-size:.875rem;cursor:pointer;text-align:start}.axm-chat-call-sidebar__action:hover{background:#ffffff0f}\n"] }]
261
- }], propDecorators: { participants: [{ type: i0.Input, args: [{ isSignal: true, alias: "participants", required: false }] }], addPeople: [{ type: i0.Output, args: ["addPeople"] }], shareLink: [{ type: i0.Output, args: ["shareLink"] }] } });
262
-
263
- //#region ----- Imports -----
264
- /**
265
- * Attaches a LiveKit track to the host media element.
266
- */
267
- class AXMChatCallVideoAttachDirective {
268
- //#endregion
269
- //#region ----- Lifecycle -----
270
- constructor() {
271
- //#region ----- Inputs -----
272
- this.axmChatCallTrack = input(null, /* @ts-ignore */
273
- ...(ngDevMode ? [{ debugName: "axmChatCallTrack" }] : /* istanbul ignore next */ []));
274
- //#endregion
275
- //#region ----- Services & Dependencies -----
276
- this.host = inject((ElementRef));
277
- effect((onCleanup) => {
278
- const track = this.axmChatCallTrack();
279
- const element = this.host.nativeElement;
280
- if (!track) {
281
- element.srcObject = null;
282
- return;
283
- }
284
- track.attach(element);
285
- void element.play().catch(() => undefined);
286
- onCleanup(() => {
287
- track.detach(element);
288
- });
289
- });
290
- }
291
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatCallVideoAttachDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
292
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.8", type: AXMChatCallVideoAttachDirective, isStandalone: true, selector: "video[axmChatCallTrack]", inputs: { axmChatCallTrack: { classPropertyName: "axmChatCallTrack", publicName: "axmChatCallTrack", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
293
- }
294
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatCallVideoAttachDirective, decorators: [{
295
- type: Directive,
296
- args: [{
297
- selector: 'video[axmChatCallTrack]',
298
- }]
299
- }], ctorParameters: () => [], propDecorators: { axmChatCallTrack: [{ type: i0.Input, args: [{ isSignal: true, alias: "axmChatCallTrack", required: false }] }] } });
300
-
301
- //#region ----- Imports -----
302
- //#endregion
303
- //#region ----- Component -----
304
- /**
305
- * Call-session popup backed by RTC/LiveKit.
306
- */
307
- class AXMChatCallSessionPageComponent extends AXBasePageComponent {
308
- //#endregion
309
- //#region ----- Lifecycle -----
310
- constructor() {
311
- super();
312
- this.sessionMedia = 'voice';
313
- this.sessionPhase = 'outgoing-waiting';
314
- //#endregion
315
- //#region ----- Services & Dependencies -----
316
- this.userApi = inject(AXMChatUserApi);
317
- this.toastService = inject(AXToastService);
318
- this.translationService = inject(AXTranslationService);
319
- this.rtc = inject(AXMChatCallRtcService);
320
- this.callSignal = inject(AXMChatCallSignalClient);
321
- this.signalInbox = inject(AXMChatCallRtcSignalInboxService);
322
- this.callEvents = inject(AXMChatCallEventsService);
323
- this.realtimeBus = inject(AXMConversationRealtimeBus);
324
- this.ringer = inject(AXMChatCallRingerService);
325
- //#endregion
326
- //#region ----- State Signals -----
327
- this.loading = signal(true, /* @ts-ignore */
328
- ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
329
- this.error = signal(false, /* @ts-ignore */
330
- ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
331
- this.errorMessage = signal(null, /* @ts-ignore */
332
- ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
333
- this.conversation = signal(null, /* @ts-ignore */
334
- ...(ngDevMode ? [{ debugName: "conversation" }] : /* istanbul ignore next */ []));
335
- this.phase = signal('outgoing-waiting', /* @ts-ignore */
336
- ...(ngDevMode ? [{ debugName: "phase" }] : /* istanbul ignore next */ []));
337
- this.mediaType = signal('voice', /* @ts-ignore */
338
- ...(ngDevMode ? [{ debugName: "mediaType" }] : /* istanbul ignore next */ []));
339
- this.videoEnabled = signal(false, /* @ts-ignore */
340
- ...(ngDevMode ? [{ debugName: "videoEnabled" }] : /* istanbul ignore next */ []));
341
- this.micMuted = signal(false, /* @ts-ignore */
342
- ...(ngDevMode ? [{ debugName: "micMuted" }] : /* istanbul ignore next */ []));
343
- this.currentUserId = signal(null, /* @ts-ignore */
344
- ...(ngDevMode ? [{ debugName: "currentUserId" }] : /* istanbul ignore next */ []));
345
- this.callId = signal(null, /* @ts-ignore */
346
- ...(ngDevMode ? [{ debugName: "callId" }] : /* istanbul ignore next */ []));
347
- this.ringTimeoutId = null;
348
- this.outcomeSub = null;
349
- this.callEventsSub = null;
350
- this.closing = false;
351
- this.accepting = false;
352
- this.audioDevices = signal([], /* @ts-ignore */
353
- ...(ngDevMode ? [{ debugName: "audioDevices" }] : /* istanbul ignore next */ []));
354
- this.videoDevices = signal([], /* @ts-ignore */
355
- ...(ngDevMode ? [{ debugName: "videoDevices" }] : /* istanbul ignore next */ []));
356
- this.selectedAudioDeviceId = signal(null, /* @ts-ignore */
357
- ...(ngDevMode ? [{ debugName: "selectedAudioDeviceId" }] : /* istanbul ignore next */ []));
358
- this.selectedVideoDeviceId = signal(null, /* @ts-ignore */
359
- ...(ngDevMode ? [{ debugName: "selectedVideoDeviceId" }] : /* istanbul ignore next */ []));
360
- this.localVideoTrack = this.rtc.localVideoTrack;
361
- this.remoteVideos = this.rtc.remoteVideos;
362
- //#endregion
363
- //#region ----- Computed Properties -----
364
- this.peerDisplayName = computed(() => {
365
- const conversation = this.conversation();
366
- if (!conversation) {
367
- return '';
368
- }
369
- return axmResolveConversationListAvatar(conversation).title;
370
- }, /* @ts-ignore */
371
- ...(ngDevMode ? [{ debugName: "peerDisplayName" }] : /* istanbul ignore next */ []));
372
- this.peerAvatarSrc = computed(() => {
373
- const conversation = this.conversation();
374
- if (!conversation) {
375
- return '';
376
- }
377
- return axmResolveConversationListAvatar(conversation).avatar ?? '';
378
- }, /* @ts-ignore */
379
- ...(ngDevMode ? [{ debugName: "peerAvatarSrc" }] : /* istanbul ignore next */ []));
380
- this.isGroupLayout = computed(() => {
381
- const conversation = this.conversation();
382
- if (!conversation) {
383
- return false;
384
- }
385
- return conversation.type === 'group' || (conversation.participants?.length ?? 0) > 2;
386
- }, /* @ts-ignore */
387
- ...(ngDevMode ? [{ debugName: "isGroupLayout" }] : /* istanbul ignore next */ []));
388
- this.showStage = computed(() => {
389
- if (this.phase() === 'active' || this.mediaType() === 'video' || this.videoEnabled()) {
390
- return true;
391
- }
392
- return this.remoteVideos().length > 0;
393
- }, /* @ts-ignore */
394
- ...(ngDevMode ? [{ debugName: "showStage" }] : /* istanbul ignore next */ []));
395
- /** Voice ring / waiting without a video stage — center the profile block. */
396
- this.idleLayout = computed(() => {
397
- if (this.showStage()) {
398
- return false;
399
- }
400
- const phase = this.phase();
401
- return phase === 'incoming' || phase === 'outgoing-waiting' || phase === 'connecting';
402
- }, /* @ts-ignore */
403
- ...(ngDevMode ? [{ debugName: "idleLayout" }] : /* istanbul ignore next */ []));
404
- this.headerAvatarSize = computed(() => (this.showStage() ? 48 : 128), /* @ts-ignore */
405
- ...(ngDevMode ? [{ debugName: "headerAvatarSize" }] : /* istanbul ignore next */ []));
406
- this.phaseTextKey = computed(() => {
407
- switch (this.phase()) {
408
- case 'incoming':
409
- return '@conversation:chat.call.is-calling';
410
- case 'connecting':
411
- return '@conversation:chat.call.connecting';
412
- case 'active':
413
- return '@conversation:chat.call.connected';
414
- default:
415
- return '@conversation:chat.call.waiting';
416
- }
417
- }, /* @ts-ignore */
418
- ...(ngDevMode ? [{ debugName: "phaseTextKey" }] : /* istanbul ignore next */ []));
419
- this.primaryRemoteVideo = computed(() => this.remoteVideos()[0] ?? null, /* @ts-ignore */
420
- ...(ngDevMode ? [{ debugName: "primaryRemoteVideo" }] : /* istanbul ignore next */ []));
421
- this.selfParticipant = computed(() => {
422
- const conversation = this.conversation();
423
- const currentUserId = this.currentUserId();
424
- if (!conversation || !currentUserId) {
425
- return null;
426
- }
427
- const self = conversation.participants?.find((participant) => participant.id === currentUserId);
428
- return {
429
- id: currentUserId,
430
- name: self?.name ?? 'You',
431
- avatar: self?.avatar,
432
- isSelf: true,
433
- micMuted: this.micMuted(),
434
- videoEnabled: this.videoEnabled(),
435
- speaking: !this.micMuted(),
436
- };
437
- }, /* @ts-ignore */
438
- ...(ngDevMode ? [{ debugName: "selfParticipant" }] : /* istanbul ignore next */ []));
439
- this.sidebarParticipants = computed(() => {
440
- const conversation = this.conversation();
441
- const currentUserId = this.currentUserId();
442
- if (!conversation) {
443
- return [];
444
- }
445
- const remotes = (conversation.participants ?? [])
446
- .filter((participant) => participant.id !== currentUserId)
447
- .map((participant) => ({
448
- id: participant.id,
449
- name: participant.name,
450
- avatar: participant.avatar,
451
- isSelf: false,
452
- micMuted: false,
453
- videoEnabled: this.remoteVideos().some((item) => item.participantId === participant.id),
454
- speaking: this.remoteVideos().some((item) => item.participantId === participant.id),
455
- }));
456
- const self = this.selfParticipant();
457
- return self ? [...remotes, self] : remotes;
458
- }, /* @ts-ignore */
459
- ...(ngDevMode ? [{ debugName: "sidebarParticipants" }] : /* istanbul ignore next */ []));
460
- effect(() => {
461
- const remoteCount = this.rtc.remoteParticipantCount();
462
- const connectionState = this.rtc.connectionState();
463
- const phase = this.phase();
464
- if (connectionState === ConnectionState.Connected &&
465
- remoteCount > 0 &&
466
- (phase === 'outgoing-waiting' || phase === 'connecting')) {
467
- this.#clearRingTimeout();
468
- this.ringer.stop();
469
- this.phase.set('active');
470
- void this.rtc.ensureAudioPlayback();
471
- }
472
- });
473
- }
474
- ngOnInit() {
475
- this.outcomeSub = this.realtimeBus.messageStream$.subscribe((message) => {
476
- const log = axmReadChatCallLog(message);
477
- const currentCallId = this.callId();
478
- if (!log || !currentCallId || log.callId !== currentCallId) {
479
- return;
480
- }
481
- void this.#closeFromRemoteLog();
482
- });
483
- this.callEventsSub = this.callEvents.events$.subscribe((payload) => {
484
- const currentCallId = this.callId();
485
- if (!currentCallId || payload.callId !== currentCallId) {
486
- return;
487
- }
488
- if (payload.fromUserId === this.currentUserId()) {
489
- return;
490
- }
491
- if (payload.action === 'accept') {
492
- void this.#onPeerAccepted();
493
- return;
494
- }
495
- if (payload.action === 'end') {
496
- void this.#closeFromRemoteEnd();
497
- return;
498
- }
499
- if (payload.action === 'decline' || payload.action === 'cancel') {
500
- void this.#closeFromRemoteLog();
501
- }
502
- });
503
- void this.#bootstrap();
504
- }
505
- ngOnDestroy() {
506
- this.ringer.stop();
507
- this.#clearRingTimeout();
508
- this.outcomeSub?.unsubscribe();
509
- this.callEventsSub?.unsubscribe();
510
- if (!this.closing) {
511
- const phase = this.phase();
512
- if (phase === 'incoming') {
513
- void this.#logOutcome('declined');
514
- }
515
- else if (phase === 'outgoing-waiting') {
516
- void this.#logOutcome('cancelled');
517
- }
518
- }
519
- this.signalInbox.releaseCall(this.callId());
520
- void this.rtc.disconnect();
521
- super.ngOnDestroy();
522
- }
523
- //#endregion
524
- //#region ----- Event Handlers -----
525
- onToggleVideo() {
526
- void this.#setVideoEnabled(!this.videoEnabled());
527
- }
528
- onToggleMute() {
529
- const nextMuted = !this.micMuted();
530
- this.micMuted.set(nextMuted);
531
- void this.rtc.setMicrophoneEnabled(!nextMuted);
532
- if (!nextMuted) {
533
- void this.rtc.ensureAudioPlayback();
534
- }
535
- }
536
- async onScreencast() {
537
- try {
538
- await this.rtc.toggleScreenShare(!this.rtc.isScreenSharing());
539
- }
540
- catch {
541
- const message = await this.translationService.translateAsync('@conversation:chat.call.media-error');
542
- this.#toast(message, 'danger');
543
- }
544
- }
545
- onEndCall() {
546
- if (this.phase() === 'outgoing-waiting') {
547
- void this.#finishWithLog('cancelled');
548
- return;
549
- }
550
- void this.#endActiveCall();
551
- }
552
- onAccept() {
553
- void this.#acceptIncoming();
554
- }
555
- onDecline() {
556
- void this.#finishWithLog('declined');
557
- }
558
- async onAddPeople() {
559
- const message = await this.translationService.translateAsync('@conversation:chat.call.add-people-soon');
560
- this.#toast(message, 'default');
561
- }
562
- onRequestDevices(kind) {
563
- void this.#refreshDevices(kind);
564
- }
565
- onPickDevice(choice) {
566
- if (choice.kind === 'audioinput') {
567
- this.selectedAudioDeviceId.set(choice.deviceId);
568
- }
569
- else {
570
- this.selectedVideoDeviceId.set(choice.deviceId);
571
- }
572
- void this.rtc.switchInputDevice(choice.kind, choice.deviceId);
573
- }
574
- async onShareLink() {
575
- try {
576
- await navigator.clipboard.writeText(window.location.href);
577
- const message = await this.translationService.translateAsync('@conversation:chat.call.link-copied');
578
- this.#toast(message, 'success');
579
- }
580
- catch {
581
- const message = await this.translationService.translateAsync('@conversation:chat.call.link-copy-failed');
582
- this.#toast(message, 'danger');
583
- }
584
- }
585
- //#endregion
586
- //#region ----- Bootstrap -----
587
- async #bootstrap() {
588
- const conversation = this.sessionConversation;
589
- const conversationId = conversation?.id?.trim();
590
- const media = this.sessionMedia === 'video' ? 'video' : 'voice';
591
- const phase = this.sessionPhase === 'incoming' ||
592
- this.sessionPhase === 'active' ||
593
- this.sessionPhase === 'connecting' ||
594
- this.sessionPhase === 'outgoing-waiting'
595
- ? this.sessionPhase
596
- : 'outgoing-waiting';
597
- this.mediaType.set(media);
598
- this.phase.set(phase);
599
- this.videoEnabled.set(media === 'video');
600
- this.callId.set(this.sessionCallId?.trim() || crypto.randomUUID());
601
- if (!conversation || !conversationId) {
602
- this.loading.set(false);
603
- this.error.set(true);
604
- return;
605
- }
606
- this.loading.set(true);
607
- this.error.set(false);
608
- this.errorMessage.set(null);
609
- this.conversation.set(conversation);
610
- try {
611
- const currentUser = await this.userApi.getCurrentUser();
612
- this.currentUserId.set(currentUser.id);
613
- this.videoEnabled.set(media === 'video');
614
- this.micMuted.set(false);
615
- if (phase === 'incoming') {
616
- this.ringer.playIncoming();
617
- void this.#refreshDevices();
618
- return;
619
- }
620
- this.phase.set('outgoing-waiting');
621
- this.ringer.playOutgoing();
622
- await this.#ringPeers(currentUser.id);
623
- this.#armRingTimeout();
624
- void this.#connectMedia(currentUser.id, currentUser.name, conversationId, media).catch(async (error) => {
625
- const fallback = await this.translationService.translateAsync('@conversation:chat.call.connect-error');
626
- this.#toast(error instanceof Error && error.message ? error.message : fallback, 'danger');
627
- });
628
- void this.#refreshDevices();
629
- }
630
- catch (error) {
631
- this.error.set(true);
632
- const fallback = await this.translationService.translateAsync('@conversation:chat.call.connect-error');
633
- this.errorMessage.set(error instanceof Error && error.message ? error.message : fallback);
634
- }
635
- finally {
636
- this.loading.set(false);
637
- }
638
- }
639
- //#endregion
640
- //#region ----- Call Signaling -----
641
- async #connectMedia(participantIdentity, participantName, conversationId, media) {
642
- await this.rtc.connect({
643
- conversationId,
644
- participantIdentity,
645
- participantName,
646
- media,
647
- });
648
- }
649
- async #ringPeers(fromUserId) {
650
- const conversation = this.conversation();
651
- const callId = this.callId();
652
- if (!conversation?.id || !callId) {
653
- return;
654
- }
655
- const targetUserIds = (conversation.participants ?? [])
656
- .map((participant) => participant.id?.trim())
657
- .filter((id) => Boolean(id) && id !== fromUserId);
658
- try {
659
- await this.callSignal.signal({
660
- conversationId: conversation.id,
661
- action: 'invite',
662
- media: this.mediaType(),
663
- callId,
664
- targetUserIds,
665
- });
666
- }
667
- catch {
668
- // Built-in realtime is the primary ring path.
669
- }
670
- }
671
- async #acceptIncoming() {
672
- if (this.accepting || this.closing) {
673
- return;
674
- }
675
- const conversationId = this.conversation()?.id?.trim();
676
- const currentUserId = this.currentUserId();
677
- if (!conversationId || !currentUserId) {
678
- return;
679
- }
680
- this.accepting = true;
681
- this.#clearRingTimeout();
682
- this.ringer.stop();
683
- this.phase.set('connecting');
684
- void this.#signalCallAction('accept');
685
- try {
686
- await this.#connectMedia(currentUserId, this.#selfDisplayName(currentUserId), conversationId, this.mediaType());
687
- this.phase.set('active');
688
- void this.rtc.ensureAudioPlayback();
689
- if (this.mediaType() === 'video') {
690
- void this.#setVideoEnabled(true);
691
- }
692
- }
693
- catch (error) {
694
- this.accepting = false;
695
- this.phase.set('incoming');
696
- this.ringer.playIncoming();
697
- const fallback = await this.translationService.translateAsync('@conversation:chat.call.connect-error');
698
- this.#toast(error instanceof Error && error.message ? error.message : fallback, 'danger');
699
- }
700
- }
701
- async #onPeerAccepted() {
702
- if (this.closing) {
703
- return;
704
- }
705
- this.#clearRingTimeout();
706
- this.ringer.stop();
707
- this.phase.set(this.rtc.connectionState() === ConnectionState.Connected ? 'active' : 'connecting');
708
- void this.rtc.ensureAudioPlayback();
709
- }
710
- async #signalCallAction(action) {
711
- const conversation = this.conversation();
712
- const callId = this.callId();
713
- const fromUserId = this.currentUserId();
714
- if (!conversation?.id || !callId || !fromUserId) {
715
- return;
716
- }
717
- const targetUserIds = (conversation.participants ?? [])
718
- .map((participant) => participant.id?.trim())
719
- .filter((id) => Boolean(id) && id !== fromUserId);
720
- try {
721
- await this.callSignal.signal({
722
- conversationId: conversation.id,
723
- action,
724
- media: this.mediaType(),
725
- callId,
726
- targetUserIds,
727
- });
728
- }
729
- catch {
730
- // Media join is the source of truth; signaling is best-effort.
731
- }
732
- }
733
- #selfDisplayName(currentUserId) {
734
- return this.conversation()?.participants?.find((participant) => participant.id === currentUserId)?.name;
735
- }
736
- #armRingTimeout() {
737
- this.#clearRingTimeout();
738
- this.ringTimeoutId = setTimeout(() => {
739
- if (this.phase() === 'outgoing-waiting') {
740
- this.ringer.stop();
741
- void this.translationService
742
- .translateAsync('@conversation:chat.call.no-answer')
743
- .then((message) => this.#toast(message, 'default'));
744
- }
745
- }, 45_000);
746
- }
747
- #clearRingTimeout() {
748
- if (this.ringTimeoutId != null) {
749
- clearTimeout(this.ringTimeoutId);
750
- this.ringTimeoutId = null;
751
- }
752
- }
753
- async #finishWithLog(outcome) {
754
- if (this.closing) {
755
- return;
756
- }
757
- this.closing = true;
758
- this.ringer.stop();
759
- this.#clearRingTimeout();
760
- await this.#signalCallAction(outcome === 'declined' ? 'decline' : 'cancel');
761
- await this.#logOutcome(outcome);
762
- await this.#closeLocal();
763
- }
764
- async #endActiveCall() {
765
- if (this.closing) {
766
- return;
767
- }
768
- this.closing = true;
769
- this.ringer.stop();
770
- this.#clearRingTimeout();
771
- await this.#signalCallAction('end');
772
- await this.#closeLocal();
773
- }
774
- async #logOutcome(outcome) {
775
- const conversationId = this.conversation()?.id?.trim();
776
- const callId = this.callId();
777
- if (!conversationId || !callId) {
778
- return;
779
- }
780
- try {
781
- await this.callSignal.log({
782
- conversationId,
783
- outcome,
784
- media: this.mediaType(),
785
- callId,
786
- });
787
- }
788
- catch {
789
- // Transcript write is best-effort; still close the session.
790
- }
791
- }
792
- async #closeFromRemoteLog() {
793
- if (this.closing || this.phase() === 'active') {
794
- return;
795
- }
796
- this.closing = true;
797
- this.ringer.stop();
798
- this.#clearRingTimeout();
799
- await this.#closeLocal();
800
- }
801
- async #closeFromRemoteEnd() {
802
- if (this.closing) {
803
- return;
804
- }
805
- this.closing = true;
806
- this.ringer.stop();
807
- this.#clearRingTimeout();
808
- await this.#closeLocal();
809
- }
810
- async #closeLocal() {
811
- this.ringer.stop();
812
- this.signalInbox.releaseCall(this.callId());
813
- await this.rtc.disconnect();
814
- this.close();
815
- }
816
- //#endregion
817
- //#region ----- Media Helpers -----
818
- async #refreshDevices(kind) {
819
- try {
820
- if (!kind || kind === 'audioinput') {
821
- this.audioDevices.set(await this.rtc.listInputDevices('audioinput'));
822
- }
823
- if (!kind || kind === 'videoinput') {
824
- this.videoDevices.set(await this.rtc.listInputDevices('videoinput'));
825
- }
826
- }
827
- catch {
828
- // Device lists stay empty until the browser grants media permission.
829
- }
830
- }
831
- async #setVideoEnabled(enabled) {
832
- this.videoEnabled.set(enabled);
833
- if (enabled) {
834
- this.mediaType.set('video');
835
- }
836
- try {
837
- await this.rtc.setCameraEnabled(enabled);
838
- }
839
- catch {
840
- this.videoEnabled.set(false);
841
- const message = await this.translationService.translateAsync('@conversation:chat.call.media-error');
842
- this.#toast(message, 'danger');
843
- }
844
- }
845
- #toast(content, color) {
846
- this.toastService.show({
847
- color,
848
- content,
849
- location: 'bottom-center',
850
- timeOut: 3000,
851
- });
852
- }
853
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatCallSessionPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
854
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: AXMChatCallSessionPageComponent, isStandalone: true, selector: "axm-chat-call-session-page", host: { classAttribute: "ax-dark" }, providers: [...AXM_CHAT_CALL_RTC_PROVIDERS], usesInheritance: true, ngImport: i0, template: "<div class=\"axm-chat-call-session\" [class.axm-chat-call-session--group]=\"isGroupLayout()\">\n <!-- ----- Loading conversation / connecting RTC ----- -->\n @if (loading()) {\n <div class=\"axm-chat-call-session__status-block\">\n {{ '@conversation:chat.call.loading' | translate | async }}\n </div>\n }\n <!-- ----- Load / connect error ----- -->\n @else if (error()) {\n <div class=\"axm-chat-call-session__status-block axm-chat-call-session__status-block--error\">\n <p>{{ errorMessage() || ('@conversation:chat.call.load-error' | translate | async) }}</p>\n <button type=\"button\" class=\"axm-chat-call-session__text-button\" (click)=\"onEndCall()\">\n {{ '@conversation:chat.call.cancel' | translate | async }}\n </button>\n </div>\n }\n <!-- ----- Call session ----- -->\n @else {\n <div class=\"axm-chat-call-session__shell\">\n <div\n class=\"axm-chat-call-session__main\"\n [class.axm-chat-call-session__main--idle]=\"idleLayout()\"\n >\n <div class=\"axm-chat-call-session__body\">\n <!-- ----- Profile header (hidden for active group grid) ----- -->\n @if (!(isGroupLayout() && phase() === 'active')) {\n <div\n class=\"axm-chat-call-session__header\"\n [class.axm-chat-call-session__header--compact]=\"showStage()\"\n >\n <axp-avatar\n [size]=\"headerAvatarSize()\"\n [fullName]=\"peerDisplayName()\"\n [src]=\"peerAvatarSrc()\"\n />\n <h1 class=\"axm-chat-call-session__name\">{{ peerDisplayName() }}</h1>\n <p class=\"axm-chat-call-session__phase-text\">{{ phaseTextKey() | translate | async }}</p>\n </div>\n }\n\n <!-- ----- Video / tile stage ----- -->\n @if (showStage()) {\n <div\n class=\"axm-chat-call-session__stage\"\n [class.axm-chat-call-session__stage--group]=\"isGroupLayout() && phase() === 'active'\"\n >\n @if (isGroupLayout() && phase() === 'active') {\n @for (remote of remoteVideos(); track remote.participantId + ':' + remote.track.sid) {\n <div class=\"axm-chat-call-session__tile axm-chat-call-session__tile--speaking\">\n <video\n class=\"axm-chat-call-session__video\"\n autoplay\n playsinline\n [axmChatCallTrack]=\"remote.track\"\n ></video>\n <span class=\"axm-chat-call-session__tile-name\">{{ remote.participantName }}</span>\n </div>\n }\n <div\n class=\"axm-chat-call-session__tile\"\n [class.axm-chat-call-session__tile--speaking]=\"!micMuted()\"\n >\n @if (videoEnabled() && localVideoTrack(); as localTrack) {\n <video\n class=\"axm-chat-call-session__video\"\n autoplay\n playsinline\n muted\n [axmChatCallTrack]=\"localTrack\"\n ></video>\n } @else if (selfParticipant(); as self) {\n <axp-avatar [size]=\"96\" [fullName]=\"self.name\" [src]=\"self.avatar ?? ''\" />\n }\n <span class=\"axm-chat-call-session__tile-name\">\n {{ selfParticipant()?.name || '' }}\n </span>\n </div>\n } @else {\n <div class=\"axm-chat-call-session__tile axm-chat-call-session__tile--primary\">\n @if (primaryRemoteVideo(); as remote) {\n <video\n class=\"axm-chat-call-session__video\"\n autoplay\n playsinline\n [axmChatCallTrack]=\"remote.track\"\n ></video>\n }\n <!-- ----- Local camera fills the stage until the peer joins ----- -->\n @else if (videoEnabled() && localVideoTrack(); as localTrack) {\n <video\n class=\"axm-chat-call-session__video\"\n autoplay\n playsinline\n muted\n [axmChatCallTrack]=\"localTrack\"\n ></video>\n }\n <!-- ----- Avatar placeholder when no camera track is published ----- -->\n @else {\n <axp-avatar [size]=\"160\" [fullName]=\"peerDisplayName()\" [src]=\"peerAvatarSrc()\" />\n }\n </div>\n\n @if (primaryRemoteVideo() && videoEnabled() && localVideoTrack(); as localTrack) {\n <div class=\"axm-chat-call-session__pip\">\n <video\n class=\"axm-chat-call-session__video\"\n autoplay\n playsinline\n muted\n [axmChatCallTrack]=\"localTrack\"\n ></video>\n </div>\n }\n }\n </div>\n }\n </div>\n\n <axm-chat-call-controls\n [phase]=\"phase()\"\n [videoEnabled]=\"videoEnabled()\"\n [micMuted]=\"micMuted()\"\n [showAddPeople]=\"phase() === 'active'\"\n [showScreencast]=\"phase() === 'active'\"\n [audioDevices]=\"audioDevices()\"\n [videoDevices]=\"videoDevices()\"\n [selectedAudioDeviceId]=\"selectedAudioDeviceId()\"\n [selectedVideoDeviceId]=\"selectedVideoDeviceId()\"\n (screencast)=\"onScreencast()\"\n (toggleVideo)=\"onToggleVideo()\"\n (toggleMute)=\"onToggleMute()\"\n (endCall)=\"onEndCall()\"\n (accept)=\"onAccept()\"\n (decline)=\"onDecline()\"\n (addPeople)=\"onAddPeople()\"\n (requestDevices)=\"onRequestDevices($event)\"\n (pickDevice)=\"onPickDevice($event)\"\n />\n </div>\n\n @if (isGroupLayout() && phase() === 'active') {\n <axm-chat-call-sidebar\n [participants]=\"sidebarParticipants()\"\n (addPeople)=\"onAddPeople()\"\n (shareLink)=\"onShareLink()\"\n />\n }\n </div>\n }\n</div>\n", styles: [".axm-chat-call-session-overlay .ax-popup-header{display:none}.axm-chat-call-session-overlay .ax-popup .ax-popup-body-container{overflow:hidden;display:flex;flex-direction:column;height:100%}axm-chat-call-session-page{display:flex;flex:1;flex-direction:column;width:100%;height:100%;min-height:0}.axm-chat-call-session{box-sizing:border-box;display:flex;flex:1;width:100%;min-height:0;height:100%}.axm-chat-call-session__shell{display:flex;flex:1;width:100%;min-height:0;height:100%}.axm-chat-call-session__main{display:flex;flex:1;flex-direction:column;align-items:center;min-width:0;min-height:0;padding:1.25rem 1.25rem .5rem}.axm-chat-call-session__body{display:flex;flex:1;flex-direction:column;align-items:center;width:100%;min-height:0}.axm-chat-call-session__main--idle .axm-chat-call-session__body{justify-content:center}.axm-chat-call-session__header{display:flex;flex-direction:column;align-items:center;gap:.5rem;margin-top:1.5rem;text-align:center}.axm-chat-call-session__main--idle .axm-chat-call-session__header{margin-top:0}.axm-chat-call-session__header--compact{margin-top:.25rem;margin-bottom:.5rem}.axm-chat-call-session__name{margin:0;font-size:1.25rem;font-weight:700;line-height:1.3}.axm-chat-call-session__header--compact .axm-chat-call-session__name{font-size:1rem}.axm-chat-call-session__phase-text{margin:0;font-size:.875rem;opacity:.65}.axm-chat-call-session__stage{position:relative;display:flex;flex:1;align-items:center;justify-content:center;width:100%;min-height:0;margin:.75rem 0;overflow:hidden}.axm-chat-call-session__stage--group{display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));gap:.75rem;align-content:center;padding:.5rem}.axm-chat-call-session__tile{position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden;border-radius:.75rem;background:#101113;border:2px solid transparent}.axm-chat-call-session__tile--primary{width:min(100%,36rem);aspect-ratio:3 / 4;max-height:min(70vh,40rem)}.axm-chat-call-session__stage--group .axm-chat-call-session__tile{aspect-ratio:3 / 4;min-height:12rem}.axm-chat-call-session__tile--speaking{border-color:#4caf50}.axm-chat-call-session__tile-name{position:absolute;inset-inline-start:.65rem;bottom:.65rem;padding:.15rem .45rem;border-radius:.35rem;background:#00000073;font-size:.75rem}.axm-chat-call-session__video{width:100%;height:100%;object-fit:cover;background:#000}.axm-chat-call-session__pip{position:absolute;right:1rem;bottom:1rem;width:8.5rem;aspect-ratio:3 / 4;overflow:hidden;border-radius:.5rem;border:1px solid rgba(255,255,255,.2);box-shadow:0 8px 24px #00000059;background:#000}.axm-chat-call-session__status-block{display:flex;flex:1;flex-direction:column;align-items:center;justify-content:center;gap:1rem;width:100%;min-height:0;padding:2rem;text-align:center}.axm-chat-call-session__status-block--error{color:#ff8a80}.axm-chat-call-session__text-button{padding:.5rem 1rem;border:1px solid currentColor;border-radius:9999px;background:transparent;color:inherit;cursor:pointer;opacity:.85}axm-chat-call-controls{flex-shrink:0;margin-top:auto;width:100%}\n"], dependencies: [{ kind: "component", type: AXPAvatarComponent, selector: "axp-avatar", inputs: ["size", "src", "fullName", "fallbackText", "placeholder"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "component", type: AXMChatCallControlsComponent, selector: "axm-chat-call-controls", inputs: ["phase", "videoEnabled", "micMuted", "showAddPeople", "showScreencast", "audioDevices", "videoDevices", "selectedAudioDeviceId", "selectedVideoDeviceId"], outputs: ["screencast", "toggleVideo", "toggleMute", "endCall", "accept", "decline", "addPeople", "pickDevice", "requestDevices"] }, { kind: "component", type: AXMChatCallSidebarComponent, selector: "axm-chat-call-sidebar", inputs: ["participants"], outputs: ["addPeople", "shareLink"] }, { kind: "directive", type: AXMChatCallVideoAttachDirective, selector: "video[axmChatCallTrack]", inputs: ["axmChatCallTrack"] }, { kind: "pipe", type: i1.AXTranslatorPipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
855
- }
856
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatCallSessionPageComponent, decorators: [{
857
- type: Component,
858
- args: [{ selector: 'axm-chat-call-session-page', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
859
- class: 'ax-dark',
860
- }, providers: [...AXM_CHAT_CALL_RTC_PROVIDERS], imports: [
861
- AXPAvatarComponent,
862
- AXTranslationModule,
863
- AsyncPipe,
864
- AXMChatCallControlsComponent,
865
- AXMChatCallSidebarComponent,
866
- AXMChatCallVideoAttachDirective,
867
- ], template: "<div class=\"axm-chat-call-session\" [class.axm-chat-call-session--group]=\"isGroupLayout()\">\n <!-- ----- Loading conversation / connecting RTC ----- -->\n @if (loading()) {\n <div class=\"axm-chat-call-session__status-block\">\n {{ '@conversation:chat.call.loading' | translate | async }}\n </div>\n }\n <!-- ----- Load / connect error ----- -->\n @else if (error()) {\n <div class=\"axm-chat-call-session__status-block axm-chat-call-session__status-block--error\">\n <p>{{ errorMessage() || ('@conversation:chat.call.load-error' | translate | async) }}</p>\n <button type=\"button\" class=\"axm-chat-call-session__text-button\" (click)=\"onEndCall()\">\n {{ '@conversation:chat.call.cancel' | translate | async }}\n </button>\n </div>\n }\n <!-- ----- Call session ----- -->\n @else {\n <div class=\"axm-chat-call-session__shell\">\n <div\n class=\"axm-chat-call-session__main\"\n [class.axm-chat-call-session__main--idle]=\"idleLayout()\"\n >\n <div class=\"axm-chat-call-session__body\">\n <!-- ----- Profile header (hidden for active group grid) ----- -->\n @if (!(isGroupLayout() && phase() === 'active')) {\n <div\n class=\"axm-chat-call-session__header\"\n [class.axm-chat-call-session__header--compact]=\"showStage()\"\n >\n <axp-avatar\n [size]=\"headerAvatarSize()\"\n [fullName]=\"peerDisplayName()\"\n [src]=\"peerAvatarSrc()\"\n />\n <h1 class=\"axm-chat-call-session__name\">{{ peerDisplayName() }}</h1>\n <p class=\"axm-chat-call-session__phase-text\">{{ phaseTextKey() | translate | async }}</p>\n </div>\n }\n\n <!-- ----- Video / tile stage ----- -->\n @if (showStage()) {\n <div\n class=\"axm-chat-call-session__stage\"\n [class.axm-chat-call-session__stage--group]=\"isGroupLayout() && phase() === 'active'\"\n >\n @if (isGroupLayout() && phase() === 'active') {\n @for (remote of remoteVideos(); track remote.participantId + ':' + remote.track.sid) {\n <div class=\"axm-chat-call-session__tile axm-chat-call-session__tile--speaking\">\n <video\n class=\"axm-chat-call-session__video\"\n autoplay\n playsinline\n [axmChatCallTrack]=\"remote.track\"\n ></video>\n <span class=\"axm-chat-call-session__tile-name\">{{ remote.participantName }}</span>\n </div>\n }\n <div\n class=\"axm-chat-call-session__tile\"\n [class.axm-chat-call-session__tile--speaking]=\"!micMuted()\"\n >\n @if (videoEnabled() && localVideoTrack(); as localTrack) {\n <video\n class=\"axm-chat-call-session__video\"\n autoplay\n playsinline\n muted\n [axmChatCallTrack]=\"localTrack\"\n ></video>\n } @else if (selfParticipant(); as self) {\n <axp-avatar [size]=\"96\" [fullName]=\"self.name\" [src]=\"self.avatar ?? ''\" />\n }\n <span class=\"axm-chat-call-session__tile-name\">\n {{ selfParticipant()?.name || '' }}\n </span>\n </div>\n } @else {\n <div class=\"axm-chat-call-session__tile axm-chat-call-session__tile--primary\">\n @if (primaryRemoteVideo(); as remote) {\n <video\n class=\"axm-chat-call-session__video\"\n autoplay\n playsinline\n [axmChatCallTrack]=\"remote.track\"\n ></video>\n }\n <!-- ----- Local camera fills the stage until the peer joins ----- -->\n @else if (videoEnabled() && localVideoTrack(); as localTrack) {\n <video\n class=\"axm-chat-call-session__video\"\n autoplay\n playsinline\n muted\n [axmChatCallTrack]=\"localTrack\"\n ></video>\n }\n <!-- ----- Avatar placeholder when no camera track is published ----- -->\n @else {\n <axp-avatar [size]=\"160\" [fullName]=\"peerDisplayName()\" [src]=\"peerAvatarSrc()\" />\n }\n </div>\n\n @if (primaryRemoteVideo() && videoEnabled() && localVideoTrack(); as localTrack) {\n <div class=\"axm-chat-call-session__pip\">\n <video\n class=\"axm-chat-call-session__video\"\n autoplay\n playsinline\n muted\n [axmChatCallTrack]=\"localTrack\"\n ></video>\n </div>\n }\n }\n </div>\n }\n </div>\n\n <axm-chat-call-controls\n [phase]=\"phase()\"\n [videoEnabled]=\"videoEnabled()\"\n [micMuted]=\"micMuted()\"\n [showAddPeople]=\"phase() === 'active'\"\n [showScreencast]=\"phase() === 'active'\"\n [audioDevices]=\"audioDevices()\"\n [videoDevices]=\"videoDevices()\"\n [selectedAudioDeviceId]=\"selectedAudioDeviceId()\"\n [selectedVideoDeviceId]=\"selectedVideoDeviceId()\"\n (screencast)=\"onScreencast()\"\n (toggleVideo)=\"onToggleVideo()\"\n (toggleMute)=\"onToggleMute()\"\n (endCall)=\"onEndCall()\"\n (accept)=\"onAccept()\"\n (decline)=\"onDecline()\"\n (addPeople)=\"onAddPeople()\"\n (requestDevices)=\"onRequestDevices($event)\"\n (pickDevice)=\"onPickDevice($event)\"\n />\n </div>\n\n @if (isGroupLayout() && phase() === 'active') {\n <axm-chat-call-sidebar\n [participants]=\"sidebarParticipants()\"\n (addPeople)=\"onAddPeople()\"\n (shareLink)=\"onShareLink()\"\n />\n }\n </div>\n }\n</div>\n", styles: [".axm-chat-call-session-overlay .ax-popup-header{display:none}.axm-chat-call-session-overlay .ax-popup .ax-popup-body-container{overflow:hidden;display:flex;flex-direction:column;height:100%}axm-chat-call-session-page{display:flex;flex:1;flex-direction:column;width:100%;height:100%;min-height:0}.axm-chat-call-session{box-sizing:border-box;display:flex;flex:1;width:100%;min-height:0;height:100%}.axm-chat-call-session__shell{display:flex;flex:1;width:100%;min-height:0;height:100%}.axm-chat-call-session__main{display:flex;flex:1;flex-direction:column;align-items:center;min-width:0;min-height:0;padding:1.25rem 1.25rem .5rem}.axm-chat-call-session__body{display:flex;flex:1;flex-direction:column;align-items:center;width:100%;min-height:0}.axm-chat-call-session__main--idle .axm-chat-call-session__body{justify-content:center}.axm-chat-call-session__header{display:flex;flex-direction:column;align-items:center;gap:.5rem;margin-top:1.5rem;text-align:center}.axm-chat-call-session__main--idle .axm-chat-call-session__header{margin-top:0}.axm-chat-call-session__header--compact{margin-top:.25rem;margin-bottom:.5rem}.axm-chat-call-session__name{margin:0;font-size:1.25rem;font-weight:700;line-height:1.3}.axm-chat-call-session__header--compact .axm-chat-call-session__name{font-size:1rem}.axm-chat-call-session__phase-text{margin:0;font-size:.875rem;opacity:.65}.axm-chat-call-session__stage{position:relative;display:flex;flex:1;align-items:center;justify-content:center;width:100%;min-height:0;margin:.75rem 0;overflow:hidden}.axm-chat-call-session__stage--group{display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));gap:.75rem;align-content:center;padding:.5rem}.axm-chat-call-session__tile{position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden;border-radius:.75rem;background:#101113;border:2px solid transparent}.axm-chat-call-session__tile--primary{width:min(100%,36rem);aspect-ratio:3 / 4;max-height:min(70vh,40rem)}.axm-chat-call-session__stage--group .axm-chat-call-session__tile{aspect-ratio:3 / 4;min-height:12rem}.axm-chat-call-session__tile--speaking{border-color:#4caf50}.axm-chat-call-session__tile-name{position:absolute;inset-inline-start:.65rem;bottom:.65rem;padding:.15rem .45rem;border-radius:.35rem;background:#00000073;font-size:.75rem}.axm-chat-call-session__video{width:100%;height:100%;object-fit:cover;background:#000}.axm-chat-call-session__pip{position:absolute;right:1rem;bottom:1rem;width:8.5rem;aspect-ratio:3 / 4;overflow:hidden;border-radius:.5rem;border:1px solid rgba(255,255,255,.2);box-shadow:0 8px 24px #00000059;background:#000}.axm-chat-call-session__status-block{display:flex;flex:1;flex-direction:column;align-items:center;justify-content:center;gap:1rem;width:100%;min-height:0;padding:2rem;text-align:center}.axm-chat-call-session__status-block--error{color:#ff8a80}.axm-chat-call-session__text-button{padding:.5rem 1rem;border:1px solid currentColor;border-radius:9999px;background:transparent;color:inherit;cursor:pointer;opacity:.85}axm-chat-call-controls{flex-shrink:0;margin-top:auto;width:100%}\n"] }]
868
- }], ctorParameters: () => [] });
869
-
870
- export { AXMChatCallSessionPageComponent };
871
- //# sourceMappingURL=acorex-platform-module-conversation-client-chat-call-session-page.component-DkX4JSbN.mjs.map