@realtimexsco/live-chat 1.5.1 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -8,9 +8,9 @@ var socket_ioClient = require('socket.io-client');
8
8
  var middleware = require('zustand/middleware');
9
9
  var clsx = require('clsx');
10
10
  var tailwindMerge = require('tailwind-merge');
11
- var classVarianceAuthority = require('class-variance-authority');
12
11
  var radixUi = require('radix-ui');
13
12
  var jsxRuntime = require('react/jsx-runtime');
13
+ var classVarianceAuthority = require('class-variance-authority');
14
14
  var React9 = require('react');
15
15
  var lucideReact = require('lucide-react');
16
16
  var reactDom = require('react-dom');
@@ -687,7 +687,7 @@ var init_call_api = __esm({
687
687
  });
688
688
 
689
689
  // src/call/call.types.ts
690
- exports.CALL_MODES = void 0; exports.CALL_STATUS = void 0; var CALL_RING_TIMEOUT_MS, CLIENT_RING_FALLBACK_MS, CALL_LOG_EVENT;
690
+ exports.CALL_MODES = void 0; exports.CALL_STATUS = void 0; var CALL_RING_TIMEOUT_MS, CLIENT_RING_FALLBACK_MS;
691
691
  var init_call_types = __esm({
692
692
  "src/call/call.types.ts"() {
693
693
  exports.CALL_MODES = {
@@ -704,22 +704,15 @@ var init_call_types = __esm({
704
704
  };
705
705
  CALL_RING_TIMEOUT_MS = 4e4;
706
706
  CLIENT_RING_FALLBACK_MS = CALL_RING_TIMEOUT_MS + 5e3;
707
- CALL_LOG_EVENT = "realtimex:call-log";
708
707
  }
709
708
  });
710
- var emitCallLog, ringTimer, clearRingTimer, scheduleRingTimer, idleState; exports.useCallStore = void 0;
709
+ var ringTimer, clearRingTimer, scheduleRingTimer, idleState; exports.useCallStore = void 0;
711
710
  var init_call_store = __esm({
712
711
  "src/call/call.store.ts"() {
713
712
  init_useStore();
714
713
  init_socket_service2();
715
714
  init_call_api();
716
715
  init_call_types();
717
- emitCallLog = (entry) => {
718
- try {
719
- window.dispatchEvent(new CustomEvent(CALL_LOG_EVENT, { detail: entry }));
720
- } catch {
721
- }
722
- };
723
716
  ringTimer = null;
724
717
  clearRingTimer = () => {
725
718
  if (ringTimer) clearTimeout(ringTimer);
@@ -817,6 +810,12 @@ var init_call_store = __esm({
817
810
  clearRingTimer();
818
811
  set({ ...idleState });
819
812
  },
813
+ notifyServerCallFailed: () => {
814
+ const { callId } = get();
815
+ if (callId) {
816
+ exports.socketService.emitCallEnd({ callId });
817
+ }
818
+ },
820
819
  _receiveIncoming: (payload) => {
821
820
  const currentStatus = get().uiStatus;
822
821
  if (currentStatus !== "idle") {
@@ -865,53 +864,60 @@ var init_call_store = __esm({
865
864
  }));
866
865
  },
867
866
  _receiveCancelled: (payload) => {
868
- if (get().callId !== payload.callId) return;
869
- clearRingTimer();
870
- const myUserId = exports.useChatStore.getState().loggedUserDetails?._id;
871
- if (payload.endedBy !== myUserId) {
872
- emitCallLog({
873
- conversationId: payload.conversationId,
874
- mode: payload.mode,
875
- outcome: "missed",
876
- durationSeconds: 0
877
- });
867
+ const state = get();
868
+ if (state.callId !== payload.callId) return;
869
+ if (state.uiStatus === "in-call") {
870
+ console.warn(
871
+ `[realtimex-call] call_cancelled for ${payload.callId} ignored \u2014 this call is already in-call locally; treating as a stale event.`
872
+ );
873
+ return;
878
874
  }
875
+ clearRingTimer();
879
876
  set({ ...idleState });
880
877
  },
878
+ // Missed/ended/declined/etc. call log messages are now persisted by the
879
+ // backend as real chat messages (CALLS_API.md §3.5) — they arrive
880
+ // through the normal dm_receive/group_message_receive pipeline with
881
+ // isSystemMessage: true, so they render automatically with zero extra
882
+ // code here. Nothing to do on this event beyond resetting local state.
881
883
  _receiveEnded: (payload) => {
882
884
  if (get().callId !== payload.callId) return;
883
885
  clearRingTimer();
884
- emitCallLog({
885
- conversationId: payload.conversationId,
886
- mode: payload.mode,
887
- outcome: payload.durationSeconds > 0 ? "completed" : "declined",
888
- durationSeconds: payload.durationSeconds
889
- });
890
886
  set({ ...idleState });
891
887
  },
892
888
  _receiveBusy: (payload) => {
893
- if (get().callId !== payload.callId) return;
889
+ const state = get();
890
+ if (state.callId !== payload.callId) return;
891
+ if (state.uiStatus === "in-call") {
892
+ console.warn(
893
+ `[realtimex-call] call_busy for ${payload.callId} ignored \u2014 this call is already in-call locally; treating as a stale event.`
894
+ );
895
+ return;
896
+ }
894
897
  clearRingTimer();
895
- emitCallLog({
896
- conversationId: payload.conversationId,
897
- mode: payload.mode,
898
- outcome: "busy",
899
- durationSeconds: 0
900
- });
901
898
  set({ ...idleState, error: "That user is already on another call." });
902
899
  },
903
900
  // Guarded by callId like every other _receive* handler — a late/stale
904
901
  // call_missed for a call that has already ended (accepted, rejected,
905
902
  // or replaced by a new one) must not clobber whatever is happening now.
903
+ //
904
+ // Live testing once caught the backend's 40s ring-timeout firing
905
+ // call_missed for a callId that was already call_accepted (timer not
906
+ // cancelled on accept). Backend has since added an atomic
907
+ // only-from-ringing guard (CALLS_API.md §3.3) so this should no longer
908
+ // happen — the uiStatus check below is kept as defense-in-depth rather
909
+ // than removed, since it costs nothing and a regression here would
910
+ // otherwise kill a live call outright.
906
911
  _receiveMissed: (payload) => {
907
- if (get().callId !== payload.callId) return;
912
+ const state = get();
913
+ if (state.callId !== payload.callId) return;
914
+ if (state.uiStatus === "in-call") {
915
+ console.warn(
916
+ `[realtimex-call] call_missed for ${payload.callId} ignored \u2014 this call is already in-call locally; treating as a stale event.`
917
+ );
918
+ return;
919
+ }
908
920
  clearRingTimer();
909
- emitCallLog({
910
- conversationId: payload.conversationId,
911
- mode: payload.mode,
912
- outcome: "missed",
913
- durationSeconds: 0
914
- });
915
921
  set({ ...idleState });
916
922
  }
917
923
  }));
@@ -4322,6 +4328,58 @@ var init_utils2 = __esm({
4322
4328
  "src/lib/utils.ts"() {
4323
4329
  }
4324
4330
  });
4331
+ function Avatar({
4332
+ className,
4333
+ size = "default",
4334
+ ...props
4335
+ }) {
4336
+ return /* @__PURE__ */ jsxRuntime.jsx(
4337
+ radixUi.Avatar.Root,
4338
+ {
4339
+ "data-slot": "avatar",
4340
+ "data-size": size,
4341
+ className: cn(
4342
+ "group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
4343
+ className
4344
+ ),
4345
+ ...props
4346
+ }
4347
+ );
4348
+ }
4349
+ function AvatarImage({
4350
+ className,
4351
+ ...props
4352
+ }) {
4353
+ return /* @__PURE__ */ jsxRuntime.jsx(
4354
+ radixUi.Avatar.Image,
4355
+ {
4356
+ "data-slot": "avatar-image",
4357
+ className: cn("aspect-square size-full", className),
4358
+ ...props
4359
+ }
4360
+ );
4361
+ }
4362
+ function AvatarFallback({
4363
+ className,
4364
+ ...props
4365
+ }) {
4366
+ return /* @__PURE__ */ jsxRuntime.jsx(
4367
+ radixUi.Avatar.Fallback,
4368
+ {
4369
+ "data-slot": "avatar-fallback",
4370
+ className: cn(
4371
+ "bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs",
4372
+ className
4373
+ ),
4374
+ ...props
4375
+ }
4376
+ );
4377
+ }
4378
+ var init_avatar = __esm({
4379
+ "src/ui/avatar.tsx"() {
4380
+ init_utils2();
4381
+ }
4382
+ });
4325
4383
  function Button({
4326
4384
  className,
4327
4385
  variant = "default",
@@ -4376,58 +4434,6 @@ var init_button = __esm({
4376
4434
  );
4377
4435
  }
4378
4436
  });
4379
- function Avatar({
4380
- className,
4381
- size = "default",
4382
- ...props
4383
- }) {
4384
- return /* @__PURE__ */ jsxRuntime.jsx(
4385
- radixUi.Avatar.Root,
4386
- {
4387
- "data-slot": "avatar",
4388
- "data-size": size,
4389
- className: cn(
4390
- "group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
4391
- className
4392
- ),
4393
- ...props
4394
- }
4395
- );
4396
- }
4397
- function AvatarImage({
4398
- className,
4399
- ...props
4400
- }) {
4401
- return /* @__PURE__ */ jsxRuntime.jsx(
4402
- radixUi.Avatar.Image,
4403
- {
4404
- "data-slot": "avatar-image",
4405
- className: cn("aspect-square size-full", className),
4406
- ...props
4407
- }
4408
- );
4409
- }
4410
- function AvatarFallback({
4411
- className,
4412
- ...props
4413
- }) {
4414
- return /* @__PURE__ */ jsxRuntime.jsx(
4415
- radixUi.Avatar.Fallback,
4416
- {
4417
- "data-slot": "avatar-fallback",
4418
- className: cn(
4419
- "bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs",
4420
- className
4421
- ),
4422
- ...props
4423
- }
4424
- );
4425
- }
4426
- var init_avatar = __esm({
4427
- "src/ui/avatar.tsx"() {
4428
- init_utils2();
4429
- }
4430
- });
4431
4437
 
4432
4438
  // src/call/videosdk.loader.ts
4433
4439
  var loadVideoSDK;
@@ -4463,6 +4469,7 @@ var init_ParticipantView = __esm({
4463
4469
  const { webcamStream, micStream, webcamOn, micOn, displayName, isActiveSpeaker } = sdk.useParticipant(participantId);
4464
4470
  const videoRef = React9.useRef(null);
4465
4471
  const audioRef = React9.useRef(null);
4472
+ const [hasFrame, setHasFrame] = React9.useState(false);
4466
4473
  React9.useEffect(() => {
4467
4474
  const el = videoRef.current;
4468
4475
  if (!el) return;
@@ -4471,6 +4478,7 @@ var init_ParticipantView = __esm({
4471
4478
  el.play().catch(() => void 0);
4472
4479
  } else {
4473
4480
  el.srcObject = null;
4481
+ setHasFrame(false);
4474
4482
  }
4475
4483
  }, [webcamOn, webcamStream]);
4476
4484
  React9.useEffect(() => {
@@ -4485,6 +4493,7 @@ var init_ParticipantView = __esm({
4485
4493
  }
4486
4494
  }, [micOn, micStream, isLocal]);
4487
4495
  const name = displayName || fallbackName || "Participant";
4496
+ const showVideo = webcamOn && hasFrame;
4488
4497
  return /* @__PURE__ */ jsxRuntime.jsxs(
4489
4498
  "div",
4490
4499
  {
@@ -4500,14 +4509,15 @@ var init_ParticipantView = __esm({
4500
4509
  ref: videoRef,
4501
4510
  muted: isLocal,
4502
4511
  playsInline: true,
4512
+ onPlaying: () => setHasFrame(true),
4503
4513
  className: cn(
4504
4514
  "size-full object-cover",
4505
- webcamOn ? "block" : "hidden"
4515
+ showVideo ? "block" : "hidden"
4506
4516
  )
4507
4517
  }
4508
4518
  ),
4509
4519
  !isLocal && /* @__PURE__ */ jsxRuntime.jsx("audio", { ref: audioRef, autoPlay: true, playsInline: true }),
4510
- !webcamOn && /* @__PURE__ */ jsxRuntime.jsxs(Avatar, { size: "lg", className: "size-20", children: [
4520
+ !showVideo && /* @__PURE__ */ jsxRuntime.jsxs(Avatar, { size: "lg", className: "size-20", children: [
4511
4521
  /* @__PURE__ */ jsxRuntime.jsx(AvatarImage, { src: fallbackImage, alt: name }),
4512
4522
  /* @__PURE__ */ jsxRuntime.jsx(AvatarFallback, { className: "bg-neutral-700 text-xl font-medium text-white", children: name.charAt(0).toUpperCase() })
4513
4523
  ] }),
@@ -4522,6 +4532,53 @@ var init_ParticipantView = __esm({
4522
4532
  ParticipantView_default = ParticipantView;
4523
4533
  }
4524
4534
  });
4535
+ var AudioCallView, AudioCallView_default;
4536
+ var init_AudioCallView = __esm({
4537
+ "src/call/AudioCallView.tsx"() {
4538
+ init_avatar();
4539
+ init_utils2();
4540
+ AudioCallView = ({
4541
+ sdk,
4542
+ remoteParticipantId,
4543
+ peerName,
4544
+ peerAvatar
4545
+ }) => {
4546
+ const remote = remoteParticipantId ? sdk.useParticipant(remoteParticipantId) : null;
4547
+ const name = remote?.displayName || peerName || "In call";
4548
+ const isSpeaking = !!remote?.isActiveSpeaker && remote?.micOn;
4549
+ const isMuted = remote != null && !remote.micOn;
4550
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-6 p-6 text-center", children: [
4551
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center justify-center", children: [
4552
+ /* @__PURE__ */ jsxRuntime.jsx(
4553
+ "span",
4554
+ {
4555
+ className: cn(
4556
+ "absolute inline-flex size-36 rounded-full bg-emerald-400/15 transition-opacity duration-300",
4557
+ isSpeaking ? "animate-ping opacity-100" : "opacity-0"
4558
+ )
4559
+ }
4560
+ ),
4561
+ /* @__PURE__ */ jsxRuntime.jsxs(
4562
+ Avatar,
4563
+ {
4564
+ className: cn(
4565
+ "relative size-28 ring-4 transition-colors duration-300",
4566
+ isSpeaking ? "ring-emerald-400/80" : "ring-white/10"
4567
+ ),
4568
+ children: [
4569
+ /* @__PURE__ */ jsxRuntime.jsx(AvatarImage, { src: peerAvatar, alt: name }),
4570
+ /* @__PURE__ */ jsxRuntime.jsx(AvatarFallback, { className: "bg-neutral-700 text-3xl font-medium text-white", children: name.charAt(0).toUpperCase() })
4571
+ ]
4572
+ }
4573
+ ),
4574
+ isMuted && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute -right-1 -bottom-1 flex size-9 items-center justify-center rounded-full bg-neutral-700 ring-4 ring-neutral-900", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MicOff, { size: 16, className: "text-white/80" }) })
4575
+ ] }),
4576
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xl font-semibold tracking-tight", children: name })
4577
+ ] });
4578
+ };
4579
+ AudioCallView_default = AudioCallView;
4580
+ }
4581
+ });
4525
4582
  var circleBtn, CallControls, CallControls_default;
4526
4583
  var init_CallControls = __esm({
4527
4584
  "src/call/CallControls.tsx"() {
@@ -4587,9 +4644,10 @@ var MeetingView, MeetingView_default;
4587
4644
  var init_MeetingView = __esm({
4588
4645
  "src/call/MeetingView.tsx"() {
4589
4646
  init_ParticipantView();
4647
+ init_AudioCallView();
4590
4648
  init_CallControls();
4591
4649
  init_call_store();
4592
- MeetingView = ({ sdk, mode }) => {
4650
+ MeetingView = ({ sdk, mode, peerName, peerAvatar }) => {
4593
4651
  const endCall = exports.useCallStore((s) => s.endCall);
4594
4652
  const [hasJoined, setHasJoined] = React9.useState(false);
4595
4653
  const hasJoinedRef = React9.useRef(false);
@@ -4612,7 +4670,23 @@ var init_MeetingView = __esm({
4612
4670
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: "Connecting\u2026" })
4613
4671
  ] });
4614
4672
  }
4615
- const remoteIds = Array.from(participants.keys());
4673
+ const remoteIds = Array.from(participants.keys()).filter(
4674
+ (id) => id !== localParticipant?.id
4675
+ );
4676
+ if (mode === "audio") {
4677
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col", children: [
4678
+ /* @__PURE__ */ jsxRuntime.jsx(
4679
+ AudioCallView_default,
4680
+ {
4681
+ sdk,
4682
+ remoteParticipantId: remoteIds[0],
4683
+ peerName,
4684
+ peerAvatar
4685
+ }
4686
+ ),
4687
+ /* @__PURE__ */ jsxRuntime.jsx(CallControls_default, { sdk, mode })
4688
+ ] });
4689
+ }
4616
4690
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col", children: [
4617
4691
  /* @__PURE__ */ jsxRuntime.jsxs(
4618
4692
  "div",
@@ -4626,7 +4700,16 @@ var init_MeetingView = __esm({
4626
4700
  },
4627
4701
  children: [
4628
4702
  localParticipant?.id && /* @__PURE__ */ jsxRuntime.jsx(ParticipantView_default, { sdk, participantId: localParticipant.id, isLocal: true }),
4629
- remoteIds.map((id) => /* @__PURE__ */ jsxRuntime.jsx(ParticipantView_default, { sdk, participantId: id }, id))
4703
+ remoteIds.map((id) => /* @__PURE__ */ jsxRuntime.jsx(
4704
+ ParticipantView_default,
4705
+ {
4706
+ sdk,
4707
+ participantId: id,
4708
+ fallbackName: peerName,
4709
+ fallbackImage: peerAvatar
4710
+ },
4711
+ id
4712
+ ))
4630
4713
  ]
4631
4714
  }
4632
4715
  ),
@@ -4648,7 +4731,14 @@ var init_CallSession = __esm({
4648
4731
  init_useStore();
4649
4732
  init_videosdk_loader();
4650
4733
  init_MeetingView();
4651
- CallSession = ({ meetingId, token, mode, onError }) => {
4734
+ CallSession = ({
4735
+ meetingId,
4736
+ token,
4737
+ mode,
4738
+ peerName,
4739
+ peerAvatar,
4740
+ onError
4741
+ }) => {
4652
4742
  const [sdk, setSdk] = React9.useState(null);
4653
4743
  const userId = exports.useChatStore((s) => s.loggedUserDetails?._id);
4654
4744
  const userName = exports.useChatStore((s) => s.loggedUserDetails?.name);
@@ -4689,7 +4779,7 @@ var init_CallSession = __esm({
4689
4779
  token,
4690
4780
  reinitialiseMeetingOnConfigChange: false,
4691
4781
  joinWithoutUserInteraction: true,
4692
- children: /* @__PURE__ */ jsxRuntime.jsx(MeetingView_default, { sdk, mode })
4782
+ children: /* @__PURE__ */ jsxRuntime.jsx(MeetingView_default, { sdk, mode, peerName, peerAvatar })
4693
4783
  }
4694
4784
  );
4695
4785
  };
@@ -5641,6 +5731,167 @@ function useEffectiveSettingsOptional() {
5641
5731
  };
5642
5732
  }
5643
5733
 
5734
+ // src/call/CallOverlay.tsx
5735
+ init_avatar();
5736
+ init_button();
5737
+ init_call_store();
5738
+
5739
+ // src/call/useRingCountdown.ts
5740
+ init_call_types();
5741
+ var useRingCountdown = (active, callId) => {
5742
+ const [secondsLeft, setSecondsLeft] = React9.useState(
5743
+ Math.ceil(CALL_RING_TIMEOUT_MS / 1e3)
5744
+ );
5745
+ React9.useEffect(() => {
5746
+ if (!active) return;
5747
+ const startedAt = Date.now();
5748
+ setSecondsLeft(Math.ceil(CALL_RING_TIMEOUT_MS / 1e3));
5749
+ const id = setInterval(() => {
5750
+ const remainingMs = CALL_RING_TIMEOUT_MS - (Date.now() - startedAt);
5751
+ setSecondsLeft(Math.max(0, Math.ceil(remainingMs / 1e3)));
5752
+ }, 1e3);
5753
+ return () => clearInterval(id);
5754
+ }, [active, callId]);
5755
+ return secondsLeft;
5756
+ };
5757
+ var useCallDuration = (startedAt) => {
5758
+ const [elapsedMs, setElapsedMs] = React9.useState(0);
5759
+ React9.useEffect(() => {
5760
+ if (!startedAt) {
5761
+ setElapsedMs(0);
5762
+ return;
5763
+ }
5764
+ setElapsedMs(Date.now() - startedAt);
5765
+ const id = setInterval(() => setElapsedMs(Date.now() - startedAt), 1e3);
5766
+ return () => clearInterval(id);
5767
+ }, [startedAt]);
5768
+ const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1e3));
5769
+ const hours = Math.floor(totalSeconds / 3600);
5770
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
5771
+ const seconds = totalSeconds % 60;
5772
+ const pad = (n) => String(n).padStart(2, "0");
5773
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
5774
+ };
5775
+ var CallSession2 = React9__namespace.default.lazy(() => Promise.resolve().then(() => (init_CallSession(), CallSession_exports)));
5776
+ var CallOverlay = () => {
5777
+ const uiStatus = exports.useCallStore((s) => s.uiStatus);
5778
+ const mode = exports.useCallStore((s) => s.mode);
5779
+ const callId = exports.useCallStore((s) => s.callId);
5780
+ const meetingId = exports.useCallStore((s) => s.meetingId);
5781
+ const token = exports.useCallStore((s) => s.token);
5782
+ const error = exports.useCallStore((s) => s.error);
5783
+ const callee = exports.useCallStore((s) => s.callee);
5784
+ const caller = exports.useCallStore((s) => s.caller);
5785
+ const callStartedAt = exports.useCallStore((s) => s.callStartedAt);
5786
+ const cancelCall = exports.useCallStore((s) => s.cancelCall);
5787
+ const reset = exports.useCallStore((s) => s.reset);
5788
+ const notifyServerCallFailed = exports.useCallStore((s) => s.notifyServerCallFailed);
5789
+ const [sessionError, setSessionError] = React9.useState(null);
5790
+ const secondsLeft = useRingCountdown(uiStatus === "ringing-out", callId);
5791
+ const duration = useCallDuration(callStartedAt);
5792
+ React9.useEffect(() => {
5793
+ setSessionError(null);
5794
+ }, [callId]);
5795
+ React9.useEffect(() => {
5796
+ if (sessionError) notifyServerCallFailed();
5797
+ }, [sessionError, notifyServerCallFailed]);
5798
+ const isOpen = uiStatus !== "idle" && uiStatus !== "incoming";
5799
+ if (!isOpen || typeof document === "undefined") return null;
5800
+ const displayError = error || sessionError;
5801
+ const peerName = callee?.name || caller?.name || "";
5802
+ const peerAvatar = callee?.avatar || caller?.profileImage || void 0;
5803
+ const isVideo = mode === "video";
5804
+ return reactDom.createPortal(
5805
+ /* @__PURE__ */ jsxRuntime.jsx(
5806
+ "div",
5807
+ {
5808
+ style: {
5809
+ position: "fixed",
5810
+ inset: 0,
5811
+ width: "100vw",
5812
+ height: "100vh",
5813
+ zIndex: 2147483647
5814
+ },
5815
+ className: "flex flex-col overflow-hidden bg-gradient-to-b from-neutral-900 via-neutral-950 to-black text-white",
5816
+ children: displayError ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-4 p-6 text-center", children: [
5817
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "max-w-sm text-sm text-white/80", children: displayError }),
5818
+ /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "secondary", onClick: reset, children: [
5819
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16, className: "mr-1" }),
5820
+ " Close"
5821
+ ] })
5822
+ ] }) : uiStatus === "in-call" && meetingId && token && mode ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col", children: [
5823
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-2 px-4 py-3 text-sm text-white/70", children: [
5824
+ peerName && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-white/90", children: peerName }),
5825
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: "text-white/30", children: "\xB7" }),
5826
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tabular-nums", children: duration })
5827
+ ] }),
5828
+ /* @__PURE__ */ jsxRuntime.jsx(
5829
+ React9__namespace.default.Suspense,
5830
+ {
5831
+ fallback: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-3 text-white/80", children: [
5832
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "size-8 animate-spin" }),
5833
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: "Loading call\u2026" })
5834
+ ] }),
5835
+ children: /* @__PURE__ */ jsxRuntime.jsx(
5836
+ CallSession2,
5837
+ {
5838
+ meetingId,
5839
+ token,
5840
+ mode,
5841
+ peerName,
5842
+ peerAvatar,
5843
+ onError: setSessionError
5844
+ }
5845
+ )
5846
+ }
5847
+ )
5848
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-6 p-6 text-center", children: [
5849
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center justify-center", children: [
5850
+ uiStatus === "ringing-out" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5851
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute inline-flex size-32 animate-ping rounded-full bg-white/10" }),
5852
+ /* @__PURE__ */ jsxRuntime.jsx(
5853
+ "span",
5854
+ {
5855
+ className: "absolute inline-flex size-32 animate-ping rounded-full bg-white/5",
5856
+ style: { animationDelay: "0.6s" }
5857
+ }
5858
+ )
5859
+ ] }),
5860
+ peerName ? /* @__PURE__ */ jsxRuntime.jsxs(Avatar, { className: "relative size-28 ring-4 ring-white/10", children: [
5861
+ /* @__PURE__ */ jsxRuntime.jsx(AvatarImage, { src: peerAvatar, alt: peerName }),
5862
+ /* @__PURE__ */ jsxRuntime.jsx(AvatarFallback, { className: "bg-neutral-700 text-3xl font-medium text-white", children: peerName.charAt(0).toUpperCase() })
5863
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative flex size-28 items-center justify-center rounded-full bg-white/10", children: isVideo ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Video, { size: 36 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Phone, { size: 36 }) })
5864
+ ] }),
5865
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
5866
+ peerName && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xl font-semibold tracking-tight", children: peerName }),
5867
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "mt-1 text-sm text-white/60", children: [
5868
+ uiStatus === "creating" && "Starting call\u2026",
5869
+ uiStatus === "ringing-out" && `${isVideo ? "Video calling" : "Calling"}\u2026 \xB7 ${secondsLeft}s`,
5870
+ uiStatus === "joining" && "Connecting\u2026"
5871
+ ] })
5872
+ ] }),
5873
+ uiStatus === "ringing-out" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 flex flex-col items-center gap-2", children: [
5874
+ /* @__PURE__ */ jsxRuntime.jsx(
5875
+ Button,
5876
+ {
5877
+ variant: "ghost",
5878
+ size: "icon",
5879
+ className: "size-16 rounded-full bg-red-600 text-white shadow-lg shadow-red-600/30 transition-transform hover:scale-105 hover:bg-red-500 active:scale-95",
5880
+ "aria-label": "Cancel call",
5881
+ onClick: cancelCall,
5882
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PhoneOff, { size: 24 })
5883
+ }
5884
+ ),
5885
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-white/60", children: "Cancel" })
5886
+ ] })
5887
+ ] })
5888
+ }
5889
+ ),
5890
+ document.body
5891
+ );
5892
+ };
5893
+ var CallOverlay_default = CallOverlay;
5894
+
5644
5895
  // src/ui/dialog.tsx
5645
5896
  init_utils2();
5646
5897
  function Dialog({
@@ -6179,154 +6430,7 @@ var ChatAlertDialogContent = ({
6179
6430
  );
6180
6431
  };
6181
6432
 
6182
- // src/call/CallOverlay.tsx
6183
- init_avatar();
6184
- init_button();
6185
- init_utils2();
6186
- init_call_store();
6187
-
6188
- // src/call/useRingCountdown.ts
6189
- init_call_types();
6190
- var useRingCountdown = (active, callId) => {
6191
- const [secondsLeft, setSecondsLeft] = React9.useState(
6192
- Math.ceil(CALL_RING_TIMEOUT_MS / 1e3)
6193
- );
6194
- React9.useEffect(() => {
6195
- if (!active) return;
6196
- const startedAt = Date.now();
6197
- setSecondsLeft(Math.ceil(CALL_RING_TIMEOUT_MS / 1e3));
6198
- const id = setInterval(() => {
6199
- const remainingMs = CALL_RING_TIMEOUT_MS - (Date.now() - startedAt);
6200
- setSecondsLeft(Math.max(0, Math.ceil(remainingMs / 1e3)));
6201
- }, 1e3);
6202
- return () => clearInterval(id);
6203
- }, [active, callId]);
6204
- return secondsLeft;
6205
- };
6206
- var useCallDuration = (startedAt) => {
6207
- const [elapsedMs, setElapsedMs] = React9.useState(0);
6208
- React9.useEffect(() => {
6209
- if (!startedAt) {
6210
- setElapsedMs(0);
6211
- return;
6212
- }
6213
- setElapsedMs(Date.now() - startedAt);
6214
- const id = setInterval(() => setElapsedMs(Date.now() - startedAt), 1e3);
6215
- return () => clearInterval(id);
6216
- }, [startedAt]);
6217
- const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1e3));
6218
- const hours = Math.floor(totalSeconds / 3600);
6219
- const minutes = Math.floor(totalSeconds % 3600 / 60);
6220
- const seconds = totalSeconds % 60;
6221
- const pad = (n) => String(n).padStart(2, "0");
6222
- return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
6223
- };
6224
- var CallSession2 = React9__namespace.default.lazy(() => Promise.resolve().then(() => (init_CallSession(), CallSession_exports)));
6225
- var CallOverlay = () => {
6226
- const uiStatus = exports.useCallStore((s) => s.uiStatus);
6227
- const mode = exports.useCallStore((s) => s.mode);
6228
- const callId = exports.useCallStore((s) => s.callId);
6229
- const meetingId = exports.useCallStore((s) => s.meetingId);
6230
- const token = exports.useCallStore((s) => s.token);
6231
- const error = exports.useCallStore((s) => s.error);
6232
- const callee = exports.useCallStore((s) => s.callee);
6233
- const caller = exports.useCallStore((s) => s.caller);
6234
- const callStartedAt = exports.useCallStore((s) => s.callStartedAt);
6235
- const cancelCall = exports.useCallStore((s) => s.cancelCall);
6236
- const reset = exports.useCallStore((s) => s.reset);
6237
- const [sessionError, setSessionError] = React9.useState(null);
6238
- const secondsLeft = useRingCountdown(uiStatus === "ringing-out", callId);
6239
- const duration = useCallDuration(callStartedAt);
6240
- const isOpen = uiStatus !== "idle" && uiStatus !== "incoming";
6241
- if (!isOpen) return null;
6242
- const displayError = error || sessionError;
6243
- const peerName = callee?.name || caller?.name || "";
6244
- const peerAvatar = callee?.avatar || caller?.profileImage || void 0;
6245
- const isVideo = mode === "video";
6246
- return /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open: isOpen, onOpenChange: () => void 0, children: /* @__PURE__ */ jsxRuntime.jsx(
6247
- ChatDialogContent,
6248
- {
6249
- showCloseButton: false,
6250
- onEscapeKeyDown: (e) => e.preventDefault(),
6251
- onPointerDownOutside: (e) => e.preventDefault(),
6252
- className: cn(
6253
- "fixed inset-0 top-0 left-0 z-100 flex h-screen w-screen max-w-none",
6254
- "translate-x-0 translate-y-0 flex-col rounded-none border-0 bg-gradient-to-b from-neutral-900 via-neutral-950 to-black p-0 text-white"
6255
- ),
6256
- children: displayError ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-4 p-6 text-center", children: [
6257
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "max-w-sm text-sm text-white/80", children: displayError }),
6258
- /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "secondary", onClick: reset, children: [
6259
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16, className: "mr-1" }),
6260
- " Close"
6261
- ] })
6262
- ] }) : uiStatus === "in-call" && meetingId && token && mode ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col", children: [
6263
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-2 px-4 py-3 text-sm text-white/70", children: [
6264
- peerName && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-white/90", children: peerName }),
6265
- /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: "text-white/30", children: "\xB7" }),
6266
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tabular-nums", children: duration })
6267
- ] }),
6268
- /* @__PURE__ */ jsxRuntime.jsx(
6269
- React9__namespace.default.Suspense,
6270
- {
6271
- fallback: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-3 text-white/80", children: [
6272
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "size-8 animate-spin" }),
6273
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: "Loading call\u2026" })
6274
- ] }),
6275
- children: /* @__PURE__ */ jsxRuntime.jsx(
6276
- CallSession2,
6277
- {
6278
- meetingId,
6279
- token,
6280
- mode,
6281
- onError: setSessionError
6282
- }
6283
- )
6284
- }
6285
- )
6286
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-6 p-6 text-center", children: [
6287
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center justify-center", children: [
6288
- uiStatus === "ringing-out" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6289
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute inline-flex size-32 animate-ping rounded-full bg-white/10" }),
6290
- /* @__PURE__ */ jsxRuntime.jsx(
6291
- "span",
6292
- {
6293
- className: "absolute inline-flex size-32 animate-ping rounded-full bg-white/5",
6294
- style: { animationDelay: "0.6s" }
6295
- }
6296
- )
6297
- ] }),
6298
- peerName ? /* @__PURE__ */ jsxRuntime.jsxs(Avatar, { className: "relative size-28 ring-4 ring-white/10", children: [
6299
- /* @__PURE__ */ jsxRuntime.jsx(AvatarImage, { src: peerAvatar, alt: peerName }),
6300
- /* @__PURE__ */ jsxRuntime.jsx(AvatarFallback, { className: "bg-neutral-700 text-3xl font-medium text-white", children: peerName.charAt(0).toUpperCase() })
6301
- ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative flex size-28 items-center justify-center rounded-full bg-white/10", children: isVideo ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Video, { size: 36 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Phone, { size: 36 }) })
6302
- ] }),
6303
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
6304
- peerName && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xl font-semibold tracking-tight", children: peerName }),
6305
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "mt-1 text-sm text-white/60", children: [
6306
- uiStatus === "creating" && "Starting call\u2026",
6307
- uiStatus === "ringing-out" && `${isVideo ? "Video calling" : "Calling"}\u2026 \xB7 ${secondsLeft}s`,
6308
- uiStatus === "joining" && "Connecting\u2026"
6309
- ] })
6310
- ] }),
6311
- uiStatus === "ringing-out" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 flex flex-col items-center gap-2", children: [
6312
- /* @__PURE__ */ jsxRuntime.jsx(
6313
- Button,
6314
- {
6315
- variant: "ghost",
6316
- size: "icon",
6317
- className: "size-16 rounded-full bg-red-600 text-white shadow-lg shadow-red-600/30 transition-transform hover:scale-105 hover:bg-red-500 active:scale-95",
6318
- "aria-label": "Cancel call",
6319
- onClick: cancelCall,
6320
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PhoneOff, { size: 24 })
6321
- }
6322
- ),
6323
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-white/60", children: "Cancel" })
6324
- ] })
6325
- ] })
6326
- }
6327
- ) });
6328
- };
6329
- var CallOverlay_default = CallOverlay;
6433
+ // src/call/IncomingCallDialog.tsx
6330
6434
  init_avatar();
6331
6435
  init_button();
6332
6436
  init_utils2();
@@ -6893,18 +6997,6 @@ var installNotificationClickBridge = (options = {}) => {
6893
6997
  };
6894
6998
 
6895
6999
  // src/hooks/useChatController.ts
6896
- init_call_types();
6897
- function formatCallLogText(entry) {
6898
- const label = entry.mode === "video" ? "video" : "voice";
6899
- if (entry.outcome === "missed") return `Missed ${label} call`;
6900
- if (entry.outcome === "declined") return `${label === "video" ? "Video" : "Voice"} call declined`;
6901
- if (entry.outcome === "busy") return `${label === "video" ? "Video" : "Voice"} call \xB7 not answered`;
6902
- const totalSeconds = Math.max(0, Math.floor(entry.durationSeconds));
6903
- const minutes = Math.floor(totalSeconds / 60);
6904
- const seconds = totalSeconds % 60;
6905
- const duration = `${minutes}:${String(seconds).padStart(2, "0")}`;
6906
- return `${label === "video" ? "Video" : "Voice"} call \xB7 ${duration}`;
6907
- }
6908
7000
  function resolveMessageType(content, attachments) {
6909
7001
  if (attachments.length === 0) return "text";
6910
7002
  if (!content.trim() && attachments.length === 1) {
@@ -6962,28 +7054,6 @@ var useChatController = () => {
6962
7054
  );
6963
7055
  }, []);
6964
7056
  const { localMessages, setLocalMessages } = useChatSync();
6965
- React9.useEffect(() => {
6966
- const handleCallLog = (event) => {
6967
- const entry = event.detail;
6968
- if (!entry?.conversationId) return;
6969
- const logMessage = {
6970
- id: `call-log-${Date.now()}-${Math.random().toString(36).slice(2)}`,
6971
- content: formatCallLogText(entry),
6972
- sender: "system",
6973
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6974
- isOwnMessage: false,
6975
- isSystemMessage: true,
6976
- channelId: entry.conversationId,
6977
- conversationId: entry.conversationId
6978
- };
6979
- setLocalMessages((prev) => ({
6980
- ...prev,
6981
- [entry.conversationId]: [...prev[entry.conversationId] || [], logMessage]
6982
- }));
6983
- };
6984
- window.addEventListener(CALL_LOG_EVENT, handleCallLog);
6985
- return () => window.removeEventListener(CALL_LOG_EVENT, handleCallLog);
6986
- }, [setLocalMessages]);
6987
7057
  const emitDmSend = exports.useChatStore((s) => s.emitDmSend);
6988
7058
  const emitGroupMessageSend2 = exports.useChatStore((s) => s.emitGroupMessageSend);
6989
7059
  const emitMarkAsRead = exports.useChatStore((s) => s.emitMarkAsRead);