@xipkg/calls-chat 0.1.24 → 0.1.26

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.d.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import * as react from 'react';
2
+ import { JSONContent } from '@tiptap/core';
2
3
  import * as zustand from 'zustand';
3
4
 
4
5
  type ChatProps = {
5
6
  /** В компакт-режиме: классы позиционирования (как у CompactCall: top-16 bottom-4 left-4 и т.д.) */
6
7
  compactPositionClassName?: string;
8
+ /** Встроить чат в родителя (PiP): без fixed/modal, на всю доступную высоту */
9
+ embedded?: boolean;
7
10
  };
8
- declare const Chat: ({ compactPositionClassName }?: ChatProps) => react.JSX.Element | null;
11
+ declare const Chat: ({ compactPositionClassName, embedded }?: ChatProps) => react.JSX.Element | null;
9
12
 
10
13
  type ChatButtonProps = {
11
14
  /** Дополнительные классы (например, для компактной панели: h-8 w-8 rounded-xl) */
@@ -14,7 +17,7 @@ type ChatButtonProps = {
14
17
  declare const ChatButton: ({ className }: ChatButtonProps) => react.JSX.Element;
15
18
 
16
19
  declare const useChat: () => {
17
- sendChatMessage: (text: string) => void;
20
+ sendChatMessage: (text: string, content?: JSONContent) => void;
18
21
  deleteChatMessage: (messageId: string) => void;
19
22
  toggleChat: () => void;
20
23
  openChat: () => void;
@@ -24,10 +27,13 @@ declare const useChat: () => {
24
27
  type ChatMessageT = {
25
28
  id: string;
26
29
  text: string;
30
+ /** TipTap JSON. Старые клиенты поле игнорируют и показывают `text`. */
31
+ content?: JSONContent;
27
32
  senderId: string;
28
33
  senderName: string;
29
34
  timestamp: number;
30
35
  };
36
+
31
37
  type useChatStoreT = {
32
38
  isChatOpen: boolean;
33
39
  chatMessages: ChatMessageT[];
package/dist/index.mjs CHANGED
@@ -1,7 +1,6 @@
1
- import { useCallback, useState, useRef, useEffect } from 'react';
1
+ import { useCallback, useRef, useEffect } from 'react';
2
2
  import { Button } from '@xipkg/button';
3
- import { Textarea } from '@xipkg/textarea';
4
- import { Close, Trash, Send, Chat as Chat$1 } from '@xipkg/icons';
3
+ import { Close, Trash, Chat as Chat$1, Send, Bold as Bold$1, Italic, Underline as Underline$1, Stroke } from '@xipkg/icons';
5
4
  import { UserProfile } from '@xipkg/userprofile';
6
5
  import { ScrollArea } from '@xipkg/scrollarea';
7
6
  import { Modal, ModalContent, ModalTitle } from '@xipkg/modal';
@@ -11,10 +10,87 @@ import { useRoom, useCalls } from '@xipkg/calls-providers';
11
10
  import { useSoundEffectsStore } from '@xipkg/calls-store';
12
11
  import { playSound } from '@xipkg/calls-utils';
13
12
  import { create } from 'zustand';
14
- import { useMediaQuery, cn } from '@xipkg/utils';
13
+ import { cn, useMediaQuery } from '@xipkg/utils';
14
+ import { useEditor, useEditorState, EditorContent } from '@tiptap/react';
15
+ import StarterKit from '@tiptap/starter-kit';
16
+ import { markPasteRule, markInputRule } from '@tiptap/core';
17
+ import { Bold, starPasteRegex, starInputRegex } from '@tiptap/extension-bold';
18
+ import { Underline } from '@tiptap/extension-underline';
19
+ import { TextSelection } from '@tiptap/pm/state';
20
+ import { BubbleMenu } from '@tiptap/react/menus';
15
21
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
16
22
 
17
23
  // src/ui/Chat.tsx
24
+
25
+ // src/editor/sanitizeContent.ts
26
+ var ALLOWED_NODES = /* @__PURE__ */ new Set(["doc", "paragraph", "text", "hardBreak", "codeBlock", "blockquote"]);
27
+ var ALLOWED_MARKS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "underline", "code", "link"]);
28
+ var MAX_NODES = 400;
29
+ var MAX_TEXT = 8e3;
30
+ var MAX_DEPTH = 16;
31
+ var MAX_HREF_LENGTH = 2e3;
32
+ var isSafeHref = (href) => {
33
+ if (typeof href !== "string" || href.length === 0 || href.length > MAX_HREF_LENGTH) {
34
+ return false;
35
+ }
36
+ try {
37
+ const url = new URL(href);
38
+ return url.protocol === "http:" || url.protocol === "https:";
39
+ } catch {
40
+ return false;
41
+ }
42
+ };
43
+ var sanitizeChatContent = (input) => {
44
+ if (!input || typeof input !== "object") return void 0;
45
+ let nodeCount = 0;
46
+ let textLength = 0;
47
+ const walk = (node, depth) => {
48
+ if (nodeCount > MAX_NODES || textLength > MAX_TEXT || depth > MAX_DEPTH) return null;
49
+ if (!node || typeof node !== "object") return null;
50
+ const raw = node;
51
+ if (typeof raw.type !== "string" || !ALLOWED_NODES.has(raw.type)) return null;
52
+ nodeCount += 1;
53
+ const result = { type: raw.type };
54
+ if (raw.type === "text") {
55
+ if (typeof raw.text !== "string") return null;
56
+ textLength += raw.text.length;
57
+ result.text = raw.text;
58
+ }
59
+ if (raw.type === "codeBlock" && raw.attrs && typeof raw.attrs.language === "string") {
60
+ result.attrs = { language: raw.attrs.language.slice(0, 32) };
61
+ }
62
+ if (Array.isArray(raw.marks) && raw.marks.length > 0) {
63
+ const marks = [];
64
+ for (const mark of raw.marks) {
65
+ if (!mark || typeof mark !== "object" || typeof mark.type !== "string") continue;
66
+ if (!ALLOWED_MARKS.has(mark.type)) continue;
67
+ if (mark.type === "link") {
68
+ const href = mark.attrs?.href;
69
+ if (!isSafeHref(href)) continue;
70
+ marks.push({
71
+ type: "link",
72
+ attrs: { href, target: "_blank", rel: "noopener noreferrer" }
73
+ });
74
+ continue;
75
+ }
76
+ marks.push({ type: mark.type });
77
+ }
78
+ if (marks.length > 0) result.marks = marks;
79
+ }
80
+ if (Array.isArray(raw.content)) {
81
+ const content = [];
82
+ for (const child of raw.content) {
83
+ const sanitized2 = walk(child, depth + 1);
84
+ if (sanitized2) content.push(sanitized2);
85
+ }
86
+ if (content.length > 0) result.content = content;
87
+ }
88
+ return result;
89
+ };
90
+ const sanitized = walk(input, 0);
91
+ if (!sanitized || sanitized.type !== "doc") return void 0;
92
+ return sanitized;
93
+ };
18
94
  var useChatStore = create()((set, get) => ({
19
95
  isChatOpen: false,
20
96
  chatMessages: [],
@@ -80,7 +156,10 @@ var useChat = () => {
80
156
  if (payload.senderId === currentParticipantInfo.senderId) {
81
157
  return;
82
158
  }
83
- addChatMessage(payload);
159
+ addChatMessage({
160
+ ...payload,
161
+ content: sanitizeChatContent(payload.content)
162
+ });
84
163
  playSound("chatMessage", chatSoundVolume);
85
164
  return;
86
165
  }
@@ -97,12 +176,14 @@ var useChat = () => {
97
176
  );
98
177
  useLiveKitDataChannelListener(handleChatMessage);
99
178
  const sendChatMessage = useCallback(
100
- (text) => {
179
+ (text, content) => {
101
180
  if (!text.trim()) return;
102
181
  const participantInfo = getCurrentParticipantInfo();
182
+ const sanitizedContent = sanitizeChatContent(content);
103
183
  const message = {
104
184
  id: `${Date.now()}-${Math.random()}`,
105
185
  text: text.trim(),
186
+ ...sanitizedContent ? { content: sanitizedContent } : {},
106
187
  senderId: participantInfo.senderId,
107
188
  senderName: participantInfo.senderName,
108
189
  timestamp: Date.now()
@@ -146,6 +227,346 @@ var useChat = () => {
146
227
  closeChat
147
228
  };
148
229
  };
230
+ var underlineInputRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))$/;
231
+ var underlinePasteRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))/g;
232
+ var DiscordBold = Bold.extend({
233
+ addInputRules() {
234
+ return [
235
+ markInputRule({
236
+ find: starInputRegex,
237
+ type: this.type
238
+ })
239
+ ];
240
+ },
241
+ addPasteRules() {
242
+ return [
243
+ markPasteRule({
244
+ find: starPasteRegex,
245
+ type: this.type
246
+ })
247
+ ];
248
+ }
249
+ });
250
+ var DiscordUnderline = Underline.extend({
251
+ addInputRules() {
252
+ return [
253
+ markInputRule({
254
+ find: underlineInputRegex,
255
+ type: this.type
256
+ })
257
+ ];
258
+ },
259
+ addPasteRules() {
260
+ return [
261
+ markPasteRule({
262
+ find: underlinePasteRegex,
263
+ type: this.type
264
+ })
265
+ ];
266
+ }
267
+ });
268
+
269
+ // src/editor/extensions.ts
270
+ var LINK_CLASS = "text-text-link hover:text-text-link cursor-pointer underline";
271
+ var chatExtensions = [
272
+ StarterKit.configure({
273
+ bold: false,
274
+ underline: false,
275
+ heading: false,
276
+ horizontalRule: false,
277
+ bulletList: false,
278
+ orderedList: false,
279
+ listItem: false,
280
+ listKeymap: false,
281
+ trailingNode: false,
282
+ link: {
283
+ openOnClick: false,
284
+ autolink: true,
285
+ defaultProtocol: "https",
286
+ HTMLAttributes: {
287
+ target: "_blank",
288
+ rel: "noopener noreferrer",
289
+ class: LINK_CLASS
290
+ }
291
+ }
292
+ }),
293
+ DiscordBold,
294
+ DiscordUnderline
295
+ ];
296
+ var chatInlineMarksClassName = cn(
297
+ "[&_strong]:font-bold",
298
+ "[&_em]:italic [&_em]:pr-[0.18em]",
299
+ "[&_s]:line-through",
300
+ "[&_u]:underline"
301
+ );
302
+ var chatProseClassName = cn(
303
+ "[&_.ProseMirror]:min-h-6 [&_.ProseMirror]:text-sm [&_.ProseMirror]:break-words [&_.ProseMirror]:outline-none",
304
+ "[&_.ProseMirror_p]:m-0 [&_.ProseMirror_p+p]:mt-1",
305
+ "[&_.ProseMirror_strong]:font-bold",
306
+ "[&_.ProseMirror_em]:italic [&_.ProseMirror_em]:pr-[0.18em]",
307
+ "[&_.ProseMirror_s]:line-through",
308
+ "[&_.ProseMirror_u]:underline",
309
+ "[&_.ProseMirror_code]:bg-background-page [&_.ProseMirror_code]:rounded [&_.ProseMirror_code]:px-1 [&_.ProseMirror_code]:py-0.5 [&_.ProseMirror_code]:font-mono [&_.ProseMirror_code]:text-[0.85em]",
310
+ "[&_.ProseMirror_pre]:bg-background-page [&_.ProseMirror_pre]:my-1 [&_.ProseMirror_pre]:overflow-x-auto [&_.ProseMirror_pre]:rounded-lg [&_.ProseMirror_pre]:p-2 [&_.ProseMirror_pre]:font-mono [&_.ProseMirror_pre]:text-xs [&_.ProseMirror_pre]:whitespace-pre-wrap",
311
+ "[&_.ProseMirror_blockquote]:border-border-default [&_.ProseMirror_blockquote]:text-text-secondary [&_.ProseMirror_blockquote]:my-1 [&_.ProseMirror_blockquote]:border-l-2 [&_.ProseMirror_blockquote]:pl-2",
312
+ "[&_.ProseMirror_a]:text-text-link [&_.ProseMirror_a]:cursor-pointer [&_.ProseMirror_a]:underline"
313
+ );
314
+ var chatMessageProseClassName = cn(
315
+ "text-sm wrap-break-word",
316
+ "[&_p]:m-0 [&_p+p]:mt-1",
317
+ chatInlineMarksClassName
318
+ );
319
+ var isValidTextSelection = (state) => {
320
+ const { doc, selection } = state;
321
+ if (!(selection instanceof TextSelection) || selection.empty) return false;
322
+ if (selection.from === 0 || selection.to === 0) return false;
323
+ try {
324
+ const $from = doc.resolve(selection.from);
325
+ const $to = doc.resolve(selection.to);
326
+ return $from.parent.isTextblock && $to.parent.isTextblock;
327
+ } catch {
328
+ return false;
329
+ }
330
+ };
331
+ var FormatButton = ({ editor, type, isActive, ariaLabel, children }) => {
332
+ return /* @__PURE__ */ jsx(
333
+ Button,
334
+ {
335
+ type: "button",
336
+ variant: "none",
337
+ "aria-label": ariaLabel,
338
+ "aria-pressed": isActive,
339
+ className: cn(
340
+ "[&_svg]:fill-icon-primary h-6 w-6 rounded-sm p-1",
341
+ isActive && "bg-status-info-background [&_svg]:fill-icon-brand"
342
+ ),
343
+ onMouseDown: (event) => {
344
+ event.preventDefault();
345
+ editor.chain().focus().toggleMark(type).run();
346
+ },
347
+ children
348
+ }
349
+ );
350
+ };
351
+ var ChatBubbleMenu = ({ editor }) => {
352
+ const { t } = useTranslation("calls");
353
+ const activeStates = useEditorState({
354
+ editor,
355
+ selector: ({ editor: current }) => ({
356
+ bold: current.isActive("bold"),
357
+ italic: current.isActive("italic"),
358
+ underline: current.isActive("underline"),
359
+ strike: current.isActive("strike")
360
+ })
361
+ });
362
+ return /* @__PURE__ */ jsxs(
363
+ BubbleMenu,
364
+ {
365
+ editor,
366
+ appendTo: () => editor.view.dom.ownerDocument.body,
367
+ shouldShow: ({ state }) => isValidTextSelection(state),
368
+ options: {
369
+ placement: "top",
370
+ strategy: "fixed",
371
+ offset: 8
372
+ },
373
+ className: "border-border-default bg-background-surface z-200 flex gap-1 rounded-lg border p-1 shadow-lg",
374
+ children: [
375
+ /* @__PURE__ */ jsx(
376
+ FormatButton,
377
+ {
378
+ editor,
379
+ type: "bold",
380
+ isActive: activeStates.bold,
381
+ ariaLabel: t("chat.formatBold"),
382
+ children: /* @__PURE__ */ jsx(Bold$1, {})
383
+ }
384
+ ),
385
+ /* @__PURE__ */ jsx(
386
+ FormatButton,
387
+ {
388
+ editor,
389
+ type: "italic",
390
+ isActive: activeStates.italic,
391
+ ariaLabel: t("chat.formatItalic"),
392
+ children: /* @__PURE__ */ jsx(Italic, {})
393
+ }
394
+ ),
395
+ /* @__PURE__ */ jsx(
396
+ FormatButton,
397
+ {
398
+ editor,
399
+ type: "underline",
400
+ isActive: activeStates.underline,
401
+ ariaLabel: t("chat.formatUnderline"),
402
+ children: /* @__PURE__ */ jsx(Underline$1, {})
403
+ }
404
+ ),
405
+ /* @__PURE__ */ jsx(
406
+ FormatButton,
407
+ {
408
+ editor,
409
+ type: "strike",
410
+ isActive: activeStates.strike,
411
+ ariaLabel: t("chat.formatStrike"),
412
+ children: /* @__PURE__ */ jsx(Stroke, {})
413
+ }
414
+ )
415
+ ]
416
+ }
417
+ );
418
+ };
419
+ var ChatComposer = ({ placeholder, onSend }) => {
420
+ const onSendRef = useRef(onSend);
421
+ onSendRef.current = onSend;
422
+ const editorRef = useRef(null);
423
+ const sendFromEditor = useCallback(() => {
424
+ const current = editorRef.current;
425
+ if (!current || current.isDestroyed) return;
426
+ const text = current.getText({ blockSeparator: "\n" }).trim();
427
+ if (!text) return;
428
+ onSendRef.current(text, current.getJSON());
429
+ current.commands.clearContent(true);
430
+ current.commands.focus();
431
+ }, []);
432
+ const sendFromEditorRef = useRef(sendFromEditor);
433
+ sendFromEditorRef.current = sendFromEditor;
434
+ const editor = useEditor({
435
+ immediatelyRender: false,
436
+ extensions: chatExtensions,
437
+ autofocus: true,
438
+ editorProps: {
439
+ attributes: {
440
+ class: "min-h-6 text-sm break-words outline-none",
441
+ "aria-label": placeholder
442
+ },
443
+ handleKeyDown: (_view, event) => {
444
+ if (event.key !== "Enter" || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) {
445
+ return false;
446
+ }
447
+ if (event.isComposing || event.keyCode === 229) return false;
448
+ event.preventDefault();
449
+ sendFromEditorRef.current();
450
+ return true;
451
+ }
452
+ }
453
+ });
454
+ useEffect(() => {
455
+ editorRef.current = editor;
456
+ }, [editor]);
457
+ const isEmpty = useEditorState({
458
+ editor,
459
+ selector: ({ editor: current }) => !current?.getText({ blockSeparator: "\n" }).trim()
460
+ }) ?? true;
461
+ return /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-end gap-2 pr-3", children: /* @__PURE__ */ jsxs(
462
+ "div",
463
+ {
464
+ className: cn(
465
+ "border-border-default flex max-h-40 w-full flex-1 items-center rounded-xl border pl-4",
466
+ chatProseClassName
467
+ ),
468
+ children: [
469
+ /* @__PURE__ */ jsxs("div", { className: "relative min-w-0 flex-1", children: [
470
+ isEmpty && /* @__PURE__ */ jsx("span", { className: "text-text-secondary pointer-events-none absolute top-3 left-0 text-sm", children: placeholder }),
471
+ /* @__PURE__ */ jsx(EditorContent, { editor, className: "my-3 max-h-32 overflow-y-auto pr-2" }),
472
+ editor && /* @__PURE__ */ jsx(ChatBubbleMenu, { editor })
473
+ ] }),
474
+ /* @__PURE__ */ jsx("div", { className: "pr-1", children: /* @__PURE__ */ jsx(
475
+ Button,
476
+ {
477
+ size: "icon",
478
+ variant: "primary",
479
+ onClick: sendFromEditor,
480
+ disabled: isEmpty,
481
+ className: "rounded-xl p-2",
482
+ children: /* @__PURE__ */ jsx(
483
+ Send,
484
+ {
485
+ className: cn(
486
+ "fill-action-primary-text group-hover:fill-action-primary-text h-6 w-6",
487
+ isEmpty && "fill-icon-secondary"
488
+ )
489
+ }
490
+ )
491
+ }
492
+ ) })
493
+ ]
494
+ }
495
+ ) });
496
+ };
497
+ var LINK_CLASS2 = "text-text-link hover:text-text-link cursor-pointer underline";
498
+ var wrapMarks = (text, marks) => {
499
+ return (marks ?? []).reduce((acc, mark) => {
500
+ switch (mark.type) {
501
+ case "bold":
502
+ return /* @__PURE__ */ jsx("strong", { className: "font-bold", children: acc });
503
+ case "italic":
504
+ return /* @__PURE__ */ jsx("em", { className: "pr-[0.18em] italic", children: acc });
505
+ case "strike":
506
+ return /* @__PURE__ */ jsx("s", { children: acc });
507
+ case "underline":
508
+ return /* @__PURE__ */ jsx("u", { children: acc });
509
+ case "code":
510
+ return /* @__PURE__ */ jsx("code", { className: "bg-background-page rounded px-1 py-0.5 font-mono text-[0.85em]", children: acc });
511
+ case "link": {
512
+ const href = typeof mark.attrs?.href === "string" ? mark.attrs.href : void 0;
513
+ if (!href) return acc;
514
+ return /* @__PURE__ */ jsx(
515
+ "a",
516
+ {
517
+ href,
518
+ target: "_blank",
519
+ rel: "noopener noreferrer",
520
+ className: LINK_CLASS2,
521
+ onClick: (event) => event.stopPropagation(),
522
+ "data-umami-event": "outbound-link-click",
523
+ "data-umami-event-url": href,
524
+ "data-umami-event-source": "chat",
525
+ children: acc
526
+ }
527
+ );
528
+ }
529
+ default:
530
+ return acc;
531
+ }
532
+ }, text);
533
+ };
534
+ var renderNode = (node, key) => {
535
+ const children = node.content?.map((child, index) => renderNode(child, `${key}-${index}`));
536
+ switch (node.type) {
537
+ case "doc":
538
+ return children;
539
+ case "paragraph":
540
+ return /* @__PURE__ */ jsx("p", { className: "m-0 min-h-[1em]", children: children ?? /* @__PURE__ */ jsx("br", {}) }, key);
541
+ case "blockquote":
542
+ return /* @__PURE__ */ jsx(
543
+ "blockquote",
544
+ {
545
+ className: "border-border-default text-text-secondary my-1 border-l-2 pl-2",
546
+ children
547
+ },
548
+ key
549
+ );
550
+ case "codeBlock":
551
+ return /* @__PURE__ */ jsx(
552
+ "pre",
553
+ {
554
+ className: "bg-background-page my-1 overflow-x-auto rounded-lg p-2 font-mono text-xs whitespace-pre-wrap",
555
+ children: /* @__PURE__ */ jsx("code", { children })
556
+ },
557
+ key
558
+ );
559
+ case "hardBreak":
560
+ return /* @__PURE__ */ jsx("br", {}, key);
561
+ case "text":
562
+ return /* @__PURE__ */ jsx("span", { children: wrapMarks(node.text ?? "", node.marks) }, key);
563
+ default:
564
+ return children;
565
+ }
566
+ };
567
+ var renderChatJsonContent = (content) => {
568
+ return renderNode(content, "chat-doc");
569
+ };
149
570
  var parseLinks = (message) => {
150
571
  const urlRegex = /(https?:\/\/[^\s]+)/g;
151
572
  const messageParts = message.split(urlRegex);
@@ -170,9 +591,11 @@ var parseLinks = (message) => {
170
591
  return messagePart;
171
592
  });
172
593
  };
173
- var Chat = ({ compactPositionClassName } = {}) => {
594
+ var ChatMessageContent = ({ text, content, className }) => {
595
+ return /* @__PURE__ */ jsx("div", { className: cn(chatMessageProseClassName, className), children: content ? renderChatJsonContent(content) : parseLinks(text) });
596
+ };
597
+ var Chat = ({ compactPositionClassName, embedded = false } = {}) => {
174
598
  const { t, i18n } = useTranslation("calls");
175
- const [messageText, setMessageText] = useState("");
176
599
  const messagesEndRef = useRef(null);
177
600
  const { sendChatMessage, deleteChatMessage, closeChat } = useChat();
178
601
  const { chatMessages, isChatOpen } = useChatStore();
@@ -181,7 +604,6 @@ var Chat = ({ compactPositionClassName } = {}) => {
181
604
  const scrollToBottom = (behavior = "smooth") => {
182
605
  messagesEndRef.current?.scrollIntoView({ behavior });
183
606
  };
184
- const textareaRef = useRef(null);
185
607
  useEffect(() => {
186
608
  if (isChatOpen) {
187
609
  requestAnimationFrame(() => {
@@ -196,32 +618,21 @@ var Chat = ({ compactPositionClassName } = {}) => {
196
618
  });
197
619
  }
198
620
  }, [chatMessages.length, isChatOpen]);
199
- const handleSendMessage = () => {
200
- if (messageText.trim()) {
201
- sendChatMessage(messageText);
202
- setMessageText("");
203
- requestAnimationFrame(() => {
204
- scrollToBottom("smooth");
205
- });
206
- }
207
- if (textareaRef.current) {
208
- textareaRef.current.style.height = "auto";
209
- }
210
- };
211
- const handleKeyDownSendMessage = (e) => {
212
- if (e.key === "Enter" && !e.shiftKey) {
213
- e.preventDefault();
214
- handleSendMessage();
215
- }
621
+ const handleSendMessage = (text, content) => {
622
+ sendChatMessage(text, content);
623
+ requestAnimationFrame(() => {
624
+ scrollToBottom("smooth");
625
+ });
216
626
  };
217
627
  const isMobile = useMediaQuery("(max-width: 639px)");
218
628
  if (!isChatOpen) return null;
629
+ const chatPanelClassName = "border-border-default bg-background-surface flex min-h-0 flex-col overflow-hidden rounded-2xl border p-4 pr-1";
219
630
  const chatContent = /* @__PURE__ */ jsxs(Fragment, { children: [
220
- /* @__PURE__ */ jsxs("div", { className: "border-border-default flex items-center justify-between pr-3", children: [
631
+ /* @__PURE__ */ jsxs("div", { className: "border-border-default flex shrink-0 items-center justify-between pr-3", children: [
221
632
  /* @__PURE__ */ jsx("h3", { className: "text-text-primary text-lg font-medium", children: t("chat.title") }),
222
633
  /* @__PURE__ */ jsx(Button, { size: "icon", variant: "none", onClick: closeChat, children: /* @__PURE__ */ jsx(Close, { className: "h-6 w-6", "aria-label": t("chat.close") }) })
223
634
  ] }),
224
- /* @__PURE__ */ jsx(ScrollArea, { className: "min-h-0 flex-1 py-2 pr-3", children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
635
+ /* @__PURE__ */ jsx(ScrollArea, { className: "h-full min-h-0 flex-1 py-2 pr-3", children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
225
636
  chatMessages.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-text-secondary text-center", children: /* @__PURE__ */ jsx("p", { children: t("chat.empty") }) }) : chatMessages.map((message) => {
226
637
  const isOwnMessage = Number(message.senderId) === Number(currentUser?.id);
227
638
  return /* @__PURE__ */ jsx(
@@ -252,13 +663,14 @@ var Chat = ({ compactPositionClassName } = {}) => {
252
663
  ] }),
253
664
  /* @__PURE__ */ jsxs("div", { className: "relative", children: [
254
665
  /* @__PURE__ */ jsx(
255
- "div",
666
+ ChatMessageContent,
256
667
  {
668
+ text: message.text,
669
+ content: message.content,
257
670
  className: cn(
258
- "cursor-text rounded-lg px-3 py-2 text-sm wrap-break-word whitespace-pre-wrap select-text",
671
+ "cursor-text rounded-lg px-3 py-2 select-text",
259
672
  isOwnMessage ? "bg-action-primary-background-disabled" : "bg-background-page"
260
- ),
261
- children: parseLinks(message.text)
673
+ )
262
674
  }
263
675
  ),
264
676
  isOwnMessage && /* @__PURE__ */ jsx(
@@ -267,7 +679,7 @@ var Chat = ({ compactPositionClassName } = {}) => {
267
679
  type: "button",
268
680
  size: "icon",
269
681
  variant: "none",
270
- className: "bg-background-surface border-border-default absolute -top-2 -left-2 h-7 w-7 rounded-full border p-1 opacity-100 shadow-sm sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100",
682
+ className: "bg-background-surface border-border-default absolute -top-2 -left-2 h-7 w-7 rounded-full border p-1 opacity-100 shadow-sm sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100",
271
683
  onClick: () => deleteChatMessage(message.id),
272
684
  "aria-label": t("chat.deleteAria"),
273
685
  children: /* @__PURE__ */ jsx(Trash, { className: "fill-icon-secondary h-4 w-4" })
@@ -281,39 +693,11 @@ var Chat = ({ compactPositionClassName } = {}) => {
281
693
  }),
282
694
  /* @__PURE__ */ jsx("div", { ref: messagesEndRef })
283
695
  ] }) }),
284
- /* @__PURE__ */ jsx("div", { className: "flex items-end gap-2 pr-3", children: /* @__PURE__ */ jsxs("div", { className: "border-border-default flex max-h-40 w-full flex-1 items-center rounded-xl border pl-4", children: [
285
- /* @__PURE__ */ jsx(
286
- Textarea,
287
- {
288
- ref: textareaRef,
289
- value: messageText,
290
- onChange: (e) => setMessageText(e.target.value),
291
- placeholder: t("chat.placeholder"),
292
- className: "my-3 max-h-32 min-w-full rounded-none border-none p-0 pr-2",
293
- onKeyDown: handleKeyDownSendMessage
294
- }
295
- ),
296
- /* @__PURE__ */ jsx("div", { className: "pr-1", children: /* @__PURE__ */ jsx(
297
- Button,
298
- {
299
- size: "icon",
300
- variant: "primary",
301
- onClick: handleSendMessage,
302
- disabled: !messageText.trim(),
303
- className: "rounded-xl p-2",
304
- children: /* @__PURE__ */ jsx(
305
- Send,
306
- {
307
- className: cn(
308
- "fill-action-primary-text group-hover:fill-action-primary-text h-6 w-6",
309
- !messageText.trim() && "fill-icon-secondary"
310
- )
311
- }
312
- )
313
- }
314
- ) })
315
- ] }) })
696
+ /* @__PURE__ */ jsx(ChatComposer, { placeholder: t("chat.placeholder"), onSend: handleSendMessage })
316
697
  ] });
698
+ if (embedded) {
699
+ return /* @__PURE__ */ jsx("div", { className: cn(chatPanelClassName, "h-full min-h-0 w-full"), children: chatContent });
700
+ }
317
701
  if (isMobile) {
318
702
  return /* @__PURE__ */ jsx(Modal, { open: isChatOpen, onOpenChange: (open) => !open && closeChat(), children: /* @__PURE__ */ jsxs(
319
703
  ModalContent,
@@ -1 +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';\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 removeChatMessage: (messageId: string) => void;\n clearUnreadMessages: () => void;\n updateStore: (type: keyof useChatStoreT, value: unknown) => void;\n};\n\n// Чат передаётся только через LiveKit DataChannel (P2P/broadcast в рамках текущего звонка),\n// серверной истории сообщений нет. Раньше стор персистился в localStorage без привязки\n// к конкретному звонку — сообщения предыдущего звонка «утекали» в следующий на том же\n// устройстве (и только у того пользователя, у кого localStorage не был очищен штатным\n// disconnect-флоу). Стор намеренно НЕ персистится: чат живёт только в памяти на время\n// звонка и одинаково пуст у всех при входе в новый звонок.\nexport const useChatStore = create<useChatStoreT>()((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 removeChatMessage: (messageId: string) => {\n set((state) => ({\n chatMessages: state.chatMessages.filter((msg) => msg.id !== messageId),\n }));\n },\n\n clearUnreadMessages: () => set({ unreadMessagesCount: 0 }),\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';\nconst CHAT_MESSAGE_DELETE_TYPE = 'chat_message_delete';\n\ntype ChatMessagePayload = {\n id: string;\n text: string;\n senderId: string;\n senderName: string;\n timestamp: number;\n};\n\ntype ChatMessageDeletePayload = {\n id: string;\n senderId: string;\n};\n\nexport const useChat = () => {\n const { sendMessage } = useLiveKitDataChannel();\n const { addChatMessage, removeChatMessage, 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 return;\n }\n\n addChatMessage(payload);\n playSound('chatMessage', chatSoundVolume);\n return;\n }\n\n if (message.type === CHAT_MESSAGE_DELETE_TYPE) {\n const payload = message.payload as ChatMessageDeletePayload;\n if (!payload?.id || !payload?.senderId) return;\n\n // Удаляем только если автор удаления совпадает с автором сообщения —\n // чужой клиент не может удалить чужие сообщения через поддельный payload.\n const existing = useChatStore.getState().chatMessages.find((msg) => msg.id === payload.id);\n if (!existing) return;\n if (String(existing.senderId) !== String(payload.senderId)) return;\n\n removeChatMessage(payload.id);\n }\n },\n [addChatMessage, removeChatMessage, 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 // Добавляем сообщение в локальный store отправителя\n addChatMessage(message);\n\n // Отправляем через DataChannel\n sendMessage(CHAT_MESSAGE_TYPE, message);\n },\n [sendMessage, getCurrentParticipantInfo, addChatMessage],\n );\n\n const deleteChatMessage = useCallback(\n (messageId: string) => {\n const existing = useChatStore.getState().chatMessages.find((msg) => msg.id === messageId);\n if (!existing) return;\n\n const participantInfo = getCurrentParticipantInfo();\n if (String(existing.senderId) !== String(participantInfo.senderId)) return;\n\n const payload: ChatMessageDeletePayload = {\n id: messageId,\n senderId: participantInfo.senderId,\n };\n\n // Optimistic: сразу убираем у себя, затем broadcast для остальных\n removeChatMessage(messageId);\n sendMessage(CHAT_MESSAGE_DELETE_TYPE, payload);\n },\n [sendMessage, getCurrentParticipantInfo, removeChatMessage],\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 deleteChatMessage,\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-text-link hover:text-text-link 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, Trash } from '@xipkg/icons';\nimport { UserProfile } from '@xipkg/userprofile';\nimport { ScrollArea } from '@xipkg/scrollarea';\nimport { Modal, ModalContent, ModalTitle } from '@xipkg/modal';\nimport { useTranslation } from 'react-i18next';\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 { t, i18n } = useTranslation('calls');\n const [messageText, setMessageText] = useState('');\n const messagesEndRef = useRef<HTMLDivElement>(null);\n const { sendChatMessage, deleteChatMessage, closeChat } = useChat();\n const { chatMessages, isChatOpen } = useChatStore();\n const { data: currentUser } = useCalls().auth.useCurrentUser();\n const timeLocale = i18n.language?.startsWith('ru')\n ? 'ru-RU'\n : i18n.language?.startsWith('en')\n ? 'en-US'\n : i18n.language;\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-border-default flex items-center justify-between pr-3\">\n <h3 className=\"text-text-primary text-lg font-medium\">{t('chat.title')}</h3>\n <Button size=\"icon\" variant=\"none\" onClick={closeChat}>\n <Close className=\"h-6 w-6\" aria-label={t('chat.close')} />\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-text-secondary text-center\">\n <p>{t('chat.empty')}</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={`group flex ${isOwnMessage ? 'justify-end' : 'justify-start'}`}\n >\n <div className=\"text-text-primary flex max-w-[90%] flex-col gap-1 rounded-lg select-text\">\n <div className=\"text-text-primary flex flex-row items-center gap-1 text-xs font-medium\">\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-text-disabled ml-auto' : 'text-text-secondary'}`}\n >\n {new Date(message.timestamp).toLocaleTimeString(timeLocale, {\n hour: '2-digit',\n minute: '2-digit',\n })}\n </div>\n </div>\n <div className=\"relative\">\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\n ? 'bg-action-primary-background-disabled'\n : 'bg-background-page',\n )}\n >\n {parseLinks(message.text)}\n </div>\n {isOwnMessage && (\n <Button\n type=\"button\"\n size=\"icon\"\n variant=\"none\"\n className=\"bg-background-surface border-border-default absolute -top-2 -left-2 h-7 w-7 rounded-full border p-1 opacity-100 shadow-sm sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100\"\n onClick={() => deleteChatMessage(message.id)}\n aria-label={t('chat.deleteAria')}\n >\n <Trash className=\"fill-icon-secondary h-4 w-4\" />\n </Button>\n )}\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-border-default 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={t('chat.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-action-primary-text group-hover:fill-action-primary-text h-6 w-6',\n !messageText.trim() && 'fill-icon-secondary',\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\n className=\"border-border-default bg-background-surface 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 aria-describedby={undefined}\n >\n <ModalTitle className=\"sr-only\">{t('chat.title')}</ModalTitle>\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-border-default bg-background-surface fixed z-100 flex min-h-0 w-[328px] 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-background-surface border-border-default sm:border-border-default fixed flex h-full min-h-0 w-full max-w-none min-w-[328px] flex-col overflow-hidden rounded-2xl border p-4 pr-1 sm:relative sm:max-w-[328px]\">\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 'bg-background-surface hover:bg-background-page text-text-primary relative m-0 rounded-xl p-0',\n !className && 'h-10 w-10 min-w-10',\n isChatOpen && 'text-text-link',\n className,\n )}\n data-umami-event=\"call-toggle-chat\"\n data-umami-event-state={isChatOpen ? 'close' : 'open'}\n >\n <Chat className={cn('h-6 w-6', isChatOpen ? 'fill-icon-brand' : 'fill-icon-primary')} />\n {unreadMessagesCount > 0 && (\n <div className=\"bg-action-primary-background-default text-action-primary-text absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center rounded-full px-1 text-xs font-medium\">\n {unreadMessagesCount > 99 ? '99+' : unreadMessagesCount}\n </div>\n )}\n </Button>\n );\n};\n"]}
1
+ {"version":3,"sources":["../src/editor/sanitizeContent.ts","../src/store/chatStore.ts","../src/hooks/useChat.ts","../src/editor/discordMarks.ts","../src/editor/extensions.ts","../src/editor/proseClassName.ts","../src/ui/ChatBubbleMenu.tsx","../src/ui/ChatComposer.tsx","../src/editor/renderJsonContent.tsx","../src/utils/chat.tsx","../src/ui/ChatMessageContent.tsx","../src/ui/Chat.tsx","../src/ui/ChatButton.tsx"],"names":["sanitized","cn","Bold","UnderlineIcon","useCallback","useEditorState","jsx","jsxs","Button","LINK_CLASS","useTranslation","useRef","useEffect","Chat"],"mappings":"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","file":"index.mjs","sourcesContent":["import type { JSONContent } from '@tiptap/core';\n\nconst ALLOWED_NODES = new Set(['doc', 'paragraph', 'text', 'hardBreak', 'codeBlock', 'blockquote']);\n\nconst ALLOWED_MARKS = new Set(['bold', 'italic', 'strike', 'underline', 'code', 'link']);\n\nconst MAX_NODES = 400;\nconst MAX_TEXT = 8000;\nconst MAX_DEPTH = 16;\nconst MAX_HREF_LENGTH = 2000;\n\nconst isSafeHref = (href: unknown): href is string => {\n if (typeof href !== 'string' || href.length === 0 || href.length > MAX_HREF_LENGTH) {\n return false;\n }\n\n try {\n const url = new URL(href);\n return url.protocol === 'http:' || url.protocol === 'https:';\n } catch {\n return false;\n }\n};\n\nexport const sanitizeChatContent = (input: unknown): JSONContent | undefined => {\n if (!input || typeof input !== 'object') return undefined;\n\n let nodeCount = 0;\n let textLength = 0;\n\n const walk = (node: unknown, depth: number): JSONContent | null => {\n if (nodeCount > MAX_NODES || textLength > MAX_TEXT || depth > MAX_DEPTH) return null;\n if (!node || typeof node !== 'object') return null;\n\n const raw = node as JSONContent;\n if (typeof raw.type !== 'string' || !ALLOWED_NODES.has(raw.type)) return null;\n\n nodeCount += 1;\n const result: JSONContent = { type: raw.type };\n\n if (raw.type === 'text') {\n if (typeof raw.text !== 'string') return null;\n textLength += raw.text.length;\n result.text = raw.text;\n }\n\n if (raw.type === 'codeBlock' && raw.attrs && typeof raw.attrs.language === 'string') {\n result.attrs = { language: raw.attrs.language.slice(0, 32) };\n }\n\n if (Array.isArray(raw.marks) && raw.marks.length > 0) {\n const marks: NonNullable<JSONContent['marks']> = [];\n\n for (const mark of raw.marks) {\n if (!mark || typeof mark !== 'object' || typeof mark.type !== 'string') continue;\n if (!ALLOWED_MARKS.has(mark.type)) continue;\n\n if (mark.type === 'link') {\n const href = mark.attrs?.href;\n if (!isSafeHref(href)) continue;\n marks.push({\n type: 'link',\n attrs: { href, target: '_blank', rel: 'noopener noreferrer' },\n });\n continue;\n }\n\n marks.push({ type: mark.type });\n }\n\n if (marks.length > 0) result.marks = marks;\n }\n\n if (Array.isArray(raw.content)) {\n const content: JSONContent[] = [];\n for (const child of raw.content) {\n const sanitized = walk(child, depth + 1);\n if (sanitized) content.push(sanitized);\n }\n if (content.length > 0) result.content = content;\n }\n\n return result;\n };\n\n const sanitized = walk(input, 0);\n if (!sanitized || sanitized.type !== 'doc') return undefined;\n return sanitized;\n};\n","import { create } from 'zustand';\nimport type { ChatMessageT } from '../types';\n\nexport type { ChatMessageT };\n\nexport type useChatStoreT = {\n isChatOpen: boolean;\n chatMessages: ChatMessageT[];\n unreadMessagesCount: number;\n addChatMessage: (message: ChatMessageT) => void;\n removeChatMessage: (messageId: string) => void;\n clearUnreadMessages: () => void;\n updateStore: (type: keyof useChatStoreT, value: unknown) => void;\n};\n\n// Чат передаётся только через LiveKit DataChannel (P2P/broadcast в рамках текущего звонка),\n// серверной истории сообщений нет. Раньше стор персистился в localStorage без привязки\n// к конкретному звонку — сообщения предыдущего звонка «утекали» в следующий на том же\n// устройстве (и только у того пользователя, у кого localStorage не был очищен штатным\n// disconnect-флоу). Стор намеренно НЕ персистится: чат живёт только в памяти на время\n// звонка и одинаково пуст у всех при входе в новый звонок.\nexport const useChatStore = create<useChatStoreT>()((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 removeChatMessage: (messageId: string) => {\n set((state) => ({\n chatMessages: state.chatMessages.filter((msg) => msg.id !== messageId),\n }));\n },\n\n clearUnreadMessages: () => set({ unreadMessagesCount: 0 }),\n}));\n","import { useCallback } from 'react';\nimport type { JSONContent } from '@tiptap/core';\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 { sanitizeChatContent } from '../editor/sanitizeContent';\nimport { useChatStore } from '../store';\nimport type { ChatMessageT } from '../types';\n\nconst CHAT_MESSAGE_TYPE = 'chat_message';\nconst CHAT_MESSAGE_DELETE_TYPE = 'chat_message_delete';\n\ntype ChatMessagePayload = ChatMessageT;\n\ntype ChatMessageDeletePayload = {\n id: string;\n senderId: string;\n};\n\nexport const useChat = () => {\n const { sendMessage } = useLiveKitDataChannel();\n const { addChatMessage, removeChatMessage, 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 return;\n }\n\n addChatMessage({\n ...payload,\n content: sanitizeChatContent(payload.content),\n });\n playSound('chatMessage', chatSoundVolume);\n return;\n }\n\n if (message.type === CHAT_MESSAGE_DELETE_TYPE) {\n const payload = message.payload as ChatMessageDeletePayload;\n if (!payload?.id || !payload?.senderId) return;\n\n // Удаляем только если автор удаления совпадает с автором сообщения —\n // чужой клиент не может удалить чужие сообщения через поддельный payload.\n const existing = useChatStore.getState().chatMessages.find((msg) => msg.id === payload.id);\n if (!existing) return;\n if (String(existing.senderId) !== String(payload.senderId)) return;\n\n removeChatMessage(payload.id);\n }\n },\n [addChatMessage, removeChatMessage, getCurrentParticipantInfo, chatSoundVolume],\n );\n\n // Слушаем сообщения чата\n useLiveKitDataChannelListener(handleChatMessage);\n\n const sendChatMessage = useCallback(\n (text: string, content?: JSONContent) => {\n if (!text.trim()) return;\n\n const participantInfo = getCurrentParticipantInfo();\n const sanitizedContent = sanitizeChatContent(content);\n const message: ChatMessagePayload = {\n id: `${Date.now()}-${Math.random()}`,\n text: text.trim(),\n ...(sanitizedContent ? { content: sanitizedContent } : {}),\n senderId: participantInfo.senderId,\n senderName: participantInfo.senderName,\n timestamp: Date.now(),\n };\n\n // Добавляем сообщение в локальный store отправителя\n addChatMessage(message);\n\n // Отправляем через DataChannel\n sendMessage(CHAT_MESSAGE_TYPE, message);\n },\n [sendMessage, getCurrentParticipantInfo, addChatMessage],\n );\n\n const deleteChatMessage = useCallback(\n (messageId: string) => {\n const existing = useChatStore.getState().chatMessages.find((msg) => msg.id === messageId);\n if (!existing) return;\n\n const participantInfo = getCurrentParticipantInfo();\n if (String(existing.senderId) !== String(participantInfo.senderId)) return;\n\n const payload: ChatMessageDeletePayload = {\n id: messageId,\n senderId: participantInfo.senderId,\n };\n\n // Optimistic: сразу убираем у себя, затем broadcast для остальных\n removeChatMessage(messageId);\n sendMessage(CHAT_MESSAGE_DELETE_TYPE, payload);\n },\n [sendMessage, getCurrentParticipantInfo, removeChatMessage],\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 deleteChatMessage,\n toggleChat,\n openChat,\n closeChat,\n };\n};\n","import { markInputRule, markPasteRule } from '@tiptap/core';\nimport { Bold, starInputRegex, starPasteRegex } from '@tiptap/extension-bold';\nimport { Underline } from '@tiptap/extension-underline';\n\n/**\n * Discord: `**жирный**`, `__подчёркнутый__`.\n * Стандартный markdown / StarterKit мапит `__` на bold — переназначаем.\n */\nconst underlineInputRegex = /(?:^|\\s)(__(?!\\s+__)((?:[^_]+))__(?!\\s+__))$/;\nconst underlinePasteRegex = /(?:^|\\s)(__(?!\\s+__)((?:[^_]+))__(?!\\s+__))/g;\n\nexport const DiscordBold = Bold.extend({\n addInputRules() {\n return [\n markInputRule({\n find: starInputRegex,\n type: this.type,\n }),\n ];\n },\n addPasteRules() {\n return [\n markPasteRule({\n find: starPasteRegex,\n type: this.type,\n }),\n ];\n },\n});\n\nexport const DiscordUnderline = Underline.extend({\n addInputRules() {\n return [\n markInputRule({\n find: underlineInputRegex,\n type: this.type,\n }),\n ];\n },\n addPasteRules() {\n return [\n markPasteRule({\n find: underlinePasteRegex,\n type: this.type,\n }),\n ];\n },\n});\n","import StarterKit from '@tiptap/starter-kit';\nimport { DiscordBold, DiscordUnderline } from './discordMarks';\n\nconst LINK_CLASS = 'text-text-link hover:text-text-link cursor-pointer underline';\n\n/** Схема чата: Discord-шорткаты + тот же набор марок, что у текста на доске. */\nexport const chatExtensions = [\n StarterKit.configure({\n bold: false,\n underline: false,\n heading: false,\n horizontalRule: false,\n bulletList: false,\n orderedList: false,\n listItem: false,\n listKeymap: false,\n trailingNode: false,\n link: {\n openOnClick: false,\n autolink: true,\n defaultProtocol: 'https',\n HTMLAttributes: {\n target: '_blank',\n rel: 'noopener noreferrer',\n class: LINK_CLASS,\n },\n },\n }),\n DiscordBold,\n DiscordUnderline,\n];\n","import { cn } from '@xipkg/utils';\n\n/**\n * Жирный — 700, не 600: при синтезе начертания (нет файла Inter Bold) 600 почти неотличим от 400.\n * Курсив — запас справа: наклон глифа вылезает из метрики и сталкивается со следующим символом.\n */\nconst chatInlineMarksClassName = cn(\n '[&_strong]:font-bold',\n '[&_em]:italic [&_em]:pr-[0.18em]',\n '[&_s]:line-through',\n '[&_u]:underline',\n);\n\n/** Общие стили ProseMirror для композера и пузырьков сообщений. */\nexport const chatProseClassName = cn(\n '[&_.ProseMirror]:min-h-6 [&_.ProseMirror]:text-sm [&_.ProseMirror]:break-words [&_.ProseMirror]:outline-none',\n '[&_.ProseMirror_p]:m-0 [&_.ProseMirror_p+p]:mt-1',\n '[&_.ProseMirror_strong]:font-bold',\n '[&_.ProseMirror_em]:italic [&_.ProseMirror_em]:pr-[0.18em]',\n '[&_.ProseMirror_s]:line-through',\n '[&_.ProseMirror_u]:underline',\n '[&_.ProseMirror_code]:bg-background-page [&_.ProseMirror_code]:rounded [&_.ProseMirror_code]:px-1 [&_.ProseMirror_code]:py-0.5 [&_.ProseMirror_code]:font-mono [&_.ProseMirror_code]:text-[0.85em]',\n '[&_.ProseMirror_pre]:bg-background-page [&_.ProseMirror_pre]:my-1 [&_.ProseMirror_pre]:overflow-x-auto [&_.ProseMirror_pre]:rounded-lg [&_.ProseMirror_pre]:p-2 [&_.ProseMirror_pre]:font-mono [&_.ProseMirror_pre]:text-xs [&_.ProseMirror_pre]:whitespace-pre-wrap',\n '[&_.ProseMirror_blockquote]:border-border-default [&_.ProseMirror_blockquote]:text-text-secondary [&_.ProseMirror_blockquote]:my-1 [&_.ProseMirror_blockquote]:border-l-2 [&_.ProseMirror_blockquote]:pl-2',\n '[&_.ProseMirror_a]:text-text-link [&_.ProseMirror_a]:cursor-pointer [&_.ProseMirror_a]:underline',\n);\n\nexport const chatMessageProseClassName = cn(\n 'text-sm wrap-break-word',\n '[&_p]:m-0 [&_p+p]:mt-1',\n chatInlineMarksClassName,\n);\n","import type { Editor } from '@tiptap/core';\nimport { TextSelection } from '@tiptap/pm/state';\nimport type { EditorState } from '@tiptap/pm/state';\nimport { BubbleMenu } from '@tiptap/react/menus';\nimport { useEditorState } from '@tiptap/react';\nimport { Bold, Italic, Stroke, Underline as UnderlineIcon } from '@xipkg/icons';\nimport { Button } from '@xipkg/button';\nimport { cn } from '@xipkg/utils';\nimport { useTranslation } from 'react-i18next';\nimport type { ReactNode } from 'react';\n\ntype FormatTypeT = 'bold' | 'italic' | 'underline' | 'strike';\n\ntype ChatBubbleMenuProps = {\n editor: Editor;\n};\n\nconst isValidTextSelection = (state: EditorState): boolean => {\n const { doc, selection } = state;\n if (!(selection instanceof TextSelection) || selection.empty) return false;\n if (selection.from === 0 || selection.to === 0) return false;\n\n try {\n const $from = doc.resolve(selection.from);\n const $to = doc.resolve(selection.to);\n return $from.parent.isTextblock && $to.parent.isTextblock;\n } catch {\n return false;\n }\n};\n\ntype FormatButtonProps = {\n editor: Editor;\n type: FormatTypeT;\n isActive: boolean;\n ariaLabel: string;\n children: ReactNode;\n};\n\nconst FormatButton = ({ editor, type, isActive, ariaLabel, children }: FormatButtonProps) => {\n return (\n <Button\n type=\"button\"\n variant=\"none\"\n aria-label={ariaLabel}\n aria-pressed={isActive}\n className={cn(\n '[&_svg]:fill-icon-primary h-6 w-6 rounded-sm p-1',\n isActive && 'bg-status-info-background [&_svg]:fill-icon-brand',\n )}\n onMouseDown={(event) => {\n event.preventDefault();\n editor.chain().focus().toggleMark(type).run();\n }}\n >\n {children}\n </Button>\n );\n};\n\nexport const ChatBubbleMenu = ({ editor }: ChatBubbleMenuProps) => {\n const { t } = useTranslation('calls');\n const activeStates = useEditorState({\n editor,\n selector: ({ editor: current }) => ({\n bold: current.isActive('bold'),\n italic: current.isActive('italic'),\n underline: current.isActive('underline'),\n strike: current.isActive('strike'),\n }),\n });\n\n return (\n <BubbleMenu\n editor={editor}\n appendTo={() => editor.view.dom.ownerDocument.body}\n shouldShow={({ state }) => isValidTextSelection(state)}\n options={{\n placement: 'top',\n strategy: 'fixed',\n offset: 8,\n }}\n className=\"border-border-default bg-background-surface z-200 flex gap-1 rounded-lg border p-1 shadow-lg\"\n >\n <FormatButton\n editor={editor}\n type=\"bold\"\n isActive={activeStates.bold}\n ariaLabel={t('chat.formatBold')}\n >\n <Bold />\n </FormatButton>\n <FormatButton\n editor={editor}\n type=\"italic\"\n isActive={activeStates.italic}\n ariaLabel={t('chat.formatItalic')}\n >\n <Italic />\n </FormatButton>\n <FormatButton\n editor={editor}\n type=\"underline\"\n isActive={activeStates.underline}\n ariaLabel={t('chat.formatUnderline')}\n >\n <UnderlineIcon />\n </FormatButton>\n <FormatButton\n editor={editor}\n type=\"strike\"\n isActive={activeStates.strike}\n ariaLabel={t('chat.formatStrike')}\n >\n <Stroke />\n </FormatButton>\n </BubbleMenu>\n );\n};\n","import { useCallback, useEffect, useRef } from 'react';\nimport type { Editor, JSONContent } from '@tiptap/core';\nimport { EditorContent, useEditor, useEditorState } from '@tiptap/react';\nimport { Button } from '@xipkg/button';\nimport { Send } from '@xipkg/icons';\nimport { cn } from '@xipkg/utils';\nimport { chatExtensions } from '../editor/extensions';\nimport { chatProseClassName } from '../editor/proseClassName';\nimport { ChatBubbleMenu } from './ChatBubbleMenu';\n\ntype ChatComposerProps = {\n placeholder: string;\n onSend: (text: string, content: JSONContent) => void;\n};\n\nexport const ChatComposer = ({ placeholder, onSend }: ChatComposerProps) => {\n const onSendRef = useRef(onSend);\n onSendRef.current = onSend;\n\n const editorRef = useRef<Editor | null>(null);\n\n const sendFromEditor = useCallback(() => {\n const current = editorRef.current;\n if (!current || current.isDestroyed) return;\n\n const text = current.getText({ blockSeparator: '\\n' }).trim();\n if (!text) return;\n\n onSendRef.current(text, current.getJSON());\n current.commands.clearContent(true);\n current.commands.focus();\n }, []);\n\n const sendFromEditorRef = useRef(sendFromEditor);\n sendFromEditorRef.current = sendFromEditor;\n\n const editor = useEditor({\n immediatelyRender: false,\n extensions: chatExtensions,\n autofocus: true,\n editorProps: {\n attributes: {\n class: 'min-h-6 text-sm break-words outline-none',\n 'aria-label': placeholder,\n },\n handleKeyDown: (_view, event) => {\n if (\n event.key !== 'Enter' ||\n event.shiftKey ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey\n ) {\n return false;\n }\n if (event.isComposing || event.keyCode === 229) return false;\n\n event.preventDefault();\n sendFromEditorRef.current();\n return true;\n },\n },\n });\n\n useEffect(() => {\n editorRef.current = editor;\n }, [editor]);\n\n const isEmpty =\n useEditorState({\n editor,\n selector: ({ editor: current }) => !current?.getText({ blockSeparator: '\\n' }).trim(),\n }) ?? true;\n\n return (\n <div className=\"flex shrink-0 items-end gap-2 pr-3\">\n <div\n className={cn(\n 'border-border-default flex max-h-40 w-full flex-1 items-center rounded-xl border pl-4',\n chatProseClassName,\n )}\n >\n <div className=\"relative min-w-0 flex-1\">\n {isEmpty && (\n <span className=\"text-text-secondary pointer-events-none absolute top-3 left-0 text-sm\">\n {placeholder}\n </span>\n )}\n <EditorContent editor={editor} className=\"my-3 max-h-32 overflow-y-auto pr-2\" />\n {editor && <ChatBubbleMenu editor={editor} />}\n </div>\n <div className=\"pr-1\">\n <Button\n size=\"icon\"\n variant=\"primary\"\n onClick={sendFromEditor}\n disabled={isEmpty}\n className=\"rounded-xl p-2\"\n >\n <Send\n className={cn(\n 'fill-action-primary-text group-hover:fill-action-primary-text h-6 w-6',\n isEmpty && 'fill-icon-secondary',\n )}\n />\n </Button>\n </div>\n </div>\n </div>\n );\n};\n","import type { JSONContent } from '@tiptap/core';\nimport type { ReactNode } from 'react';\n\nconst LINK_CLASS = 'text-text-link hover:text-text-link cursor-pointer underline';\n\nconst wrapMarks = (text: string, marks: JSONContent['marks']): ReactNode => {\n return (marks ?? []).reduce<ReactNode>((acc, mark) => {\n switch (mark.type) {\n case 'bold':\n return <strong className=\"font-bold\">{acc}</strong>;\n case 'italic':\n return <em className=\"pr-[0.18em] italic\">{acc}</em>;\n case 'strike':\n return <s>{acc}</s>;\n case 'underline':\n return <u>{acc}</u>;\n case 'code':\n return (\n <code className=\"bg-background-page rounded px-1 py-0.5 font-mono text-[0.85em]\">\n {acc}\n </code>\n );\n case 'link': {\n const href = typeof mark.attrs?.href === 'string' ? mark.attrs.href : undefined;\n if (!href) return acc;\n return (\n <a\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={LINK_CLASS}\n onClick={(event) => event.stopPropagation()}\n data-umami-event=\"outbound-link-click\"\n data-umami-event-url={href}\n data-umami-event-source=\"chat\"\n >\n {acc}\n </a>\n );\n }\n default:\n return acc;\n }\n }, text);\n};\n\nconst renderNode = (node: JSONContent, key: string): ReactNode => {\n const children = node.content?.map((child, index) => renderNode(child, `${key}-${index}`));\n\n switch (node.type) {\n case 'doc':\n return children;\n case 'paragraph':\n return (\n <p key={key} className=\"m-0 min-h-[1em]\">\n {children ?? <br />}\n </p>\n );\n case 'blockquote':\n return (\n <blockquote\n key={key}\n className=\"border-border-default text-text-secondary my-1 border-l-2 pl-2\"\n >\n {children}\n </blockquote>\n );\n case 'codeBlock':\n return (\n <pre\n key={key}\n className=\"bg-background-page my-1 overflow-x-auto rounded-lg p-2 font-mono text-xs whitespace-pre-wrap\"\n >\n <code>{children}</code>\n </pre>\n );\n case 'hardBreak':\n return <br key={key} />;\n case 'text':\n return <span key={key}>{wrapMarks(node.text ?? '', node.marks)}</span>;\n default:\n return children;\n }\n};\n\nexport const renderChatJsonContent = (content: JSONContent): ReactNode => {\n return renderNode(content, 'chat-doc');\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-text-link hover:text-text-link 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 type { JSONContent } from '@tiptap/core';\nimport { cn } from '@xipkg/utils';\nimport { renderChatJsonContent } from '../editor/renderJsonContent';\nimport { chatMessageProseClassName } from '../editor/proseClassName';\nimport { parseLinks } from '../utils/chat';\n\ntype ChatMessageContentProps = {\n text: string;\n content?: JSONContent;\n className?: string;\n};\n\nexport const ChatMessageContent = ({ text, content, className }: ChatMessageContentProps) => {\n return (\n <div className={cn(chatMessageProseClassName, className)}>\n {content ? renderChatJsonContent(content) : parseLinks(text)}\n </div>\n );\n};\n","import { useRef, useEffect } from 'react';\nimport type { JSONContent } from '@tiptap/core';\nimport { Button } from '@xipkg/button';\nimport { Close, Trash } from '@xipkg/icons';\nimport { UserProfile } from '@xipkg/userprofile';\nimport { ScrollArea } from '@xipkg/scrollarea';\nimport { Modal, ModalContent, ModalTitle } from '@xipkg/modal';\nimport { useTranslation } from 'react-i18next';\nimport { useChat } from '../hooks';\nimport { useCalls } from '@xipkg/calls-providers';\nimport { useChatStore } from '../store';\nimport { cn, useMediaQuery } from '@xipkg/utils';\nimport { ChatComposer } from './ChatComposer';\nimport { ChatMessageContent } from './ChatMessageContent';\n\ntype ChatProps = {\n /** В компакт-режиме: классы позиционирования (как у CompactCall: top-16 bottom-4 left-4 и т.д.) */\n compactPositionClassName?: string;\n /** Встроить чат в родителя (PiP): без fixed/modal, на всю доступную высоту */\n embedded?: boolean;\n};\n\nexport const Chat = ({ compactPositionClassName, embedded = false }: ChatProps = {}) => {\n const { t, i18n } = useTranslation('calls');\n const messagesEndRef = useRef<HTMLDivElement>(null);\n const { sendChatMessage, deleteChatMessage, closeChat } = useChat();\n const { chatMessages, isChatOpen } = useChatStore();\n const { data: currentUser } = useCalls().auth.useCurrentUser();\n const timeLocale = i18n.language?.startsWith('ru')\n ? 'ru-RU'\n : i18n.language?.startsWith('en')\n ? 'en-US'\n : i18n.language;\n\n const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {\n messagesEndRef.current?.scrollIntoView({ behavior });\n };\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 = (text: string, content?: JSONContent) => {\n sendChatMessage(text, content);\n requestAnimationFrame(() => {\n scrollToBottom('smooth');\n });\n };\n\n const isMobile = useMediaQuery('(max-width: 639px)');\n if (!isChatOpen) return null;\n\n const chatPanelClassName =\n 'border-border-default bg-background-surface flex min-h-0 flex-col overflow-hidden rounded-2xl border p-4 pr-1';\n\n const chatContent = (\n <>\n {/* Заголовок */}\n <div className=\"border-border-default flex shrink-0 items-center justify-between pr-3\">\n <h3 className=\"text-text-primary text-lg font-medium\">{t('chat.title')}</h3>\n <Button size=\"icon\" variant=\"none\" onClick={closeChat}>\n <Close className=\"h-6 w-6\" aria-label={t('chat.close')} />\n </Button>\n </div>\n\n {/* Сообщения */}\n <ScrollArea className=\"h-full min-h-0 flex-1 py-2 pr-3\">\n <div className=\"space-y-4\">\n {chatMessages.length === 0 ? (\n <div className=\"text-text-secondary text-center\">\n <p>{t('chat.empty')}</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={`group flex ${isOwnMessage ? 'justify-end' : 'justify-start'}`}\n >\n <div className=\"text-text-primary flex max-w-[90%] flex-col gap-1 rounded-lg select-text\">\n <div className=\"text-text-primary flex flex-row items-center gap-1 text-xs font-medium\">\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-text-disabled ml-auto' : 'text-text-secondary'}`}\n >\n {new Date(message.timestamp).toLocaleTimeString(timeLocale, {\n hour: '2-digit',\n minute: '2-digit',\n })}\n </div>\n </div>\n <div className=\"relative\">\n <ChatMessageContent\n text={message.text}\n content={message.content}\n className={cn(\n 'cursor-text rounded-lg px-3 py-2 select-text',\n isOwnMessage\n ? 'bg-action-primary-background-disabled'\n : 'bg-background-page',\n )}\n />\n {isOwnMessage && (\n <Button\n type=\"button\"\n size=\"icon\"\n variant=\"none\"\n className=\"bg-background-surface border-border-default absolute -top-2 -left-2 h-7 w-7 rounded-full border p-1 opacity-100 shadow-sm sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100\"\n onClick={() => deleteChatMessage(message.id)}\n aria-label={t('chat.deleteAria')}\n >\n <Trash className=\"fill-icon-secondary h-4 w-4\" />\n </Button>\n )}\n </div>\n </div>\n </div>\n );\n })\n )}\n <div ref={messagesEndRef} />\n </div>\n </ScrollArea>\n\n <ChatComposer placeholder={t('chat.placeholder')} onSend={handleSendMessage} />\n </>\n );\n\n if (embedded) {\n return <div className={cn(chatPanelClassName, 'h-full min-h-0 w-full')}>{chatContent}</div>;\n }\n\n if (isMobile) {\n return (\n <Modal open={isChatOpen} onOpenChange={(open) => !open && closeChat()}>\n <ModalContent\n className=\"border-border-default bg-background-surface 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 aria-describedby={undefined}\n >\n <ModalTitle className=\"sr-only\">{t('chat.title')}</ModalTitle>\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-border-default bg-background-surface fixed z-100 flex min-h-0 w-[328px] 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-background-surface border-border-default sm:border-border-default fixed flex h-full min-h-0 w-full max-w-none min-w-[328px] flex-col overflow-hidden rounded-2xl border p-4 pr-1 sm:relative sm:max-w-[328px]\">\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 'bg-background-surface hover:bg-background-page text-text-primary relative m-0 rounded-xl p-0',\n !className && 'h-10 w-10 min-w-10',\n isChatOpen && 'text-text-link',\n className,\n )}\n data-umami-event=\"call-toggle-chat\"\n data-umami-event-state={isChatOpen ? 'close' : 'open'}\n >\n <Chat className={cn('h-6 w-6', isChatOpen ? 'fill-icon-brand' : 'fill-icon-primary')} />\n {unreadMessagesCount > 0 && (\n <div className=\"bg-action-primary-background-default text-action-primary-text absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center rounded-full px-1 text-xs font-medium\">\n {unreadMessagesCount > 99 ? '99+' : unreadMessagesCount}\n </div>\n )}\n </Button>\n );\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xipkg/calls-chat",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -12,31 +12,38 @@
12
12
  },
13
13
  "license": "MIT",
14
14
  "dependencies": {
15
+ "@tiptap/core": "3.30.1",
16
+ "@tiptap/extension-bold": "3.30.1",
17
+ "@tiptap/extension-bubble-menu": "3.30.1",
18
+ "@tiptap/extension-underline": "3.30.1",
19
+ "@tiptap/pm": "3.30.1",
20
+ "@tiptap/react": "3.30.1",
21
+ "@tiptap/starter-kit": "3.30.1",
22
+ "@xipkg/button": "4.3.0",
23
+ "@xipkg/calls-config": "^0.1.26",
24
+ "@xipkg/calls-hooks": "^0.1.26",
25
+ "@xipkg/calls-providers": "^0.1.26",
26
+ "@xipkg/calls-store": "^0.1.26",
27
+ "@xipkg/calls-types": "^0.1.26",
28
+ "@xipkg/calls-ui": "^0.1.26",
29
+ "@xipkg/calls-utils": "^0.1.26",
30
+ "@xipkg/icons": "^3.1.0",
31
+ "@xipkg/modal": "4.6.0",
15
32
  "@xipkg/scrollarea": "2.3.0",
16
33
  "@xipkg/userprofile": "4.2.0",
17
- "@xipkg/icons": "^3.1.0",
18
- "@xipkg/textarea": "1.3.0",
19
- "@xipkg/button": "4.3.0",
20
- "@xipkg/calls-ui": "^0.1.24",
21
- "@xipkg/calls-utils": "^0.1.24",
22
- "@xipkg/calls-types": "^0.1.24",
23
- "@xipkg/calls-config": "^0.1.24",
24
- "@xipkg/calls-providers": "^0.1.24",
25
- "@xipkg/calls-store": "^0.1.24",
26
- "@xipkg/calls-hooks": "^0.1.24",
27
- "zustand": "5.0.14",
28
- "@xipkg/utils": "^1.8.0"
34
+ "@xipkg/utils": "^1.8.0",
35
+ "zustand": "5.0.14"
29
36
  },
30
37
  "devDependencies": {
31
- "@tanstack/react-query-devtools": "5.101.4",
32
38
  "@eslint/js": "^9.19.0",
33
- "common.typescript": "*",
39
+ "@tanstack/react-query-devtools": "5.101.4",
34
40
  "@types/node": "^20.3.1",
35
41
  "@types/react": "^19.2.17",
36
42
  "@types/react-dom": "^19.0.2",
37
43
  "@xipkg/eslint": "4.6.0",
38
44
  "@xipkg/typescript": "0.2.0",
39
45
  "common.eslint": "*",
46
+ "common.typescript": "*",
40
47
  "eslint": "^9.19.0",
41
48
  "eslint-plugin-react-hooks": "^5.0.0",
42
49
  "eslint-plugin-react-refresh": "^0.4.18",