@realtimexsco/live-chat 1.5.5 → 1.5.6

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
@@ -9,7 +9,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
9
9
  import { cva } from 'class-variance-authority';
10
10
  import * as React11 from 'react';
11
11
  import React11__default, { createContext, useState, useEffect, useMemo, useContext, useCallback, useRef, useLayoutEffect, Fragment as Fragment$1, useSyncExternalStore } from 'react';
12
- import { Loader2, X, Users, Video, Phone, PhoneOff, Sun, Moon, Settings, Search, UserPlus, ChevronDown, ArrowLeft, 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, LayoutGrid, Rows3, MessageCircle, ExternalLink, Download, ChevronLeft, ZoomOut, ZoomIn, RotateCcw, Music, Hand, MicOff, MonitorUp, XIcon, ArrowRight, MoreHorizontal, PinOff, StarOff, ImageIcon, Film, Play, Pause, FileText, FileSpreadsheet, FileIcon, MessageSquare, Eye, Captions, Mic, VideoOff } from 'lucide-react';
12
+ import { Loader2, X, Users, Video, Phone, PhoneOff, Sun, Moon, Settings, Search, UserPlus, ChevronDown, ArrowLeft, 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, LayoutGrid, Rows3, MessageCircle, ExternalLink, Download, ChevronLeft, ZoomOut, ZoomIn, RotateCcw, Music, Hand, MicOff, MonitorUp, XIcon, ArrowRight, MoreHorizontal, PinOff, StarOff, ImageIcon, Film, Play, Pause, FileText, FileSpreadsheet, FileIcon, MessageSquare, Eye, Captions, Mic, VideoOff, Circle } from 'lucide-react';
13
13
  import { flushSync } from 'react-dom';
14
14
  import EmojiPicker, { Theme } from 'emoji-picker-react';
15
15
  import { getDefaultClassNames, DayPicker } from 'react-day-picker';
@@ -884,12 +884,13 @@ var init_store_helpers = __esm({
884
884
  });
885
885
 
886
886
  // src/notifications/open-conversation.ts
887
- var OPEN_CONVERSATION_EVENT, NOTIFICATION_CLICK_SW_TYPE, PUSH_BROADCAST_CHANNEL, PUSH_DATA_CACHE_NAME, PENDING_STORAGE_KEY, PENDING_OPEN_CACHE_PATH, resolveConversationIdFromPushData, normalizePushMeta, parsePendingRaw, peekPendingOpenConversation, peekPendingOpenRequest, consumePendingOpenConversation, consumePendingOpenRequest, requestOpenConversation, buildChannelFromPushRequest, mapConversationToChannel, stripConversationIdFromUrl, cacheRequestForPath, consumePendingOpenFromPushCache, readConversationIdFromLocation, consumedUrlConversationIds, lastHandledOpenId, lastHandledOpenAt, shouldHandleOpen, handleNotificationOpen, installNotificationClickBridge;
887
+ var OPEN_CONVERSATION_EVENT, NOTIFICATION_CLICK_SW_TYPE, CHAT_HEARTBEAT_SW_TYPE, PUSH_BROADCAST_CHANNEL, PUSH_DATA_CACHE_NAME, PENDING_STORAGE_KEY, PENDING_OPEN_CACHE_PATH, resolveConversationIdFromPushData, normalizePushMeta, parsePendingRaw, peekPendingOpenConversation, peekPendingOpenRequest, consumePendingOpenConversation, consumePendingOpenRequest, requestOpenConversation, buildChannelFromPushRequest, mapConversationToChannel, stripConversationIdFromUrl, cacheRequestForPath, consumePendingOpenFromPushCache, readConversationIdFromLocation, consumedUrlConversationIds, lastHandledOpenId, lastHandledOpenAt, shouldHandleOpen, handleNotificationOpen, installNotificationClickBridge;
888
888
  var init_open_conversation = __esm({
889
889
  "src/notifications/open-conversation.ts"() {
890
890
  init_store_helpers();
891
891
  OPEN_CONVERSATION_EVENT = "realtimex:open-conversation";
892
892
  NOTIFICATION_CLICK_SW_TYPE = "realtimex:notification-click";
893
+ CHAT_HEARTBEAT_SW_TYPE = "realtimex:chat-heartbeat";
893
894
  PUSH_BROADCAST_CHANNEL = "realtimex-push";
894
895
  PUSH_DATA_CACHE_NAME = "realtimex-push-data-v1";
895
896
  PENDING_STORAGE_KEY = "realtimex:pending-open-conversation";
@@ -4811,9 +4812,8 @@ var init_ParticipantView = __esm({
4811
4812
  isHandRaised = false,
4812
4813
  className
4813
4814
  }) => {
4814
- const { webcamStream, micStream, webcamOn, micOn, displayName, isActiveSpeaker } = sdk.useParticipant(participantId);
4815
+ const { webcamStream, webcamOn, micOn, displayName, isActiveSpeaker } = sdk.useParticipant(participantId);
4815
4816
  const videoRef = useRef(null);
4816
- const audioRef = useRef(null);
4817
4817
  const [hasFrame, setHasFrame] = useState(false);
4818
4818
  useEffect(() => {
4819
4819
  const el = videoRef.current;
@@ -4826,17 +4826,6 @@ var init_ParticipantView = __esm({
4826
4826
  setHasFrame(false);
4827
4827
  }
4828
4828
  }, [webcamOn, webcamStream]);
4829
- useEffect(() => {
4830
- if (isLocal) return;
4831
- const el = audioRef.current;
4832
- if (!el) return;
4833
- if (micOn && micStream?.track) {
4834
- el.srcObject = new MediaStream([micStream.track]);
4835
- el.play().catch(() => void 0);
4836
- } else {
4837
- el.srcObject = null;
4838
- }
4839
- }, [micOn, micStream, isLocal]);
4840
4829
  const name = displayName || fallbackName || "Participant";
4841
4830
  const showVideo = webcamOn && hasFrame;
4842
4831
  return /* @__PURE__ */ jsxs(
@@ -4869,7 +4858,6 @@ var init_ParticipantView = __esm({
4869
4858
  )
4870
4859
  }
4871
4860
  ),
4872
- !isLocal && /* @__PURE__ */ jsx("audio", { ref: audioRef, autoPlay: true, playsInline: true }),
4873
4861
  isHandRaised && /* @__PURE__ */ jsx("span", { className: "absolute top-2 left-2 flex size-7 items-center justify-center rounded-full bg-amber-400 text-neutral-900 shadow-lg shadow-amber-400/30 animate-[bounce_1s_ease-in-out_2]", children: /* @__PURE__ */ jsx(Hand, { size: 14 }) }),
4874
4862
  !showVideo && /* @__PURE__ */ jsxs(Avatar, { size: "lg", className: "size-20", children: [
4875
4863
  /* @__PURE__ */ jsx(AvatarImage, { src: fallbackImage, alt: name }),
@@ -5036,7 +5024,8 @@ var init_CallControls = __esm({
5036
5024
  isHandRaised,
5037
5025
  onToggleRaiseHand,
5038
5026
  isChatOpen,
5039
- onToggleChat
5027
+ onToggleChat,
5028
+ isVideoActive
5040
5029
  }) => {
5041
5030
  const {
5042
5031
  toggleMic,
@@ -5045,9 +5034,28 @@ var init_CallControls = __esm({
5045
5034
  localWebcamOn,
5046
5035
  localScreenShareOn,
5047
5036
  toggleScreenShare,
5037
+ recordingState,
5038
+ startRecording,
5039
+ stopRecording,
5048
5040
  leave
5049
5041
  } = sdk.useMeeting();
5050
5042
  const endCall = useCallStore((s) => s.endCall);
5043
+ const isRecording = recordingState === "RECORDING_STARTED" || recordingState === "RECORDING_STARTING";
5044
+ const isRecordingBusy = recordingState === "RECORDING_STARTING" || recordingState === "RECORDING_STOPPING";
5045
+ const handleToggleRecording = () => {
5046
+ if (isRecordingBusy) return;
5047
+ if (isRecording) {
5048
+ stopRecording();
5049
+ return;
5050
+ }
5051
+ startRecording("", "", {
5052
+ layout: { type: "GRID", priority: "SPEAKER", gridSize: 4 },
5053
+ orientation: "landscape",
5054
+ theme: "DEFAULT",
5055
+ mode: "video-and-audio",
5056
+ quality: "high"
5057
+ });
5058
+ };
5051
5059
  const handleLeave = () => {
5052
5060
  leave();
5053
5061
  endCall();
@@ -5063,7 +5071,7 @@ var init_CallControls = __esm({
5063
5071
  onClick: () => toggleMic()
5064
5072
  }
5065
5073
  ),
5066
- /* @__PURE__ */ jsx(
5074
+ isVideoActive ? /* @__PURE__ */ jsx(
5067
5075
  ControlButton,
5068
5076
  {
5069
5077
  icon: localWebcamOn ? /* @__PURE__ */ jsx(Video, { size: 20 }) : /* @__PURE__ */ jsx(VideoOff, { size: 20 }),
@@ -5071,6 +5079,13 @@ var init_CallControls = __esm({
5071
5079
  active: !localWebcamOn,
5072
5080
  onClick: () => toggleWebcam()
5073
5081
  }
5082
+ ) : /* @__PURE__ */ jsx(
5083
+ ControlButton,
5084
+ {
5085
+ icon: /* @__PURE__ */ jsx(Video, { size: 20 }),
5086
+ label: "Switch to video call",
5087
+ onClick: () => toggleWebcam()
5088
+ }
5074
5089
  ),
5075
5090
  /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "mx-0.5 h-7 w-px shrink-0 bg-white/15" }),
5076
5091
  /* @__PURE__ */ jsx(
@@ -5083,6 +5098,16 @@ var init_CallControls = __esm({
5083
5098
  onClick: () => toggleScreenShare()
5084
5099
  }
5085
5100
  ),
5101
+ /* @__PURE__ */ jsx(
5102
+ ControlButton,
5103
+ {
5104
+ icon: /* @__PURE__ */ jsx(Circle, { size: 14, className: cn("fill-current", isRecording && "animate-pulse") }),
5105
+ label: isRecording ? "Stop recording" : "Start recording",
5106
+ active: isRecording,
5107
+ activeClassName: "bg-red-600 hover:bg-red-500",
5108
+ onClick: handleToggleRecording
5109
+ }
5110
+ ),
5086
5111
  /* @__PURE__ */ jsx(
5087
5112
  ControlButton,
5088
5113
  {
@@ -5262,7 +5287,7 @@ var init_useCallReactions = __esm({
5262
5287
  };
5263
5288
  }
5264
5289
  });
5265
- var RemoteWebcamWatcher, MeetingView, MeetingView_default;
5290
+ var RemoteParticipantWatcher, MeetingView, MeetingView_default;
5266
5291
  var init_MeetingView = __esm({
5267
5292
  "src/call/MeetingView.tsx"() {
5268
5293
  init_ParticipantView();
@@ -5273,17 +5298,28 @@ var init_MeetingView = __esm({
5273
5298
  init_useCallReactions();
5274
5299
  init_call_store();
5275
5300
  init_utils2();
5276
- RemoteWebcamWatcher = ({
5301
+ RemoteParticipantWatcher = ({
5277
5302
  sdk,
5278
5303
  participantId,
5279
- onChange
5304
+ onWebcamChange
5280
5305
  }) => {
5281
- const { webcamOn } = sdk.useParticipant(participantId);
5306
+ const { webcamOn, micStream, micOn } = sdk.useParticipant(participantId);
5307
+ const audioRef = useRef(null);
5282
5308
  useEffect(() => {
5283
- onChange(participantId, webcamOn);
5284
- return () => onChange(participantId, false);
5309
+ onWebcamChange(participantId, webcamOn);
5310
+ return () => onWebcamChange(participantId, false);
5285
5311
  }, [participantId, webcamOn]);
5286
- return null;
5312
+ useEffect(() => {
5313
+ const el = audioRef.current;
5314
+ if (!el) return;
5315
+ if (micOn && micStream?.track) {
5316
+ el.srcObject = new MediaStream([micStream.track]);
5317
+ el.play().catch(() => void 0);
5318
+ } else {
5319
+ el.srcObject = null;
5320
+ }
5321
+ }, [micOn, micStream]);
5322
+ return /* @__PURE__ */ jsx("audio", { ref: audioRef, autoPlay: true, playsInline: true });
5287
5323
  };
5288
5324
  MeetingView = ({
5289
5325
  sdk,
@@ -5336,7 +5372,17 @@ var init_MeetingView = __esm({
5336
5372
  (id) => id !== localParticipant?.id
5337
5373
  );
5338
5374
  const isGroup = remoteIds.length > 1;
5339
- const webcamWatchers = remoteIds.map((id) => /* @__PURE__ */ jsx(RemoteWebcamWatcher, { sdk, participantId: id, onChange: handleRemoteWebcamChange }, id));
5375
+ const remoteParticipantWatchers = remoteIds.map((id) => /* @__PURE__ */ jsx(
5376
+ RemoteParticipantWatcher,
5377
+ {
5378
+ sdk,
5379
+ participantId: id,
5380
+ onWebcamChange: handleRemoteWebcamChange
5381
+ },
5382
+ id
5383
+ ));
5384
+ const anyRemoteWebcamOn = Object.values(remoteWebcamOn).some(Boolean);
5385
+ const showVideoGrid = mode === "video" || localWebcamOn || anyRemoteWebcamOn;
5340
5386
  const controls = /* @__PURE__ */ jsx(
5341
5387
  CallControls_default,
5342
5388
  {
@@ -5345,7 +5391,8 @@ var init_MeetingView = __esm({
5345
5391
  isHandRaised,
5346
5392
  onToggleRaiseHand: toggleRaiseHand,
5347
5393
  isChatOpen,
5348
- onToggleChat
5394
+ onToggleChat,
5395
+ isVideoActive: showVideoGrid
5349
5396
  }
5350
5397
  );
5351
5398
  const connectionStatusText = meetingState === "CONNECTING" ? "Connecting\u2026" : meetingState === "DISCONNECTED" ? "Reconnecting\u2026" : meetingState === "FAILED" ? "Connection lost" : null;
@@ -5355,7 +5402,7 @@ var init_MeetingView = __esm({
5355
5402
  ] });
5356
5403
  if (presenterId) {
5357
5404
  return /* @__PURE__ */ jsxs(Fragment, { children: [
5358
- webcamWatchers,
5405
+ remoteParticipantWatchers,
5359
5406
  /* @__PURE__ */ jsxs("div", { className: "relative flex flex-1 flex-col", children: [
5360
5407
  statusBanner,
5361
5408
  /* @__PURE__ */ jsx(FloatingReactions_default, { reactions: floatingReactions }),
@@ -5396,11 +5443,9 @@ var init_MeetingView = __esm({
5396
5443
  ] })
5397
5444
  ] });
5398
5445
  }
5399
- const anyRemoteWebcamOn = Object.values(remoteWebcamOn).some(Boolean);
5400
- const showVideoGrid = mode === "video" || localWebcamOn || anyRemoteWebcamOn;
5401
5446
  if (!showVideoGrid) {
5402
5447
  return /* @__PURE__ */ jsxs(Fragment, { children: [
5403
- webcamWatchers,
5448
+ remoteParticipantWatchers,
5404
5449
  /* @__PURE__ */ jsxs("div", { className: "relative flex flex-1 flex-col", children: [
5405
5450
  statusBanner,
5406
5451
  /* @__PURE__ */ jsx(FloatingReactions_default, { reactions: floatingReactions }),
@@ -5423,7 +5468,7 @@ var init_MeetingView = __esm({
5423
5468
  (id) => !!id && id !== focusedId
5424
5469
  ) : [];
5425
5470
  return /* @__PURE__ */ jsxs(Fragment, { children: [
5426
- webcamWatchers,
5471
+ remoteParticipantWatchers,
5427
5472
  /* @__PURE__ */ jsxs("div", { className: "relative flex flex-1 flex-col", children: [
5428
5473
  statusBanner,
5429
5474
  /* @__PURE__ */ jsx(FloatingReactions_default, { reactions: floatingReactions }),
@@ -6533,7 +6578,186 @@ init_useStore();
6533
6578
  init_store_helpers();
6534
6579
  init_utils2();
6535
6580
  init_button();
6581
+
6582
+ // src/chat/lib/chat-theme.ts
6583
+ init_utils2();
6584
+ var getChatScopeClass = (isDark) => cn("chat-theme-scope", isDark && "dark");
6585
+ var chatSheetContentClass = "flex w-full flex-col gap-0 p-0 bg-(--chat-surface) text-(--chat-text)";
6586
+ var chatSheetHeaderClass = "border-b border-(--chat-border) px-4 py-3 pr-11 text-left";
6587
+ var chatSheetBodyClass = "chat-message-scroll flex-1 overflow-y-auto bg-(--chat-surface) p-3";
6588
+ var chatSheetTitleClass = "text-[13px] font-semibold tracking-tight text-(--chat-text)";
6589
+ var chatSheetSubtitleClass = "text-[11px] text-(--chat-muted)";
6590
+ var chatDetailRowClass = "flex items-start justify-between gap-3 border-b border-(--chat-border) py-2.5 last:border-b-0";
6591
+ var chatDetailLabelClass = "text-[11px] text-(--chat-muted)";
6592
+ var chatDetailValueClass = "text-[13px] font-medium text-(--chat-text) text-right";
6593
+ var chatDialogContentClass = "overflow-hidden rounded-2xl border border-(--chat-border) bg-(--chat-surface) p-0 text-(--chat-text) shadow-[var(--chat-shadow)]";
6594
+ var chatDropdownContentClass = "rounded-xl border border-(--chat-border) bg-(--chat-surface) text-(--chat-text) shadow-lg";
6595
+ var chatOptionsMenuClass = "min-w-[220px] rounded-xl border border-(--chat-border) bg-(--chat-surface) p-1.5 text-(--chat-text) shadow-[0_10px_40px_rgba(0,0,0,0.12)]";
6596
+ var chatOptionsMenuLabelClass = "px-2.5 pb-0.5 pt-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-(--chat-muted)";
6597
+ var chatOptionsMenuSeparatorClass = "my-1.5 h-px bg-(--chat-border)";
6598
+ var chatOptionsMenuItemClass = "cursor-pointer gap-2.5 rounded-lg px-2.5 py-2 text-[13px] font-medium text-(--chat-text) focus:bg-(--chat-hover) focus:text-(--chat-text) data-[highlighted]:bg-(--chat-hover) [&_svg]:size-[15px] [&_svg]:shrink-0 [&_svg]:text-(--chat-muted)";
6599
+ var chatOptionsMenuItemDestructiveClass = "cursor-pointer gap-2.5 rounded-lg px-2.5 py-2 text-[13px] font-medium text-(--chat-danger-text) focus:bg-red-500/10 focus:text-(--chat-danger-text) data-[highlighted]:bg-red-500/10 data-[highlighted]:text-(--chat-danger-text) [&_svg]:size-[15px] [&_svg]:shrink-0 [&_svg]:!text-(--chat-danger-text)";
6600
+ var chatOptionsMenuSubTriggerClass = "cursor-pointer gap-2.5 rounded-lg px-2.5 py-2 pr-7 text-[13px] font-medium text-(--chat-text) focus:bg-(--chat-hover) focus:text-(--chat-text) data-[highlighted]:bg-(--chat-hover) data-[state=open]:bg-(--chat-hover) [&_svg]:size-[15px] [&_svg]:shrink-0 [&_svg]:text-(--chat-muted)";
6601
+ var chatPopoverContentClass = "w-auto border border-(--chat-border) bg-(--chat-surface) p-0 text-(--chat-text) shadow-2xl";
6602
+ var chatInputClass = "border border-(--chat-border) bg-(--chat-input-bg) text-(--chat-text) shadow-none placeholder:text-(--chat-muted) focus-visible:border-(--chat-theme-20) focus-visible:ring-2 focus-visible:ring-(--chat-theme-10)";
6603
+ var chatSidebarSearchShellClass = "relative flex min-w-0 flex-1 items-center rounded-lg border border-(--chat-border) bg-(--chat-search-bg) px-2.5 transition-colors focus-within:border-(--chat-theme) focus-within:shadow-[0_0_0_2px_var(--chat-theme-10)]";
6604
+ var chatSidebarSearchInputClass = "h-8 w-full min-w-0 border-0 bg-transparent px-1.5 text-xs text-(--chat-text) shadow-none outline-none placeholder:text-(--chat-muted)/60 focus-visible:border-0 focus-visible:shadow-none focus-visible:ring-0";
6605
+ var chatSelectClass = "h-10 w-full cursor-pointer appearance-none rounded-xl border border-(--chat-border) bg-(--chat-input-bg) pl-3 pr-9 text-sm text-(--chat-text) shadow-none focus-visible:border-(--chat-theme-20) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--chat-theme-10)";
6606
+ var chatListItemClass = "transition-colors hover:bg-(--chat-hover)";
6607
+ var chatLoadMoreButtonClass = "h-7 gap-1 rounded-full border border-(--chat-border) bg-(--chat-elevated) px-4 text-[10px] font-medium text-(--chat-text) transition-colors hover:border-(--chat-theme-20) hover:bg-(--chat-theme-5) hover:text-(--chat-theme) disabled:opacity-50 [&_svg]:size-3 [&_svg]:text-current";
6608
+ var chatDrawerSectionHeadingClass = "mb-1 px-0.5 text-[10px] font-semibold tracking-wide text-(--chat-muted)";
6609
+ var chatHeaderToolbarIconProps = { size: 14, strokeWidth: 1.75 };
6610
+ var chatSavedMessageModalIconClass = "shrink-0 text-(--chat-theme)";
6611
+ var chatHeaderToolbarIconLgProps = { size: 22, strokeWidth: 1.75 };
6612
+ var chatHeaderToolbarIconSmProps = { size: 11, strokeWidth: 1.75 };
6613
+ var chatDrawerFileRowClass = "flex items-center gap-2 rounded-md border border-(--chat-border)/70 bg-(--chat-elevated) px-2 py-1.5 transition-colors hover:bg-(--chat-hover)/50";
6614
+ var chatDrawerFileIconWrapClass = "flex size-8 shrink-0 items-center justify-center rounded-md bg-(--chat-hover)";
6615
+ var chatDrawerMediaFrameClass = "overflow-hidden rounded-md border border-(--chat-border)/70 bg-(--chat-elevated)";
6616
+ var buildChatThemeStyles = (themeColor) => ({
6617
+ "--chat-theme": themeColor,
6618
+ "--chat-theme-5": `${themeColor}0D`,
6619
+ "--chat-theme-10": `${themeColor}1A`,
6620
+ "--chat-theme-20": `${themeColor}33`,
6621
+ "--chat-theme-40": `${themeColor}66`,
6622
+ "--chat-theme-60": `${themeColor}99`,
6623
+ "--read-tick-color": themeColor.toLowerCase() === "#0000ff" || themeColor.toLowerCase().includes("007aff") ? "#00e5ff" : "#40C4FF"
6624
+ });
6625
+
6626
+ // src/chat/lib/chat-theme-storage.ts
6627
+ var STORAGE_KEY = "chat-color-mode";
6628
+ var loadChatColorMode = () => {
6629
+ if (typeof window === "undefined") return null;
6630
+ try {
6631
+ const stored = localStorage.getItem(STORAGE_KEY);
6632
+ return stored === "dark" || stored === "light" ? stored : null;
6633
+ } catch {
6634
+ return null;
6635
+ }
6636
+ };
6637
+ var saveChatColorMode = (mode) => {
6638
+ if (typeof window === "undefined") return;
6639
+ try {
6640
+ localStorage.setItem(STORAGE_KEY, mode);
6641
+ } catch {
6642
+ }
6643
+ };
6644
+ var CHAT_THEME_SHELL_CLASS = "chat-theme-shell";
6645
+ function withThemeTransition(apply) {
6646
+ if (typeof document === "undefined") {
6647
+ apply();
6648
+ return;
6649
+ }
6650
+ flushSync(apply);
6651
+ }
6652
+
6653
+ // src/context/ChatThemeContext.tsx
6654
+ init_utils2();
6655
+ var ChatThemeContext = createContext(null);
6656
+ var ChatThemeProvider = ({
6657
+ children,
6658
+ themeColor,
6659
+ colorMode,
6660
+ defaultColorMode = "light",
6661
+ onColorModeChange,
6662
+ showColorModeToggle = true
6663
+ }) => {
6664
+ const isControlled = colorMode !== void 0;
6665
+ const [internalMode, setInternalMode] = useState(defaultColorMode);
6666
+ const [isThemeTransitionReady, setIsThemeTransitionReady] = useState(false);
6667
+ const skipThemeTransitionRef = useRef(true);
6668
+ useLayoutEffect(() => {
6669
+ if (!isControlled) {
6670
+ const stored = loadChatColorMode();
6671
+ if (stored) {
6672
+ setInternalMode(stored);
6673
+ }
6674
+ }
6675
+ const frame = requestAnimationFrame(() => {
6676
+ skipThemeTransitionRef.current = false;
6677
+ setIsThemeTransitionReady(true);
6678
+ });
6679
+ return () => cancelAnimationFrame(frame);
6680
+ }, [isControlled]);
6681
+ const resolvedMode = isControlled ? colorMode : internalMode;
6682
+ const isDark = resolvedMode === "dark";
6683
+ const setColorMode = useCallback(
6684
+ (mode) => {
6685
+ const apply = () => {
6686
+ if (!isControlled) {
6687
+ setInternalMode(mode);
6688
+ saveChatColorMode(mode);
6689
+ }
6690
+ onColorModeChange?.(mode);
6691
+ };
6692
+ if (skipThemeTransitionRef.current) {
6693
+ apply();
6694
+ return;
6695
+ }
6696
+ withThemeTransition(apply);
6697
+ },
6698
+ [isControlled, onColorModeChange]
6699
+ );
6700
+ const toggleColorMode = useCallback(() => {
6701
+ setColorMode(resolvedMode === "dark" ? "light" : "dark");
6702
+ }, [resolvedMode, setColorMode]);
6703
+ const value = useMemo(
6704
+ () => ({
6705
+ colorMode: resolvedMode,
6706
+ isDark,
6707
+ isThemeTransitionReady,
6708
+ themeColor,
6709
+ themeStyles: buildChatThemeStyles(themeColor),
6710
+ scopeClassName: cn(
6711
+ getChatScopeClass(isDark),
6712
+ !isThemeTransitionReady && "chat-theme-no-transition"
6713
+ ),
6714
+ setColorMode,
6715
+ toggleColorMode,
6716
+ showColorModeToggle
6717
+ }),
6718
+ [
6719
+ resolvedMode,
6720
+ isDark,
6721
+ isThemeTransitionReady,
6722
+ themeColor,
6723
+ setColorMode,
6724
+ toggleColorMode,
6725
+ showColorModeToggle
6726
+ ]
6727
+ );
6728
+ return /* @__PURE__ */ jsx(ChatThemeContext.Provider, { value, children });
6729
+ };
6730
+ var useChatTheme = () => {
6731
+ const context = useContext(ChatThemeContext);
6732
+ if (!context) {
6733
+ throw new Error("useChatTheme must be used within ChatThemeProvider");
6734
+ }
6735
+ return context;
6736
+ };
6737
+
6738
+ // src/chat/lib/chat-message-bubble.ts
6739
+ init_utils2();
6740
+ function getBubbleCornerClasses(isSender) {
6741
+ return isSender ? "rounded-tl-[18px] rounded-tr-[18px] rounded-bl-[18px] rounded-br-none" : "rounded-tl-[18px] rounded-tr-[18px] rounded-br-[18px] rounded-bl-none";
6742
+ }
6743
+ function getChatBubbleClasses({
6744
+ isSender = false,
6745
+ surface,
6746
+ className
6747
+ }) {
6748
+ const resolvedSurface = surface ?? (isSender ? "sent" : "received");
6749
+ const isNeutral = resolvedSurface === "neutral";
6750
+ return cn(
6751
+ "relative",
6752
+ getBubbleCornerClasses(isSender),
6753
+ resolvedSurface === "sent" && "chat-bubble-sent bg-(--chat-theme) text-white",
6754
+ resolvedSurface === "received" && "chat-bubble-received border border-(--chat-border)/60 bg-(--chat-incoming-bubble) text-(--chat-incoming-text)",
6755
+ isNeutral && "chat-bubble-received border border-(--chat-border)/60 bg-(--chat-incoming-bubble) text-(--chat-incoming-text)",
6756
+ className
6757
+ );
6758
+ }
6536
6759
  var CallChatPanel = ({ conversationId, isGroupCall, onClose, className }) => {
6760
+ const { scopeClassName, themeStyles } = useChatTheme();
6537
6761
  const rawMessages = useStore((s) => s.messagesByConversation[conversationId]);
6538
6762
  const loggedUserDetails = useStore((s) => s.loggedUserDetails);
6539
6763
  const conversations = useStore((s) => s.conversations);
@@ -6604,68 +6828,89 @@ var CallChatPanel = ({ conversationId, isGroupCall, onClose, className }) => {
6604
6828
  emitDmSend(payload);
6605
6829
  }
6606
6830
  };
6607
- return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col border-l border-white/10 bg-neutral-950/95", className), children: [
6608
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b border-white/10 px-4 py-3", children: [
6609
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-white/90", children: "In-call chat" }),
6610
- /* @__PURE__ */ jsx(
6611
- "button",
6612
- {
6613
- type: "button",
6614
- "aria-label": "Close chat panel",
6615
- onClick: onClose,
6616
- className: "flex size-7 items-center justify-center rounded-lg text-white/60 transition-colors hover:bg-white/10 hover:text-white",
6617
- children: /* @__PURE__ */ jsx(X, { size: 16 })
6618
- }
6619
- )
6620
- ] }),
6621
- /* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-3 overflow-y-auto px-4 py-3", children: [
6622
- messages.length === 0 && /* @__PURE__ */ jsx("p", { className: "pt-6 text-center text-xs text-white/40", children: "No messages yet \u2014 say hi without leaving the call." }),
6623
- messages.map((message) => /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col", message.isOwn && "items-end"), children: [
6624
- /* @__PURE__ */ jsx("span", { className: "mb-0.5 text-[11px] font-medium text-white/40", children: message.isOwn ? "You" : message.senderName }),
6625
- /* @__PURE__ */ jsx(
6626
- "div",
6627
- {
6628
- className: cn(
6629
- "max-w-[85%] rounded-2xl px-3 py-1.5 text-sm break-words",
6630
- message.isOwn ? "rounded-br-sm bg-(--chat-theme,#4f46e5) text-white" : "rounded-bl-sm bg-white/10 text-white/90",
6631
- message.pending && "opacity-60"
6632
- ),
6633
- children: message.content
6634
- }
6635
- )
6636
- ] }, message.id)),
6637
- /* @__PURE__ */ jsx("div", { ref: listEndRef })
6638
- ] }),
6639
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-t border-white/10 p-3", children: [
6640
- /* @__PURE__ */ jsx(
6641
- "input",
6642
- {
6643
- value: text,
6644
- onChange: (e) => setText(e.target.value),
6645
- onKeyDown: (e) => {
6646
- if (e.key === "Enter" && !e.shiftKey) {
6647
- e.preventDefault();
6648
- handleSend();
6649
- }
6650
- },
6651
- placeholder: "Message\u2026",
6652
- className: "min-w-0 flex-1 rounded-full border border-white/10 bg-white/5 px-3.5 py-2 text-sm text-white placeholder:text-white/40 focus:outline-none focus:ring-1 focus:ring-white/30"
6653
- }
6831
+ return /* @__PURE__ */ jsxs(
6832
+ "div",
6833
+ {
6834
+ className: cn(
6835
+ scopeClassName,
6836
+ "flex flex-col border-l border-(--chat-border) bg-(--chat-surface) text-(--chat-text)",
6837
+ className
6654
6838
  ),
6655
- /* @__PURE__ */ jsx(
6656
- Button,
6657
- {
6658
- variant: "ghost",
6659
- size: "icon",
6660
- "aria-label": "Send message",
6661
- disabled: !text.trim(),
6662
- onClick: handleSend,
6663
- className: "size-9 shrink-0 rounded-full bg-white/10 text-white hover:bg-white/20 disabled:opacity-30",
6664
- children: /* @__PURE__ */ jsx(Send, { size: 16 })
6665
- }
6666
- )
6667
- ] })
6668
- ] });
6839
+ style: themeStyles,
6840
+ children: [
6841
+ /* @__PURE__ */ jsxs("div", { className: cn(chatSheetHeaderClass, "flex items-center justify-between"), children: [
6842
+ /* @__PURE__ */ jsx("p", { className: chatSheetTitleClass, children: "In-call chat" }),
6843
+ /* @__PURE__ */ jsx(
6844
+ "button",
6845
+ {
6846
+ type: "button",
6847
+ "aria-label": "Close chat panel",
6848
+ onClick: onClose,
6849
+ className: "flex size-7 items-center justify-center rounded-lg text-(--chat-muted) transition-colors hover:bg-(--chat-hover) hover:text-(--chat-text)",
6850
+ children: /* @__PURE__ */ jsx(X, { size: 16 })
6851
+ }
6852
+ )
6853
+ ] }),
6854
+ /* @__PURE__ */ jsxs("div", { className: cn(chatSheetBodyClass, "space-y-3"), children: [
6855
+ messages.length === 0 && /* @__PURE__ */ jsx("p", { className: "pt-6 text-center text-xs text-(--chat-muted)", children: "No messages yet \u2014 say hi without leaving the call." }),
6856
+ messages.map((message) => /* @__PURE__ */ jsxs(
6857
+ "div",
6858
+ {
6859
+ className: cn("flex flex-col", message.isOwn ? "items-end" : "items-start"),
6860
+ children: [
6861
+ /* @__PURE__ */ jsx("span", { className: "mb-0.5 text-[11px] font-medium text-(--chat-muted)", children: message.isOwn ? "You" : message.senderName }),
6862
+ /* @__PURE__ */ jsx(
6863
+ "div",
6864
+ {
6865
+ className: cn(
6866
+ getChatBubbleClasses({ isSender: message.isOwn }),
6867
+ "max-w-[85%] px-3 py-1.5 text-sm break-words",
6868
+ message.pending && "opacity-60"
6869
+ ),
6870
+ children: message.content
6871
+ }
6872
+ )
6873
+ ]
6874
+ },
6875
+ message.id
6876
+ )),
6877
+ /* @__PURE__ */ jsx("div", { ref: listEndRef })
6878
+ ] }),
6879
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-t border-(--chat-border) p-3", children: [
6880
+ /* @__PURE__ */ jsx(
6881
+ "input",
6882
+ {
6883
+ value: text,
6884
+ onChange: (e) => setText(e.target.value),
6885
+ onKeyDown: (e) => {
6886
+ if (e.key === "Enter" && !e.shiftKey) {
6887
+ e.preventDefault();
6888
+ handleSend();
6889
+ }
6890
+ },
6891
+ placeholder: "Message\u2026",
6892
+ className: cn(chatInputClass, "min-w-0 flex-1 rounded-full px-3.5 py-2 text-sm")
6893
+ }
6894
+ ),
6895
+ /* @__PURE__ */ jsx(
6896
+ Button,
6897
+ {
6898
+ variant: "ghost",
6899
+ size: "icon",
6900
+ "aria-label": "Send message",
6901
+ disabled: !text.trim(),
6902
+ onClick: handleSend,
6903
+ className: cn(
6904
+ "size-9 shrink-0 rounded-full transition-colors",
6905
+ text.trim() ? "bg-(--chat-theme) text-white hover:opacity-90" : "bg-(--chat-hover) text-(--chat-muted)"
6906
+ ),
6907
+ children: /* @__PURE__ */ jsx(Send, { size: 16 })
6908
+ }
6909
+ )
6910
+ ] })
6911
+ ]
6912
+ }
6913
+ );
6669
6914
  };
6670
6915
  var CallChatPanel_default = CallChatPanel;
6671
6916
  var CallSession2 = React11__default.lazy(() => Promise.resolve().then(() => (init_CallSession(), CallSession_exports)));
@@ -7125,162 +7370,6 @@ function AlertDialogCancel({
7125
7370
 
7126
7371
  // src/chat/ChatOverlay.tsx
7127
7372
  init_utils2();
7128
-
7129
- // src/chat/lib/chat-theme.ts
7130
- init_utils2();
7131
- var getChatScopeClass = (isDark) => cn("chat-theme-scope", isDark && "dark");
7132
- var chatSheetContentClass = "flex w-full flex-col gap-0 p-0 bg-(--chat-surface) text-(--chat-text)";
7133
- var chatSheetHeaderClass = "border-b border-(--chat-border) px-4 py-3 pr-11 text-left";
7134
- var chatSheetBodyClass = "chat-message-scroll flex-1 overflow-y-auto bg-(--chat-surface) p-3";
7135
- var chatSheetTitleClass = "text-[13px] font-semibold tracking-tight text-(--chat-text)";
7136
- var chatSheetSubtitleClass = "text-[11px] text-(--chat-muted)";
7137
- var chatDetailRowClass = "flex items-start justify-between gap-3 border-b border-(--chat-border) py-2.5 last:border-b-0";
7138
- var chatDetailLabelClass = "text-[11px] text-(--chat-muted)";
7139
- var chatDetailValueClass = "text-[13px] font-medium text-(--chat-text) text-right";
7140
- var chatDialogContentClass = "overflow-hidden rounded-2xl border border-(--chat-border) bg-(--chat-surface) p-0 text-(--chat-text) shadow-[var(--chat-shadow)]";
7141
- var chatDropdownContentClass = "rounded-xl border border-(--chat-border) bg-(--chat-surface) text-(--chat-text) shadow-lg";
7142
- var chatOptionsMenuClass = "min-w-[220px] rounded-xl border border-(--chat-border) bg-(--chat-surface) p-1.5 text-(--chat-text) shadow-[0_10px_40px_rgba(0,0,0,0.12)]";
7143
- var chatOptionsMenuLabelClass = "px-2.5 pb-0.5 pt-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-(--chat-muted)";
7144
- var chatOptionsMenuSeparatorClass = "my-1.5 h-px bg-(--chat-border)";
7145
- var chatOptionsMenuItemClass = "cursor-pointer gap-2.5 rounded-lg px-2.5 py-2 text-[13px] font-medium text-(--chat-text) focus:bg-(--chat-hover) focus:text-(--chat-text) data-[highlighted]:bg-(--chat-hover) [&_svg]:size-[15px] [&_svg]:shrink-0 [&_svg]:text-(--chat-muted)";
7146
- var chatOptionsMenuItemDestructiveClass = "cursor-pointer gap-2.5 rounded-lg px-2.5 py-2 text-[13px] font-medium text-(--chat-danger-text) focus:bg-red-500/10 focus:text-(--chat-danger-text) data-[highlighted]:bg-red-500/10 data-[highlighted]:text-(--chat-danger-text) [&_svg]:size-[15px] [&_svg]:shrink-0 [&_svg]:!text-(--chat-danger-text)";
7147
- var chatOptionsMenuSubTriggerClass = "cursor-pointer gap-2.5 rounded-lg px-2.5 py-2 pr-7 text-[13px] font-medium text-(--chat-text) focus:bg-(--chat-hover) focus:text-(--chat-text) data-[highlighted]:bg-(--chat-hover) data-[state=open]:bg-(--chat-hover) [&_svg]:size-[15px] [&_svg]:shrink-0 [&_svg]:text-(--chat-muted)";
7148
- var chatPopoverContentClass = "w-auto border border-(--chat-border) bg-(--chat-surface) p-0 text-(--chat-text) shadow-2xl";
7149
- var chatInputClass = "border border-(--chat-border) bg-(--chat-input-bg) text-(--chat-text) shadow-none placeholder:text-(--chat-muted) focus-visible:border-(--chat-theme-20) focus-visible:ring-2 focus-visible:ring-(--chat-theme-10)";
7150
- var chatSidebarSearchShellClass = "relative flex min-w-0 flex-1 items-center rounded-lg border border-(--chat-border) bg-(--chat-search-bg) px-2.5 transition-colors focus-within:border-(--chat-theme) focus-within:shadow-[0_0_0_2px_var(--chat-theme-10)]";
7151
- var chatSidebarSearchInputClass = "h-8 w-full min-w-0 border-0 bg-transparent px-1.5 text-xs text-(--chat-text) shadow-none outline-none placeholder:text-(--chat-muted)/60 focus-visible:border-0 focus-visible:shadow-none focus-visible:ring-0";
7152
- var chatSelectClass = "h-10 w-full cursor-pointer appearance-none rounded-xl border border-(--chat-border) bg-(--chat-input-bg) pl-3 pr-9 text-sm text-(--chat-text) shadow-none focus-visible:border-(--chat-theme-20) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--chat-theme-10)";
7153
- var chatListItemClass = "transition-colors hover:bg-(--chat-hover)";
7154
- var chatLoadMoreButtonClass = "h-7 gap-1 rounded-full border border-(--chat-border) bg-(--chat-elevated) px-4 text-[10px] font-medium text-(--chat-text) transition-colors hover:border-(--chat-theme-20) hover:bg-(--chat-theme-5) hover:text-(--chat-theme) disabled:opacity-50 [&_svg]:size-3 [&_svg]:text-current";
7155
- var chatDrawerSectionHeadingClass = "mb-1 px-0.5 text-[10px] font-semibold tracking-wide text-(--chat-muted)";
7156
- var chatHeaderToolbarIconProps = { size: 14, strokeWidth: 1.75 };
7157
- var chatSavedMessageModalIconClass = "shrink-0 text-(--chat-theme)";
7158
- var chatHeaderToolbarIconLgProps = { size: 22, strokeWidth: 1.75 };
7159
- var chatHeaderToolbarIconSmProps = { size: 11, strokeWidth: 1.75 };
7160
- var chatDrawerFileRowClass = "flex items-center gap-2 rounded-md border border-(--chat-border)/70 bg-(--chat-elevated) px-2 py-1.5 transition-colors hover:bg-(--chat-hover)/50";
7161
- var chatDrawerFileIconWrapClass = "flex size-8 shrink-0 items-center justify-center rounded-md bg-(--chat-hover)";
7162
- var chatDrawerMediaFrameClass = "overflow-hidden rounded-md border border-(--chat-border)/70 bg-(--chat-elevated)";
7163
- var buildChatThemeStyles = (themeColor) => ({
7164
- "--chat-theme": themeColor,
7165
- "--chat-theme-5": `${themeColor}0D`,
7166
- "--chat-theme-10": `${themeColor}1A`,
7167
- "--chat-theme-20": `${themeColor}33`,
7168
- "--chat-theme-40": `${themeColor}66`,
7169
- "--chat-theme-60": `${themeColor}99`,
7170
- "--read-tick-color": themeColor.toLowerCase() === "#0000ff" || themeColor.toLowerCase().includes("007aff") ? "#00e5ff" : "#40C4FF"
7171
- });
7172
-
7173
- // src/chat/lib/chat-theme-storage.ts
7174
- var STORAGE_KEY = "chat-color-mode";
7175
- var loadChatColorMode = () => {
7176
- if (typeof window === "undefined") return null;
7177
- try {
7178
- const stored = localStorage.getItem(STORAGE_KEY);
7179
- return stored === "dark" || stored === "light" ? stored : null;
7180
- } catch {
7181
- return null;
7182
- }
7183
- };
7184
- var saveChatColorMode = (mode) => {
7185
- if (typeof window === "undefined") return;
7186
- try {
7187
- localStorage.setItem(STORAGE_KEY, mode);
7188
- } catch {
7189
- }
7190
- };
7191
- var CHAT_THEME_SHELL_CLASS = "chat-theme-shell";
7192
- function withThemeTransition(apply) {
7193
- if (typeof document === "undefined") {
7194
- apply();
7195
- return;
7196
- }
7197
- flushSync(apply);
7198
- }
7199
-
7200
- // src/context/ChatThemeContext.tsx
7201
- init_utils2();
7202
- var ChatThemeContext = createContext(null);
7203
- var ChatThemeProvider = ({
7204
- children,
7205
- themeColor,
7206
- colorMode,
7207
- defaultColorMode = "light",
7208
- onColorModeChange,
7209
- showColorModeToggle = true
7210
- }) => {
7211
- const isControlled = colorMode !== void 0;
7212
- const [internalMode, setInternalMode] = useState(defaultColorMode);
7213
- const [isThemeTransitionReady, setIsThemeTransitionReady] = useState(false);
7214
- const skipThemeTransitionRef = useRef(true);
7215
- useLayoutEffect(() => {
7216
- if (!isControlled) {
7217
- const stored = loadChatColorMode();
7218
- if (stored) {
7219
- setInternalMode(stored);
7220
- }
7221
- }
7222
- const frame = requestAnimationFrame(() => {
7223
- skipThemeTransitionRef.current = false;
7224
- setIsThemeTransitionReady(true);
7225
- });
7226
- return () => cancelAnimationFrame(frame);
7227
- }, [isControlled]);
7228
- const resolvedMode = isControlled ? colorMode : internalMode;
7229
- const isDark = resolvedMode === "dark";
7230
- const setColorMode = useCallback(
7231
- (mode) => {
7232
- const apply = () => {
7233
- if (!isControlled) {
7234
- setInternalMode(mode);
7235
- saveChatColorMode(mode);
7236
- }
7237
- onColorModeChange?.(mode);
7238
- };
7239
- if (skipThemeTransitionRef.current) {
7240
- apply();
7241
- return;
7242
- }
7243
- withThemeTransition(apply);
7244
- },
7245
- [isControlled, onColorModeChange]
7246
- );
7247
- const toggleColorMode = useCallback(() => {
7248
- setColorMode(resolvedMode === "dark" ? "light" : "dark");
7249
- }, [resolvedMode, setColorMode]);
7250
- const value = useMemo(
7251
- () => ({
7252
- colorMode: resolvedMode,
7253
- isDark,
7254
- isThemeTransitionReady,
7255
- themeColor,
7256
- themeStyles: buildChatThemeStyles(themeColor),
7257
- scopeClassName: cn(
7258
- getChatScopeClass(isDark),
7259
- !isThemeTransitionReady && "chat-theme-no-transition"
7260
- ),
7261
- setColorMode,
7262
- toggleColorMode,
7263
- showColorModeToggle
7264
- }),
7265
- [
7266
- resolvedMode,
7267
- isDark,
7268
- isThemeTransitionReady,
7269
- themeColor,
7270
- setColorMode,
7271
- toggleColorMode,
7272
- showColorModeToggle
7273
- ]
7274
- );
7275
- return /* @__PURE__ */ jsx(ChatThemeContext.Provider, { value, children });
7276
- };
7277
- var useChatTheme = () => {
7278
- const context = useContext(ChatThemeContext);
7279
- if (!context) {
7280
- throw new Error("useChatTheme must be used within ChatThemeProvider");
7281
- }
7282
- return context;
7283
- };
7284
7373
  var ChatSheetContent = ({
7285
7374
  className,
7286
7375
  style,
@@ -7336,6 +7425,66 @@ init_avatar();
7336
7425
  init_button();
7337
7426
  init_utils2();
7338
7427
  init_call_store();
7428
+
7429
+ // src/call/ringtone.ts
7430
+ var audioCtx = null;
7431
+ var scheduledTimer = null;
7432
+ var activeOscillators = [];
7433
+ var RING_ON_MS = 1200;
7434
+ var RING_OFF_MS = 2400;
7435
+ var RING_FREQUENCIES = [440, 480];
7436
+ var getContext = () => {
7437
+ if (typeof window === "undefined") return null;
7438
+ const Ctor = window.AudioContext || window.webkitAudioContext;
7439
+ if (!Ctor) return null;
7440
+ if (!audioCtx || audioCtx.state === "closed") audioCtx = new Ctor();
7441
+ return audioCtx;
7442
+ };
7443
+ var stopOscillators = () => {
7444
+ activeOscillators.forEach((osc) => {
7445
+ try {
7446
+ osc.stop();
7447
+ } catch {
7448
+ }
7449
+ });
7450
+ activeOscillators = [];
7451
+ };
7452
+ var ringBurst = (ctx) => {
7453
+ const onSeconds = RING_ON_MS / 1e3;
7454
+ const gain = ctx.createGain();
7455
+ gain.gain.setValueAtTime(0, ctx.currentTime);
7456
+ gain.gain.linearRampToValueAtTime(0.15, ctx.currentTime + 0.05);
7457
+ gain.gain.setValueAtTime(0.15, ctx.currentTime + onSeconds - 0.05);
7458
+ gain.gain.linearRampToValueAtTime(0, ctx.currentTime + onSeconds);
7459
+ gain.connect(ctx.destination);
7460
+ RING_FREQUENCIES.forEach((freq) => {
7461
+ const osc = ctx.createOscillator();
7462
+ osc.type = "sine";
7463
+ osc.frequency.value = freq;
7464
+ osc.connect(gain);
7465
+ osc.start();
7466
+ osc.stop(ctx.currentTime + onSeconds);
7467
+ activeOscillators.push(osc);
7468
+ });
7469
+ };
7470
+ var playDefaultRingtone = () => {
7471
+ if (scheduledTimer) return;
7472
+ const ctx = getContext();
7473
+ if (!ctx) return;
7474
+ ctx.resume().catch(() => void 0);
7475
+ const cycle = () => {
7476
+ ringBurst(ctx);
7477
+ scheduledTimer = setTimeout(cycle, RING_ON_MS + RING_OFF_MS);
7478
+ };
7479
+ cycle();
7480
+ };
7481
+ var stopDefaultRingtone = () => {
7482
+ if (scheduledTimer) {
7483
+ clearTimeout(scheduledTimer);
7484
+ scheduledTimer = null;
7485
+ }
7486
+ stopOscillators();
7487
+ };
7339
7488
  var IncomingCallDialog = ({ ringtoneUrl }) => {
7340
7489
  const uiStatus = useCallStore((s) => s.uiStatus);
7341
7490
  const callId = useCallStore((s) => s.callId);
@@ -7358,6 +7507,12 @@ var IncomingCallDialog = ({ ringtoneUrl }) => {
7358
7507
  el.pause();
7359
7508
  }
7360
7509
  }, [isOpen, ringtoneUrl]);
7510
+ useEffect(() => {
7511
+ if (ringtoneUrl) return;
7512
+ if (isOpen) playDefaultRingtone();
7513
+ else stopDefaultRingtone();
7514
+ return () => stopDefaultRingtone();
7515
+ }, [isOpen, ringtoneUrl]);
7361
7516
  if (!isOpen) return null;
7362
7517
  const callerName = caller?.name || "Unknown caller";
7363
7518
  const name = isGroupCall ? peer?.name || callerName : callerName;
@@ -11934,28 +12089,6 @@ function MessageForwardedLabel({ isSender, className, overlay }) {
11934
12089
  }
11935
12090
  );
11936
12091
  }
11937
-
11938
- // src/chat/lib/chat-message-bubble.ts
11939
- init_utils2();
11940
- function getBubbleCornerClasses(isSender) {
11941
- return isSender ? "rounded-tl-[18px] rounded-tr-[18px] rounded-bl-[18px] rounded-br-none" : "rounded-tl-[18px] rounded-tr-[18px] rounded-br-[18px] rounded-bl-none";
11942
- }
11943
- function getChatBubbleClasses({
11944
- isSender = false,
11945
- surface,
11946
- className
11947
- }) {
11948
- const resolvedSurface = surface ?? (isSender ? "sent" : "received");
11949
- const isNeutral = resolvedSurface === "neutral";
11950
- return cn(
11951
- "relative",
11952
- getBubbleCornerClasses(isSender),
11953
- resolvedSurface === "sent" && "chat-bubble-sent bg-(--chat-theme) text-white",
11954
- resolvedSurface === "received" && "chat-bubble-received border border-(--chat-border)/60 bg-(--chat-incoming-bubble) text-(--chat-incoming-text)",
11955
- isNeutral && "chat-bubble-received border border-(--chat-border)/60 bg-(--chat-incoming-bubble) text-(--chat-incoming-text)",
11956
- className
11957
- );
11958
- }
11959
12092
  init_utils2();
11960
12093
  init_client();
11961
12094
  function getPreviewKind(name) {
@@ -20983,6 +21116,7 @@ init_normalize_helpers();
20983
21116
  init_useStore();
20984
21117
  init_open_conversation();
20985
21118
  var RETRY_DELAYS_MS = [1e3, 3e3, 8e3, 2e4];
21119
+ var CHAT_HEARTBEAT_INTERVAL_MS = 15e3;
20986
21120
  var recentToastIds = /* @__PURE__ */ new Set();
20987
21121
  var rememberToastId = (id) => {
20988
21122
  if (!id) return true;
@@ -21205,6 +21339,36 @@ var ForegroundPushBridge = ({ onPush }) => {
21205
21339
  window.removeEventListener(PUSH_CHANGED_EVENT, onPushChanged);
21206
21340
  };
21207
21341
  }, []);
21342
+ useEffect(() => {
21343
+ if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
21344
+ return;
21345
+ }
21346
+ let interval = null;
21347
+ const sendHeartbeat = () => {
21348
+ navigator.serviceWorker.getRegistration().then(
21349
+ (registration) => registration?.active?.postMessage({ type: CHAT_HEARTBEAT_SW_TYPE })
21350
+ ).catch(() => void 0);
21351
+ };
21352
+ const startHeartbeat = () => {
21353
+ if (interval || !getStoredPushToken()) return;
21354
+ sendHeartbeat();
21355
+ interval = setInterval(sendHeartbeat, CHAT_HEARTBEAT_INTERVAL_MS);
21356
+ };
21357
+ const stopHeartbeat = () => {
21358
+ if (interval) clearInterval(interval);
21359
+ interval = null;
21360
+ };
21361
+ startHeartbeat();
21362
+ const onPushChanged = (event) => {
21363
+ if (event.detail?.enabled) startHeartbeat();
21364
+ else stopHeartbeat();
21365
+ };
21366
+ window.addEventListener(PUSH_CHANGED_EVENT, onPushChanged);
21367
+ return () => {
21368
+ stopHeartbeat();
21369
+ window.removeEventListener(PUSH_CHANGED_EVENT, onPushChanged);
21370
+ };
21371
+ }, []);
21208
21372
  useEffect(() => {
21209
21373
  const onOpen = (event) => {
21210
21374
  const id = String(