@realtimexsco/live-chat 1.5.2 → 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);
@@ -871,53 +864,60 @@ var init_call_store = __esm({
871
864
  }));
872
865
  },
873
866
  _receiveCancelled: (payload) => {
874
- if (get().callId !== payload.callId) return;
875
- clearRingTimer();
876
- const myUserId = exports.useChatStore.getState().loggedUserDetails?._id;
877
- if (payload.endedBy !== myUserId) {
878
- emitCallLog({
879
- conversationId: payload.conversationId,
880
- mode: payload.mode,
881
- outcome: "missed",
882
- durationSeconds: 0
883
- });
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;
884
874
  }
875
+ clearRingTimer();
885
876
  set({ ...idleState });
886
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.
887
883
  _receiveEnded: (payload) => {
888
884
  if (get().callId !== payload.callId) return;
889
885
  clearRingTimer();
890
- emitCallLog({
891
- conversationId: payload.conversationId,
892
- mode: payload.mode,
893
- outcome: payload.durationSeconds > 0 ? "completed" : "declined",
894
- durationSeconds: payload.durationSeconds
895
- });
896
886
  set({ ...idleState });
897
887
  },
898
888
  _receiveBusy: (payload) => {
899
- 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
+ }
900
897
  clearRingTimer();
901
- emitCallLog({
902
- conversationId: payload.conversationId,
903
- mode: payload.mode,
904
- outcome: "busy",
905
- durationSeconds: 0
906
- });
907
898
  set({ ...idleState, error: "That user is already on another call." });
908
899
  },
909
900
  // Guarded by callId like every other _receive* handler — a late/stale
910
901
  // call_missed for a call that has already ended (accepted, rejected,
911
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.
912
911
  _receiveMissed: (payload) => {
913
- 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
+ }
914
920
  clearRingTimer();
915
- emitCallLog({
916
- conversationId: payload.conversationId,
917
- mode: payload.mode,
918
- outcome: "missed",
919
- durationSeconds: 0
920
- });
921
921
  set({ ...idleState });
922
922
  }
923
923
  }));
@@ -4328,6 +4328,58 @@ var init_utils2 = __esm({
4328
4328
  "src/lib/utils.ts"() {
4329
4329
  }
4330
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
+ });
4331
4383
  function Button({
4332
4384
  className,
4333
4385
  variant = "default",
@@ -4382,58 +4434,6 @@ var init_button = __esm({
4382
4434
  );
4383
4435
  }
4384
4436
  });
4385
- function Avatar({
4386
- className,
4387
- size = "default",
4388
- ...props
4389
- }) {
4390
- return /* @__PURE__ */ jsxRuntime.jsx(
4391
- radixUi.Avatar.Root,
4392
- {
4393
- "data-slot": "avatar",
4394
- "data-size": size,
4395
- className: cn(
4396
- "group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
4397
- className
4398
- ),
4399
- ...props
4400
- }
4401
- );
4402
- }
4403
- function AvatarImage({
4404
- className,
4405
- ...props
4406
- }) {
4407
- return /* @__PURE__ */ jsxRuntime.jsx(
4408
- radixUi.Avatar.Image,
4409
- {
4410
- "data-slot": "avatar-image",
4411
- className: cn("aspect-square size-full", className),
4412
- ...props
4413
- }
4414
- );
4415
- }
4416
- function AvatarFallback({
4417
- className,
4418
- ...props
4419
- }) {
4420
- return /* @__PURE__ */ jsxRuntime.jsx(
4421
- radixUi.Avatar.Fallback,
4422
- {
4423
- "data-slot": "avatar-fallback",
4424
- className: cn(
4425
- "bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs",
4426
- className
4427
- ),
4428
- ...props
4429
- }
4430
- );
4431
- }
4432
- var init_avatar = __esm({
4433
- "src/ui/avatar.tsx"() {
4434
- init_utils2();
4435
- }
4436
- });
4437
4437
 
4438
4438
  // src/call/videosdk.loader.ts
4439
4439
  var loadVideoSDK;
@@ -4469,6 +4469,7 @@ var init_ParticipantView = __esm({
4469
4469
  const { webcamStream, micStream, webcamOn, micOn, displayName, isActiveSpeaker } = sdk.useParticipant(participantId);
4470
4470
  const videoRef = React9.useRef(null);
4471
4471
  const audioRef = React9.useRef(null);
4472
+ const [hasFrame, setHasFrame] = React9.useState(false);
4472
4473
  React9.useEffect(() => {
4473
4474
  const el = videoRef.current;
4474
4475
  if (!el) return;
@@ -4477,6 +4478,7 @@ var init_ParticipantView = __esm({
4477
4478
  el.play().catch(() => void 0);
4478
4479
  } else {
4479
4480
  el.srcObject = null;
4481
+ setHasFrame(false);
4480
4482
  }
4481
4483
  }, [webcamOn, webcamStream]);
4482
4484
  React9.useEffect(() => {
@@ -4491,6 +4493,7 @@ var init_ParticipantView = __esm({
4491
4493
  }
4492
4494
  }, [micOn, micStream, isLocal]);
4493
4495
  const name = displayName || fallbackName || "Participant";
4496
+ const showVideo = webcamOn && hasFrame;
4494
4497
  return /* @__PURE__ */ jsxRuntime.jsxs(
4495
4498
  "div",
4496
4499
  {
@@ -4506,14 +4509,15 @@ var init_ParticipantView = __esm({
4506
4509
  ref: videoRef,
4507
4510
  muted: isLocal,
4508
4511
  playsInline: true,
4512
+ onPlaying: () => setHasFrame(true),
4509
4513
  className: cn(
4510
4514
  "size-full object-cover",
4511
- webcamOn ? "block" : "hidden"
4515
+ showVideo ? "block" : "hidden"
4512
4516
  )
4513
4517
  }
4514
4518
  ),
4515
4519
  !isLocal && /* @__PURE__ */ jsxRuntime.jsx("audio", { ref: audioRef, autoPlay: true, playsInline: true }),
4516
- !webcamOn && /* @__PURE__ */ jsxRuntime.jsxs(Avatar, { size: "lg", className: "size-20", children: [
4520
+ !showVideo && /* @__PURE__ */ jsxRuntime.jsxs(Avatar, { size: "lg", className: "size-20", children: [
4517
4521
  /* @__PURE__ */ jsxRuntime.jsx(AvatarImage, { src: fallbackImage, alt: name }),
4518
4522
  /* @__PURE__ */ jsxRuntime.jsx(AvatarFallback, { className: "bg-neutral-700 text-xl font-medium text-white", children: name.charAt(0).toUpperCase() })
4519
4523
  ] }),
@@ -4528,6 +4532,53 @@ var init_ParticipantView = __esm({
4528
4532
  ParticipantView_default = ParticipantView;
4529
4533
  }
4530
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
+ });
4531
4582
  var circleBtn, CallControls, CallControls_default;
4532
4583
  var init_CallControls = __esm({
4533
4584
  "src/call/CallControls.tsx"() {
@@ -4593,9 +4644,10 @@ var MeetingView, MeetingView_default;
4593
4644
  var init_MeetingView = __esm({
4594
4645
  "src/call/MeetingView.tsx"() {
4595
4646
  init_ParticipantView();
4647
+ init_AudioCallView();
4596
4648
  init_CallControls();
4597
4649
  init_call_store();
4598
- MeetingView = ({ sdk, mode }) => {
4650
+ MeetingView = ({ sdk, mode, peerName, peerAvatar }) => {
4599
4651
  const endCall = exports.useCallStore((s) => s.endCall);
4600
4652
  const [hasJoined, setHasJoined] = React9.useState(false);
4601
4653
  const hasJoinedRef = React9.useRef(false);
@@ -4618,7 +4670,23 @@ var init_MeetingView = __esm({
4618
4670
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: "Connecting\u2026" })
4619
4671
  ] });
4620
4672
  }
4621
- 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
+ }
4622
4690
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col", children: [
4623
4691
  /* @__PURE__ */ jsxRuntime.jsxs(
4624
4692
  "div",
@@ -4632,7 +4700,16 @@ var init_MeetingView = __esm({
4632
4700
  },
4633
4701
  children: [
4634
4702
  localParticipant?.id && /* @__PURE__ */ jsxRuntime.jsx(ParticipantView_default, { sdk, participantId: localParticipant.id, isLocal: true }),
4635
- 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
+ ))
4636
4713
  ]
4637
4714
  }
4638
4715
  ),
@@ -4654,7 +4731,14 @@ var init_CallSession = __esm({
4654
4731
  init_useStore();
4655
4732
  init_videosdk_loader();
4656
4733
  init_MeetingView();
4657
- CallSession = ({ meetingId, token, mode, onError }) => {
4734
+ CallSession = ({
4735
+ meetingId,
4736
+ token,
4737
+ mode,
4738
+ peerName,
4739
+ peerAvatar,
4740
+ onError
4741
+ }) => {
4658
4742
  const [sdk, setSdk] = React9.useState(null);
4659
4743
  const userId = exports.useChatStore((s) => s.loggedUserDetails?._id);
4660
4744
  const userName = exports.useChatStore((s) => s.loggedUserDetails?.name);
@@ -4695,7 +4779,7 @@ var init_CallSession = __esm({
4695
4779
  token,
4696
4780
  reinitialiseMeetingOnConfigChange: false,
4697
4781
  joinWithoutUserInteraction: true,
4698
- children: /* @__PURE__ */ jsxRuntime.jsx(MeetingView_default, { sdk, mode })
4782
+ children: /* @__PURE__ */ jsxRuntime.jsx(MeetingView_default, { sdk, mode, peerName, peerAvatar })
4699
4783
  }
4700
4784
  );
4701
4785
  };
@@ -5647,6 +5731,167 @@ function useEffectiveSettingsOptional() {
5647
5731
  };
5648
5732
  }
5649
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
+
5650
5895
  // src/ui/dialog.tsx
5651
5896
  init_utils2();
5652
5897
  function Dialog({
@@ -6185,161 +6430,7 @@ var ChatAlertDialogContent = ({
6185
6430
  );
6186
6431
  };
6187
6432
 
6188
- // src/call/CallOverlay.tsx
6189
- init_avatar();
6190
- init_button();
6191
- init_utils2();
6192
- init_call_store();
6193
-
6194
- // src/call/useRingCountdown.ts
6195
- init_call_types();
6196
- var useRingCountdown = (active, callId) => {
6197
- const [secondsLeft, setSecondsLeft] = React9.useState(
6198
- Math.ceil(CALL_RING_TIMEOUT_MS / 1e3)
6199
- );
6200
- React9.useEffect(() => {
6201
- if (!active) return;
6202
- const startedAt = Date.now();
6203
- setSecondsLeft(Math.ceil(CALL_RING_TIMEOUT_MS / 1e3));
6204
- const id = setInterval(() => {
6205
- const remainingMs = CALL_RING_TIMEOUT_MS - (Date.now() - startedAt);
6206
- setSecondsLeft(Math.max(0, Math.ceil(remainingMs / 1e3)));
6207
- }, 1e3);
6208
- return () => clearInterval(id);
6209
- }, [active, callId]);
6210
- return secondsLeft;
6211
- };
6212
- var useCallDuration = (startedAt) => {
6213
- const [elapsedMs, setElapsedMs] = React9.useState(0);
6214
- React9.useEffect(() => {
6215
- if (!startedAt) {
6216
- setElapsedMs(0);
6217
- return;
6218
- }
6219
- setElapsedMs(Date.now() - startedAt);
6220
- const id = setInterval(() => setElapsedMs(Date.now() - startedAt), 1e3);
6221
- return () => clearInterval(id);
6222
- }, [startedAt]);
6223
- const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1e3));
6224
- const hours = Math.floor(totalSeconds / 3600);
6225
- const minutes = Math.floor(totalSeconds % 3600 / 60);
6226
- const seconds = totalSeconds % 60;
6227
- const pad = (n) => String(n).padStart(2, "0");
6228
- return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
6229
- };
6230
- var CallSession2 = React9__namespace.default.lazy(() => Promise.resolve().then(() => (init_CallSession(), CallSession_exports)));
6231
- var CallOverlay = () => {
6232
- const uiStatus = exports.useCallStore((s) => s.uiStatus);
6233
- const mode = exports.useCallStore((s) => s.mode);
6234
- const callId = exports.useCallStore((s) => s.callId);
6235
- const meetingId = exports.useCallStore((s) => s.meetingId);
6236
- const token = exports.useCallStore((s) => s.token);
6237
- const error = exports.useCallStore((s) => s.error);
6238
- const callee = exports.useCallStore((s) => s.callee);
6239
- const caller = exports.useCallStore((s) => s.caller);
6240
- const callStartedAt = exports.useCallStore((s) => s.callStartedAt);
6241
- const cancelCall = exports.useCallStore((s) => s.cancelCall);
6242
- const reset = exports.useCallStore((s) => s.reset);
6243
- const notifyServerCallFailed = exports.useCallStore((s) => s.notifyServerCallFailed);
6244
- const [sessionError, setSessionError] = React9.useState(null);
6245
- const secondsLeft = useRingCountdown(uiStatus === "ringing-out", callId);
6246
- const duration = useCallDuration(callStartedAt);
6247
- React9.useEffect(() => {
6248
- setSessionError(null);
6249
- }, [callId]);
6250
- React9.useEffect(() => {
6251
- if (sessionError) notifyServerCallFailed();
6252
- }, [sessionError, notifyServerCallFailed]);
6253
- const isOpen = uiStatus !== "idle" && uiStatus !== "incoming";
6254
- if (!isOpen) return null;
6255
- const displayError = error || sessionError;
6256
- const peerName = callee?.name || caller?.name || "";
6257
- const peerAvatar = callee?.avatar || caller?.profileImage || void 0;
6258
- const isVideo = mode === "video";
6259
- return /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open: isOpen, onOpenChange: () => void 0, children: /* @__PURE__ */ jsxRuntime.jsx(
6260
- ChatDialogContent,
6261
- {
6262
- showCloseButton: false,
6263
- onEscapeKeyDown: (e) => e.preventDefault(),
6264
- onPointerDownOutside: (e) => e.preventDefault(),
6265
- className: cn(
6266
- "fixed inset-0 top-0 left-0 z-100 flex h-screen w-screen max-w-none",
6267
- "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"
6268
- ),
6269
- children: displayError ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-4 p-6 text-center", children: [
6270
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "max-w-sm text-sm text-white/80", children: displayError }),
6271
- /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "secondary", onClick: reset, children: [
6272
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16, className: "mr-1" }),
6273
- " Close"
6274
- ] })
6275
- ] }) : uiStatus === "in-call" && meetingId && token && mode ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col", children: [
6276
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-2 px-4 py-3 text-sm text-white/70", children: [
6277
- peerName && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-white/90", children: peerName }),
6278
- /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: "text-white/30", children: "\xB7" }),
6279
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tabular-nums", children: duration })
6280
- ] }),
6281
- /* @__PURE__ */ jsxRuntime.jsx(
6282
- React9__namespace.default.Suspense,
6283
- {
6284
- fallback: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-3 text-white/80", children: [
6285
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "size-8 animate-spin" }),
6286
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: "Loading call\u2026" })
6287
- ] }),
6288
- children: /* @__PURE__ */ jsxRuntime.jsx(
6289
- CallSession2,
6290
- {
6291
- meetingId,
6292
- token,
6293
- mode,
6294
- onError: setSessionError
6295
- }
6296
- )
6297
- }
6298
- )
6299
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-6 p-6 text-center", children: [
6300
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center justify-center", children: [
6301
- uiStatus === "ringing-out" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6302
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute inline-flex size-32 animate-ping rounded-full bg-white/10" }),
6303
- /* @__PURE__ */ jsxRuntime.jsx(
6304
- "span",
6305
- {
6306
- className: "absolute inline-flex size-32 animate-ping rounded-full bg-white/5",
6307
- style: { animationDelay: "0.6s" }
6308
- }
6309
- )
6310
- ] }),
6311
- peerName ? /* @__PURE__ */ jsxRuntime.jsxs(Avatar, { className: "relative size-28 ring-4 ring-white/10", children: [
6312
- /* @__PURE__ */ jsxRuntime.jsx(AvatarImage, { src: peerAvatar, alt: peerName }),
6313
- /* @__PURE__ */ jsxRuntime.jsx(AvatarFallback, { className: "bg-neutral-700 text-3xl font-medium text-white", children: peerName.charAt(0).toUpperCase() })
6314
- ] }) : /* @__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 }) })
6315
- ] }),
6316
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
6317
- peerName && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xl font-semibold tracking-tight", children: peerName }),
6318
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "mt-1 text-sm text-white/60", children: [
6319
- uiStatus === "creating" && "Starting call\u2026",
6320
- uiStatus === "ringing-out" && `${isVideo ? "Video calling" : "Calling"}\u2026 \xB7 ${secondsLeft}s`,
6321
- uiStatus === "joining" && "Connecting\u2026"
6322
- ] })
6323
- ] }),
6324
- uiStatus === "ringing-out" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 flex flex-col items-center gap-2", children: [
6325
- /* @__PURE__ */ jsxRuntime.jsx(
6326
- Button,
6327
- {
6328
- variant: "ghost",
6329
- size: "icon",
6330
- 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",
6331
- "aria-label": "Cancel call",
6332
- onClick: cancelCall,
6333
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PhoneOff, { size: 24 })
6334
- }
6335
- ),
6336
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-white/60", children: "Cancel" })
6337
- ] })
6338
- ] })
6339
- }
6340
- ) });
6341
- };
6342
- var CallOverlay_default = CallOverlay;
6433
+ // src/call/IncomingCallDialog.tsx
6343
6434
  init_avatar();
6344
6435
  init_button();
6345
6436
  init_utils2();
@@ -6906,18 +6997,6 @@ var installNotificationClickBridge = (options = {}) => {
6906
6997
  };
6907
6998
 
6908
6999
  // src/hooks/useChatController.ts
6909
- init_call_types();
6910
- function formatCallLogText(entry) {
6911
- const label = entry.mode === "video" ? "video" : "voice";
6912
- if (entry.outcome === "missed") return `Missed ${label} call`;
6913
- if (entry.outcome === "declined") return `${label === "video" ? "Video" : "Voice"} call declined`;
6914
- if (entry.outcome === "busy") return `${label === "video" ? "Video" : "Voice"} call \xB7 not answered`;
6915
- const totalSeconds = Math.max(0, Math.floor(entry.durationSeconds));
6916
- const minutes = Math.floor(totalSeconds / 60);
6917
- const seconds = totalSeconds % 60;
6918
- const duration = `${minutes}:${String(seconds).padStart(2, "0")}`;
6919
- return `${label === "video" ? "Video" : "Voice"} call \xB7 ${duration}`;
6920
- }
6921
7000
  function resolveMessageType(content, attachments) {
6922
7001
  if (attachments.length === 0) return "text";
6923
7002
  if (!content.trim() && attachments.length === 1) {
@@ -6975,28 +7054,6 @@ var useChatController = () => {
6975
7054
  );
6976
7055
  }, []);
6977
7056
  const { localMessages, setLocalMessages } = useChatSync();
6978
- React9.useEffect(() => {
6979
- const handleCallLog = (event) => {
6980
- const entry = event.detail;
6981
- if (!entry?.conversationId) return;
6982
- const logMessage = {
6983
- id: `call-log-${Date.now()}-${Math.random().toString(36).slice(2)}`,
6984
- content: formatCallLogText(entry),
6985
- sender: "system",
6986
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6987
- isOwnMessage: false,
6988
- isSystemMessage: true,
6989
- channelId: entry.conversationId,
6990
- conversationId: entry.conversationId
6991
- };
6992
- setLocalMessages((prev) => ({
6993
- ...prev,
6994
- [entry.conversationId]: [...prev[entry.conversationId] || [], logMessage]
6995
- }));
6996
- };
6997
- window.addEventListener(CALL_LOG_EVENT, handleCallLog);
6998
- return () => window.removeEventListener(CALL_LOG_EVENT, handleCallLog);
6999
- }, [setLocalMessages]);
7000
7057
  const emitDmSend = exports.useChatStore((s) => s.emitDmSend);
7001
7058
  const emitGroupMessageSend2 = exports.useChatStore((s) => s.emitGroupMessageSend);
7002
7059
  const emitMarkAsRead = exports.useChatStore((s) => s.emitMarkAsRead);