@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.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);
@@ -789,6 +782,12 @@ var init_call_store = __esm({
789
782
  clearRingTimer();
790
783
  set({ ...idleState });
791
784
  },
785
+ notifyServerCallFailed: () => {
786
+ const { callId } = get();
787
+ if (callId) {
788
+ socket_service_default.emitCallEnd({ callId });
789
+ }
790
+ },
792
791
  _receiveIncoming: (payload) => {
793
792
  const currentStatus = get().uiStatus;
794
793
  if (currentStatus !== "idle") {
@@ -837,53 +836,60 @@ var init_call_store = __esm({
837
836
  }));
838
837
  },
839
838
  _receiveCancelled: (payload) => {
840
- if (get().callId !== payload.callId) return;
841
- clearRingTimer();
842
- const myUserId = useStore.getState().loggedUserDetails?._id;
843
- if (payload.endedBy !== myUserId) {
844
- emitCallLog({
845
- conversationId: payload.conversationId,
846
- mode: payload.mode,
847
- outcome: "missed",
848
- durationSeconds: 0
849
- });
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;
850
846
  }
847
+ clearRingTimer();
851
848
  set({ ...idleState });
852
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.
853
855
  _receiveEnded: (payload) => {
854
856
  if (get().callId !== payload.callId) return;
855
857
  clearRingTimer();
856
- emitCallLog({
857
- conversationId: payload.conversationId,
858
- mode: payload.mode,
859
- outcome: payload.durationSeconds > 0 ? "completed" : "declined",
860
- durationSeconds: payload.durationSeconds
861
- });
862
858
  set({ ...idleState });
863
859
  },
864
860
  _receiveBusy: (payload) => {
865
- 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
+ }
866
869
  clearRingTimer();
867
- emitCallLog({
868
- conversationId: payload.conversationId,
869
- mode: payload.mode,
870
- outcome: "busy",
871
- durationSeconds: 0
872
- });
873
870
  set({ ...idleState, error: "That user is already on another call." });
874
871
  },
875
872
  // Guarded by callId like every other _receive* handler — a late/stale
876
873
  // call_missed for a call that has already ended (accepted, rejected,
877
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.
878
883
  _receiveMissed: (payload) => {
879
- 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
+ }
880
892
  clearRingTimer();
881
- emitCallLog({
882
- conversationId: payload.conversationId,
883
- mode: payload.mode,
884
- outcome: "missed",
885
- durationSeconds: 0
886
- });
887
893
  set({ ...idleState });
888
894
  }
889
895
  }));
@@ -4294,6 +4300,58 @@ var init_utils2 = __esm({
4294
4300
  "src/lib/utils.ts"() {
4295
4301
  }
4296
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
+ });
4297
4355
  function Button({
4298
4356
  className,
4299
4357
  variant = "default",
@@ -4348,58 +4406,6 @@ var init_button = __esm({
4348
4406
  );
4349
4407
  }
4350
4408
  });
4351
- function Avatar({
4352
- className,
4353
- size = "default",
4354
- ...props
4355
- }) {
4356
- return /* @__PURE__ */ jsx(
4357
- Avatar$1.Root,
4358
- {
4359
- "data-slot": "avatar",
4360
- "data-size": size,
4361
- className: cn(
4362
- "group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
4363
- className
4364
- ),
4365
- ...props
4366
- }
4367
- );
4368
- }
4369
- function AvatarImage({
4370
- className,
4371
- ...props
4372
- }) {
4373
- return /* @__PURE__ */ jsx(
4374
- Avatar$1.Image,
4375
- {
4376
- "data-slot": "avatar-image",
4377
- className: cn("aspect-square size-full", className),
4378
- ...props
4379
- }
4380
- );
4381
- }
4382
- function AvatarFallback({
4383
- className,
4384
- ...props
4385
- }) {
4386
- return /* @__PURE__ */ jsx(
4387
- Avatar$1.Fallback,
4388
- {
4389
- "data-slot": "avatar-fallback",
4390
- className: cn(
4391
- "bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs",
4392
- className
4393
- ),
4394
- ...props
4395
- }
4396
- );
4397
- }
4398
- var init_avatar = __esm({
4399
- "src/ui/avatar.tsx"() {
4400
- init_utils2();
4401
- }
4402
- });
4403
4409
 
4404
4410
  // src/call/videosdk.loader.ts
4405
4411
  var loadVideoSDK;
@@ -4435,6 +4441,7 @@ var init_ParticipantView = __esm({
4435
4441
  const { webcamStream, micStream, webcamOn, micOn, displayName, isActiveSpeaker } = sdk.useParticipant(participantId);
4436
4442
  const videoRef = useRef(null);
4437
4443
  const audioRef = useRef(null);
4444
+ const [hasFrame, setHasFrame] = useState(false);
4438
4445
  useEffect(() => {
4439
4446
  const el = videoRef.current;
4440
4447
  if (!el) return;
@@ -4443,6 +4450,7 @@ var init_ParticipantView = __esm({
4443
4450
  el.play().catch(() => void 0);
4444
4451
  } else {
4445
4452
  el.srcObject = null;
4453
+ setHasFrame(false);
4446
4454
  }
4447
4455
  }, [webcamOn, webcamStream]);
4448
4456
  useEffect(() => {
@@ -4457,6 +4465,7 @@ var init_ParticipantView = __esm({
4457
4465
  }
4458
4466
  }, [micOn, micStream, isLocal]);
4459
4467
  const name = displayName || fallbackName || "Participant";
4468
+ const showVideo = webcamOn && hasFrame;
4460
4469
  return /* @__PURE__ */ jsxs(
4461
4470
  "div",
4462
4471
  {
@@ -4472,14 +4481,15 @@ var init_ParticipantView = __esm({
4472
4481
  ref: videoRef,
4473
4482
  muted: isLocal,
4474
4483
  playsInline: true,
4484
+ onPlaying: () => setHasFrame(true),
4475
4485
  className: cn(
4476
4486
  "size-full object-cover",
4477
- webcamOn ? "block" : "hidden"
4487
+ showVideo ? "block" : "hidden"
4478
4488
  )
4479
4489
  }
4480
4490
  ),
4481
4491
  !isLocal && /* @__PURE__ */ jsx("audio", { ref: audioRef, autoPlay: true, playsInline: true }),
4482
- !webcamOn && /* @__PURE__ */ jsxs(Avatar, { size: "lg", className: "size-20", children: [
4492
+ !showVideo && /* @__PURE__ */ jsxs(Avatar, { size: "lg", className: "size-20", children: [
4483
4493
  /* @__PURE__ */ jsx(AvatarImage, { src: fallbackImage, alt: name }),
4484
4494
  /* @__PURE__ */ jsx(AvatarFallback, { className: "bg-neutral-700 text-xl font-medium text-white", children: name.charAt(0).toUpperCase() })
4485
4495
  ] }),
@@ -4494,6 +4504,53 @@ var init_ParticipantView = __esm({
4494
4504
  ParticipantView_default = ParticipantView;
4495
4505
  }
4496
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
+ });
4497
4554
  var circleBtn, CallControls, CallControls_default;
4498
4555
  var init_CallControls = __esm({
4499
4556
  "src/call/CallControls.tsx"() {
@@ -4559,9 +4616,10 @@ var MeetingView, MeetingView_default;
4559
4616
  var init_MeetingView = __esm({
4560
4617
  "src/call/MeetingView.tsx"() {
4561
4618
  init_ParticipantView();
4619
+ init_AudioCallView();
4562
4620
  init_CallControls();
4563
4621
  init_call_store();
4564
- MeetingView = ({ sdk, mode }) => {
4622
+ MeetingView = ({ sdk, mode, peerName, peerAvatar }) => {
4565
4623
  const endCall = useCallStore((s) => s.endCall);
4566
4624
  const [hasJoined, setHasJoined] = useState(false);
4567
4625
  const hasJoinedRef = useRef(false);
@@ -4584,7 +4642,23 @@ var init_MeetingView = __esm({
4584
4642
  /* @__PURE__ */ jsx("p", { className: "text-sm", children: "Connecting\u2026" })
4585
4643
  ] });
4586
4644
  }
4587
- 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
+ }
4588
4662
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col", children: [
4589
4663
  /* @__PURE__ */ jsxs(
4590
4664
  "div",
@@ -4598,7 +4672,16 @@ var init_MeetingView = __esm({
4598
4672
  },
4599
4673
  children: [
4600
4674
  localParticipant?.id && /* @__PURE__ */ jsx(ParticipantView_default, { sdk, participantId: localParticipant.id, isLocal: true }),
4601
- 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
+ ))
4602
4685
  ]
4603
4686
  }
4604
4687
  ),
@@ -4620,7 +4703,14 @@ var init_CallSession = __esm({
4620
4703
  init_useStore();
4621
4704
  init_videosdk_loader();
4622
4705
  init_MeetingView();
4623
- CallSession = ({ meetingId, token, mode, onError }) => {
4706
+ CallSession = ({
4707
+ meetingId,
4708
+ token,
4709
+ mode,
4710
+ peerName,
4711
+ peerAvatar,
4712
+ onError
4713
+ }) => {
4624
4714
  const [sdk, setSdk] = useState(null);
4625
4715
  const userId = useStore((s) => s.loggedUserDetails?._id);
4626
4716
  const userName = useStore((s) => s.loggedUserDetails?.name);
@@ -4661,7 +4751,7 @@ var init_CallSession = __esm({
4661
4751
  token,
4662
4752
  reinitialiseMeetingOnConfigChange: false,
4663
4753
  joinWithoutUserInteraction: true,
4664
- children: /* @__PURE__ */ jsx(MeetingView_default, { sdk, mode })
4754
+ children: /* @__PURE__ */ jsx(MeetingView_default, { sdk, mode, peerName, peerAvatar })
4665
4755
  }
4666
4756
  );
4667
4757
  };
@@ -5613,6 +5703,167 @@ function useEffectiveSettingsOptional() {
5613
5703
  };
5614
5704
  }
5615
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
+
5616
5867
  // src/ui/dialog.tsx
5617
5868
  init_utils2();
5618
5869
  function Dialog({
@@ -6151,154 +6402,7 @@ var ChatAlertDialogContent = ({
6151
6402
  );
6152
6403
  };
6153
6404
 
6154
- // src/call/CallOverlay.tsx
6155
- init_avatar();
6156
- init_button();
6157
- init_utils2();
6158
- init_call_store();
6159
-
6160
- // src/call/useRingCountdown.ts
6161
- init_call_types();
6162
- var useRingCountdown = (active, callId) => {
6163
- const [secondsLeft, setSecondsLeft] = useState(
6164
- Math.ceil(CALL_RING_TIMEOUT_MS / 1e3)
6165
- );
6166
- useEffect(() => {
6167
- if (!active) return;
6168
- const startedAt = Date.now();
6169
- setSecondsLeft(Math.ceil(CALL_RING_TIMEOUT_MS / 1e3));
6170
- const id = setInterval(() => {
6171
- const remainingMs = CALL_RING_TIMEOUT_MS - (Date.now() - startedAt);
6172
- setSecondsLeft(Math.max(0, Math.ceil(remainingMs / 1e3)));
6173
- }, 1e3);
6174
- return () => clearInterval(id);
6175
- }, [active, callId]);
6176
- return secondsLeft;
6177
- };
6178
- var useCallDuration = (startedAt) => {
6179
- const [elapsedMs, setElapsedMs] = useState(0);
6180
- useEffect(() => {
6181
- if (!startedAt) {
6182
- setElapsedMs(0);
6183
- return;
6184
- }
6185
- setElapsedMs(Date.now() - startedAt);
6186
- const id = setInterval(() => setElapsedMs(Date.now() - startedAt), 1e3);
6187
- return () => clearInterval(id);
6188
- }, [startedAt]);
6189
- const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1e3));
6190
- const hours = Math.floor(totalSeconds / 3600);
6191
- const minutes = Math.floor(totalSeconds % 3600 / 60);
6192
- const seconds = totalSeconds % 60;
6193
- const pad = (n) => String(n).padStart(2, "0");
6194
- return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
6195
- };
6196
- var CallSession2 = React9__default.lazy(() => Promise.resolve().then(() => (init_CallSession(), CallSession_exports)));
6197
- var CallOverlay = () => {
6198
- const uiStatus = useCallStore((s) => s.uiStatus);
6199
- const mode = useCallStore((s) => s.mode);
6200
- const callId = useCallStore((s) => s.callId);
6201
- const meetingId = useCallStore((s) => s.meetingId);
6202
- const token = useCallStore((s) => s.token);
6203
- const error = useCallStore((s) => s.error);
6204
- const callee = useCallStore((s) => s.callee);
6205
- const caller = useCallStore((s) => s.caller);
6206
- const callStartedAt = useCallStore((s) => s.callStartedAt);
6207
- const cancelCall = useCallStore((s) => s.cancelCall);
6208
- const reset = useCallStore((s) => s.reset);
6209
- const [sessionError, setSessionError] = useState(null);
6210
- const secondsLeft = useRingCountdown(uiStatus === "ringing-out", callId);
6211
- const duration = useCallDuration(callStartedAt);
6212
- const isOpen = uiStatus !== "idle" && uiStatus !== "incoming";
6213
- if (!isOpen) return null;
6214
- const displayError = error || sessionError;
6215
- const peerName = callee?.name || caller?.name || "";
6216
- const peerAvatar = callee?.avatar || caller?.profileImage || void 0;
6217
- const isVideo = mode === "video";
6218
- return /* @__PURE__ */ jsx(Dialog, { open: isOpen, onOpenChange: () => void 0, children: /* @__PURE__ */ jsx(
6219
- ChatDialogContent,
6220
- {
6221
- showCloseButton: false,
6222
- onEscapeKeyDown: (e) => e.preventDefault(),
6223
- onPointerDownOutside: (e) => e.preventDefault(),
6224
- className: cn(
6225
- "fixed inset-0 top-0 left-0 z-100 flex h-screen w-screen max-w-none",
6226
- "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"
6227
- ),
6228
- children: displayError ? /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-4 p-6 text-center", children: [
6229
- /* @__PURE__ */ jsx("p", { className: "max-w-sm text-sm text-white/80", children: displayError }),
6230
- /* @__PURE__ */ jsxs(Button, { variant: "secondary", onClick: reset, children: [
6231
- /* @__PURE__ */ jsx(X, { size: 16, className: "mr-1" }),
6232
- " Close"
6233
- ] })
6234
- ] }) : uiStatus === "in-call" && meetingId && token && mode ? /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col", children: [
6235
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2 px-4 py-3 text-sm text-white/70", children: [
6236
- peerName && /* @__PURE__ */ jsx("span", { className: "font-medium text-white/90", children: peerName }),
6237
- /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "text-white/30", children: "\xB7" }),
6238
- /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: duration })
6239
- ] }),
6240
- /* @__PURE__ */ jsx(
6241
- React9__default.Suspense,
6242
- {
6243
- fallback: /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-3 text-white/80", children: [
6244
- /* @__PURE__ */ jsx(Loader2, { className: "size-8 animate-spin" }),
6245
- /* @__PURE__ */ jsx("p", { className: "text-sm", children: "Loading call\u2026" })
6246
- ] }),
6247
- children: /* @__PURE__ */ jsx(
6248
- CallSession2,
6249
- {
6250
- meetingId,
6251
- token,
6252
- mode,
6253
- onError: setSessionError
6254
- }
6255
- )
6256
- }
6257
- )
6258
- ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-6 p-6 text-center", children: [
6259
- /* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-center", children: [
6260
- uiStatus === "ringing-out" && /* @__PURE__ */ jsxs(Fragment, { children: [
6261
- /* @__PURE__ */ jsx("span", { className: "absolute inline-flex size-32 animate-ping rounded-full bg-white/10" }),
6262
- /* @__PURE__ */ jsx(
6263
- "span",
6264
- {
6265
- className: "absolute inline-flex size-32 animate-ping rounded-full bg-white/5",
6266
- style: { animationDelay: "0.6s" }
6267
- }
6268
- )
6269
- ] }),
6270
- peerName ? /* @__PURE__ */ jsxs(Avatar, { className: "relative size-28 ring-4 ring-white/10", children: [
6271
- /* @__PURE__ */ jsx(AvatarImage, { src: peerAvatar, alt: peerName }),
6272
- /* @__PURE__ */ jsx(AvatarFallback, { className: "bg-neutral-700 text-3xl font-medium text-white", children: peerName.charAt(0).toUpperCase() })
6273
- ] }) : /* @__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 }) })
6274
- ] }),
6275
- /* @__PURE__ */ jsxs("div", { children: [
6276
- peerName && /* @__PURE__ */ jsx("p", { className: "text-xl font-semibold tracking-tight", children: peerName }),
6277
- /* @__PURE__ */ jsxs("p", { className: "mt-1 text-sm text-white/60", children: [
6278
- uiStatus === "creating" && "Starting call\u2026",
6279
- uiStatus === "ringing-out" && `${isVideo ? "Video calling" : "Calling"}\u2026 \xB7 ${secondsLeft}s`,
6280
- uiStatus === "joining" && "Connecting\u2026"
6281
- ] })
6282
- ] }),
6283
- uiStatus === "ringing-out" && /* @__PURE__ */ jsxs("div", { className: "mt-1 flex flex-col items-center gap-2", children: [
6284
- /* @__PURE__ */ jsx(
6285
- Button,
6286
- {
6287
- variant: "ghost",
6288
- size: "icon",
6289
- 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",
6290
- "aria-label": "Cancel call",
6291
- onClick: cancelCall,
6292
- children: /* @__PURE__ */ jsx(PhoneOff, { size: 24 })
6293
- }
6294
- ),
6295
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-white/60", children: "Cancel" })
6296
- ] })
6297
- ] })
6298
- }
6299
- ) });
6300
- };
6301
- var CallOverlay_default = CallOverlay;
6405
+ // src/call/IncomingCallDialog.tsx
6302
6406
  init_avatar();
6303
6407
  init_button();
6304
6408
  init_utils2();
@@ -6865,18 +6969,6 @@ var installNotificationClickBridge = (options = {}) => {
6865
6969
  };
6866
6970
 
6867
6971
  // src/hooks/useChatController.ts
6868
- init_call_types();
6869
- function formatCallLogText(entry) {
6870
- const label = entry.mode === "video" ? "video" : "voice";
6871
- if (entry.outcome === "missed") return `Missed ${label} call`;
6872
- if (entry.outcome === "declined") return `${label === "video" ? "Video" : "Voice"} call declined`;
6873
- if (entry.outcome === "busy") return `${label === "video" ? "Video" : "Voice"} call \xB7 not answered`;
6874
- const totalSeconds = Math.max(0, Math.floor(entry.durationSeconds));
6875
- const minutes = Math.floor(totalSeconds / 60);
6876
- const seconds = totalSeconds % 60;
6877
- const duration = `${minutes}:${String(seconds).padStart(2, "0")}`;
6878
- return `${label === "video" ? "Video" : "Voice"} call \xB7 ${duration}`;
6879
- }
6880
6972
  function resolveMessageType(content, attachments) {
6881
6973
  if (attachments.length === 0) return "text";
6882
6974
  if (!content.trim() && attachments.length === 1) {
@@ -6934,28 +7026,6 @@ var useChatController = () => {
6934
7026
  );
6935
7027
  }, []);
6936
7028
  const { localMessages, setLocalMessages } = useChatSync();
6937
- useEffect(() => {
6938
- const handleCallLog = (event) => {
6939
- const entry = event.detail;
6940
- if (!entry?.conversationId) return;
6941
- const logMessage = {
6942
- id: `call-log-${Date.now()}-${Math.random().toString(36).slice(2)}`,
6943
- content: formatCallLogText(entry),
6944
- sender: "system",
6945
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6946
- isOwnMessage: false,
6947
- isSystemMessage: true,
6948
- channelId: entry.conversationId,
6949
- conversationId: entry.conversationId
6950
- };
6951
- setLocalMessages((prev) => ({
6952
- ...prev,
6953
- [entry.conversationId]: [...prev[entry.conversationId] || [], logMessage]
6954
- }));
6955
- };
6956
- window.addEventListener(CALL_LOG_EVENT, handleCallLog);
6957
- return () => window.removeEventListener(CALL_LOG_EVENT, handleCallLog);
6958
- }, [setLocalMessages]);
6959
7029
  const emitDmSend = useStore((s) => s.emitDmSend);
6960
7030
  const emitGroupMessageSend2 = useStore((s) => s.emitGroupMessageSend);
6961
7031
  const emitMarkAsRead = useStore((s) => s.emitMarkAsRead);