@acorex-platform/module-conversation-client 0.0.21

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 (35) hide show
  1. package/fesm2022/acorex-platform-module-conversation-client-acorex-platform-module-conversation-client-DsjrXRZ6.mjs +12766 -0
  2. package/fesm2022/acorex-platform-module-conversation-client-acorex-platform-module-conversation-client-DsjrXRZ6.mjs.map +1 -0
  3. package/fesm2022/acorex-platform-module-conversation-client-after-message-sent.command-D-DZ9ylV.mjs +35 -0
  4. package/fesm2022/acorex-platform-module-conversation-client-after-message-sent.command-D-DZ9ylV.mjs.map +1 -0
  5. package/fesm2022/acorex-platform-module-conversation-client-assist-delegated-agent-detail-popup.component-BfjJCQ-e.mjs +492 -0
  6. package/fesm2022/acorex-platform-module-conversation-client-assist-delegated-agent-detail-popup.component-BfjJCQ-e.mjs.map +1 -0
  7. package/fesm2022/acorex-platform-module-conversation-client-assist-tool-detail-popup.component-GC58B_n0.mjs +120 -0
  8. package/fesm2022/acorex-platform-module-conversation-client-assist-tool-detail-popup.component-GC58B_n0.mjs.map +1 -0
  9. package/fesm2022/acorex-platform-module-conversation-client-chat-call-session-page.component-DkX4JSbN.mjs +871 -0
  10. package/fesm2022/acorex-platform-module-conversation-client-chat-call-session-page.component-DkX4JSbN.mjs.map +1 -0
  11. package/fesm2022/acorex-platform-module-conversation-client-chat-call-start-popup.component-D4go5xbr.mjs +89 -0
  12. package/fesm2022/acorex-platform-module-conversation-client-chat-call-start-popup.component-D4go5xbr.mjs.map +1 -0
  13. package/fesm2022/acorex-platform-module-conversation-client-chat-conversation-info-media-view.component-BVJdVPke.mjs +108 -0
  14. package/fesm2022/acorex-platform-module-conversation-client-chat-conversation-info-media-view.component-BVJdVPke.mjs.map +1 -0
  15. package/fesm2022/acorex-platform-module-conversation-client-comments-page.component-Bq4SdK0_.mjs +83 -0
  16. package/fesm2022/acorex-platform-module-conversation-client-comments-page.component-Bq4SdK0_.mjs.map +1 -0
  17. package/fesm2022/acorex-platform-module-conversation-client-contracts.mjs +225 -0
  18. package/fesm2022/acorex-platform-module-conversation-client-contracts.mjs.map +1 -0
  19. package/fesm2022/acorex-platform-module-conversation-client-emit-typing-indicator.command-BzsN1_ns.mjs +29 -0
  20. package/fesm2022/acorex-platform-module-conversation-client-emit-typing-indicator.command-BzsN1_ns.mjs.map +1 -0
  21. package/fesm2022/acorex-platform-module-conversation-client-mark-conversation-unread.command-BlYDwGVe.mjs +30 -0
  22. package/fesm2022/acorex-platform-module-conversation-client-mark-conversation-unread.command-BlYDwGVe.mjs.map +1 -0
  23. package/fesm2022/acorex-platform-module-conversation-client-open-chat.command-B03q60Qx.mjs +42 -0
  24. package/fesm2022/acorex-platform-module-conversation-client-open-chat.command-B03q60Qx.mjs.map +1 -0
  25. package/fesm2022/acorex-platform-module-conversation-client-retry-assist-response.command-BQLK6F69.mjs +34 -0
  26. package/fesm2022/acorex-platform-module-conversation-client-retry-assist-response.command-BQLK6F69.mjs.map +1 -0
  27. package/fesm2022/acorex-platform-module-conversation-client-start-direct-chat.command-CfSIVHDu.mjs +80 -0
  28. package/fesm2022/acorex-platform-module-conversation-client-start-direct-chat.command-CfSIVHDu.mjs.map +1 -0
  29. package/fesm2022/acorex-platform-module-conversation-client-sync-read-state.command-B1Z_gZ6O.mjs +142 -0
  30. package/fesm2022/acorex-platform-module-conversation-client-sync-read-state.command-B1Z_gZ6O.mjs.map +1 -0
  31. package/fesm2022/acorex-platform-module-conversation-client.mjs +2 -0
  32. package/fesm2022/acorex-platform-module-conversation-client.mjs.map +1 -0
  33. package/package.json +28 -0
  34. package/types/acorex-platform-module-conversation-client-contracts.d.ts +497 -0
  35. package/types/acorex-platform-module-conversation-client.d.ts +1614 -0
@@ -0,0 +1,871 @@
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