@realtimexsco/live-chat 1.4.16 → 1.4.18

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/PERMISSIONS.md ADDED
@@ -0,0 +1,236 @@
1
+ # Admin permissions & custom UI
2
+
3
+ How `@realtimexsco/live-chat` applies workspace **effective settings**, and how to keep the same rules when you replace UI with your own components.
4
+
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ After the chat session is ready, the package fetches:
10
+
11
+ ```http
12
+ GET /settings/effective
13
+ ```
14
+
15
+ Settings are stored in `ChatEffectiveSettingsProvider` and used to **gate** features in the default UI:
16
+
17
+ | Rule | Behavior |
18
+ |------|----------|
19
+ | Feature denied | Control stays **visible** |
20
+ | Interaction | **Disabled** |
21
+ | Hover | Tooltip with reason (e.g. *This feature is disabled by your administrator.*) |
22
+
23
+ Default components already honor these gates. **Custom slot components must honor them too** — the package cannot force-disable arbitrary host JSX.
24
+
25
+ ---
26
+
27
+ ## What the API controls
28
+
29
+ | API field | UI effect |
30
+ |-----------|-----------|
31
+ | `pushNotificationEnabled` | Settings → push toggle (visible, disabled + tooltip if off) |
32
+ | `audioCallEnabled` / `videoCallEnabled` | Header phone / video buttons |
33
+ | `calls.allowedByPlatform` | Checked first for calls |
34
+ | `calls.configured` | Calls not configured message |
35
+ | `calls.enabled` | Calls disabled by admin |
36
+ | `blockUsersEnabled` | Block user menu item |
37
+ | `attachments.enabled` | Paperclip / attach |
38
+ | `attachments.maxFileSizeMB` | Max upload size |
39
+ | `attachments.allowedMimeTypes` | Allowed MIME (`*` / `*/*` = all) |
40
+ | `attachments.maxAttachmentsPerMessage` | Max files per message |
41
+ | `groupCreationEnabled` | “New group” tab |
42
+ | `groupPolicy.defaults` / `locked` | Group permission drawer |
43
+ | `messageTimers.*` | Edit / delete time windows (DM vs group) |
44
+ | `maxAdminsPerGroup` | Max group admins |
45
+ | `maxParticipantsPerGroup` | Max group members |
46
+ | `maxPinnedMessagesPerConversation` | Max pins |
47
+
48
+ ### Calls check order
49
+
50
+ 1. `calls.allowedByPlatform`
51
+ 2. `calls.configured`
52
+ 3. `calls.enabled`
53
+ 4. `audioCallEnabled` / `videoCallEnabled`
54
+
55
+ Same rules apply in **DM and group** chats.
56
+
57
+ ---
58
+
59
+ ## Exports you need
60
+
61
+ ```ts
62
+ import {
63
+ useAdminFeatureGate,
64
+ useEffectiveSettingsOptional,
65
+ AdminPermissionTooltip,
66
+ ADMIN_PERMISSION_DENIED_MESSAGE,
67
+ type CallFeatureGate,
68
+ type AdminFeatureKey,
69
+ } from "@realtimexsco/live-chat";
70
+ ```
71
+
72
+ ### Feature keys (`useAdminFeatureGate`)
73
+
74
+ | Key | Gate |
75
+ |-----|------|
76
+ | `"audioCall"` | Voice call |
77
+ | `"videoCall"` | Video call |
78
+ | `"pushNotification"` | Push toggle |
79
+ | `"blockUsers"` | Block user |
80
+ | `"attachments"` | Attach files |
81
+ | `"groupCreation"` | Create group |
82
+
83
+ ### Gate shape (`CallFeatureGate`)
84
+
85
+ ```ts
86
+ {
87
+ visible: boolean; // keep UI visible
88
+ enabled: boolean; // allow click / toggle
89
+ disabledReason: string | null; // tooltip text when disabled
90
+ }
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Two ways to customize (important)
96
+
97
+ ### Option A — Prefer small slots (recommended)
98
+
99
+ Override only the look of a control. Let the package wrapper apply gates + tooltips.
100
+
101
+ ```ts
102
+ // ✅ Good — package HeaderCallActions still applies permissions
103
+ components: {
104
+ PhoneCallButton: MyPhoneButton,
105
+ VideoCallButton: MyVideoButton,
106
+ }
107
+
108
+ // ❌ Avoid unless you wire gates yourself
109
+ components: {
110
+ HeaderCallActions: MyCallActions, // replaces gated wrapper
111
+ }
112
+ ```
113
+
114
+ Your button should still honor props the package passes:
115
+
116
+ ```tsx
117
+ function MyVideoButton({
118
+ onVideoCall,
119
+ disabled,
120
+ disabledReason,
121
+ className,
122
+ }: VideoCallButtonSlotProps) {
123
+ return (
124
+ <button
125
+ type="button"
126
+ className={className}
127
+ disabled={disabled}
128
+ aria-disabled={disabled}
129
+ onClick={disabled ? undefined : onVideoCall}
130
+ >
131
+ Video
132
+ </button>
133
+ );
134
+ }
135
+ ```
136
+
137
+ (The package wraps this with `AdminPermissionTooltip` when used via default `HeaderCallActions`.)
138
+
139
+ ### Option B — Full custom slot
140
+
141
+ If you replace a whole slot (e.g. `HeaderCallActions`, custom attach button, custom push row), **you** must read the gate and apply disable + tooltip.
142
+
143
+ ```tsx
144
+ import {
145
+ useAdminFeatureGate,
146
+ AdminPermissionTooltip,
147
+ } from "@realtimexsco/live-chat";
148
+
149
+ function MyVideoCallButton({ onVideoCall, className }) {
150
+ const gate = useAdminFeatureGate("videoCall");
151
+
152
+ return (
153
+ <AdminPermissionTooltip
154
+ disabled={!gate.enabled}
155
+ message={gate.disabledReason}
156
+ side="bottom"
157
+ >
158
+ <button
159
+ type="button"
160
+ className={className}
161
+ disabled={!gate.enabled}
162
+ onClick={gate.enabled ? onVideoCall : undefined}
163
+ >
164
+ Video
165
+ </button>
166
+ </AdminPermissionTooltip>
167
+ );
168
+ }
169
+ ```
170
+
171
+ Same pattern for other features — change the key only:
172
+
173
+ ```ts
174
+ useAdminFeatureGate("audioCall");
175
+ useAdminFeatureGate("pushNotification");
176
+ useAdminFeatureGate("blockUsers");
177
+ useAdminFeatureGate("attachments");
178
+ useAdminFeatureGate("groupCreation");
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Contract for every custom permission-aware control
184
+
185
+ | Do | Don’t |
186
+ |----|--------|
187
+ | Keep the control **visible** when denied | Hide the button when permission is off |
188
+ | Set `disabled={!gate.enabled}` | Leave it clickable |
189
+ | Wrap with `AdminPermissionTooltip` | Skip the tooltip |
190
+ | Use `useAdminFeatureGate` / gate props | Hardcode your own permission logic |
191
+ | Clear `onClick` when disabled | Fire the action when disabled |
192
+
193
+ ---
194
+
195
+ ## Advanced: raw settings
196
+
197
+ For limits (file size, max participants, timers, locked group policy), use:
198
+
199
+ ```ts
200
+ const { settings, loading, isGroupPermissionLocked } =
201
+ useEffectiveSettingsOptional();
202
+
203
+ settings.attachments.maxFileSizeMB;
204
+ settings.maxParticipantsPerGroup;
205
+ settings.messageTimers.dmEditMinutes;
206
+ isGroupPermissionLocked("onlyAdminCanSendMessage");
207
+ ```
208
+
209
+ While `loading` is `true`, show a spinner instead of flashing the wrong enabled state (same idea as the push settings row).
210
+
211
+ ---
212
+
213
+ ## Loading state
214
+
215
+ Effective settings and feature-specific APIs (e.g. push config) may load after the modal/UI opens. Recommended pattern:
216
+
217
+ 1. If `loading` → show row + spinner
218
+ 2. When ready → show control with correct enabled/disabled state
219
+ 3. Never flash “enabled” then suddenly disable without explanation
220
+
221
+ ---
222
+
223
+ ## Checklist for host apps
224
+
225
+ - [ ] Default UI: no extra work — gates are built in
226
+ - [ ] Custom `PhoneCallButton` / `VideoCallButton`: honor `disabled` (+ optional `disabledReason`)
227
+ - [ ] Custom `HeaderCallActions` / full call UI: use `useAdminFeatureGate` + `AdminPermissionTooltip`
228
+ - [ ] Custom push / attach / block / create-group UI: same gate + tooltip pattern
229
+ - [ ] Do not override `HeaderCallActions` only for styling — override `PhoneCallButton` / `VideoCallButton` instead
230
+
231
+ ---
232
+
233
+ ## Related
234
+
235
+ - Package docs: [https://realtimex.softwareco.com/documentations/package](https://realtimex.softwareco.com/documentations/package)
236
+ - npm: [https://www.npmjs.com/package/@realtimexsco/live-chat](https://www.npmjs.com/package/@realtimexsco/live-chat)
package/README.md CHANGED
@@ -4,20 +4,24 @@
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@realtimexsco/live-chat.svg)](https://www.npmjs.com/package/@realtimexsco/live-chat)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/@realtimexsco/live-chat.svg)](https://www.npmjs.com/package/@realtimexsco/live-chat)
7
- [![license](https://img.shields.io/npm/l/@realtimexsco/live-chat.svg)](#license)
7
+ [![License: MIT](https://img.shields.io/npm/l/@realtimexsco/live-chat.svg)](#license)
8
+
9
+
10
+ | | |
11
+ | ------------ | ---------------------------------------------------------------------------------- |
12
+ | **Package** | `[@realtimexsco/live-chat](https://www.npmjs.com/package/@realtimexsco/live-chat)` |
13
+ | **Styles** | `@realtimexsco/live-chat/styles.css` |
14
+ | **Requires** | React 17+, Tailwind CSS **v4**, Zustand 4+ |
15
+ | **Docs** | [Package documentation](https://realtimex.softwareco.com/documentations/package) |
16
+ | **License** | [MIT](#license) |
8
17
 
9
- | | |
10
- |---|---|
11
- | **Package** | [`@realtimexsco/live-chat`](https://www.npmjs.com/package/@realtimexsco/live-chat) |
12
- | **Styles** | `@realtimexsco/live-chat/styles.css` |
13
- | **Requires** | React 17+, Tailwind CSS **v4**, Zustand 4+ |
14
- | **Docs** | [Package documentation](https://realtimex.softwareco.com/documentations/package) |
15
- | **License** | [MIT](#license) |
16
18
 
17
19
  Socket.IO, REST client, Zustand store, Radix UI, emoji picker, and chat UI are **bundled**. Your app only installs peer dependencies.
18
20
 
19
21
  ---
20
22
 
23
+
24
+
21
25
  ## Install
22
26
 
23
27
  ```bash
@@ -49,23 +53,27 @@ export default function ChatPage() {
49
53
 
50
54
  ---
51
55
 
56
+
57
+
52
58
  ## Features
53
59
 
54
60
  DMs · groups · Socket.IO · reactions · replies · forward · edit/delete · pin/star · attachments · search · typing · read receipts · light/dark · locale/timezone · full UI customization
55
61
 
56
62
  ---
57
63
 
64
+
65
+
58
66
  ## More information
59
67
 
60
- | Topic | Link |
61
- |-------|------|
68
+
69
+ | Topic | Link |
70
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------ |
62
71
  | Full setup & docs | [https://realtimex.softwareco.com/documentations/package](https://realtimex.softwareco.com/documentations/package) |
63
- | npm package | [npmjs.com/package/@realtimexsco/live-chat](https://www.npmjs.com/package/@realtimexsco/live-chat) |
64
- | License | [MIT](#license) |
72
+ | npm package | [npmjs.com/package/@realtimexsco/live-chat](https://www.npmjs.com/package/@realtimexsco/live-chat) |
73
+ | License | [MIT](#license) |
65
74
 
66
- ---
67
75
 
68
- <a id="license"></a>
76
+ ---
69
77
 
70
78
  ## License
71
79
 
@@ -77,4 +85,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
77
85
 
78
86
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
79
87
 
80
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
88
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/dist/index.cjs CHANGED
@@ -9039,23 +9039,24 @@ var DefaultHeaderCallActions = ({
9039
9039
  showVideoCall = true,
9040
9040
  onPhoneCall,
9041
9041
  onVideoCall,
9042
+ phoneCallDisabled = false,
9043
+ videoCallDisabled = false,
9044
+ phoneCallDisabledReason = null,
9045
+ videoCallDisabledReason = null,
9042
9046
  className
9043
9047
  }) => {
9044
9048
  const { components, slotClassName } = useChatCustomizationOptional();
9045
- const { getAudioCallGate, getVideoCallGate } = useEffectiveSettingsOptional();
9046
9049
  const PhoneSlot = components.PhoneCallButton;
9047
9050
  const VideoSlot = components.VideoCallButton;
9048
- const audioGate = getAudioCallGate();
9049
- const videoGate = getVideoCallGate();
9050
9051
  if (!showPhoneCall && !showVideoCall) return null;
9051
- const phoneEnabled = showPhoneCall && audioGate.enabled;
9052
- const videoEnabled = showVideoCall && videoGate.enabled;
9052
+ const phoneEnabled = showPhoneCall && !phoneCallDisabled;
9053
+ const videoEnabled = showVideoCall && !videoCallDisabled;
9053
9054
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9054
9055
  showPhoneCall && (PhoneSlot ? /* @__PURE__ */ jsxRuntime.jsx(
9055
9056
  AdminPermissionTooltip,
9056
9057
  {
9057
- disabled: !audioGate.enabled,
9058
- message: audioGate.disabledReason,
9058
+ disabled: phoneCallDisabled,
9059
+ message: phoneCallDisabledReason,
9059
9060
  side: "bottom",
9060
9061
  children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex", children: /* @__PURE__ */ jsxRuntime.jsx(
9061
9062
  PhoneSlot,
@@ -9063,10 +9064,12 @@ var DefaultHeaderCallActions = ({
9063
9064
  activeChannel,
9064
9065
  conversationId,
9065
9066
  onPhoneCall: phoneEnabled ? onPhoneCall : void 0,
9067
+ disabled: phoneCallDisabled,
9068
+ disabledReason: phoneCallDisabledReason,
9066
9069
  className: cn(
9067
9070
  className,
9068
9071
  slotClassName("phoneCallButton"),
9069
- !audioGate.enabled && "opacity-40"
9072
+ phoneCallDisabled && "opacity-40"
9070
9073
  )
9071
9074
  }
9072
9075
  ) })
@@ -9074,8 +9077,8 @@ var DefaultHeaderCallActions = ({
9074
9077
  ) : /* @__PURE__ */ jsxRuntime.jsx(
9075
9078
  AdminPermissionTooltip,
9076
9079
  {
9077
- disabled: !audioGate.enabled,
9078
- message: audioGate.disabledReason,
9080
+ disabled: phoneCallDisabled,
9081
+ message: phoneCallDisabledReason,
9079
9082
  side: "bottom",
9080
9083
  children: /* @__PURE__ */ jsxRuntime.jsx(
9081
9084
  Button,
@@ -9084,7 +9087,7 @@ var DefaultHeaderCallActions = ({
9084
9087
  size: "icon",
9085
9088
  className: cn(actionBtnClass, className, slotClassName("phoneCallButton")),
9086
9089
  "aria-label": "Voice call",
9087
- disabled: !audioGate.enabled,
9090
+ disabled: phoneCallDisabled,
9088
9091
  onClick: phoneEnabled ? onPhoneCall : void 0,
9089
9092
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Phone, { size: 15 })
9090
9093
  }
@@ -9094,8 +9097,8 @@ var DefaultHeaderCallActions = ({
9094
9097
  showVideoCall && (VideoSlot ? /* @__PURE__ */ jsxRuntime.jsx(
9095
9098
  AdminPermissionTooltip,
9096
9099
  {
9097
- disabled: !videoGate.enabled,
9098
- message: videoGate.disabledReason,
9100
+ disabled: videoCallDisabled,
9101
+ message: videoCallDisabledReason,
9099
9102
  side: "bottom",
9100
9103
  children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex", children: /* @__PURE__ */ jsxRuntime.jsx(
9101
9104
  VideoSlot,
@@ -9103,10 +9106,12 @@ var DefaultHeaderCallActions = ({
9103
9106
  activeChannel,
9104
9107
  conversationId,
9105
9108
  onVideoCall: videoEnabled ? onVideoCall : void 0,
9109
+ disabled: videoCallDisabled,
9110
+ disabledReason: videoCallDisabledReason,
9106
9111
  className: cn(
9107
9112
  className,
9108
9113
  slotClassName("videoCallButton"),
9109
- !videoGate.enabled && "opacity-40"
9114
+ videoCallDisabled && "opacity-40"
9110
9115
  )
9111
9116
  }
9112
9117
  ) })
@@ -9114,8 +9119,8 @@ var DefaultHeaderCallActions = ({
9114
9119
  ) : /* @__PURE__ */ jsxRuntime.jsx(
9115
9120
  AdminPermissionTooltip,
9116
9121
  {
9117
- disabled: !videoGate.enabled,
9118
- message: videoGate.disabledReason,
9122
+ disabled: videoCallDisabled,
9123
+ message: videoCallDisabledReason,
9119
9124
  side: "bottom",
9120
9125
  children: /* @__PURE__ */ jsxRuntime.jsx(
9121
9126
  Button,
@@ -9124,7 +9129,7 @@ var DefaultHeaderCallActions = ({
9124
9129
  size: "icon",
9125
9130
  className: cn(actionBtnClass, className, slotClassName("videoCallButton")),
9126
9131
  "aria-label": "Video call",
9127
- disabled: !videoGate.enabled,
9132
+ disabled: videoCallDisabled,
9128
9133
  onClick: videoEnabled ? onVideoCall : void 0,
9129
9134
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Video, { size: 15 })
9130
9135
  }
@@ -9135,12 +9140,27 @@ var DefaultHeaderCallActions = ({
9135
9140
  };
9136
9141
  var HeaderCallActions = (props) => {
9137
9142
  const { components, slotClassName } = useChatCustomizationOptional();
9143
+ const { getAudioCallGate, getVideoCallGate } = useEffectiveSettingsOptional();
9138
9144
  const Custom = components.HeaderCallActions;
9139
9145
  const showPhoneCall = props.showPhoneCall ?? true;
9140
9146
  const showVideoCall = props.showVideoCall ?? true;
9147
+ const audioGate = getAudioCallGate();
9148
+ const videoGate = getVideoCallGate();
9141
9149
  if (!showPhoneCall && !showVideoCall) return null;
9142
- const useCustom = Custom != null && Custom !== HeaderCallActions;
9143
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex items-center gap-0.5", slotClassName("headerCallActions")), children: useCustom ? /* @__PURE__ */ jsxRuntime.jsx(Custom, { ...props }) : /* @__PURE__ */ jsxRuntime.jsx(DefaultHeaderCallActions, { ...props }) });
9150
+ const gatedProps = {
9151
+ ...props,
9152
+ showPhoneCall,
9153
+ showVideoCall,
9154
+ onPhoneCall: audioGate.enabled ? props.onPhoneCall : void 0,
9155
+ onVideoCall: videoGate.enabled ? props.onVideoCall : void 0,
9156
+ phoneCallDisabled: !audioGate.enabled,
9157
+ videoCallDisabled: !videoGate.enabled,
9158
+ phoneCallDisabledReason: audioGate.disabledReason,
9159
+ videoCallDisabledReason: videoGate.disabledReason
9160
+ };
9161
+ const hasButtonSlots = components.PhoneCallButton != null || components.VideoCallButton != null;
9162
+ const useCustom = Custom != null && Custom !== HeaderCallActions && !hasButtonSlots;
9163
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex items-center gap-0.5", slotClassName("headerCallActions")), children: useCustom ? /* @__PURE__ */ jsxRuntime.jsx(Custom, { ...gatedProps }) : /* @__PURE__ */ jsxRuntime.jsx(DefaultHeaderCallActions, { ...gatedProps }) });
9144
9164
  };
9145
9165
  var HeaderCallActions_default = HeaderCallActions;
9146
9166
  function OptionsMenuItem({
@@ -9803,10 +9823,10 @@ function AudioAttachmentPlayer({
9803
9823
  "div",
9804
9824
  {
9805
9825
  className: cn(
9806
- "chat-audio-player w-full min-w-[220px] max-w-[280px] rounded-xl border p-2.5",
9826
+ "chat-audio-player w-full min-w-[220px] max-w-[280px] rounded-xl border p-2.5 transition-shadow duration-150",
9807
9827
  onThemeBubble && "chat-audio-player-on-dark border-white/20 bg-white/12 text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] backdrop-blur-[2px]",
9808
- onIncomingBubble && "border-(--chat-border)/70 bg-(--chat-elevated) text-(--chat-text) shadow-sm [.dark_&]:bg-(--chat-surface)",
9809
- !inBubble && "border-(--chat-border) bg-(--chat-elevated) text-(--chat-text) shadow-sm",
9828
+ onIncomingBubble && "border-(--chat-border)/70 bg-(--chat-elevated) text-(--chat-text) shadow-sm hover:shadow-md [.dark_&]:bg-(--chat-surface)",
9829
+ !inBubble && "border-(--chat-border) bg-(--chat-elevated) text-(--chat-text) shadow-sm hover:shadow-md",
9810
9830
  className
9811
9831
  ),
9812
9832
  children: [
@@ -9842,7 +9862,7 @@ function AudioAttachmentPlayer({
9842
9862
  onClick: () => void togglePlay(),
9843
9863
  disabled: !url,
9844
9864
  className: cn(
9845
- "flex size-8 shrink-0 items-center justify-center rounded-full border transition-colors",
9865
+ "flex size-8 shrink-0 items-center justify-center rounded-full border transition-all duration-150 hover:scale-105",
9846
9866
  onThemeBubble ? "border-white/30 bg-white/10 text-white hover:bg-white/15" : "border-(--chat-border) bg-(--chat-input-bg) text-(--chat-text) hover:bg-(--chat-hover)",
9847
9867
  !url && "opacity-40"
9848
9868
  ),
@@ -9896,10 +9916,10 @@ function getMediaCardClasses(inBubble, isSender) {
9896
9916
  const onThemeBubble = inBubble && isSender;
9897
9917
  const onIncomingBubble = inBubble && !isSender;
9898
9918
  return cn(
9899
- "w-full min-w-[220px] max-w-[280px] rounded-xl border p-2.5",
9919
+ "w-full min-w-[220px] max-w-[280px] rounded-xl border p-2.5 transition-shadow duration-150",
9900
9920
  onThemeBubble && "border-white/20 bg-white/12 text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] backdrop-blur-[2px]",
9901
- onIncomingBubble && "border-(--chat-border)/70 bg-(--chat-elevated) text-(--chat-text) shadow-sm [.dark_&]:bg-(--chat-surface)",
9902
- !inBubble && "border-(--chat-border) bg-(--chat-elevated) text-(--chat-text) shadow-sm"
9921
+ onIncomingBubble && "border-(--chat-border)/70 bg-(--chat-elevated) text-(--chat-text) shadow-sm hover:shadow-md [.dark_&]:bg-(--chat-surface)",
9922
+ !inBubble && "border-(--chat-border) bg-(--chat-elevated) text-(--chat-text) shadow-sm hover:shadow-md"
9903
9923
  );
9904
9924
  }
9905
9925
  function VideoAttachmentPlayer({
@@ -10023,10 +10043,10 @@ function VideoAttachmentPlayer({
10023
10043
  {
10024
10044
  type: "button",
10025
10045
  onClick: () => void togglePlay(),
10026
- className: "absolute inset-0 flex items-center justify-center bg-black/20 transition-colors hover:bg-black/30",
10046
+ className: "group/play absolute inset-0 flex items-center justify-center bg-black/20 transition-colors hover:bg-black/30",
10027
10047
  "aria-label": `Play ${name}`,
10028
10048
  children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(
10029
- "flex items-center justify-center rounded-full shadow-md",
10049
+ "flex items-center justify-center rounded-full shadow-md transition-transform duration-150 group-hover/play:scale-110",
10030
10050
  compact ? "size-9" : "size-11",
10031
10051
  onThemeBubble ? "bg-white/95 text-(--chat-theme)" : "bg-(--chat-surface)/95 text-(--chat-text)"
10032
10052
  ), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Play, { size: compact ? 15 : 18, className: "ml-0.5 fill-current" }) })
@@ -10069,13 +10089,13 @@ function getAttachmentIcon(fileName, type, className, size = 20) {
10069
10089
  }
10070
10090
  function getAttachmentIconBoxClass(fileName, type) {
10071
10091
  const ext = fileName.split(".").pop()?.toLowerCase() || "";
10072
- if (["mp3", "wav", "ogg", "m4a"].includes(ext)) return "bg-violet-500";
10073
- if (["mp4", "mov", "webm"].includes(ext)) return "bg-pink-500";
10074
- if (ext === "pdf") return "bg-red-500";
10075
- if (["doc", "docx"].includes(ext)) return "bg-blue-500";
10076
- if (["xls", "xlsx", "csv"].includes(ext)) return "bg-emerald-600";
10077
- if (["ppt", "pptx"].includes(ext)) return "bg-orange-500";
10078
- return "bg-slate-500";
10092
+ if (["mp3", "wav", "ogg", "m4a"].includes(ext)) return "bg-linear-to-br from-violet-500 to-fuchsia-500";
10093
+ if (["mp4", "mov", "webm"].includes(ext)) return "bg-linear-to-br from-pink-500 to-rose-500";
10094
+ if (ext === "pdf") return "bg-linear-to-br from-red-500 to-rose-600";
10095
+ if (["doc", "docx"].includes(ext)) return "bg-linear-to-br from-blue-500 to-indigo-500";
10096
+ if (["xls", "xlsx", "csv"].includes(ext)) return "bg-linear-to-br from-emerald-500 to-teal-600";
10097
+ if (["ppt", "pptx"].includes(ext)) return "bg-linear-to-br from-orange-500 to-amber-500";
10098
+ return "bg-linear-to-br from-slate-500 to-slate-600";
10079
10099
  }
10080
10100
  var MessageStatus = ({ status, isPending, className }) => {
10081
10101
  const resolvedStatus = status || "sent";
@@ -11225,28 +11245,28 @@ function ImageTile({ att, className, fill, embeddedInBubble, showImageCaption, o
11225
11245
  {
11226
11246
  type: "button",
11227
11247
  className: cn(
11228
- "relative block w-full overflow-hidden bg-(--chat-elevated)",
11248
+ "group relative block w-full overflow-hidden bg-(--chat-elevated)",
11229
11249
  fill ? "h-full min-h-0" : "",
11230
11250
  className
11231
11251
  ),
11232
11252
  onClick: onOpen,
11233
11253
  "aria-label": `Open ${att.name || "image"}`,
11234
11254
  children: [
11235
- mediaUrl ? (
11236
- // eslint-disable-next-line @next/next/no-img-element -- remote chat attachment URLs are dynamic/user-supplied
11255
+ mediaUrl ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11237
11256
  /* @__PURE__ */ jsxRuntime.jsx(
11238
11257
  "img",
11239
11258
  {
11240
11259
  src: mediaUrl,
11241
11260
  alt: att.name || "Image",
11242
11261
  className: cn(
11243
- "block",
11262
+ "block transition-transform duration-300 ease-out group-hover:scale-[1.04]",
11244
11263
  fill || embeddedInBubble ? "size-full object-cover" : "max-h-[300px] w-full rounded-xl object-contain"
11245
11264
  ),
11246
11265
  loading: "lazy"
11247
11266
  }
11248
- )
11249
- ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex size-full min-h-[80px] items-center justify-center text-xs text-(--chat-muted)", children: "Image unavailable" }),
11267
+ ),
11268
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "pointer-events-none absolute inset-0 bg-black/0 transition-colors duration-200 group-hover:bg-black/10", "aria-hidden": true })
11269
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex size-full min-h-[80px] items-center justify-center text-xs text-(--chat-muted)", children: "Image unavailable" }),
11250
11270
  showImageCaption && imageCaption ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 z-10 bg-linear-to-t from-black/75 via-black/35 to-transparent px-1.5 pb-1 pt-5 text-left", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-3 text-[10px] leading-snug text-white", children: imageCaption }) }) : null
11251
11271
  ]
11252
11272
  }
@@ -11681,14 +11701,14 @@ function FileAttachmentRow({
11681
11701
  "div",
11682
11702
  {
11683
11703
  className: cn(
11684
- "group w-full transition-opacity",
11704
+ "group w-full transition-all duration-150",
11685
11705
  !stacked && "flex min-w-[220px] items-center gap-2",
11686
11706
  stacked && "py-1.5",
11687
11707
  stacked && showDivider && (onThemeBubble ? "border-t border-white/15" : "border-t border-(--chat-border)/35"),
11688
11708
  !stacked && "rounded-xl border p-2.5",
11689
- !stacked && onThemeBubble && "border-white/20 bg-white/12 text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] backdrop-blur-[2px]",
11690
- !stacked && onIncomingBubble && "border-(--chat-border)/70 bg-(--chat-elevated) text-(--chat-text) shadow-sm [.dark_&]:bg-(--chat-surface)",
11691
- !stacked && !inBubble && "border-(--chat-border) bg-(--chat-surface) shadow-sm",
11709
+ !stacked && onThemeBubble && "border-white/20 bg-white/12 text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] backdrop-blur-[2px] hover:bg-white/16",
11710
+ !stacked && onIncomingBubble && "border-(--chat-border)/70 bg-(--chat-elevated) text-(--chat-text) shadow-sm hover:border-(--chat-theme)/30 hover:shadow-md [.dark_&]:bg-(--chat-surface)",
11711
+ !stacked && !inBubble && "border-(--chat-border) bg-(--chat-surface) shadow-sm hover:border-(--chat-theme)/30 hover:shadow-md",
11692
11712
  !fileUrl && "pointer-events-none opacity-60",
11693
11713
  isCaptionActive && "opacity-90"
11694
11714
  ),
@@ -11703,7 +11723,7 @@ function FileAttachmentRow({
11703
11723
  className: "flex min-w-0 flex-1 items-center gap-2.5 text-left disabled:cursor-default",
11704
11724
  children: [
11705
11725
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(
11706
- "flex size-10 shrink-0 items-center justify-center rounded-lg text-white shadow-sm",
11726
+ "flex size-10 shrink-0 items-center justify-center rounded-lg text-white shadow-sm transition-transform duration-150 group-hover:scale-105",
11707
11727
  getAttachmentIconBoxClass(rawName)
11708
11728
  ), children: getAttachmentIcon(rawName, "file", "text-white", 18) }),
11709
11729
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
@@ -19736,11 +19756,35 @@ var packageDefaultComponents = {
19736
19756
  ChatSocketStatus: ChatSocketStatus_default
19737
19757
  };
19738
19758
 
19759
+ // src/hooks/useAdminFeatureGate.ts
19760
+ function useAdminFeatureGate(feature) {
19761
+ const ctx = useEffectiveSettingsOptional();
19762
+ switch (feature) {
19763
+ case "audioCall":
19764
+ return ctx.getAudioCallGate();
19765
+ case "videoCall":
19766
+ return ctx.getVideoCallGate();
19767
+ case "pushNotification":
19768
+ return ctx.getPushNotificationGate();
19769
+ case "blockUsers":
19770
+ return ctx.getBlockUsersGate();
19771
+ case "attachments":
19772
+ return ctx.getAttachmentsGate();
19773
+ case "groupCreation":
19774
+ return ctx.getGroupCreationGate();
19775
+ default: {
19776
+ const _exhaustive = feature;
19777
+ return _exhaustive;
19778
+ }
19779
+ }
19780
+ }
19781
+
19739
19782
  // src/index.ts
19740
19783
  init_settings_types();
19741
19784
  init_api_service();
19742
19785
  var index_default = ChatMain_default;
19743
19786
 
19787
+ exports.AdminPermissionTooltip = AdminPermissionTooltip;
19744
19788
  exports.Calendar = Calendar;
19745
19789
  exports.CalendarDayButton = CalendarDayButton;
19746
19790
  exports.Channel = Channel;
@@ -19823,6 +19867,7 @@ exports.smoothScrollToMessage = smoothScrollToMessage;
19823
19867
  exports.startOfLocalDay = startOfLocalDay;
19824
19868
  exports.toDateInputValue = toDateInputValue;
19825
19869
  exports.uniqueList = uniqueList;
19870
+ exports.useAdminFeatureGate = useAdminFeatureGate;
19826
19871
  exports.useChatAlerts = useChatAlerts;
19827
19872
  exports.useChatContext = useChatContext;
19828
19873
  exports.useChatController = useChatController;