@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.mjs CHANGED
@@ -4,13 +4,13 @@ import { io } from 'socket.io-client';
4
4
  import { persist } from 'zustand/middleware';
5
5
  import { clsx } from 'clsx';
6
6
  import { twMerge } from 'tailwind-merge';
7
- import { cva } from 'class-variance-authority';
8
- import { Dialog as Dialog$1, Slot, Avatar as Avatar$1, Tooltip as Tooltip$1, Switch as Switch$1, ScrollArea as ScrollArea$1, Popover as Popover$1, DropdownMenu as DropdownMenu$1, AlertDialog as AlertDialog$1 } from 'radix-ui';
7
+ import { Slot, Avatar as Avatar$1, Dialog as Dialog$1, Tooltip as Tooltip$1, Switch as Switch$1, ScrollArea as ScrollArea$1, Popover as Popover$1, DropdownMenu as DropdownMenu$1, AlertDialog as AlertDialog$1 } from 'radix-ui';
9
8
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
9
+ import { cva } from 'class-variance-authority';
10
10
  import * as React9 from 'react';
11
11
  import React9__default, { createContext, useState, useEffect, useMemo, useContext, useCallback, useRef, useLayoutEffect, Fragment as Fragment$1, useSyncExternalStore } from 'react';
12
12
  import { Loader2, X, Video, Phone, PhoneOff, Sun, Moon, Settings, Search, UserPlus, ChevronDown, ArrowLeft, Users, WifiOff, Wifi, Sparkles, MessageSquareText, ChevronRight, Pin, Star, MoreVertical, Info, UserKey, ShieldUser, Paperclip, Clock, MessageSquareX, Trash2, LogOut, Ban, CheckCheck, Check, Camera, Save, Pencil, Mail, CornerUpLeft, CheckSquare, Smile, ChevronLeftIcon, ChevronRightIcon, ChevronDownIcon, MessageCircleMore, History, Shield, Send, AlertCircle, ShieldCheck, UserMinus, MessageCircle, ExternalLink, Download, ChevronLeft, ZoomOut, ZoomIn, RotateCcw, Music, MicOff, Mic, VideoOff, XIcon, ArrowRight, MoreHorizontal, PinOff, StarOff, ImageIcon, Film, Play, Pause, FileText, FileSpreadsheet, FileIcon, MessageSquare, Eye, Captions } from 'lucide-react';
13
- import { flushSync } from 'react-dom';
13
+ import { createPortal, flushSync } from 'react-dom';
14
14
  import EmojiPicker, { Theme } from 'emoji-picker-react';
15
15
  import { getDefaultClassNames, DayPicker } from 'react-day-picker';
16
16
  import toast$1, { Toaster, toast } from 'react-hot-toast';
@@ -659,7 +659,7 @@ var init_call_api = __esm({
659
659
  });
660
660
 
661
661
  // src/call/call.types.ts
662
- var CALL_MODES, CALL_STATUS, CALL_RING_TIMEOUT_MS, CLIENT_RING_FALLBACK_MS, CALL_LOG_EVENT;
662
+ var CALL_MODES, CALL_STATUS, CALL_RING_TIMEOUT_MS, CLIENT_RING_FALLBACK_MS;
663
663
  var init_call_types = __esm({
664
664
  "src/call/call.types.ts"() {
665
665
  CALL_MODES = {
@@ -676,22 +676,15 @@ var init_call_types = __esm({
676
676
  };
677
677
  CALL_RING_TIMEOUT_MS = 4e4;
678
678
  CLIENT_RING_FALLBACK_MS = CALL_RING_TIMEOUT_MS + 5e3;
679
- CALL_LOG_EVENT = "realtimex:call-log";
680
679
  }
681
680
  });
682
- var emitCallLog, ringTimer, clearRingTimer, scheduleRingTimer, idleState, useCallStore;
681
+ var ringTimer, clearRingTimer, scheduleRingTimer, idleState, useCallStore;
683
682
  var init_call_store = __esm({
684
683
  "src/call/call.store.ts"() {
685
684
  init_useStore();
686
685
  init_socket_service2();
687
686
  init_call_api();
688
687
  init_call_types();
689
- emitCallLog = (entry) => {
690
- try {
691
- window.dispatchEvent(new CustomEvent(CALL_LOG_EVENT, { detail: entry }));
692
- } catch {
693
- }
694
- };
695
688
  ringTimer = null;
696
689
  clearRingTimer = () => {
697
690
  if (ringTimer) clearTimeout(ringTimer);
@@ -843,53 +836,60 @@ var init_call_store = __esm({
843
836
  }));
844
837
  },
845
838
  _receiveCancelled: (payload) => {
846
- if (get().callId !== payload.callId) return;
847
- clearRingTimer();
848
- const myUserId = useStore.getState().loggedUserDetails?._id;
849
- if (payload.endedBy !== myUserId) {
850
- emitCallLog({
851
- conversationId: payload.conversationId,
852
- mode: payload.mode,
853
- outcome: "missed",
854
- durationSeconds: 0
855
- });
839
+ const state = get();
840
+ if (state.callId !== payload.callId) return;
841
+ if (state.uiStatus === "in-call") {
842
+ console.warn(
843
+ `[realtimex-call] call_cancelled for ${payload.callId} ignored \u2014 this call is already in-call locally; treating as a stale event.`
844
+ );
845
+ return;
856
846
  }
847
+ clearRingTimer();
857
848
  set({ ...idleState });
858
849
  },
850
+ // Missed/ended/declined/etc. call log messages are now persisted by the
851
+ // backend as real chat messages (CALLS_API.md §3.5) — they arrive
852
+ // through the normal dm_receive/group_message_receive pipeline with
853
+ // isSystemMessage: true, so they render automatically with zero extra
854
+ // code here. Nothing to do on this event beyond resetting local state.
859
855
  _receiveEnded: (payload) => {
860
856
  if (get().callId !== payload.callId) return;
861
857
  clearRingTimer();
862
- emitCallLog({
863
- conversationId: payload.conversationId,
864
- mode: payload.mode,
865
- outcome: payload.durationSeconds > 0 ? "completed" : "declined",
866
- durationSeconds: payload.durationSeconds
867
- });
868
858
  set({ ...idleState });
869
859
  },
870
860
  _receiveBusy: (payload) => {
871
- if (get().callId !== payload.callId) return;
861
+ const state = get();
862
+ if (state.callId !== payload.callId) return;
863
+ if (state.uiStatus === "in-call") {
864
+ console.warn(
865
+ `[realtimex-call] call_busy for ${payload.callId} ignored \u2014 this call is already in-call locally; treating as a stale event.`
866
+ );
867
+ return;
868
+ }
872
869
  clearRingTimer();
873
- emitCallLog({
874
- conversationId: payload.conversationId,
875
- mode: payload.mode,
876
- outcome: "busy",
877
- durationSeconds: 0
878
- });
879
870
  set({ ...idleState, error: "That user is already on another call." });
880
871
  },
881
872
  // Guarded by callId like every other _receive* handler — a late/stale
882
873
  // call_missed for a call that has already ended (accepted, rejected,
883
874
  // or replaced by a new one) must not clobber whatever is happening now.
875
+ //
876
+ // Live testing once caught the backend's 40s ring-timeout firing
877
+ // call_missed for a callId that was already call_accepted (timer not
878
+ // cancelled on accept). Backend has since added an atomic
879
+ // only-from-ringing guard (CALLS_API.md §3.3) so this should no longer
880
+ // happen — the uiStatus check below is kept as defense-in-depth rather
881
+ // than removed, since it costs nothing and a regression here would
882
+ // otherwise kill a live call outright.
884
883
  _receiveMissed: (payload) => {
885
- if (get().callId !== payload.callId) return;
884
+ const state = get();
885
+ if (state.callId !== payload.callId) return;
886
+ if (state.uiStatus === "in-call") {
887
+ console.warn(
888
+ `[realtimex-call] call_missed for ${payload.callId} ignored \u2014 this call is already in-call locally; treating as a stale event.`
889
+ );
890
+ return;
891
+ }
886
892
  clearRingTimer();
887
- emitCallLog({
888
- conversationId: payload.conversationId,
889
- mode: payload.mode,
890
- outcome: "missed",
891
- durationSeconds: 0
892
- });
893
893
  set({ ...idleState });
894
894
  }
895
895
  }));
@@ -4300,6 +4300,58 @@ var init_utils2 = __esm({
4300
4300
  "src/lib/utils.ts"() {
4301
4301
  }
4302
4302
  });
4303
+ function Avatar({
4304
+ className,
4305
+ size = "default",
4306
+ ...props
4307
+ }) {
4308
+ return /* @__PURE__ */ jsx(
4309
+ Avatar$1.Root,
4310
+ {
4311
+ "data-slot": "avatar",
4312
+ "data-size": size,
4313
+ className: cn(
4314
+ "group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
4315
+ className
4316
+ ),
4317
+ ...props
4318
+ }
4319
+ );
4320
+ }
4321
+ function AvatarImage({
4322
+ className,
4323
+ ...props
4324
+ }) {
4325
+ return /* @__PURE__ */ jsx(
4326
+ Avatar$1.Image,
4327
+ {
4328
+ "data-slot": "avatar-image",
4329
+ className: cn("aspect-square size-full", className),
4330
+ ...props
4331
+ }
4332
+ );
4333
+ }
4334
+ function AvatarFallback({
4335
+ className,
4336
+ ...props
4337
+ }) {
4338
+ return /* @__PURE__ */ jsx(
4339
+ Avatar$1.Fallback,
4340
+ {
4341
+ "data-slot": "avatar-fallback",
4342
+ className: cn(
4343
+ "bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs",
4344
+ className
4345
+ ),
4346
+ ...props
4347
+ }
4348
+ );
4349
+ }
4350
+ var init_avatar = __esm({
4351
+ "src/ui/avatar.tsx"() {
4352
+ init_utils2();
4353
+ }
4354
+ });
4303
4355
  function Button({
4304
4356
  className,
4305
4357
  variant = "default",
@@ -4354,58 +4406,6 @@ var init_button = __esm({
4354
4406
  );
4355
4407
  }
4356
4408
  });
4357
- function Avatar({
4358
- className,
4359
- size = "default",
4360
- ...props
4361
- }) {
4362
- return /* @__PURE__ */ jsx(
4363
- Avatar$1.Root,
4364
- {
4365
- "data-slot": "avatar",
4366
- "data-size": size,
4367
- className: cn(
4368
- "group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
4369
- className
4370
- ),
4371
- ...props
4372
- }
4373
- );
4374
- }
4375
- function AvatarImage({
4376
- className,
4377
- ...props
4378
- }) {
4379
- return /* @__PURE__ */ jsx(
4380
- Avatar$1.Image,
4381
- {
4382
- "data-slot": "avatar-image",
4383
- className: cn("aspect-square size-full", className),
4384
- ...props
4385
- }
4386
- );
4387
- }
4388
- function AvatarFallback({
4389
- className,
4390
- ...props
4391
- }) {
4392
- return /* @__PURE__ */ jsx(
4393
- Avatar$1.Fallback,
4394
- {
4395
- "data-slot": "avatar-fallback",
4396
- className: cn(
4397
- "bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs",
4398
- className
4399
- ),
4400
- ...props
4401
- }
4402
- );
4403
- }
4404
- var init_avatar = __esm({
4405
- "src/ui/avatar.tsx"() {
4406
- init_utils2();
4407
- }
4408
- });
4409
4409
 
4410
4410
  // src/call/videosdk.loader.ts
4411
4411
  var loadVideoSDK;
@@ -4441,6 +4441,7 @@ var init_ParticipantView = __esm({
4441
4441
  const { webcamStream, micStream, webcamOn, micOn, displayName, isActiveSpeaker } = sdk.useParticipant(participantId);
4442
4442
  const videoRef = useRef(null);
4443
4443
  const audioRef = useRef(null);
4444
+ const [hasFrame, setHasFrame] = useState(false);
4444
4445
  useEffect(() => {
4445
4446
  const el = videoRef.current;
4446
4447
  if (!el) return;
@@ -4449,6 +4450,7 @@ var init_ParticipantView = __esm({
4449
4450
  el.play().catch(() => void 0);
4450
4451
  } else {
4451
4452
  el.srcObject = null;
4453
+ setHasFrame(false);
4452
4454
  }
4453
4455
  }, [webcamOn, webcamStream]);
4454
4456
  useEffect(() => {
@@ -4463,6 +4465,7 @@ var init_ParticipantView = __esm({
4463
4465
  }
4464
4466
  }, [micOn, micStream, isLocal]);
4465
4467
  const name = displayName || fallbackName || "Participant";
4468
+ const showVideo = webcamOn && hasFrame;
4466
4469
  return /* @__PURE__ */ jsxs(
4467
4470
  "div",
4468
4471
  {
@@ -4478,14 +4481,15 @@ var init_ParticipantView = __esm({
4478
4481
  ref: videoRef,
4479
4482
  muted: isLocal,
4480
4483
  playsInline: true,
4484
+ onPlaying: () => setHasFrame(true),
4481
4485
  className: cn(
4482
4486
  "size-full object-cover",
4483
- webcamOn ? "block" : "hidden"
4487
+ showVideo ? "block" : "hidden"
4484
4488
  )
4485
4489
  }
4486
4490
  ),
4487
4491
  !isLocal && /* @__PURE__ */ jsx("audio", { ref: audioRef, autoPlay: true, playsInline: true }),
4488
- !webcamOn && /* @__PURE__ */ jsxs(Avatar, { size: "lg", className: "size-20", children: [
4492
+ !showVideo && /* @__PURE__ */ jsxs(Avatar, { size: "lg", className: "size-20", children: [
4489
4493
  /* @__PURE__ */ jsx(AvatarImage, { src: fallbackImage, alt: name }),
4490
4494
  /* @__PURE__ */ jsx(AvatarFallback, { className: "bg-neutral-700 text-xl font-medium text-white", children: name.charAt(0).toUpperCase() })
4491
4495
  ] }),
@@ -4500,6 +4504,53 @@ var init_ParticipantView = __esm({
4500
4504
  ParticipantView_default = ParticipantView;
4501
4505
  }
4502
4506
  });
4507
+ var AudioCallView, AudioCallView_default;
4508
+ var init_AudioCallView = __esm({
4509
+ "src/call/AudioCallView.tsx"() {
4510
+ init_avatar();
4511
+ init_utils2();
4512
+ AudioCallView = ({
4513
+ sdk,
4514
+ remoteParticipantId,
4515
+ peerName,
4516
+ peerAvatar
4517
+ }) => {
4518
+ const remote = remoteParticipantId ? sdk.useParticipant(remoteParticipantId) : null;
4519
+ const name = remote?.displayName || peerName || "In call";
4520
+ const isSpeaking = !!remote?.isActiveSpeaker && remote?.micOn;
4521
+ const isMuted = remote != null && !remote.micOn;
4522
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-6 p-6 text-center", children: [
4523
+ /* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-center", children: [
4524
+ /* @__PURE__ */ jsx(
4525
+ "span",
4526
+ {
4527
+ className: cn(
4528
+ "absolute inline-flex size-36 rounded-full bg-emerald-400/15 transition-opacity duration-300",
4529
+ isSpeaking ? "animate-ping opacity-100" : "opacity-0"
4530
+ )
4531
+ }
4532
+ ),
4533
+ /* @__PURE__ */ jsxs(
4534
+ Avatar,
4535
+ {
4536
+ className: cn(
4537
+ "relative size-28 ring-4 transition-colors duration-300",
4538
+ isSpeaking ? "ring-emerald-400/80" : "ring-white/10"
4539
+ ),
4540
+ children: [
4541
+ /* @__PURE__ */ jsx(AvatarImage, { src: peerAvatar, alt: name }),
4542
+ /* @__PURE__ */ jsx(AvatarFallback, { className: "bg-neutral-700 text-3xl font-medium text-white", children: name.charAt(0).toUpperCase() })
4543
+ ]
4544
+ }
4545
+ ),
4546
+ isMuted && /* @__PURE__ */ 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__ */ jsx(MicOff, { size: 16, className: "text-white/80" }) })
4547
+ ] }),
4548
+ /* @__PURE__ */ jsx("p", { className: "text-xl font-semibold tracking-tight", children: name })
4549
+ ] });
4550
+ };
4551
+ AudioCallView_default = AudioCallView;
4552
+ }
4553
+ });
4503
4554
  var circleBtn, CallControls, CallControls_default;
4504
4555
  var init_CallControls = __esm({
4505
4556
  "src/call/CallControls.tsx"() {
@@ -4565,9 +4616,10 @@ var MeetingView, MeetingView_default;
4565
4616
  var init_MeetingView = __esm({
4566
4617
  "src/call/MeetingView.tsx"() {
4567
4618
  init_ParticipantView();
4619
+ init_AudioCallView();
4568
4620
  init_CallControls();
4569
4621
  init_call_store();
4570
- MeetingView = ({ sdk, mode }) => {
4622
+ MeetingView = ({ sdk, mode, peerName, peerAvatar }) => {
4571
4623
  const endCall = useCallStore((s) => s.endCall);
4572
4624
  const [hasJoined, setHasJoined] = useState(false);
4573
4625
  const hasJoinedRef = useRef(false);
@@ -4590,7 +4642,23 @@ var init_MeetingView = __esm({
4590
4642
  /* @__PURE__ */ jsx("p", { className: "text-sm", children: "Connecting\u2026" })
4591
4643
  ] });
4592
4644
  }
4593
- const remoteIds = Array.from(participants.keys());
4645
+ const remoteIds = Array.from(participants.keys()).filter(
4646
+ (id) => id !== localParticipant?.id
4647
+ );
4648
+ if (mode === "audio") {
4649
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col", children: [
4650
+ /* @__PURE__ */ jsx(
4651
+ AudioCallView_default,
4652
+ {
4653
+ sdk,
4654
+ remoteParticipantId: remoteIds[0],
4655
+ peerName,
4656
+ peerAvatar
4657
+ }
4658
+ ),
4659
+ /* @__PURE__ */ jsx(CallControls_default, { sdk, mode })
4660
+ ] });
4661
+ }
4594
4662
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col", children: [
4595
4663
  /* @__PURE__ */ jsxs(
4596
4664
  "div",
@@ -4604,7 +4672,16 @@ var init_MeetingView = __esm({
4604
4672
  },
4605
4673
  children: [
4606
4674
  localParticipant?.id && /* @__PURE__ */ jsx(ParticipantView_default, { sdk, participantId: localParticipant.id, isLocal: true }),
4607
- remoteIds.map((id) => /* @__PURE__ */ jsx(ParticipantView_default, { sdk, participantId: id }, id))
4675
+ remoteIds.map((id) => /* @__PURE__ */ jsx(
4676
+ ParticipantView_default,
4677
+ {
4678
+ sdk,
4679
+ participantId: id,
4680
+ fallbackName: peerName,
4681
+ fallbackImage: peerAvatar
4682
+ },
4683
+ id
4684
+ ))
4608
4685
  ]
4609
4686
  }
4610
4687
  ),
@@ -4626,7 +4703,14 @@ var init_CallSession = __esm({
4626
4703
  init_useStore();
4627
4704
  init_videosdk_loader();
4628
4705
  init_MeetingView();
4629
- CallSession = ({ meetingId, token, mode, onError }) => {
4706
+ CallSession = ({
4707
+ meetingId,
4708
+ token,
4709
+ mode,
4710
+ peerName,
4711
+ peerAvatar,
4712
+ onError
4713
+ }) => {
4630
4714
  const [sdk, setSdk] = useState(null);
4631
4715
  const userId = useStore((s) => s.loggedUserDetails?._id);
4632
4716
  const userName = useStore((s) => s.loggedUserDetails?.name);
@@ -4667,7 +4751,7 @@ var init_CallSession = __esm({
4667
4751
  token,
4668
4752
  reinitialiseMeetingOnConfigChange: false,
4669
4753
  joinWithoutUserInteraction: true,
4670
- children: /* @__PURE__ */ jsx(MeetingView_default, { sdk, mode })
4754
+ children: /* @__PURE__ */ jsx(MeetingView_default, { sdk, mode, peerName, peerAvatar })
4671
4755
  }
4672
4756
  );
4673
4757
  };
@@ -5619,6 +5703,167 @@ function useEffectiveSettingsOptional() {
5619
5703
  };
5620
5704
  }
5621
5705
 
5706
+ // src/call/CallOverlay.tsx
5707
+ init_avatar();
5708
+ init_button();
5709
+ init_call_store();
5710
+
5711
+ // src/call/useRingCountdown.ts
5712
+ init_call_types();
5713
+ var useRingCountdown = (active, callId) => {
5714
+ const [secondsLeft, setSecondsLeft] = useState(
5715
+ Math.ceil(CALL_RING_TIMEOUT_MS / 1e3)
5716
+ );
5717
+ useEffect(() => {
5718
+ if (!active) return;
5719
+ const startedAt = Date.now();
5720
+ setSecondsLeft(Math.ceil(CALL_RING_TIMEOUT_MS / 1e3));
5721
+ const id = setInterval(() => {
5722
+ const remainingMs = CALL_RING_TIMEOUT_MS - (Date.now() - startedAt);
5723
+ setSecondsLeft(Math.max(0, Math.ceil(remainingMs / 1e3)));
5724
+ }, 1e3);
5725
+ return () => clearInterval(id);
5726
+ }, [active, callId]);
5727
+ return secondsLeft;
5728
+ };
5729
+ var useCallDuration = (startedAt) => {
5730
+ const [elapsedMs, setElapsedMs] = useState(0);
5731
+ useEffect(() => {
5732
+ if (!startedAt) {
5733
+ setElapsedMs(0);
5734
+ return;
5735
+ }
5736
+ setElapsedMs(Date.now() - startedAt);
5737
+ const id = setInterval(() => setElapsedMs(Date.now() - startedAt), 1e3);
5738
+ return () => clearInterval(id);
5739
+ }, [startedAt]);
5740
+ const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1e3));
5741
+ const hours = Math.floor(totalSeconds / 3600);
5742
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
5743
+ const seconds = totalSeconds % 60;
5744
+ const pad = (n) => String(n).padStart(2, "0");
5745
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
5746
+ };
5747
+ var CallSession2 = React9__default.lazy(() => Promise.resolve().then(() => (init_CallSession(), CallSession_exports)));
5748
+ var CallOverlay = () => {
5749
+ const uiStatus = useCallStore((s) => s.uiStatus);
5750
+ const mode = useCallStore((s) => s.mode);
5751
+ const callId = useCallStore((s) => s.callId);
5752
+ const meetingId = useCallStore((s) => s.meetingId);
5753
+ const token = useCallStore((s) => s.token);
5754
+ const error = useCallStore((s) => s.error);
5755
+ const callee = useCallStore((s) => s.callee);
5756
+ const caller = useCallStore((s) => s.caller);
5757
+ const callStartedAt = useCallStore((s) => s.callStartedAt);
5758
+ const cancelCall = useCallStore((s) => s.cancelCall);
5759
+ const reset = useCallStore((s) => s.reset);
5760
+ const notifyServerCallFailed = useCallStore((s) => s.notifyServerCallFailed);
5761
+ const [sessionError, setSessionError] = useState(null);
5762
+ const secondsLeft = useRingCountdown(uiStatus === "ringing-out", callId);
5763
+ const duration = useCallDuration(callStartedAt);
5764
+ useEffect(() => {
5765
+ setSessionError(null);
5766
+ }, [callId]);
5767
+ useEffect(() => {
5768
+ if (sessionError) notifyServerCallFailed();
5769
+ }, [sessionError, notifyServerCallFailed]);
5770
+ const isOpen = uiStatus !== "idle" && uiStatus !== "incoming";
5771
+ if (!isOpen || typeof document === "undefined") return null;
5772
+ const displayError = error || sessionError;
5773
+ const peerName = callee?.name || caller?.name || "";
5774
+ const peerAvatar = callee?.avatar || caller?.profileImage || void 0;
5775
+ const isVideo = mode === "video";
5776
+ return createPortal(
5777
+ /* @__PURE__ */ jsx(
5778
+ "div",
5779
+ {
5780
+ style: {
5781
+ position: "fixed",
5782
+ inset: 0,
5783
+ width: "100vw",
5784
+ height: "100vh",
5785
+ zIndex: 2147483647
5786
+ },
5787
+ className: "flex flex-col overflow-hidden bg-gradient-to-b from-neutral-900 via-neutral-950 to-black text-white",
5788
+ children: displayError ? /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-4 p-6 text-center", children: [
5789
+ /* @__PURE__ */ jsx("p", { className: "max-w-sm text-sm text-white/80", children: displayError }),
5790
+ /* @__PURE__ */ jsxs(Button, { variant: "secondary", onClick: reset, children: [
5791
+ /* @__PURE__ */ jsx(X, { size: 16, className: "mr-1" }),
5792
+ " Close"
5793
+ ] })
5794
+ ] }) : uiStatus === "in-call" && meetingId && token && mode ? /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col", children: [
5795
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2 px-4 py-3 text-sm text-white/70", children: [
5796
+ peerName && /* @__PURE__ */ jsx("span", { className: "font-medium text-white/90", children: peerName }),
5797
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "text-white/30", children: "\xB7" }),
5798
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: duration })
5799
+ ] }),
5800
+ /* @__PURE__ */ jsx(
5801
+ React9__default.Suspense,
5802
+ {
5803
+ fallback: /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-3 text-white/80", children: [
5804
+ /* @__PURE__ */ jsx(Loader2, { className: "size-8 animate-spin" }),
5805
+ /* @__PURE__ */ jsx("p", { className: "text-sm", children: "Loading call\u2026" })
5806
+ ] }),
5807
+ children: /* @__PURE__ */ jsx(
5808
+ CallSession2,
5809
+ {
5810
+ meetingId,
5811
+ token,
5812
+ mode,
5813
+ peerName,
5814
+ peerAvatar,
5815
+ onError: setSessionError
5816
+ }
5817
+ )
5818
+ }
5819
+ )
5820
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-6 p-6 text-center", children: [
5821
+ /* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-center", children: [
5822
+ uiStatus === "ringing-out" && /* @__PURE__ */ jsxs(Fragment, { children: [
5823
+ /* @__PURE__ */ jsx("span", { className: "absolute inline-flex size-32 animate-ping rounded-full bg-white/10" }),
5824
+ /* @__PURE__ */ jsx(
5825
+ "span",
5826
+ {
5827
+ className: "absolute inline-flex size-32 animate-ping rounded-full bg-white/5",
5828
+ style: { animationDelay: "0.6s" }
5829
+ }
5830
+ )
5831
+ ] }),
5832
+ peerName ? /* @__PURE__ */ jsxs(Avatar, { className: "relative size-28 ring-4 ring-white/10", children: [
5833
+ /* @__PURE__ */ jsx(AvatarImage, { src: peerAvatar, alt: peerName }),
5834
+ /* @__PURE__ */ jsx(AvatarFallback, { className: "bg-neutral-700 text-3xl font-medium text-white", children: peerName.charAt(0).toUpperCase() })
5835
+ ] }) : /* @__PURE__ */ jsx("div", { className: "relative flex size-28 items-center justify-center rounded-full bg-white/10", children: isVideo ? /* @__PURE__ */ jsx(Video, { size: 36 }) : /* @__PURE__ */ jsx(Phone, { size: 36 }) })
5836
+ ] }),
5837
+ /* @__PURE__ */ jsxs("div", { children: [
5838
+ peerName && /* @__PURE__ */ jsx("p", { className: "text-xl font-semibold tracking-tight", children: peerName }),
5839
+ /* @__PURE__ */ jsxs("p", { className: "mt-1 text-sm text-white/60", children: [
5840
+ uiStatus === "creating" && "Starting call\u2026",
5841
+ uiStatus === "ringing-out" && `${isVideo ? "Video calling" : "Calling"}\u2026 \xB7 ${secondsLeft}s`,
5842
+ uiStatus === "joining" && "Connecting\u2026"
5843
+ ] })
5844
+ ] }),
5845
+ uiStatus === "ringing-out" && /* @__PURE__ */ jsxs("div", { className: "mt-1 flex flex-col items-center gap-2", children: [
5846
+ /* @__PURE__ */ jsx(
5847
+ Button,
5848
+ {
5849
+ variant: "ghost",
5850
+ size: "icon",
5851
+ 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",
5852
+ "aria-label": "Cancel call",
5853
+ onClick: cancelCall,
5854
+ children: /* @__PURE__ */ jsx(PhoneOff, { size: 24 })
5855
+ }
5856
+ ),
5857
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-white/60", children: "Cancel" })
5858
+ ] })
5859
+ ] })
5860
+ }
5861
+ ),
5862
+ document.body
5863
+ );
5864
+ };
5865
+ var CallOverlay_default = CallOverlay;
5866
+
5622
5867
  // src/ui/dialog.tsx
5623
5868
  init_utils2();
5624
5869
  function Dialog({
@@ -6157,161 +6402,7 @@ var ChatAlertDialogContent = ({
6157
6402
  );
6158
6403
  };
6159
6404
 
6160
- // src/call/CallOverlay.tsx
6161
- init_avatar();
6162
- init_button();
6163
- init_utils2();
6164
- init_call_store();
6165
-
6166
- // src/call/useRingCountdown.ts
6167
- init_call_types();
6168
- var useRingCountdown = (active, callId) => {
6169
- const [secondsLeft, setSecondsLeft] = useState(
6170
- Math.ceil(CALL_RING_TIMEOUT_MS / 1e3)
6171
- );
6172
- useEffect(() => {
6173
- if (!active) return;
6174
- const startedAt = Date.now();
6175
- setSecondsLeft(Math.ceil(CALL_RING_TIMEOUT_MS / 1e3));
6176
- const id = setInterval(() => {
6177
- const remainingMs = CALL_RING_TIMEOUT_MS - (Date.now() - startedAt);
6178
- setSecondsLeft(Math.max(0, Math.ceil(remainingMs / 1e3)));
6179
- }, 1e3);
6180
- return () => clearInterval(id);
6181
- }, [active, callId]);
6182
- return secondsLeft;
6183
- };
6184
- var useCallDuration = (startedAt) => {
6185
- const [elapsedMs, setElapsedMs] = useState(0);
6186
- useEffect(() => {
6187
- if (!startedAt) {
6188
- setElapsedMs(0);
6189
- return;
6190
- }
6191
- setElapsedMs(Date.now() - startedAt);
6192
- const id = setInterval(() => setElapsedMs(Date.now() - startedAt), 1e3);
6193
- return () => clearInterval(id);
6194
- }, [startedAt]);
6195
- const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1e3));
6196
- const hours = Math.floor(totalSeconds / 3600);
6197
- const minutes = Math.floor(totalSeconds % 3600 / 60);
6198
- const seconds = totalSeconds % 60;
6199
- const pad = (n) => String(n).padStart(2, "0");
6200
- return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
6201
- };
6202
- var CallSession2 = React9__default.lazy(() => Promise.resolve().then(() => (init_CallSession(), CallSession_exports)));
6203
- var CallOverlay = () => {
6204
- const uiStatus = useCallStore((s) => s.uiStatus);
6205
- const mode = useCallStore((s) => s.mode);
6206
- const callId = useCallStore((s) => s.callId);
6207
- const meetingId = useCallStore((s) => s.meetingId);
6208
- const token = useCallStore((s) => s.token);
6209
- const error = useCallStore((s) => s.error);
6210
- const callee = useCallStore((s) => s.callee);
6211
- const caller = useCallStore((s) => s.caller);
6212
- const callStartedAt = useCallStore((s) => s.callStartedAt);
6213
- const cancelCall = useCallStore((s) => s.cancelCall);
6214
- const reset = useCallStore((s) => s.reset);
6215
- const notifyServerCallFailed = useCallStore((s) => s.notifyServerCallFailed);
6216
- const [sessionError, setSessionError] = useState(null);
6217
- const secondsLeft = useRingCountdown(uiStatus === "ringing-out", callId);
6218
- const duration = useCallDuration(callStartedAt);
6219
- useEffect(() => {
6220
- setSessionError(null);
6221
- }, [callId]);
6222
- useEffect(() => {
6223
- if (sessionError) notifyServerCallFailed();
6224
- }, [sessionError, notifyServerCallFailed]);
6225
- const isOpen = uiStatus !== "idle" && uiStatus !== "incoming";
6226
- if (!isOpen) return null;
6227
- const displayError = error || sessionError;
6228
- const peerName = callee?.name || caller?.name || "";
6229
- const peerAvatar = callee?.avatar || caller?.profileImage || void 0;
6230
- const isVideo = mode === "video";
6231
- return /* @__PURE__ */ jsx(Dialog, { open: isOpen, onOpenChange: () => void 0, children: /* @__PURE__ */ jsx(
6232
- ChatDialogContent,
6233
- {
6234
- showCloseButton: false,
6235
- onEscapeKeyDown: (e) => e.preventDefault(),
6236
- onPointerDownOutside: (e) => e.preventDefault(),
6237
- className: cn(
6238
- "fixed inset-0 top-0 left-0 z-100 flex h-screen w-screen max-w-none",
6239
- "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"
6240
- ),
6241
- children: displayError ? /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-4 p-6 text-center", children: [
6242
- /* @__PURE__ */ jsx("p", { className: "max-w-sm text-sm text-white/80", children: displayError }),
6243
- /* @__PURE__ */ jsxs(Button, { variant: "secondary", onClick: reset, children: [
6244
- /* @__PURE__ */ jsx(X, { size: 16, className: "mr-1" }),
6245
- " Close"
6246
- ] })
6247
- ] }) : uiStatus === "in-call" && meetingId && token && mode ? /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col", children: [
6248
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2 px-4 py-3 text-sm text-white/70", children: [
6249
- peerName && /* @__PURE__ */ jsx("span", { className: "font-medium text-white/90", children: peerName }),
6250
- /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "text-white/30", children: "\xB7" }),
6251
- /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: duration })
6252
- ] }),
6253
- /* @__PURE__ */ jsx(
6254
- React9__default.Suspense,
6255
- {
6256
- fallback: /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-3 text-white/80", children: [
6257
- /* @__PURE__ */ jsx(Loader2, { className: "size-8 animate-spin" }),
6258
- /* @__PURE__ */ jsx("p", { className: "text-sm", children: "Loading call\u2026" })
6259
- ] }),
6260
- children: /* @__PURE__ */ jsx(
6261
- CallSession2,
6262
- {
6263
- meetingId,
6264
- token,
6265
- mode,
6266
- onError: setSessionError
6267
- }
6268
- )
6269
- }
6270
- )
6271
- ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-6 p-6 text-center", children: [
6272
- /* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-center", children: [
6273
- uiStatus === "ringing-out" && /* @__PURE__ */ jsxs(Fragment, { children: [
6274
- /* @__PURE__ */ jsx("span", { className: "absolute inline-flex size-32 animate-ping rounded-full bg-white/10" }),
6275
- /* @__PURE__ */ jsx(
6276
- "span",
6277
- {
6278
- className: "absolute inline-flex size-32 animate-ping rounded-full bg-white/5",
6279
- style: { animationDelay: "0.6s" }
6280
- }
6281
- )
6282
- ] }),
6283
- peerName ? /* @__PURE__ */ jsxs(Avatar, { className: "relative size-28 ring-4 ring-white/10", children: [
6284
- /* @__PURE__ */ jsx(AvatarImage, { src: peerAvatar, alt: peerName }),
6285
- /* @__PURE__ */ jsx(AvatarFallback, { className: "bg-neutral-700 text-3xl font-medium text-white", children: peerName.charAt(0).toUpperCase() })
6286
- ] }) : /* @__PURE__ */ jsx("div", { className: "relative flex size-28 items-center justify-center rounded-full bg-white/10", children: isVideo ? /* @__PURE__ */ jsx(Video, { size: 36 }) : /* @__PURE__ */ jsx(Phone, { size: 36 }) })
6287
- ] }),
6288
- /* @__PURE__ */ jsxs("div", { children: [
6289
- peerName && /* @__PURE__ */ jsx("p", { className: "text-xl font-semibold tracking-tight", children: peerName }),
6290
- /* @__PURE__ */ jsxs("p", { className: "mt-1 text-sm text-white/60", children: [
6291
- uiStatus === "creating" && "Starting call\u2026",
6292
- uiStatus === "ringing-out" && `${isVideo ? "Video calling" : "Calling"}\u2026 \xB7 ${secondsLeft}s`,
6293
- uiStatus === "joining" && "Connecting\u2026"
6294
- ] })
6295
- ] }),
6296
- uiStatus === "ringing-out" && /* @__PURE__ */ jsxs("div", { className: "mt-1 flex flex-col items-center gap-2", children: [
6297
- /* @__PURE__ */ jsx(
6298
- Button,
6299
- {
6300
- variant: "ghost",
6301
- size: "icon",
6302
- 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",
6303
- "aria-label": "Cancel call",
6304
- onClick: cancelCall,
6305
- children: /* @__PURE__ */ jsx(PhoneOff, { size: 24 })
6306
- }
6307
- ),
6308
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-white/60", children: "Cancel" })
6309
- ] })
6310
- ] })
6311
- }
6312
- ) });
6313
- };
6314
- var CallOverlay_default = CallOverlay;
6405
+ // src/call/IncomingCallDialog.tsx
6315
6406
  init_avatar();
6316
6407
  init_button();
6317
6408
  init_utils2();
@@ -6878,18 +6969,6 @@ var installNotificationClickBridge = (options = {}) => {
6878
6969
  };
6879
6970
 
6880
6971
  // src/hooks/useChatController.ts
6881
- init_call_types();
6882
- function formatCallLogText(entry) {
6883
- const label = entry.mode === "video" ? "video" : "voice";
6884
- if (entry.outcome === "missed") return `Missed ${label} call`;
6885
- if (entry.outcome === "declined") return `${label === "video" ? "Video" : "Voice"} call declined`;
6886
- if (entry.outcome === "busy") return `${label === "video" ? "Video" : "Voice"} call \xB7 not answered`;
6887
- const totalSeconds = Math.max(0, Math.floor(entry.durationSeconds));
6888
- const minutes = Math.floor(totalSeconds / 60);
6889
- const seconds = totalSeconds % 60;
6890
- const duration = `${minutes}:${String(seconds).padStart(2, "0")}`;
6891
- return `${label === "video" ? "Video" : "Voice"} call \xB7 ${duration}`;
6892
- }
6893
6972
  function resolveMessageType(content, attachments) {
6894
6973
  if (attachments.length === 0) return "text";
6895
6974
  if (!content.trim() && attachments.length === 1) {
@@ -6947,28 +7026,6 @@ var useChatController = () => {
6947
7026
  );
6948
7027
  }, []);
6949
7028
  const { localMessages, setLocalMessages } = useChatSync();
6950
- useEffect(() => {
6951
- const handleCallLog = (event) => {
6952
- const entry = event.detail;
6953
- if (!entry?.conversationId) return;
6954
- const logMessage = {
6955
- id: `call-log-${Date.now()}-${Math.random().toString(36).slice(2)}`,
6956
- content: formatCallLogText(entry),
6957
- sender: "system",
6958
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6959
- isOwnMessage: false,
6960
- isSystemMessage: true,
6961
- channelId: entry.conversationId,
6962
- conversationId: entry.conversationId
6963
- };
6964
- setLocalMessages((prev) => ({
6965
- ...prev,
6966
- [entry.conversationId]: [...prev[entry.conversationId] || [], logMessage]
6967
- }));
6968
- };
6969
- window.addEventListener(CALL_LOG_EVENT, handleCallLog);
6970
- return () => window.removeEventListener(CALL_LOG_EVENT, handleCallLog);
6971
- }, [setLocalMessages]);
6972
7029
  const emitDmSend = useStore((s) => s.emitDmSend);
6973
7030
  const emitGroupMessageSend2 = useStore((s) => s.emitGroupMessageSend);
6974
7031
  const emitMarkAsRead = useStore((s) => s.emitMarkAsRead);