@xipkg/calls-chat 0.0.0
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/README.md +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.mjs +325 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Основной компонент приложения
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as zustand_middleware from 'zustand/middleware';
|
|
3
|
+
import * as zustand from 'zustand';
|
|
4
|
+
|
|
5
|
+
type ChatProps = {
|
|
6
|
+
/** В компакт-режиме: классы позиционирования (как у CompactCall: top-16 bottom-4 left-4 и т.д.) */
|
|
7
|
+
compactPositionClassName?: string;
|
|
8
|
+
};
|
|
9
|
+
declare const Chat: ({ compactPositionClassName }?: ChatProps) => react_jsx_runtime.JSX.Element | null;
|
|
10
|
+
|
|
11
|
+
type ChatButtonProps = {
|
|
12
|
+
/** Дополнительные классы (например, для компактной панели: h-8 w-8 rounded-xl) */
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
15
|
+
declare const ChatButton: ({ className }: ChatButtonProps) => react_jsx_runtime.JSX.Element;
|
|
16
|
+
|
|
17
|
+
declare const useChat: () => {
|
|
18
|
+
sendChatMessage: (text: string) => void;
|
|
19
|
+
toggleChat: () => void;
|
|
20
|
+
openChat: () => void;
|
|
21
|
+
closeChat: () => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ChatMessageT = {
|
|
25
|
+
id: string;
|
|
26
|
+
text: string;
|
|
27
|
+
senderId: string;
|
|
28
|
+
senderName: string;
|
|
29
|
+
timestamp: number;
|
|
30
|
+
};
|
|
31
|
+
type useChatStoreT = {
|
|
32
|
+
isChatOpen: boolean;
|
|
33
|
+
chatMessages: ChatMessageT[];
|
|
34
|
+
unreadMessagesCount: number;
|
|
35
|
+
addChatMessage: (message: ChatMessageT) => void;
|
|
36
|
+
clearUnreadMessages: () => void;
|
|
37
|
+
updateStore: (type: keyof useChatStoreT, value: unknown) => void;
|
|
38
|
+
};
|
|
39
|
+
declare const useChatStore: zustand.UseBoundStore<Omit<zustand.StoreApi<useChatStoreT>, "persist"> & {
|
|
40
|
+
persist: {
|
|
41
|
+
setOptions: (options: Partial<zustand_middleware.PersistOptions<useChatStoreT, useChatStoreT>>) => void;
|
|
42
|
+
clearStorage: () => void;
|
|
43
|
+
rehydrate: () => Promise<void> | void;
|
|
44
|
+
hasHydrated: () => boolean;
|
|
45
|
+
onHydrate: (fn: (state: useChatStoreT) => void) => () => void;
|
|
46
|
+
onFinishHydration: (fn: (state: useChatStoreT) => void) => () => void;
|
|
47
|
+
getOptions: () => Partial<zustand_middleware.PersistOptions<useChatStoreT, useChatStoreT>>;
|
|
48
|
+
};
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
export { Chat, ChatButton, useChat, useChatStore };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { useCallback, useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { Button } from '@xipkg/button';
|
|
3
|
+
import { Textarea } from '@xipkg/textarea';
|
|
4
|
+
import { Close, Send, Chat as Chat$1 } from '@xipkg/icons';
|
|
5
|
+
import { UserProfile } from '@xipkg/userprofile';
|
|
6
|
+
import { ScrollArea } from '@xipkg/scrollarea';
|
|
7
|
+
import { Modal, ModalContent } from '@xipkg/modal';
|
|
8
|
+
import { useLiveKitDataChannel, useLiveKitDataChannelListener } from '@xipkg/calls-hooks';
|
|
9
|
+
import { useRoom, useCalls } from '@xipkg/calls-providers';
|
|
10
|
+
import { useSoundEffectsStore } from '@xipkg/calls-store';
|
|
11
|
+
import { playSound } from '@xipkg/calls-utils';
|
|
12
|
+
import { create } from 'zustand';
|
|
13
|
+
import { persist } from 'zustand/middleware';
|
|
14
|
+
import { useMediaQuery, cn } from '@xipkg/utils';
|
|
15
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
16
|
+
|
|
17
|
+
// src/ui/Chat.tsx
|
|
18
|
+
var useChatStore = create()(
|
|
19
|
+
persist(
|
|
20
|
+
(set, get) => ({
|
|
21
|
+
isChatOpen: false,
|
|
22
|
+
chatMessages: [],
|
|
23
|
+
unreadMessagesCount: 0,
|
|
24
|
+
updateStore: (type, value) => set({ [type]: value }),
|
|
25
|
+
addChatMessage: (message) => {
|
|
26
|
+
const { isChatOpen, unreadMessagesCount, chatMessages } = get();
|
|
27
|
+
const messageExists = chatMessages.some((msg) => msg.id === message.id);
|
|
28
|
+
if (messageExists) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
set((state) => ({
|
|
32
|
+
chatMessages: [...state.chatMessages, message],
|
|
33
|
+
unreadMessagesCount: isChatOpen ? unreadMessagesCount : unreadMessagesCount + 1
|
|
34
|
+
}));
|
|
35
|
+
},
|
|
36
|
+
clearUnreadMessages: () => set({ unreadMessagesCount: 0 })
|
|
37
|
+
}),
|
|
38
|
+
{
|
|
39
|
+
name: "chat-store",
|
|
40
|
+
version: 1
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// src/hooks/useChat.ts
|
|
46
|
+
var CHAT_MESSAGE_TYPE = "chat_message";
|
|
47
|
+
var useChat = () => {
|
|
48
|
+
const { sendMessage } = useLiveKitDataChannel();
|
|
49
|
+
const { addChatMessage, clearUnreadMessages, updateStore } = useChatStore();
|
|
50
|
+
const chatSoundVolume = useSoundEffectsStore((s) => s.chatMessageVolume);
|
|
51
|
+
const { room } = useRoom();
|
|
52
|
+
const getCurrentParticipantInfo = useCallback(() => {
|
|
53
|
+
if (!room?.localParticipant) {
|
|
54
|
+
return {
|
|
55
|
+
senderId: "unknown",
|
|
56
|
+
senderName: "Unknown User"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const participant = room.localParticipant;
|
|
60
|
+
try {
|
|
61
|
+
const metadata = participant.metadata;
|
|
62
|
+
if (metadata) {
|
|
63
|
+
const userInfo = JSON.parse(metadata);
|
|
64
|
+
return {
|
|
65
|
+
senderId: userInfo?.user_id || userInfo?.id || participant.identity,
|
|
66
|
+
senderName: userInfo?.display_name || userInfo?.name || userInfo?.username || participant.name || participant.identity
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.warn("\u26A0\uFE0F Failed to parse participant metadata:", error);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
senderId: participant.identity,
|
|
74
|
+
senderName: participant.name || participant.identity
|
|
75
|
+
};
|
|
76
|
+
}, [room]);
|
|
77
|
+
const handleChatMessage = useCallback(
|
|
78
|
+
(message) => {
|
|
79
|
+
if (message.type === CHAT_MESSAGE_TYPE) {
|
|
80
|
+
const payload = message.payload;
|
|
81
|
+
const currentParticipantInfo = getCurrentParticipantInfo();
|
|
82
|
+
if (payload.senderId === currentParticipantInfo.senderId) {
|
|
83
|
+
console.log("\u{1F4AC} Ignoring own message:", payload);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
console.log("\u{1F4AC} Received chat message:", payload);
|
|
87
|
+
addChatMessage(payload);
|
|
88
|
+
playSound("chatMessage", chatSoundVolume);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
[addChatMessage, getCurrentParticipantInfo, chatSoundVolume]
|
|
92
|
+
);
|
|
93
|
+
useLiveKitDataChannelListener(handleChatMessage);
|
|
94
|
+
const sendChatMessage = useCallback(
|
|
95
|
+
(text) => {
|
|
96
|
+
if (!text.trim()) return;
|
|
97
|
+
const participantInfo = getCurrentParticipantInfo();
|
|
98
|
+
const message = {
|
|
99
|
+
id: `${Date.now()}-${Math.random()}`,
|
|
100
|
+
text: text.trim(),
|
|
101
|
+
senderId: participantInfo.senderId,
|
|
102
|
+
senderName: participantInfo.senderName,
|
|
103
|
+
timestamp: Date.now()
|
|
104
|
+
};
|
|
105
|
+
console.log("\u{1F4E4} Sending chat message:", message);
|
|
106
|
+
addChatMessage(message);
|
|
107
|
+
sendMessage(CHAT_MESSAGE_TYPE, message);
|
|
108
|
+
},
|
|
109
|
+
[sendMessage, getCurrentParticipantInfo, addChatMessage]
|
|
110
|
+
);
|
|
111
|
+
const toggleChat = useCallback(() => {
|
|
112
|
+
updateStore("isChatOpen", !useChatStore.getState().isChatOpen);
|
|
113
|
+
clearUnreadMessages();
|
|
114
|
+
}, [updateStore, clearUnreadMessages]);
|
|
115
|
+
const openChat = useCallback(() => {
|
|
116
|
+
updateStore("isChatOpen", true);
|
|
117
|
+
clearUnreadMessages();
|
|
118
|
+
}, [updateStore, clearUnreadMessages]);
|
|
119
|
+
const closeChat = useCallback(() => {
|
|
120
|
+
updateStore("isChatOpen", false);
|
|
121
|
+
}, [updateStore]);
|
|
122
|
+
return {
|
|
123
|
+
sendChatMessage,
|
|
124
|
+
toggleChat,
|
|
125
|
+
openChat,
|
|
126
|
+
closeChat
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
var parseLinks = (message) => {
|
|
130
|
+
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
|
131
|
+
const messageParts = message.split(urlRegex);
|
|
132
|
+
return messageParts.map((messagePart, index) => {
|
|
133
|
+
if (urlRegex.test(messagePart)) {
|
|
134
|
+
return /* @__PURE__ */ jsx(
|
|
135
|
+
"a",
|
|
136
|
+
{
|
|
137
|
+
href: messagePart,
|
|
138
|
+
target: "_blank",
|
|
139
|
+
rel: "noopener noreferrer",
|
|
140
|
+
className: "text-brand-80 hover:text-brand-100 cursor-pointer underline",
|
|
141
|
+
onClick: (e) => e.stopPropagation(),
|
|
142
|
+
"data-umami-event": "outbound-link-click",
|
|
143
|
+
"data-umami-event-url": messagePart,
|
|
144
|
+
"data-umami-event-source": "chat",
|
|
145
|
+
children: messagePart
|
|
146
|
+
},
|
|
147
|
+
index
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return messagePart;
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
var Chat = ({ compactPositionClassName } = {}) => {
|
|
154
|
+
const [messageText, setMessageText] = useState("");
|
|
155
|
+
const messagesEndRef = useRef(null);
|
|
156
|
+
const { sendChatMessage, closeChat } = useChat();
|
|
157
|
+
const { chatMessages, isChatOpen } = useChatStore();
|
|
158
|
+
const { data: currentUser } = useCalls().auth.useCurrentUser();
|
|
159
|
+
const scrollToBottom = (behavior = "smooth") => {
|
|
160
|
+
messagesEndRef.current?.scrollIntoView({ behavior });
|
|
161
|
+
};
|
|
162
|
+
const textareaRef = useRef(null);
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (isChatOpen) {
|
|
165
|
+
requestAnimationFrame(() => {
|
|
166
|
+
scrollToBottom("auto");
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}, [isChatOpen]);
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (isChatOpen && chatMessages.length > 0) {
|
|
172
|
+
requestAnimationFrame(() => {
|
|
173
|
+
scrollToBottom("smooth");
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}, [chatMessages.length, isChatOpen]);
|
|
177
|
+
const handleSendMessage = () => {
|
|
178
|
+
if (messageText.trim()) {
|
|
179
|
+
sendChatMessage(messageText);
|
|
180
|
+
setMessageText("");
|
|
181
|
+
requestAnimationFrame(() => {
|
|
182
|
+
scrollToBottom("smooth");
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (textareaRef.current) {
|
|
186
|
+
textareaRef.current.style.height = "auto";
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const handleKeyDownSendMessage = (e) => {
|
|
190
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
191
|
+
e.preventDefault();
|
|
192
|
+
handleSendMessage();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
const isMobile = useMediaQuery("(max-width: 639px)");
|
|
196
|
+
if (!isChatOpen) return null;
|
|
197
|
+
const chatContent = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
198
|
+
/* @__PURE__ */ jsxs("div", { className: "border-gray-20 flex items-center justify-between pr-3", children: [
|
|
199
|
+
/* @__PURE__ */ jsx("h3", { className: "text-lg font-medium text-gray-100", children: "\u0427\u0430\u0442" }),
|
|
200
|
+
/* @__PURE__ */ jsx(Button, { size: "icon", variant: "none", onClick: closeChat, children: /* @__PURE__ */ jsx(Close, { className: "h-6 w-6", "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C \u0447\u0430\u0442" }) })
|
|
201
|
+
] }),
|
|
202
|
+
/* @__PURE__ */ jsx(ScrollArea, { className: "min-h-0 flex-1 py-2 pr-3", children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
203
|
+
chatMessages.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-gray-60 text-center", children: /* @__PURE__ */ jsx("p", { children: "\u041D\u0430\u0447\u043D\u0438\u0442\u0435 \u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0432 \u0447\u0430\u0442\u0435" }) }) : chatMessages.map((message) => {
|
|
204
|
+
const isOwnMessage = Number(message.senderId) === Number(currentUser?.id);
|
|
205
|
+
return /* @__PURE__ */ jsx(
|
|
206
|
+
"div",
|
|
207
|
+
{
|
|
208
|
+
className: `flex ${isOwnMessage ? "justify-end" : "justify-start"}`,
|
|
209
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex max-w-[90%] flex-col gap-1 rounded-lg text-gray-100 select-text", children: [
|
|
210
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-row items-center gap-1 text-xs font-medium text-gray-100", children: [
|
|
211
|
+
!isOwnMessage && /* @__PURE__ */ jsx(
|
|
212
|
+
UserProfile,
|
|
213
|
+
{
|
|
214
|
+
size: "s",
|
|
215
|
+
userId: Number(message.senderId),
|
|
216
|
+
text: message.senderName,
|
|
217
|
+
src: `https://api.sovlium.ru/files/users/${message.senderId}/avatar.webp`
|
|
218
|
+
}
|
|
219
|
+
),
|
|
220
|
+
/* @__PURE__ */ jsx(
|
|
221
|
+
"div",
|
|
222
|
+
{
|
|
223
|
+
className: `text-xs-base ${isOwnMessage ? "text-brand-20 ml-auto" : "text-gray-60"}`,
|
|
224
|
+
children: new Date(message.timestamp).toLocaleTimeString("ru-RU", {
|
|
225
|
+
hour: "2-digit",
|
|
226
|
+
minute: "2-digit"
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
] }),
|
|
231
|
+
/* @__PURE__ */ jsx(
|
|
232
|
+
"div",
|
|
233
|
+
{
|
|
234
|
+
className: cn(
|
|
235
|
+
"cursor-text rounded-lg px-3 py-2 text-sm wrap-break-word whitespace-pre-wrap select-text",
|
|
236
|
+
isOwnMessage ? "bg-brand-20" : "bg-gray-5"
|
|
237
|
+
),
|
|
238
|
+
children: parseLinks(message.text)
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
] })
|
|
242
|
+
},
|
|
243
|
+
message.id
|
|
244
|
+
);
|
|
245
|
+
}),
|
|
246
|
+
/* @__PURE__ */ jsx("div", { ref: messagesEndRef })
|
|
247
|
+
] }) }),
|
|
248
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-end gap-2 pr-3", children: /* @__PURE__ */ jsxs("div", { className: "border-gray-20 flex max-h-40 w-full flex-1 items-center rounded-xl border pl-4", children: [
|
|
249
|
+
/* @__PURE__ */ jsx(
|
|
250
|
+
Textarea,
|
|
251
|
+
{
|
|
252
|
+
ref: textareaRef,
|
|
253
|
+
value: messageText,
|
|
254
|
+
onChange: (e) => setMessageText(e.target.value),
|
|
255
|
+
placeholder: "\u041D\u0430\u043F\u0438\u0448\u0438\u0442\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435...",
|
|
256
|
+
className: "my-3 max-h-32 min-w-full rounded-none border-none p-0 pr-2",
|
|
257
|
+
onKeyDown: handleKeyDownSendMessage
|
|
258
|
+
}
|
|
259
|
+
),
|
|
260
|
+
/* @__PURE__ */ jsx("div", { className: "pr-1", children: /* @__PURE__ */ jsx(
|
|
261
|
+
Button,
|
|
262
|
+
{
|
|
263
|
+
size: "icon",
|
|
264
|
+
variant: "primary",
|
|
265
|
+
onClick: handleSendMessage,
|
|
266
|
+
disabled: !messageText.trim(),
|
|
267
|
+
className: "rounded-xl p-2",
|
|
268
|
+
children: /* @__PURE__ */ jsx(
|
|
269
|
+
Send,
|
|
270
|
+
{
|
|
271
|
+
className: cn(
|
|
272
|
+
"fill-gray-0 h-6 w-6 group-hover:fill-gray-100",
|
|
273
|
+
!messageText.trim() && "fill-gray-100"
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
) })
|
|
279
|
+
] }) })
|
|
280
|
+
] });
|
|
281
|
+
if (isMobile) {
|
|
282
|
+
return /* @__PURE__ */ jsx(Modal, { open: isChatOpen, onOpenChange: (open) => !open && closeChat(), children: /* @__PURE__ */ jsx(ModalContent, { className: "border-gray-20 bg-gray-0 flex h-[85dvh] max-h-[85dvh] w-[calc(100vw-32px)] max-w-[calc(100vw-32px)] flex-col gap-0 overflow-hidden rounded-2xl border p-4 pr-1", children: /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden", children: chatContent }) }) });
|
|
283
|
+
}
|
|
284
|
+
if (compactPositionClassName) {
|
|
285
|
+
return /* @__PURE__ */ jsx(
|
|
286
|
+
"div",
|
|
287
|
+
{
|
|
288
|
+
className: cn(
|
|
289
|
+
"border-gray-20 bg-gray-0 fixed z-100 flex min-h-0 w-82 flex-col overflow-hidden rounded-2xl border p-4 pr-1 shadow-lg",
|
|
290
|
+
compactPositionClassName
|
|
291
|
+
),
|
|
292
|
+
children: chatContent
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return /* @__PURE__ */ jsx("div", { className: "bg-gray-0 border-gray-0 sm:border-gray-20 fixed flex h-full min-h-0 w-full max-w-none min-w-82 flex-col overflow-hidden rounded-2xl border p-4 pr-1 sm:relative sm:max-w-82", children: chatContent });
|
|
297
|
+
};
|
|
298
|
+
var ChatButton = ({ className }) => {
|
|
299
|
+
const { toggleChat } = useChat();
|
|
300
|
+
const { isChatOpen, unreadMessagesCount } = useChatStore();
|
|
301
|
+
return /* @__PURE__ */ jsxs(
|
|
302
|
+
Button,
|
|
303
|
+
{
|
|
304
|
+
size: "icon",
|
|
305
|
+
variant: "none",
|
|
306
|
+
onClick: toggleChat,
|
|
307
|
+
className: cn(
|
|
308
|
+
"relative m-0 p-0",
|
|
309
|
+
!className && "h-10 w-10 min-w-10 rounded-lg",
|
|
310
|
+
isChatOpen ? "bg-brand-0 text-brand-100" : "hover:bg-gray-5 text-gray-100",
|
|
311
|
+
className
|
|
312
|
+
),
|
|
313
|
+
"data-umami-event": "call-toggle-chat",
|
|
314
|
+
"data-umami-event-state": isChatOpen ? "close" : "open",
|
|
315
|
+
children: [
|
|
316
|
+
/* @__PURE__ */ jsx(Chat$1, {}),
|
|
317
|
+
unreadMessagesCount > 0 && /* @__PURE__ */ jsx("div", { className: "text-red-0 bg-brand-100 absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full text-xs font-medium", children: unreadMessagesCount > 99 ? "99+" : unreadMessagesCount })
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
export { Chat, ChatButton, useChat, useChatStore };
|
|
324
|
+
//# sourceMappingURL=index.mjs.map
|
|
325
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store/chatStore.ts","../src/hooks/useChat.ts","../src/utils/chat.tsx","../src/ui/Chat.tsx","../src/ui/ChatButton.tsx"],"names":["jsx","jsxs","Button","cn","Chat"],"mappings":"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","file":"index.mjs","sourcesContent":["import { create } from 'zustand';\nimport { persist } from 'zustand/middleware';\n\ntype ChatMessageT = {\n id: string;\n text: string;\n senderId: string;\n senderName: string;\n timestamp: number;\n};\n\nexport type useChatStoreT = {\n isChatOpen: boolean;\n chatMessages: ChatMessageT[];\n unreadMessagesCount: number;\n addChatMessage: (message: ChatMessageT) => void;\n clearUnreadMessages: () => void;\n updateStore: (type: keyof useChatStoreT, value: unknown) => void;\n};\n\nexport const useChatStore = create<useChatStoreT>()(\n persist(\n (set, get) => ({\n isChatOpen: false,\n chatMessages: [],\n unreadMessagesCount: 0,\n updateStore: (type: keyof useChatStoreT, value: unknown) => set({ [type]: value }),\n\n addChatMessage: (message: ChatMessageT) => {\n const { isChatOpen, unreadMessagesCount, chatMessages } = get();\n\n // Проверяем, нет ли уже сообщения с таким ID (дедупликация)\n const messageExists = chatMessages.some((msg) => msg.id === message.id);\n if (messageExists) {\n return;\n }\n\n set((state) => ({\n chatMessages: [...state.chatMessages, message],\n unreadMessagesCount: isChatOpen ? unreadMessagesCount : unreadMessagesCount + 1,\n }));\n },\n\n clearUnreadMessages: () => set({ unreadMessagesCount: 0 }),\n }),\n {\n name: 'chat-store',\n version: 1,\n },\n ),\n);\n","import { useCallback } from 'react';\nimport { useLiveKitDataChannel, useLiveKitDataChannelListener } from '@xipkg/calls-hooks';\nimport { useRoom } from '@xipkg/calls-providers';\nimport { useSoundEffectsStore } from '@xipkg/calls-store';\nimport { playSound } from '@xipkg/calls-utils';\nimport { useChatStore } from '../store';\n\nconst CHAT_MESSAGE_TYPE = 'chat_message';\n\ntype ChatMessagePayload = {\n id: string;\n text: string;\n senderId: string;\n senderName: string;\n timestamp: number;\n};\n\nexport const useChat = () => {\n const { sendMessage } = useLiveKitDataChannel();\n const { addChatMessage, clearUnreadMessages, updateStore } = useChatStore();\n const chatSoundVolume = useSoundEffectsStore((s) => s.chatMessageVolume);\n const { room } = useRoom();\n\n // Получаем информацию о текущем участнике из LiveKit\n const getCurrentParticipantInfo = useCallback(() => {\n if (!room?.localParticipant) {\n return {\n senderId: 'unknown',\n senderName: 'Unknown User',\n };\n }\n\n const participant = room.localParticipant;\n\n try {\n // Парсим метаданные участника\n const metadata = participant.metadata;\n if (metadata) {\n const userInfo = JSON.parse(metadata);\n return {\n senderId: userInfo?.user_id || userInfo?.id || participant.identity,\n senderName:\n userInfo?.display_name ||\n userInfo?.name ||\n userInfo?.username ||\n participant.name ||\n participant.identity,\n };\n }\n } catch (error) {\n console.warn('⚠️ Failed to parse participant metadata:', error);\n }\n\n // Fallback на стандартные поля LiveKit\n return {\n senderId: participant.identity,\n senderName: participant.name || participant.identity,\n };\n }, [room]);\n\n const handleChatMessage = useCallback(\n (message: { type: string; payload: unknown }) => {\n if (message.type === CHAT_MESSAGE_TYPE) {\n const payload = message.payload as ChatMessagePayload;\n\n // Проверяем, что это не наше собственное сообщение\n const currentParticipantInfo = getCurrentParticipantInfo();\n if (payload.senderId === currentParticipantInfo.senderId) {\n console.log('💬 Ignoring own message:', payload);\n return;\n }\n\n console.log('💬 Received chat message:', payload);\n addChatMessage(payload);\n playSound('chatMessage', chatSoundVolume);\n }\n },\n [addChatMessage, getCurrentParticipantInfo, chatSoundVolume],\n );\n\n // Слушаем сообщения чата\n useLiveKitDataChannelListener(handleChatMessage);\n\n const sendChatMessage = useCallback(\n (text: string) => {\n if (!text.trim()) return;\n\n const participantInfo = getCurrentParticipantInfo();\n const message: ChatMessagePayload = {\n id: `${Date.now()}-${Math.random()}`,\n text: text.trim(),\n senderId: participantInfo.senderId,\n senderName: participantInfo.senderName,\n timestamp: Date.now(),\n };\n\n console.log('📤 Sending chat message:', message);\n\n // Добавляем сообщение в локальный store отправителя\n addChatMessage(message);\n\n // Отправляем через DataChannel\n sendMessage(CHAT_MESSAGE_TYPE, message);\n },\n [sendMessage, getCurrentParticipantInfo, addChatMessage],\n );\n\n const toggleChat = useCallback(() => {\n updateStore('isChatOpen', !useChatStore.getState().isChatOpen);\n clearUnreadMessages();\n }, [updateStore, clearUnreadMessages]);\n\n const openChat = useCallback(() => {\n updateStore('isChatOpen', true);\n clearUnreadMessages();\n }, [updateStore, clearUnreadMessages]);\n\n const closeChat = useCallback(() => {\n updateStore('isChatOpen', false);\n }, [updateStore]);\n\n return {\n sendChatMessage,\n toggleChat,\n openChat,\n closeChat,\n };\n};\n","export const parseLinks = (message: string) => {\n const urlRegex = /(https?:\\/\\/[^\\s]+)/g;\n const messageParts = message.split(urlRegex);\n\n return messageParts.map((messagePart, index) => {\n if (urlRegex.test(messagePart)) {\n return (\n <a\n key={index}\n href={messagePart}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-brand-80 hover:text-brand-100 cursor-pointer underline\"\n onClick={(e) => e.stopPropagation()}\n data-umami-event=\"outbound-link-click\"\n data-umami-event-url={messagePart}\n data-umami-event-source=\"chat\"\n >\n {messagePart}\n </a>\n );\n }\n\n return messagePart;\n });\n};\n","import { useState, useRef, useEffect } from 'react';\nimport { Button } from '@xipkg/button';\nimport { Textarea } from '@xipkg/textarea';\nimport { Send, Close } from '@xipkg/icons';\nimport { UserProfile } from '@xipkg/userprofile';\nimport { ScrollArea } from '@xipkg/scrollarea';\nimport { Modal, ModalContent } from '@xipkg/modal';\nimport { useChat } from '../hooks';\nimport { useCalls } from '@xipkg/calls-providers';\nimport { useChatStore } from '../store';\nimport { cn, useMediaQuery } from '@xipkg/utils';\nimport { parseLinks } from '../utils/chat';\n\ntype ChatProps = {\n /** В компакт-режиме: классы позиционирования (как у CompactCall: top-16 bottom-4 left-4 и т.д.) */\n compactPositionClassName?: string;\n};\n\nexport const Chat = ({ compactPositionClassName }: ChatProps = {}) => {\n const [messageText, setMessageText] = useState('');\n const messagesEndRef = useRef<HTMLDivElement>(null);\n const { sendChatMessage, closeChat } = useChat();\n const { chatMessages, isChatOpen } = useChatStore();\n const { data: currentUser } = useCalls().auth.useCurrentUser();\n\n const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {\n messagesEndRef.current?.scrollIntoView({ behavior });\n };\n\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n useEffect(() => {\n if (isChatOpen) {\n requestAnimationFrame(() => {\n scrollToBottom('auto');\n });\n }\n }, [isChatOpen]);\n\n // Автоматическая прокрутка при получении новых сообщений\n useEffect(() => {\n if (isChatOpen && chatMessages.length > 0) {\n requestAnimationFrame(() => {\n scrollToBottom('smooth');\n });\n }\n }, [chatMessages.length, isChatOpen]);\n\n const handleSendMessage = () => {\n if (messageText.trim()) {\n sendChatMessage(messageText);\n setMessageText('');\n requestAnimationFrame(() => {\n scrollToBottom('smooth');\n });\n }\n\n if (textareaRef.current) {\n textareaRef.current.style.height = 'auto';\n }\n };\n\n const handleKeyDownSendMessage = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n // Enter без Shift - отправка сообщения\n // Shift+Enter - перенос строки (разрешаем стандартное поведение)\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleSendMessage();\n }\n // Если Shift+Enter, не предотвращаем стандартное поведение - будет перенос строки\n };\n\n const isMobile = useMediaQuery('(max-width: 639px)');\n if (!isChatOpen) return null;\n\n const chatContent = (\n <>\n {/* Заголовок */}\n <div className=\"border-gray-20 flex items-center justify-between pr-3\">\n <h3 className=\"text-lg font-medium text-gray-100\">Чат</h3>\n <Button size=\"icon\" variant=\"none\" onClick={closeChat}>\n <Close className=\"h-6 w-6\" aria-label=\"Закрыть чат\" />\n </Button>\n </div>\n\n {/* Сообщения */}\n <ScrollArea className=\"min-h-0 flex-1 py-2 pr-3\">\n <div className=\"space-y-4\">\n {chatMessages.length === 0 ? (\n <div className=\"text-gray-60 text-center\">\n <p>Начните общение в чате</p>\n </div>\n ) : (\n chatMessages.map((message) => {\n const isOwnMessage = Number(message.senderId) === Number(currentUser?.id);\n return (\n <div\n key={message.id}\n className={`flex ${isOwnMessage ? 'justify-end' : 'justify-start'}`}\n >\n <div className=\"flex max-w-[90%] flex-col gap-1 rounded-lg text-gray-100 select-text\">\n <div className=\"flex flex-row items-center gap-1 text-xs font-medium text-gray-100\">\n {!isOwnMessage && (\n <UserProfile\n size=\"s\"\n userId={Number(message.senderId)}\n text={message.senderName}\n src={`https://api.sovlium.ru/files/users/${message.senderId}/avatar.webp`}\n />\n )}\n <div\n className={`text-xs-base ${isOwnMessage ? 'text-brand-20 ml-auto' : 'text-gray-60'}`}\n >\n {new Date(message.timestamp).toLocaleTimeString('ru-RU', {\n hour: '2-digit',\n minute: '2-digit',\n })}\n </div>\n </div>\n <div\n className={cn(\n 'cursor-text rounded-lg px-3 py-2 text-sm wrap-break-word whitespace-pre-wrap select-text',\n isOwnMessage ? 'bg-brand-20' : 'bg-gray-5',\n )}\n >\n {parseLinks(message.text)}\n </div>\n </div>\n </div>\n );\n })\n )}\n <div ref={messagesEndRef} />\n </div>\n </ScrollArea>\n\n {/* Поле ввода */}\n <div className=\"flex items-end gap-2 pr-3\">\n <div className=\"border-gray-20 flex max-h-40 w-full flex-1 items-center rounded-xl border pl-4\">\n <Textarea\n ref={textareaRef}\n value={messageText}\n onChange={(e) => setMessageText(e.target.value)}\n placeholder=\"Напишите сообщение...\"\n className=\"my-3 max-h-32 min-w-full rounded-none border-none p-0 pr-2\"\n onKeyDown={handleKeyDownSendMessage}\n />\n <div className=\"pr-1\">\n <Button\n size=\"icon\"\n variant=\"primary\"\n onClick={handleSendMessage}\n disabled={!messageText.trim()}\n className=\"rounded-xl p-2\"\n >\n <Send\n className={cn(\n 'fill-gray-0 h-6 w-6 group-hover:fill-gray-100',\n !messageText.trim() && 'fill-gray-100',\n )}\n />\n </Button>\n </div>\n </div>\n </div>\n </>\n );\n\n if (isMobile) {\n return (\n <Modal open={isChatOpen} onOpenChange={(open) => !open && closeChat()}>\n <ModalContent className=\"border-gray-20 bg-gray-0 flex h-[85dvh] max-h-[85dvh] w-[calc(100vw-32px)] max-w-[calc(100vw-32px)] flex-col gap-0 overflow-hidden rounded-2xl border p-4 pr-1\">\n <div className=\"flex min-h-0 flex-1 flex-col overflow-hidden\">{chatContent}</div>\n </ModalContent>\n </Modal>\n );\n }\n\n if (compactPositionClassName) {\n return (\n <div\n className={cn(\n 'border-gray-20 bg-gray-0 fixed z-100 flex min-h-0 w-82 flex-col overflow-hidden rounded-2xl border p-4 pr-1 shadow-lg',\n compactPositionClassName,\n )}\n >\n {chatContent}\n </div>\n );\n }\n\n return (\n <div className=\"bg-gray-0 border-gray-0 sm:border-gray-20 fixed flex h-full min-h-0 w-full max-w-none min-w-82 flex-col overflow-hidden rounded-2xl border p-4 pr-1 sm:relative sm:max-w-82\">\n {chatContent}\n </div>\n );\n};\n","import { Button } from '@xipkg/button';\nimport { Chat } from '@xipkg/icons';\nimport { cn } from '@xipkg/utils';\nimport { useChat } from '../hooks/useChat';\nimport { useChatStore } from '../store/chatStore';\n\ntype ChatButtonProps = {\n /** Дополнительные классы (например, для компактной панели: h-8 w-8 rounded-xl) */\n className?: string;\n};\n\nexport const ChatButton = ({ className }: ChatButtonProps) => {\n const { toggleChat } = useChat();\n const { isChatOpen, unreadMessagesCount } = useChatStore();\n\n return (\n <Button\n size=\"icon\"\n variant=\"none\"\n onClick={toggleChat}\n className={cn(\n 'relative m-0 p-0',\n !className && 'h-10 w-10 min-w-10 rounded-lg',\n isChatOpen ? 'bg-brand-0 text-brand-100' : 'hover:bg-gray-5 text-gray-100',\n className,\n )}\n data-umami-event=\"call-toggle-chat\"\n data-umami-event-state={isChatOpen ? 'close' : 'open'}\n >\n <Chat />\n {unreadMessagesCount > 0 && (\n <div className=\"text-red-0 bg-brand-100 absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full text-xs font-medium\">\n {unreadMessagesCount > 99 ? '99+' : unreadMessagesCount}\n </div>\n )}\n </Button>\n );\n};\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xipkg/calls-chat",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"development": "./index.ts",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.mjs",
|
|
10
|
+
"default": "./dist/index.mjs"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"lint": "eslint \"**/*.{ts,tsx}\"",
|
|
16
|
+
"dev": "tsup --watch",
|
|
17
|
+
"build": "tsup"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@xipkg/scrollarea": "2.2.0",
|
|
21
|
+
"@xipkg/userprofile": "4.0.14",
|
|
22
|
+
"@xipkg/icons": "^2.7.0",
|
|
23
|
+
"@xipkg/textarea": "1.1.0",
|
|
24
|
+
"@xipkg/button": "3.2.0",
|
|
25
|
+
"@xipkg/calls-ui": "*",
|
|
26
|
+
"@xipkg/calls-utils": "*",
|
|
27
|
+
"@xipkg/calls-types": "*",
|
|
28
|
+
"@xipkg/calls-config": "*",
|
|
29
|
+
"@xipkg/calls-providers": "*",
|
|
30
|
+
"@xipkg/calls-store": "*",
|
|
31
|
+
"@xipkg/calls-hooks": "*",
|
|
32
|
+
"zustand": "5.0.3"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@tanstack/react-query-devtools": "5.73.3",
|
|
36
|
+
"@eslint/js": "^9.19.0",
|
|
37
|
+
"common.typescript": "*",
|
|
38
|
+
"@types/node": "^20.3.1",
|
|
39
|
+
"@types/react": "^19.0.2",
|
|
40
|
+
"@types/react-dom": "^19.0.2",
|
|
41
|
+
"@xipkg/eslint": "3.2.0",
|
|
42
|
+
"@xipkg/typescript": "latest",
|
|
43
|
+
"common.eslint": "*",
|
|
44
|
+
"eslint": "^9.19.0",
|
|
45
|
+
"eslint-plugin-react-hooks": "^5.0.0",
|
|
46
|
+
"eslint-plugin-react-refresh": "^0.4.18",
|
|
47
|
+
"globals": "^15.14.0",
|
|
48
|
+
"typescript": "~5.7.2",
|
|
49
|
+
"typescript-eslint": "^8.22.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"react": "19"
|
|
53
|
+
},
|
|
54
|
+
"description": "calls package",
|
|
55
|
+
"author": "xi.effect",
|
|
56
|
+
"main": "./dist/index.mjs",
|
|
57
|
+
"types": "./dist/index.d.ts",
|
|
58
|
+
"files": [
|
|
59
|
+
"dist"
|
|
60
|
+
],
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
}
|
|
64
|
+
}
|