@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.cjs CHANGED
@@ -912,12 +912,13 @@ var init_store_helpers = __esm({
912
912
  });
913
913
 
914
914
  // src/notifications/open-conversation.ts
915
- exports.OPEN_CONVERSATION_EVENT = void 0; exports.NOTIFICATION_CLICK_SW_TYPE = void 0; exports.PUSH_BROADCAST_CHANNEL = void 0; exports.PUSH_DATA_CACHE_NAME = void 0; var PENDING_STORAGE_KEY, PENDING_OPEN_CACHE_PATH; exports.resolveConversationIdFromPushData = void 0; var normalizePushMeta, parsePendingRaw; exports.peekPendingOpenConversation = void 0; exports.peekPendingOpenRequest = void 0; exports.consumePendingOpenConversation = void 0; exports.consumePendingOpenRequest = void 0; exports.requestOpenConversation = void 0; exports.buildChannelFromPushRequest = void 0; var mapConversationToChannel, stripConversationIdFromUrl, cacheRequestForPath; exports.consumePendingOpenFromPushCache = void 0; var readConversationIdFromLocation, consumedUrlConversationIds, lastHandledOpenId, lastHandledOpenAt, shouldHandleOpen, handleNotificationOpen; exports.installNotificationClickBridge = void 0;
915
+ exports.OPEN_CONVERSATION_EVENT = void 0; exports.NOTIFICATION_CLICK_SW_TYPE = void 0; var CHAT_HEARTBEAT_SW_TYPE; exports.PUSH_BROADCAST_CHANNEL = void 0; exports.PUSH_DATA_CACHE_NAME = void 0; var PENDING_STORAGE_KEY, PENDING_OPEN_CACHE_PATH; exports.resolveConversationIdFromPushData = void 0; var normalizePushMeta, parsePendingRaw; exports.peekPendingOpenConversation = void 0; exports.peekPendingOpenRequest = void 0; exports.consumePendingOpenConversation = void 0; exports.consumePendingOpenRequest = void 0; exports.requestOpenConversation = void 0; exports.buildChannelFromPushRequest = void 0; var mapConversationToChannel, stripConversationIdFromUrl, cacheRequestForPath; exports.consumePendingOpenFromPushCache = void 0; var readConversationIdFromLocation, consumedUrlConversationIds, lastHandledOpenId, lastHandledOpenAt, shouldHandleOpen, handleNotificationOpen; exports.installNotificationClickBridge = void 0;
916
916
  var init_open_conversation = __esm({
917
917
  "src/notifications/open-conversation.ts"() {
918
918
  init_store_helpers();
919
919
  exports.OPEN_CONVERSATION_EVENT = "realtimex:open-conversation";
920
920
  exports.NOTIFICATION_CLICK_SW_TYPE = "realtimex:notification-click";
921
+ CHAT_HEARTBEAT_SW_TYPE = "realtimex:chat-heartbeat";
921
922
  exports.PUSH_BROADCAST_CHANNEL = "realtimex-push";
922
923
  exports.PUSH_DATA_CACHE_NAME = "realtimex-push-data-v1";
923
924
  PENDING_STORAGE_KEY = "realtimex:pending-open-conversation";
@@ -4839,9 +4840,8 @@ var init_ParticipantView = __esm({
4839
4840
  isHandRaised = false,
4840
4841
  className
4841
4842
  }) => {
4842
- const { webcamStream, micStream, webcamOn, micOn, displayName, isActiveSpeaker } = sdk.useParticipant(participantId);
4843
+ const { webcamStream, webcamOn, micOn, displayName, isActiveSpeaker } = sdk.useParticipant(participantId);
4843
4844
  const videoRef = React11.useRef(null);
4844
- const audioRef = React11.useRef(null);
4845
4845
  const [hasFrame, setHasFrame] = React11.useState(false);
4846
4846
  React11.useEffect(() => {
4847
4847
  const el = videoRef.current;
@@ -4854,17 +4854,6 @@ var init_ParticipantView = __esm({
4854
4854
  setHasFrame(false);
4855
4855
  }
4856
4856
  }, [webcamOn, webcamStream]);
4857
- React11.useEffect(() => {
4858
- if (isLocal) return;
4859
- const el = audioRef.current;
4860
- if (!el) return;
4861
- if (micOn && micStream?.track) {
4862
- el.srcObject = new MediaStream([micStream.track]);
4863
- el.play().catch(() => void 0);
4864
- } else {
4865
- el.srcObject = null;
4866
- }
4867
- }, [micOn, micStream, isLocal]);
4868
4857
  const name = displayName || fallbackName || "Participant";
4869
4858
  const showVideo = webcamOn && hasFrame;
4870
4859
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -4897,7 +4886,6 @@ var init_ParticipantView = __esm({
4897
4886
  )
4898
4887
  }
4899
4888
  ),
4900
- !isLocal && /* @__PURE__ */ jsxRuntime.jsx("audio", { ref: audioRef, autoPlay: true, playsInline: true }),
4901
4889
  isHandRaised && /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(lucideReact.Hand, { size: 14 }) }),
4902
4890
  !showVideo && /* @__PURE__ */ jsxRuntime.jsxs(Avatar, { size: "lg", className: "size-20", children: [
4903
4891
  /* @__PURE__ */ jsxRuntime.jsx(AvatarImage, { src: fallbackImage, alt: name }),
@@ -5064,7 +5052,8 @@ var init_CallControls = __esm({
5064
5052
  isHandRaised,
5065
5053
  onToggleRaiseHand,
5066
5054
  isChatOpen,
5067
- onToggleChat
5055
+ onToggleChat,
5056
+ isVideoActive
5068
5057
  }) => {
5069
5058
  const {
5070
5059
  toggleMic,
@@ -5073,9 +5062,28 @@ var init_CallControls = __esm({
5073
5062
  localWebcamOn,
5074
5063
  localScreenShareOn,
5075
5064
  toggleScreenShare,
5065
+ recordingState,
5066
+ startRecording,
5067
+ stopRecording,
5076
5068
  leave
5077
5069
  } = sdk.useMeeting();
5078
5070
  const endCall = exports.useCallStore((s) => s.endCall);
5071
+ const isRecording = recordingState === "RECORDING_STARTED" || recordingState === "RECORDING_STARTING";
5072
+ const isRecordingBusy = recordingState === "RECORDING_STARTING" || recordingState === "RECORDING_STOPPING";
5073
+ const handleToggleRecording = () => {
5074
+ if (isRecordingBusy) return;
5075
+ if (isRecording) {
5076
+ stopRecording();
5077
+ return;
5078
+ }
5079
+ startRecording("", "", {
5080
+ layout: { type: "GRID", priority: "SPEAKER", gridSize: 4 },
5081
+ orientation: "landscape",
5082
+ theme: "DEFAULT",
5083
+ mode: "video-and-audio",
5084
+ quality: "high"
5085
+ });
5086
+ };
5079
5087
  const handleLeave = () => {
5080
5088
  leave();
5081
5089
  endCall();
@@ -5091,7 +5099,7 @@ var init_CallControls = __esm({
5091
5099
  onClick: () => toggleMic()
5092
5100
  }
5093
5101
  ),
5094
- /* @__PURE__ */ jsxRuntime.jsx(
5102
+ isVideoActive ? /* @__PURE__ */ jsxRuntime.jsx(
5095
5103
  ControlButton,
5096
5104
  {
5097
5105
  icon: localWebcamOn ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Video, { size: 20 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.VideoOff, { size: 20 }),
@@ -5099,6 +5107,13 @@ var init_CallControls = __esm({
5099
5107
  active: !localWebcamOn,
5100
5108
  onClick: () => toggleWebcam()
5101
5109
  }
5110
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
5111
+ ControlButton,
5112
+ {
5113
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Video, { size: 20 }),
5114
+ label: "Switch to video call",
5115
+ onClick: () => toggleWebcam()
5116
+ }
5102
5117
  ),
5103
5118
  /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: "mx-0.5 h-7 w-px shrink-0 bg-white/15" }),
5104
5119
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -5111,6 +5126,16 @@ var init_CallControls = __esm({
5111
5126
  onClick: () => toggleScreenShare()
5112
5127
  }
5113
5128
  ),
5129
+ /* @__PURE__ */ jsxRuntime.jsx(
5130
+ ControlButton,
5131
+ {
5132
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Circle, { size: 14, className: cn("fill-current", isRecording && "animate-pulse") }),
5133
+ label: isRecording ? "Stop recording" : "Start recording",
5134
+ active: isRecording,
5135
+ activeClassName: "bg-red-600 hover:bg-red-500",
5136
+ onClick: handleToggleRecording
5137
+ }
5138
+ ),
5114
5139
  /* @__PURE__ */ jsxRuntime.jsx(
5115
5140
  ControlButton,
5116
5141
  {
@@ -5290,7 +5315,7 @@ var init_useCallReactions = __esm({
5290
5315
  };
5291
5316
  }
5292
5317
  });
5293
- var RemoteWebcamWatcher, MeetingView, MeetingView_default;
5318
+ var RemoteParticipantWatcher, MeetingView, MeetingView_default;
5294
5319
  var init_MeetingView = __esm({
5295
5320
  "src/call/MeetingView.tsx"() {
5296
5321
  init_ParticipantView();
@@ -5301,17 +5326,28 @@ var init_MeetingView = __esm({
5301
5326
  init_useCallReactions();
5302
5327
  init_call_store();
5303
5328
  init_utils2();
5304
- RemoteWebcamWatcher = ({
5329
+ RemoteParticipantWatcher = ({
5305
5330
  sdk,
5306
5331
  participantId,
5307
- onChange
5332
+ onWebcamChange
5308
5333
  }) => {
5309
- const { webcamOn } = sdk.useParticipant(participantId);
5334
+ const { webcamOn, micStream, micOn } = sdk.useParticipant(participantId);
5335
+ const audioRef = React11.useRef(null);
5310
5336
  React11.useEffect(() => {
5311
- onChange(participantId, webcamOn);
5312
- return () => onChange(participantId, false);
5337
+ onWebcamChange(participantId, webcamOn);
5338
+ return () => onWebcamChange(participantId, false);
5313
5339
  }, [participantId, webcamOn]);
5314
- return null;
5340
+ React11.useEffect(() => {
5341
+ const el = audioRef.current;
5342
+ if (!el) return;
5343
+ if (micOn && micStream?.track) {
5344
+ el.srcObject = new MediaStream([micStream.track]);
5345
+ el.play().catch(() => void 0);
5346
+ } else {
5347
+ el.srcObject = null;
5348
+ }
5349
+ }, [micOn, micStream]);
5350
+ return /* @__PURE__ */ jsxRuntime.jsx("audio", { ref: audioRef, autoPlay: true, playsInline: true });
5315
5351
  };
5316
5352
  MeetingView = ({
5317
5353
  sdk,
@@ -5364,7 +5400,17 @@ var init_MeetingView = __esm({
5364
5400
  (id) => id !== localParticipant?.id
5365
5401
  );
5366
5402
  const isGroup = remoteIds.length > 1;
5367
- const webcamWatchers = remoteIds.map((id) => /* @__PURE__ */ jsxRuntime.jsx(RemoteWebcamWatcher, { sdk, participantId: id, onChange: handleRemoteWebcamChange }, id));
5403
+ const remoteParticipantWatchers = remoteIds.map((id) => /* @__PURE__ */ jsxRuntime.jsx(
5404
+ RemoteParticipantWatcher,
5405
+ {
5406
+ sdk,
5407
+ participantId: id,
5408
+ onWebcamChange: handleRemoteWebcamChange
5409
+ },
5410
+ id
5411
+ ));
5412
+ const anyRemoteWebcamOn = Object.values(remoteWebcamOn).some(Boolean);
5413
+ const showVideoGrid = mode === "video" || localWebcamOn || anyRemoteWebcamOn;
5368
5414
  const controls = /* @__PURE__ */ jsxRuntime.jsx(
5369
5415
  CallControls_default,
5370
5416
  {
@@ -5373,7 +5419,8 @@ var init_MeetingView = __esm({
5373
5419
  isHandRaised,
5374
5420
  onToggleRaiseHand: toggleRaiseHand,
5375
5421
  isChatOpen,
5376
- onToggleChat
5422
+ onToggleChat,
5423
+ isVideoActive: showVideoGrid
5377
5424
  }
5378
5425
  );
5379
5426
  const connectionStatusText = meetingState === "CONNECTING" ? "Connecting\u2026" : meetingState === "DISCONNECTED" ? "Reconnecting\u2026" : meetingState === "FAILED" ? "Connection lost" : null;
@@ -5383,7 +5430,7 @@ var init_MeetingView = __esm({
5383
5430
  ] });
5384
5431
  if (presenterId) {
5385
5432
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5386
- webcamWatchers,
5433
+ remoteParticipantWatchers,
5387
5434
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex flex-1 flex-col", children: [
5388
5435
  statusBanner,
5389
5436
  /* @__PURE__ */ jsxRuntime.jsx(FloatingReactions_default, { reactions: floatingReactions }),
@@ -5424,11 +5471,9 @@ var init_MeetingView = __esm({
5424
5471
  ] })
5425
5472
  ] });
5426
5473
  }
5427
- const anyRemoteWebcamOn = Object.values(remoteWebcamOn).some(Boolean);
5428
- const showVideoGrid = mode === "video" || localWebcamOn || anyRemoteWebcamOn;
5429
5474
  if (!showVideoGrid) {
5430
5475
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5431
- webcamWatchers,
5476
+ remoteParticipantWatchers,
5432
5477
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex flex-1 flex-col", children: [
5433
5478
  statusBanner,
5434
5479
  /* @__PURE__ */ jsxRuntime.jsx(FloatingReactions_default, { reactions: floatingReactions }),
@@ -5451,7 +5496,7 @@ var init_MeetingView = __esm({
5451
5496
  (id) => !!id && id !== focusedId
5452
5497
  ) : [];
5453
5498
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5454
- webcamWatchers,
5499
+ remoteParticipantWatchers,
5455
5500
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex flex-1 flex-col", children: [
5456
5501
  statusBanner,
5457
5502
  /* @__PURE__ */ jsxRuntime.jsx(FloatingReactions_default, { reactions: floatingReactions }),
@@ -6561,7 +6606,186 @@ init_useStore();
6561
6606
  init_store_helpers();
6562
6607
  init_utils2();
6563
6608
  init_button();
6609
+
6610
+ // src/chat/lib/chat-theme.ts
6611
+ init_utils2();
6612
+ var getChatScopeClass = (isDark) => cn("chat-theme-scope", isDark && "dark");
6613
+ var chatSheetContentClass = "flex w-full flex-col gap-0 p-0 bg-(--chat-surface) text-(--chat-text)";
6614
+ var chatSheetHeaderClass = "border-b border-(--chat-border) px-4 py-3 pr-11 text-left";
6615
+ var chatSheetBodyClass = "chat-message-scroll flex-1 overflow-y-auto bg-(--chat-surface) p-3";
6616
+ var chatSheetTitleClass = "text-[13px] font-semibold tracking-tight text-(--chat-text)";
6617
+ var chatSheetSubtitleClass = "text-[11px] text-(--chat-muted)";
6618
+ var chatDetailRowClass = "flex items-start justify-between gap-3 border-b border-(--chat-border) py-2.5 last:border-b-0";
6619
+ var chatDetailLabelClass = "text-[11px] text-(--chat-muted)";
6620
+ var chatDetailValueClass = "text-[13px] font-medium text-(--chat-text) text-right";
6621
+ var chatDialogContentClass = "overflow-hidden rounded-2xl border border-(--chat-border) bg-(--chat-surface) p-0 text-(--chat-text) shadow-[var(--chat-shadow)]";
6622
+ var chatDropdownContentClass = "rounded-xl border border-(--chat-border) bg-(--chat-surface) text-(--chat-text) shadow-lg";
6623
+ 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)]";
6624
+ var chatOptionsMenuLabelClass = "px-2.5 pb-0.5 pt-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-(--chat-muted)";
6625
+ var chatOptionsMenuSeparatorClass = "my-1.5 h-px bg-(--chat-border)";
6626
+ 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)";
6627
+ 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)";
6628
+ 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)";
6629
+ var chatPopoverContentClass = "w-auto border border-(--chat-border) bg-(--chat-surface) p-0 text-(--chat-text) shadow-2xl";
6630
+ 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)";
6631
+ 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)]";
6632
+ 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";
6633
+ 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)";
6634
+ var chatListItemClass = "transition-colors hover:bg-(--chat-hover)";
6635
+ 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";
6636
+ var chatDrawerSectionHeadingClass = "mb-1 px-0.5 text-[10px] font-semibold tracking-wide text-(--chat-muted)";
6637
+ var chatHeaderToolbarIconProps = { size: 14, strokeWidth: 1.75 };
6638
+ var chatSavedMessageModalIconClass = "shrink-0 text-(--chat-theme)";
6639
+ var chatHeaderToolbarIconLgProps = { size: 22, strokeWidth: 1.75 };
6640
+ var chatHeaderToolbarIconSmProps = { size: 11, strokeWidth: 1.75 };
6641
+ 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";
6642
+ var chatDrawerFileIconWrapClass = "flex size-8 shrink-0 items-center justify-center rounded-md bg-(--chat-hover)";
6643
+ var chatDrawerMediaFrameClass = "overflow-hidden rounded-md border border-(--chat-border)/70 bg-(--chat-elevated)";
6644
+ var buildChatThemeStyles = (themeColor) => ({
6645
+ "--chat-theme": themeColor,
6646
+ "--chat-theme-5": `${themeColor}0D`,
6647
+ "--chat-theme-10": `${themeColor}1A`,
6648
+ "--chat-theme-20": `${themeColor}33`,
6649
+ "--chat-theme-40": `${themeColor}66`,
6650
+ "--chat-theme-60": `${themeColor}99`,
6651
+ "--read-tick-color": themeColor.toLowerCase() === "#0000ff" || themeColor.toLowerCase().includes("007aff") ? "#00e5ff" : "#40C4FF"
6652
+ });
6653
+
6654
+ // src/chat/lib/chat-theme-storage.ts
6655
+ var STORAGE_KEY = "chat-color-mode";
6656
+ var loadChatColorMode = () => {
6657
+ if (typeof window === "undefined") return null;
6658
+ try {
6659
+ const stored = localStorage.getItem(STORAGE_KEY);
6660
+ return stored === "dark" || stored === "light" ? stored : null;
6661
+ } catch {
6662
+ return null;
6663
+ }
6664
+ };
6665
+ var saveChatColorMode = (mode) => {
6666
+ if (typeof window === "undefined") return;
6667
+ try {
6668
+ localStorage.setItem(STORAGE_KEY, mode);
6669
+ } catch {
6670
+ }
6671
+ };
6672
+ var CHAT_THEME_SHELL_CLASS = "chat-theme-shell";
6673
+ function withThemeTransition(apply) {
6674
+ if (typeof document === "undefined") {
6675
+ apply();
6676
+ return;
6677
+ }
6678
+ reactDom.flushSync(apply);
6679
+ }
6680
+
6681
+ // src/context/ChatThemeContext.tsx
6682
+ init_utils2();
6683
+ var ChatThemeContext = React11.createContext(null);
6684
+ var ChatThemeProvider = ({
6685
+ children,
6686
+ themeColor,
6687
+ colorMode,
6688
+ defaultColorMode = "light",
6689
+ onColorModeChange,
6690
+ showColorModeToggle = true
6691
+ }) => {
6692
+ const isControlled = colorMode !== void 0;
6693
+ const [internalMode, setInternalMode] = React11.useState(defaultColorMode);
6694
+ const [isThemeTransitionReady, setIsThemeTransitionReady] = React11.useState(false);
6695
+ const skipThemeTransitionRef = React11.useRef(true);
6696
+ React11.useLayoutEffect(() => {
6697
+ if (!isControlled) {
6698
+ const stored = loadChatColorMode();
6699
+ if (stored) {
6700
+ setInternalMode(stored);
6701
+ }
6702
+ }
6703
+ const frame = requestAnimationFrame(() => {
6704
+ skipThemeTransitionRef.current = false;
6705
+ setIsThemeTransitionReady(true);
6706
+ });
6707
+ return () => cancelAnimationFrame(frame);
6708
+ }, [isControlled]);
6709
+ const resolvedMode = isControlled ? colorMode : internalMode;
6710
+ const isDark = resolvedMode === "dark";
6711
+ const setColorMode = React11.useCallback(
6712
+ (mode) => {
6713
+ const apply = () => {
6714
+ if (!isControlled) {
6715
+ setInternalMode(mode);
6716
+ saveChatColorMode(mode);
6717
+ }
6718
+ onColorModeChange?.(mode);
6719
+ };
6720
+ if (skipThemeTransitionRef.current) {
6721
+ apply();
6722
+ return;
6723
+ }
6724
+ withThemeTransition(apply);
6725
+ },
6726
+ [isControlled, onColorModeChange]
6727
+ );
6728
+ const toggleColorMode = React11.useCallback(() => {
6729
+ setColorMode(resolvedMode === "dark" ? "light" : "dark");
6730
+ }, [resolvedMode, setColorMode]);
6731
+ const value = React11.useMemo(
6732
+ () => ({
6733
+ colorMode: resolvedMode,
6734
+ isDark,
6735
+ isThemeTransitionReady,
6736
+ themeColor,
6737
+ themeStyles: buildChatThemeStyles(themeColor),
6738
+ scopeClassName: cn(
6739
+ getChatScopeClass(isDark),
6740
+ !isThemeTransitionReady && "chat-theme-no-transition"
6741
+ ),
6742
+ setColorMode,
6743
+ toggleColorMode,
6744
+ showColorModeToggle
6745
+ }),
6746
+ [
6747
+ resolvedMode,
6748
+ isDark,
6749
+ isThemeTransitionReady,
6750
+ themeColor,
6751
+ setColorMode,
6752
+ toggleColorMode,
6753
+ showColorModeToggle
6754
+ ]
6755
+ );
6756
+ return /* @__PURE__ */ jsxRuntime.jsx(ChatThemeContext.Provider, { value, children });
6757
+ };
6758
+ var useChatTheme = () => {
6759
+ const context = React11.useContext(ChatThemeContext);
6760
+ if (!context) {
6761
+ throw new Error("useChatTheme must be used within ChatThemeProvider");
6762
+ }
6763
+ return context;
6764
+ };
6765
+
6766
+ // src/chat/lib/chat-message-bubble.ts
6767
+ init_utils2();
6768
+ function getBubbleCornerClasses(isSender) {
6769
+ 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";
6770
+ }
6771
+ function getChatBubbleClasses({
6772
+ isSender = false,
6773
+ surface,
6774
+ className
6775
+ }) {
6776
+ const resolvedSurface = surface ?? (isSender ? "sent" : "received");
6777
+ const isNeutral = resolvedSurface === "neutral";
6778
+ return cn(
6779
+ "relative",
6780
+ getBubbleCornerClasses(isSender),
6781
+ resolvedSurface === "sent" && "chat-bubble-sent bg-(--chat-theme) text-white",
6782
+ resolvedSurface === "received" && "chat-bubble-received border border-(--chat-border)/60 bg-(--chat-incoming-bubble) text-(--chat-incoming-text)",
6783
+ isNeutral && "chat-bubble-received border border-(--chat-border)/60 bg-(--chat-incoming-bubble) text-(--chat-incoming-text)",
6784
+ className
6785
+ );
6786
+ }
6564
6787
  var CallChatPanel = ({ conversationId, isGroupCall, onClose, className }) => {
6788
+ const { scopeClassName, themeStyles } = useChatTheme();
6565
6789
  const rawMessages = exports.useChatStore((s) => s.messagesByConversation[conversationId]);
6566
6790
  const loggedUserDetails = exports.useChatStore((s) => s.loggedUserDetails);
6567
6791
  const conversations = exports.useChatStore((s) => s.conversations);
@@ -6632,68 +6856,89 @@ var CallChatPanel = ({ conversationId, isGroupCall, onClose, className }) => {
6632
6856
  emitDmSend(payload);
6633
6857
  }
6634
6858
  };
6635
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col border-l border-white/10 bg-neutral-950/95", className), children: [
6636
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between border-b border-white/10 px-4 py-3", children: [
6637
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold text-white/90", children: "In-call chat" }),
6638
- /* @__PURE__ */ jsxRuntime.jsx(
6639
- "button",
6640
- {
6641
- type: "button",
6642
- "aria-label": "Close chat panel",
6643
- onClick: onClose,
6644
- className: "flex size-7 items-center justify-center rounded-lg text-white/60 transition-colors hover:bg-white/10 hover:text-white",
6645
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16 })
6646
- }
6647
- )
6648
- ] }),
6649
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 space-y-3 overflow-y-auto px-4 py-3", children: [
6650
- messages.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "pt-6 text-center text-xs text-white/40", children: "No messages yet \u2014 say hi without leaving the call." }),
6651
- messages.map((message) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col", message.isOwn && "items-end"), children: [
6652
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mb-0.5 text-[11px] font-medium text-white/40", children: message.isOwn ? "You" : message.senderName }),
6653
- /* @__PURE__ */ jsxRuntime.jsx(
6654
- "div",
6655
- {
6656
- className: cn(
6657
- "max-w-[85%] rounded-2xl px-3 py-1.5 text-sm break-words",
6658
- message.isOwn ? "rounded-br-sm bg-(--chat-theme,#4f46e5) text-white" : "rounded-bl-sm bg-white/10 text-white/90",
6659
- message.pending && "opacity-60"
6660
- ),
6661
- children: message.content
6662
- }
6663
- )
6664
- ] }, message.id)),
6665
- /* @__PURE__ */ jsxRuntime.jsx("div", { ref: listEndRef })
6666
- ] }),
6667
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 border-t border-white/10 p-3", children: [
6668
- /* @__PURE__ */ jsxRuntime.jsx(
6669
- "input",
6670
- {
6671
- value: text,
6672
- onChange: (e) => setText(e.target.value),
6673
- onKeyDown: (e) => {
6674
- if (e.key === "Enter" && !e.shiftKey) {
6675
- e.preventDefault();
6676
- handleSend();
6677
- }
6678
- },
6679
- placeholder: "Message\u2026",
6680
- 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"
6681
- }
6859
+ return /* @__PURE__ */ jsxRuntime.jsxs(
6860
+ "div",
6861
+ {
6862
+ className: cn(
6863
+ scopeClassName,
6864
+ "flex flex-col border-l border-(--chat-border) bg-(--chat-surface) text-(--chat-text)",
6865
+ className
6682
6866
  ),
6683
- /* @__PURE__ */ jsxRuntime.jsx(
6684
- Button,
6685
- {
6686
- variant: "ghost",
6687
- size: "icon",
6688
- "aria-label": "Send message",
6689
- disabled: !text.trim(),
6690
- onClick: handleSend,
6691
- className: "size-9 shrink-0 rounded-full bg-white/10 text-white hover:bg-white/20 disabled:opacity-30",
6692
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Send, { size: 16 })
6693
- }
6694
- )
6695
- ] })
6696
- ] });
6867
+ style: themeStyles,
6868
+ children: [
6869
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(chatSheetHeaderClass, "flex items-center justify-between"), children: [
6870
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: chatSheetTitleClass, children: "In-call chat" }),
6871
+ /* @__PURE__ */ jsxRuntime.jsx(
6872
+ "button",
6873
+ {
6874
+ type: "button",
6875
+ "aria-label": "Close chat panel",
6876
+ onClick: onClose,
6877
+ className: "flex size-7 items-center justify-center rounded-lg text-(--chat-muted) transition-colors hover:bg-(--chat-hover) hover:text-(--chat-text)",
6878
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16 })
6879
+ }
6880
+ )
6881
+ ] }),
6882
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(chatSheetBodyClass, "space-y-3"), children: [
6883
+ messages.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "pt-6 text-center text-xs text-(--chat-muted)", children: "No messages yet \u2014 say hi without leaving the call." }),
6884
+ messages.map((message) => /* @__PURE__ */ jsxRuntime.jsxs(
6885
+ "div",
6886
+ {
6887
+ className: cn("flex flex-col", message.isOwn ? "items-end" : "items-start"),
6888
+ children: [
6889
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mb-0.5 text-[11px] font-medium text-(--chat-muted)", children: message.isOwn ? "You" : message.senderName }),
6890
+ /* @__PURE__ */ jsxRuntime.jsx(
6891
+ "div",
6892
+ {
6893
+ className: cn(
6894
+ getChatBubbleClasses({ isSender: message.isOwn }),
6895
+ "max-w-[85%] px-3 py-1.5 text-sm break-words",
6896
+ message.pending && "opacity-60"
6897
+ ),
6898
+ children: message.content
6899
+ }
6900
+ )
6901
+ ]
6902
+ },
6903
+ message.id
6904
+ )),
6905
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: listEndRef })
6906
+ ] }),
6907
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 border-t border-(--chat-border) p-3", children: [
6908
+ /* @__PURE__ */ jsxRuntime.jsx(
6909
+ "input",
6910
+ {
6911
+ value: text,
6912
+ onChange: (e) => setText(e.target.value),
6913
+ onKeyDown: (e) => {
6914
+ if (e.key === "Enter" && !e.shiftKey) {
6915
+ e.preventDefault();
6916
+ handleSend();
6917
+ }
6918
+ },
6919
+ placeholder: "Message\u2026",
6920
+ className: cn(chatInputClass, "min-w-0 flex-1 rounded-full px-3.5 py-2 text-sm")
6921
+ }
6922
+ ),
6923
+ /* @__PURE__ */ jsxRuntime.jsx(
6924
+ Button,
6925
+ {
6926
+ variant: "ghost",
6927
+ size: "icon",
6928
+ "aria-label": "Send message",
6929
+ disabled: !text.trim(),
6930
+ onClick: handleSend,
6931
+ className: cn(
6932
+ "size-9 shrink-0 rounded-full transition-colors",
6933
+ text.trim() ? "bg-(--chat-theme) text-white hover:opacity-90" : "bg-(--chat-hover) text-(--chat-muted)"
6934
+ ),
6935
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Send, { size: 16 })
6936
+ }
6937
+ )
6938
+ ] })
6939
+ ]
6940
+ }
6941
+ );
6697
6942
  };
6698
6943
  var CallChatPanel_default = CallChatPanel;
6699
6944
  var CallSession2 = React11__namespace.default.lazy(() => Promise.resolve().then(() => (init_CallSession(), CallSession_exports)));
@@ -7153,162 +7398,6 @@ function AlertDialogCancel({
7153
7398
 
7154
7399
  // src/chat/ChatOverlay.tsx
7155
7400
  init_utils2();
7156
-
7157
- // src/chat/lib/chat-theme.ts
7158
- init_utils2();
7159
- var getChatScopeClass = (isDark) => cn("chat-theme-scope", isDark && "dark");
7160
- var chatSheetContentClass = "flex w-full flex-col gap-0 p-0 bg-(--chat-surface) text-(--chat-text)";
7161
- var chatSheetHeaderClass = "border-b border-(--chat-border) px-4 py-3 pr-11 text-left";
7162
- var chatSheetBodyClass = "chat-message-scroll flex-1 overflow-y-auto bg-(--chat-surface) p-3";
7163
- var chatSheetTitleClass = "text-[13px] font-semibold tracking-tight text-(--chat-text)";
7164
- var chatSheetSubtitleClass = "text-[11px] text-(--chat-muted)";
7165
- var chatDetailRowClass = "flex items-start justify-between gap-3 border-b border-(--chat-border) py-2.5 last:border-b-0";
7166
- var chatDetailLabelClass = "text-[11px] text-(--chat-muted)";
7167
- var chatDetailValueClass = "text-[13px] font-medium text-(--chat-text) text-right";
7168
- var chatDialogContentClass = "overflow-hidden rounded-2xl border border-(--chat-border) bg-(--chat-surface) p-0 text-(--chat-text) shadow-[var(--chat-shadow)]";
7169
- var chatDropdownContentClass = "rounded-xl border border-(--chat-border) bg-(--chat-surface) text-(--chat-text) shadow-lg";
7170
- 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)]";
7171
- var chatOptionsMenuLabelClass = "px-2.5 pb-0.5 pt-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-(--chat-muted)";
7172
- var chatOptionsMenuSeparatorClass = "my-1.5 h-px bg-(--chat-border)";
7173
- 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)";
7174
- 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)";
7175
- 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)";
7176
- var chatPopoverContentClass = "w-auto border border-(--chat-border) bg-(--chat-surface) p-0 text-(--chat-text) shadow-2xl";
7177
- 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)";
7178
- 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)]";
7179
- 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";
7180
- 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)";
7181
- var chatListItemClass = "transition-colors hover:bg-(--chat-hover)";
7182
- 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";
7183
- var chatDrawerSectionHeadingClass = "mb-1 px-0.5 text-[10px] font-semibold tracking-wide text-(--chat-muted)";
7184
- var chatHeaderToolbarIconProps = { size: 14, strokeWidth: 1.75 };
7185
- var chatSavedMessageModalIconClass = "shrink-0 text-(--chat-theme)";
7186
- var chatHeaderToolbarIconLgProps = { size: 22, strokeWidth: 1.75 };
7187
- var chatHeaderToolbarIconSmProps = { size: 11, strokeWidth: 1.75 };
7188
- 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";
7189
- var chatDrawerFileIconWrapClass = "flex size-8 shrink-0 items-center justify-center rounded-md bg-(--chat-hover)";
7190
- var chatDrawerMediaFrameClass = "overflow-hidden rounded-md border border-(--chat-border)/70 bg-(--chat-elevated)";
7191
- var buildChatThemeStyles = (themeColor) => ({
7192
- "--chat-theme": themeColor,
7193
- "--chat-theme-5": `${themeColor}0D`,
7194
- "--chat-theme-10": `${themeColor}1A`,
7195
- "--chat-theme-20": `${themeColor}33`,
7196
- "--chat-theme-40": `${themeColor}66`,
7197
- "--chat-theme-60": `${themeColor}99`,
7198
- "--read-tick-color": themeColor.toLowerCase() === "#0000ff" || themeColor.toLowerCase().includes("007aff") ? "#00e5ff" : "#40C4FF"
7199
- });
7200
-
7201
- // src/chat/lib/chat-theme-storage.ts
7202
- var STORAGE_KEY = "chat-color-mode";
7203
- var loadChatColorMode = () => {
7204
- if (typeof window === "undefined") return null;
7205
- try {
7206
- const stored = localStorage.getItem(STORAGE_KEY);
7207
- return stored === "dark" || stored === "light" ? stored : null;
7208
- } catch {
7209
- return null;
7210
- }
7211
- };
7212
- var saveChatColorMode = (mode) => {
7213
- if (typeof window === "undefined") return;
7214
- try {
7215
- localStorage.setItem(STORAGE_KEY, mode);
7216
- } catch {
7217
- }
7218
- };
7219
- var CHAT_THEME_SHELL_CLASS = "chat-theme-shell";
7220
- function withThemeTransition(apply) {
7221
- if (typeof document === "undefined") {
7222
- apply();
7223
- return;
7224
- }
7225
- reactDom.flushSync(apply);
7226
- }
7227
-
7228
- // src/context/ChatThemeContext.tsx
7229
- init_utils2();
7230
- var ChatThemeContext = React11.createContext(null);
7231
- var ChatThemeProvider = ({
7232
- children,
7233
- themeColor,
7234
- colorMode,
7235
- defaultColorMode = "light",
7236
- onColorModeChange,
7237
- showColorModeToggle = true
7238
- }) => {
7239
- const isControlled = colorMode !== void 0;
7240
- const [internalMode, setInternalMode] = React11.useState(defaultColorMode);
7241
- const [isThemeTransitionReady, setIsThemeTransitionReady] = React11.useState(false);
7242
- const skipThemeTransitionRef = React11.useRef(true);
7243
- React11.useLayoutEffect(() => {
7244
- if (!isControlled) {
7245
- const stored = loadChatColorMode();
7246
- if (stored) {
7247
- setInternalMode(stored);
7248
- }
7249
- }
7250
- const frame = requestAnimationFrame(() => {
7251
- skipThemeTransitionRef.current = false;
7252
- setIsThemeTransitionReady(true);
7253
- });
7254
- return () => cancelAnimationFrame(frame);
7255
- }, [isControlled]);
7256
- const resolvedMode = isControlled ? colorMode : internalMode;
7257
- const isDark = resolvedMode === "dark";
7258
- const setColorMode = React11.useCallback(
7259
- (mode) => {
7260
- const apply = () => {
7261
- if (!isControlled) {
7262
- setInternalMode(mode);
7263
- saveChatColorMode(mode);
7264
- }
7265
- onColorModeChange?.(mode);
7266
- };
7267
- if (skipThemeTransitionRef.current) {
7268
- apply();
7269
- return;
7270
- }
7271
- withThemeTransition(apply);
7272
- },
7273
- [isControlled, onColorModeChange]
7274
- );
7275
- const toggleColorMode = React11.useCallback(() => {
7276
- setColorMode(resolvedMode === "dark" ? "light" : "dark");
7277
- }, [resolvedMode, setColorMode]);
7278
- const value = React11.useMemo(
7279
- () => ({
7280
- colorMode: resolvedMode,
7281
- isDark,
7282
- isThemeTransitionReady,
7283
- themeColor,
7284
- themeStyles: buildChatThemeStyles(themeColor),
7285
- scopeClassName: cn(
7286
- getChatScopeClass(isDark),
7287
- !isThemeTransitionReady && "chat-theme-no-transition"
7288
- ),
7289
- setColorMode,
7290
- toggleColorMode,
7291
- showColorModeToggle
7292
- }),
7293
- [
7294
- resolvedMode,
7295
- isDark,
7296
- isThemeTransitionReady,
7297
- themeColor,
7298
- setColorMode,
7299
- toggleColorMode,
7300
- showColorModeToggle
7301
- ]
7302
- );
7303
- return /* @__PURE__ */ jsxRuntime.jsx(ChatThemeContext.Provider, { value, children });
7304
- };
7305
- var useChatTheme = () => {
7306
- const context = React11.useContext(ChatThemeContext);
7307
- if (!context) {
7308
- throw new Error("useChatTheme must be used within ChatThemeProvider");
7309
- }
7310
- return context;
7311
- };
7312
7401
  var ChatSheetContent = ({
7313
7402
  className,
7314
7403
  style,
@@ -7364,6 +7453,66 @@ init_avatar();
7364
7453
  init_button();
7365
7454
  init_utils2();
7366
7455
  init_call_store();
7456
+
7457
+ // src/call/ringtone.ts
7458
+ var audioCtx = null;
7459
+ var scheduledTimer = null;
7460
+ var activeOscillators = [];
7461
+ var RING_ON_MS = 1200;
7462
+ var RING_OFF_MS = 2400;
7463
+ var RING_FREQUENCIES = [440, 480];
7464
+ var getContext = () => {
7465
+ if (typeof window === "undefined") return null;
7466
+ const Ctor = window.AudioContext || window.webkitAudioContext;
7467
+ if (!Ctor) return null;
7468
+ if (!audioCtx || audioCtx.state === "closed") audioCtx = new Ctor();
7469
+ return audioCtx;
7470
+ };
7471
+ var stopOscillators = () => {
7472
+ activeOscillators.forEach((osc) => {
7473
+ try {
7474
+ osc.stop();
7475
+ } catch {
7476
+ }
7477
+ });
7478
+ activeOscillators = [];
7479
+ };
7480
+ var ringBurst = (ctx) => {
7481
+ const onSeconds = RING_ON_MS / 1e3;
7482
+ const gain = ctx.createGain();
7483
+ gain.gain.setValueAtTime(0, ctx.currentTime);
7484
+ gain.gain.linearRampToValueAtTime(0.15, ctx.currentTime + 0.05);
7485
+ gain.gain.setValueAtTime(0.15, ctx.currentTime + onSeconds - 0.05);
7486
+ gain.gain.linearRampToValueAtTime(0, ctx.currentTime + onSeconds);
7487
+ gain.connect(ctx.destination);
7488
+ RING_FREQUENCIES.forEach((freq) => {
7489
+ const osc = ctx.createOscillator();
7490
+ osc.type = "sine";
7491
+ osc.frequency.value = freq;
7492
+ osc.connect(gain);
7493
+ osc.start();
7494
+ osc.stop(ctx.currentTime + onSeconds);
7495
+ activeOscillators.push(osc);
7496
+ });
7497
+ };
7498
+ var playDefaultRingtone = () => {
7499
+ if (scheduledTimer) return;
7500
+ const ctx = getContext();
7501
+ if (!ctx) return;
7502
+ ctx.resume().catch(() => void 0);
7503
+ const cycle = () => {
7504
+ ringBurst(ctx);
7505
+ scheduledTimer = setTimeout(cycle, RING_ON_MS + RING_OFF_MS);
7506
+ };
7507
+ cycle();
7508
+ };
7509
+ var stopDefaultRingtone = () => {
7510
+ if (scheduledTimer) {
7511
+ clearTimeout(scheduledTimer);
7512
+ scheduledTimer = null;
7513
+ }
7514
+ stopOscillators();
7515
+ };
7367
7516
  var IncomingCallDialog = ({ ringtoneUrl }) => {
7368
7517
  const uiStatus = exports.useCallStore((s) => s.uiStatus);
7369
7518
  const callId = exports.useCallStore((s) => s.callId);
@@ -7386,6 +7535,12 @@ var IncomingCallDialog = ({ ringtoneUrl }) => {
7386
7535
  el.pause();
7387
7536
  }
7388
7537
  }, [isOpen, ringtoneUrl]);
7538
+ React11.useEffect(() => {
7539
+ if (ringtoneUrl) return;
7540
+ if (isOpen) playDefaultRingtone();
7541
+ else stopDefaultRingtone();
7542
+ return () => stopDefaultRingtone();
7543
+ }, [isOpen, ringtoneUrl]);
7389
7544
  if (!isOpen) return null;
7390
7545
  const callerName = caller?.name || "Unknown caller";
7391
7546
  const name = isGroupCall ? peer?.name || callerName : callerName;
@@ -11962,28 +12117,6 @@ function MessageForwardedLabel({ isSender, className, overlay }) {
11962
12117
  }
11963
12118
  );
11964
12119
  }
11965
-
11966
- // src/chat/lib/chat-message-bubble.ts
11967
- init_utils2();
11968
- function getBubbleCornerClasses(isSender) {
11969
- 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";
11970
- }
11971
- function getChatBubbleClasses({
11972
- isSender = false,
11973
- surface,
11974
- className
11975
- }) {
11976
- const resolvedSurface = surface ?? (isSender ? "sent" : "received");
11977
- const isNeutral = resolvedSurface === "neutral";
11978
- return cn(
11979
- "relative",
11980
- getBubbleCornerClasses(isSender),
11981
- resolvedSurface === "sent" && "chat-bubble-sent bg-(--chat-theme) text-white",
11982
- resolvedSurface === "received" && "chat-bubble-received border border-(--chat-border)/60 bg-(--chat-incoming-bubble) text-(--chat-incoming-text)",
11983
- isNeutral && "chat-bubble-received border border-(--chat-border)/60 bg-(--chat-incoming-bubble) text-(--chat-incoming-text)",
11984
- className
11985
- );
11986
- }
11987
12120
  init_utils2();
11988
12121
  init_client();
11989
12122
  function getPreviewKind(name) {
@@ -21011,6 +21144,7 @@ init_normalize_helpers();
21011
21144
  init_useStore();
21012
21145
  init_open_conversation();
21013
21146
  var RETRY_DELAYS_MS = [1e3, 3e3, 8e3, 2e4];
21147
+ var CHAT_HEARTBEAT_INTERVAL_MS = 15e3;
21014
21148
  var recentToastIds = /* @__PURE__ */ new Set();
21015
21149
  var rememberToastId = (id) => {
21016
21150
  if (!id) return true;
@@ -21233,6 +21367,36 @@ var ForegroundPushBridge = ({ onPush }) => {
21233
21367
  window.removeEventListener(PUSH_CHANGED_EVENT, onPushChanged);
21234
21368
  };
21235
21369
  }, []);
21370
+ React11.useEffect(() => {
21371
+ if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
21372
+ return;
21373
+ }
21374
+ let interval = null;
21375
+ const sendHeartbeat = () => {
21376
+ navigator.serviceWorker.getRegistration().then(
21377
+ (registration) => registration?.active?.postMessage({ type: CHAT_HEARTBEAT_SW_TYPE })
21378
+ ).catch(() => void 0);
21379
+ };
21380
+ const startHeartbeat = () => {
21381
+ if (interval || !getStoredPushToken()) return;
21382
+ sendHeartbeat();
21383
+ interval = setInterval(sendHeartbeat, CHAT_HEARTBEAT_INTERVAL_MS);
21384
+ };
21385
+ const stopHeartbeat = () => {
21386
+ if (interval) clearInterval(interval);
21387
+ interval = null;
21388
+ };
21389
+ startHeartbeat();
21390
+ const onPushChanged = (event) => {
21391
+ if (event.detail?.enabled) startHeartbeat();
21392
+ else stopHeartbeat();
21393
+ };
21394
+ window.addEventListener(PUSH_CHANGED_EVENT, onPushChanged);
21395
+ return () => {
21396
+ stopHeartbeat();
21397
+ window.removeEventListener(PUSH_CHANGED_EVENT, onPushChanged);
21398
+ };
21399
+ }, []);
21236
21400
  React11.useEffect(() => {
21237
21401
  const onOpen = (event) => {
21238
21402
  const id = String(