@hamzasaleemorg/convex-comments 1.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/CHANGELOG.md +17 -0
- package/LICENSE +201 -0
- package/README.md +581 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/index.d.ts +745 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +579 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +44 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +673 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/lib.d.ts +17 -0
- package/dist/component/lib.d.ts.map +1 -0
- package/dist/component/lib.js +18 -0
- package/dist/component/lib.js.map +1 -0
- package/dist/component/messages.d.ts +173 -0
- package/dist/component/messages.d.ts.map +1 -0
- package/dist/component/messages.js +410 -0
- package/dist/component/messages.js.map +1 -0
- package/dist/component/reactions.d.ts +51 -0
- package/dist/component/reactions.d.ts.map +1 -0
- package/dist/component/reactions.js +191 -0
- package/dist/component/reactions.js.map +1 -0
- package/dist/component/schema.d.ts +274 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +159 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/threads.d.ts +110 -0
- package/dist/component/threads.d.ts.map +1 -0
- package/dist/component/threads.js +276 -0
- package/dist/component/threads.js.map +1 -0
- package/dist/component/typing.d.ts +31 -0
- package/dist/component/typing.d.ts.map +1 -0
- package/dist/component/typing.js +147 -0
- package/dist/component/typing.js.map +1 -0
- package/dist/component/zones.d.ts +63 -0
- package/dist/component/zones.d.ts.map +1 -0
- package/dist/component/zones.js +159 -0
- package/dist/component/zones.js.map +1 -0
- package/dist/react/AddComment.d.ts +57 -0
- package/dist/react/AddComment.d.ts.map +1 -0
- package/dist/react/AddComment.js +285 -0
- package/dist/react/AddComment.js.map +1 -0
- package/dist/react/Comment.d.ts +70 -0
- package/dist/react/Comment.d.ts.map +1 -0
- package/dist/react/Comment.js +259 -0
- package/dist/react/Comment.js.map +1 -0
- package/dist/react/Comments.d.ts +74 -0
- package/dist/react/Comments.d.ts.map +1 -0
- package/dist/react/Comments.js +108 -0
- package/dist/react/Comments.js.map +1 -0
- package/dist/react/CommentsProvider.d.ts +104 -0
- package/dist/react/CommentsProvider.d.ts.map +1 -0
- package/dist/react/CommentsProvider.js +98 -0
- package/dist/react/CommentsProvider.js.map +1 -0
- package/dist/react/ReactionPicker.d.ts +28 -0
- package/dist/react/ReactionPicker.d.ts.map +1 -0
- package/dist/react/ReactionPicker.js +56 -0
- package/dist/react/ReactionPicker.js.map +1 -0
- package/dist/react/Thread.d.ts +84 -0
- package/dist/react/Thread.d.ts.map +1 -0
- package/dist/react/Thread.js +124 -0
- package/dist/react/Thread.js.map +1 -0
- package/dist/react/TypingIndicator.d.ts +25 -0
- package/dist/react/TypingIndicator.d.ts.map +1 -0
- package/dist/react/TypingIndicator.js +99 -0
- package/dist/react/TypingIndicator.js.map +1 -0
- package/dist/react/index.d.ts +15 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +15 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +106 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/index.ts +813 -0
- package/src/component/_generated/api.ts +60 -0
- package/src/component/_generated/component.ts +784 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/lib.ts +57 -0
- package/src/component/messages.ts +476 -0
- package/src/component/reactions.ts +222 -0
- package/src/component/schema.ts +169 -0
- package/src/component/threads.ts +319 -0
- package/src/component/typing.ts +168 -0
- package/src/component/zones.ts +180 -0
- package/src/react/AddComment.tsx +463 -0
- package/src/react/Comment.tsx +519 -0
- package/src/react/Comments.tsx +276 -0
- package/src/react/CommentsProvider.tsx +197 -0
- package/src/react/ReactionPicker.tsx +95 -0
- package/src/react/Thread.tsx +336 -0
- package/src/react/TypingIndicator.tsx +144 -0
- package/src/react/index.ts +45 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread Component
|
|
3
|
+
*
|
|
4
|
+
* Displays a single thread with all its messages.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useEffect, useRef, type ReactNode } from "react";
|
|
8
|
+
import { useComments } from "./CommentsProvider";
|
|
9
|
+
import { Comment, type MessageData } from "./Comment";
|
|
10
|
+
import { AddComment } from "./AddComment";
|
|
11
|
+
import { TypingIndicator } from "./TypingIndicator";
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
export interface ThreadInfo {
|
|
18
|
+
_id: string;
|
|
19
|
+
zoneId: string;
|
|
20
|
+
resolved: boolean;
|
|
21
|
+
resolvedBy?: string;
|
|
22
|
+
resolvedAt?: number;
|
|
23
|
+
createdAt: number;
|
|
24
|
+
lastActivityAt: number;
|
|
25
|
+
position?: { x: number; y: number; anchor?: string };
|
|
26
|
+
metadata?: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ThreadProps {
|
|
30
|
+
/** Thread data */
|
|
31
|
+
thread: ThreadInfo;
|
|
32
|
+
/** Messages in this thread */
|
|
33
|
+
messages: MessageData[];
|
|
34
|
+
/** Typing users in this thread */
|
|
35
|
+
typingUsers?: Array<{ userId: string; updatedAt: number }>;
|
|
36
|
+
/** Whether there are more messages to load */
|
|
37
|
+
hasMore?: boolean;
|
|
38
|
+
/** Loading state */
|
|
39
|
+
isLoading?: boolean;
|
|
40
|
+
/** Callback when load more is triggered */
|
|
41
|
+
onLoadMore?: () => void;
|
|
42
|
+
/** Callback when a new message is submitted */
|
|
43
|
+
onSubmit?: (body: string, attachments?: Array<{ type: "url" | "file" | "image"; url: string; name?: string }>) => void;
|
|
44
|
+
/** Callback when typing state changes */
|
|
45
|
+
onTypingChange?: (isTyping: boolean) => void;
|
|
46
|
+
/** Callback when a reaction is toggled */
|
|
47
|
+
onToggleReaction?: (messageId: string, emoji: string) => void;
|
|
48
|
+
/** Callback when a message is edited */
|
|
49
|
+
onEditMessage?: (messageId: string, newBody: string) => void;
|
|
50
|
+
/** Callback when a message is deleted */
|
|
51
|
+
onDeleteMessage?: (messageId: string) => void;
|
|
52
|
+
/** Callback to resolve the thread */
|
|
53
|
+
onResolve?: () => void;
|
|
54
|
+
/** Callback to unresolve the thread */
|
|
55
|
+
onUnresolve?: () => void;
|
|
56
|
+
/** Callback to close/go back */
|
|
57
|
+
onClose?: () => void;
|
|
58
|
+
/** Whether editing is allowed */
|
|
59
|
+
allowEditing?: boolean;
|
|
60
|
+
/** Whether to auto-scroll to bottom on new messages */
|
|
61
|
+
autoScroll?: boolean;
|
|
62
|
+
/** Custom message renderer */
|
|
63
|
+
renderMessage?: (message: MessageData, mine: boolean) => ReactNode;
|
|
64
|
+
/** CSS class name */
|
|
65
|
+
className?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Component
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Component to display a single thread with its messages.
|
|
74
|
+
*
|
|
75
|
+
* Usage:
|
|
76
|
+
* ```tsx
|
|
77
|
+
* <Thread
|
|
78
|
+
* thread={thread}
|
|
79
|
+
* messages={messages}
|
|
80
|
+
* typingUsers={typingUsers}
|
|
81
|
+
* onSubmit={(body) => addComment({ threadId: thread._id, body })}
|
|
82
|
+
* onTypingChange={(isTyping) => setIsTyping({ threadId: thread._id, isTyping })}
|
|
83
|
+
* onToggleReaction={(messageId, emoji) => toggleReaction({ messageId, emoji })}
|
|
84
|
+
* />
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function Thread({
|
|
88
|
+
thread,
|
|
89
|
+
messages,
|
|
90
|
+
typingUsers = [],
|
|
91
|
+
hasMore = false,
|
|
92
|
+
isLoading = false,
|
|
93
|
+
onLoadMore,
|
|
94
|
+
onSubmit,
|
|
95
|
+
onTypingChange,
|
|
96
|
+
onToggleReaction,
|
|
97
|
+
onEditMessage,
|
|
98
|
+
onDeleteMessage,
|
|
99
|
+
onResolve,
|
|
100
|
+
onUnresolve,
|
|
101
|
+
onClose,
|
|
102
|
+
allowEditing = true,
|
|
103
|
+
autoScroll = true,
|
|
104
|
+
renderMessage,
|
|
105
|
+
className = "",
|
|
106
|
+
}: ThreadProps) {
|
|
107
|
+
const { userId, styles } = useComments();
|
|
108
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
109
|
+
const prevMessageCount = useRef(messages.length);
|
|
110
|
+
|
|
111
|
+
// Auto-scroll to bottom on new messages
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (autoScroll && messages.length > prevMessageCount.current) {
|
|
114
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
115
|
+
}
|
|
116
|
+
prevMessageCount.current = messages.length;
|
|
117
|
+
}, [messages.length, autoScroll]);
|
|
118
|
+
|
|
119
|
+
const handleSubmit = (body: string, attachments?: Array<{ type: "url" | "file" | "image"; url: string; name?: string }>) => {
|
|
120
|
+
onSubmit?.(body, attachments);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div
|
|
125
|
+
className={`thread-container ${className}`}
|
|
126
|
+
style={{
|
|
127
|
+
fontFamily: styles?.fontFamily ?? "system-ui, -apple-system, sans-serif",
|
|
128
|
+
display: "flex",
|
|
129
|
+
flexDirection: "column",
|
|
130
|
+
height: "100%",
|
|
131
|
+
border: "1px solid #e5e7eb",
|
|
132
|
+
borderRadius: styles?.borderRadius ?? "8px",
|
|
133
|
+
overflow: "hidden",
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{/* Header */}
|
|
137
|
+
<div
|
|
138
|
+
style={{
|
|
139
|
+
padding: "12px 16px",
|
|
140
|
+
borderBottom: "1px solid #e5e7eb",
|
|
141
|
+
display: "flex",
|
|
142
|
+
justifyContent: "space-between",
|
|
143
|
+
alignItems: "center",
|
|
144
|
+
backgroundColor: thread.resolved ? "#f0fdf4" : "#f9fafb",
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
|
148
|
+
{onClose && (
|
|
149
|
+
<button
|
|
150
|
+
onClick={onClose}
|
|
151
|
+
style={{
|
|
152
|
+
padding: "4px 8px",
|
|
153
|
+
border: "none",
|
|
154
|
+
background: "transparent",
|
|
155
|
+
cursor: "pointer",
|
|
156
|
+
fontSize: "16px",
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
←
|
|
160
|
+
</button>
|
|
161
|
+
)}
|
|
162
|
+
<span style={{ fontWeight: 600, fontSize: "14px" }}>
|
|
163
|
+
Thread
|
|
164
|
+
{thread.resolved && (
|
|
165
|
+
<span style={{ color: "#16a34a", marginLeft: "8px", fontWeight: 400 }}>
|
|
166
|
+
✓ Resolved
|
|
167
|
+
</span>
|
|
168
|
+
)}
|
|
169
|
+
</span>
|
|
170
|
+
</div>
|
|
171
|
+
<div style={{ display: "flex", gap: "8px" }}>
|
|
172
|
+
{thread.resolved && onUnresolve && (
|
|
173
|
+
<button
|
|
174
|
+
onClick={onUnresolve}
|
|
175
|
+
style={{
|
|
176
|
+
padding: "6px 12px",
|
|
177
|
+
fontSize: "13px",
|
|
178
|
+
border: "1px solid #d1d5db",
|
|
179
|
+
borderRadius: "6px",
|
|
180
|
+
background: "white",
|
|
181
|
+
cursor: "pointer",
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
Reopen
|
|
185
|
+
</button>
|
|
186
|
+
)}
|
|
187
|
+
{!thread.resolved && onResolve && userId && (
|
|
188
|
+
<button
|
|
189
|
+
onClick={onResolve}
|
|
190
|
+
style={{
|
|
191
|
+
padding: "6px 12px",
|
|
192
|
+
fontSize: "13px",
|
|
193
|
+
fontWeight: 500,
|
|
194
|
+
border: "none",
|
|
195
|
+
borderRadius: "6px",
|
|
196
|
+
backgroundColor: "#16a34a",
|
|
197
|
+
color: "white",
|
|
198
|
+
cursor: "pointer",
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
Resolve
|
|
202
|
+
</button>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{/* Messages */}
|
|
208
|
+
<div
|
|
209
|
+
style={{
|
|
210
|
+
flex: 1,
|
|
211
|
+
overflowY: "auto",
|
|
212
|
+
padding: "16px",
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
{/* Load more button */}
|
|
216
|
+
{hasMore && (
|
|
217
|
+
<div style={{ textAlign: "center", marginBottom: "16px" }}>
|
|
218
|
+
<button
|
|
219
|
+
onClick={onLoadMore}
|
|
220
|
+
disabled={isLoading}
|
|
221
|
+
style={{
|
|
222
|
+
padding: "8px 16px",
|
|
223
|
+
fontSize: "13px",
|
|
224
|
+
border: "1px solid #d1d5db",
|
|
225
|
+
borderRadius: "6px",
|
|
226
|
+
background: "white",
|
|
227
|
+
cursor: isLoading ? "not-allowed" : "pointer",
|
|
228
|
+
opacity: isLoading ? 0.6 : 1,
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
{isLoading ? "Loading..." : "Load Earlier Messages"}
|
|
232
|
+
</button>
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
{/* Message list */}
|
|
237
|
+
{messages.length === 0 ? (
|
|
238
|
+
<div
|
|
239
|
+
style={{
|
|
240
|
+
textAlign: "center",
|
|
241
|
+
color: "#6b7280",
|
|
242
|
+
fontSize: "14px",
|
|
243
|
+
padding: "40px 0",
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
No messages yet. Start the conversation!
|
|
247
|
+
</div>
|
|
248
|
+
) : (
|
|
249
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
|
|
250
|
+
{messages.map((message) => {
|
|
251
|
+
const isMine = userId === message.message.authorId;
|
|
252
|
+
return renderMessage ? (
|
|
253
|
+
renderMessage(message, isMine)
|
|
254
|
+
) : (
|
|
255
|
+
<Comment
|
|
256
|
+
key={message.message._id}
|
|
257
|
+
comment={message}
|
|
258
|
+
mine={isMine}
|
|
259
|
+
onToggleReaction={
|
|
260
|
+
onToggleReaction
|
|
261
|
+
? (emoji) => onToggleReaction(message.message._id, emoji)
|
|
262
|
+
: undefined
|
|
263
|
+
}
|
|
264
|
+
onEdit={
|
|
265
|
+
onEditMessage && isMine
|
|
266
|
+
? (newBody) => onEditMessage(message.message._id, newBody)
|
|
267
|
+
: undefined
|
|
268
|
+
}
|
|
269
|
+
onDelete={
|
|
270
|
+
onDeleteMessage && isMine
|
|
271
|
+
? () => onDeleteMessage(message.message._id)
|
|
272
|
+
: undefined
|
|
273
|
+
}
|
|
274
|
+
/>
|
|
275
|
+
);
|
|
276
|
+
})}
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
|
|
280
|
+
{/* Typing indicator */}
|
|
281
|
+
{typingUsers.length > 0 && (
|
|
282
|
+
<div style={{ marginTop: "12px" }}>
|
|
283
|
+
<TypingIndicator users={typingUsers.map((u) => u.userId)} />
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
{/* Scroll anchor */}
|
|
288
|
+
<div ref={messagesEndRef} />
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
{/* Composer */}
|
|
292
|
+
{userId && !thread.resolved && (
|
|
293
|
+
<div style={{ borderTop: "1px solid #e5e7eb" }}>
|
|
294
|
+
<AddComment
|
|
295
|
+
onSubmit={handleSubmit}
|
|
296
|
+
onTypingChange={onTypingChange}
|
|
297
|
+
allowEditing={allowEditing}
|
|
298
|
+
placeholder="Write a reply..."
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
301
|
+
)}
|
|
302
|
+
|
|
303
|
+
{/* Resolved state message */}
|
|
304
|
+
{thread.resolved && (
|
|
305
|
+
<div
|
|
306
|
+
style={{
|
|
307
|
+
padding: "12px 16px",
|
|
308
|
+
borderTop: "1px solid #e5e7eb",
|
|
309
|
+
textAlign: "center",
|
|
310
|
+
color: "#6b7280",
|
|
311
|
+
fontSize: "13px",
|
|
312
|
+
backgroundColor: "#f9fafb",
|
|
313
|
+
}}
|
|
314
|
+
>
|
|
315
|
+
This thread has been resolved.
|
|
316
|
+
{onUnresolve && (
|
|
317
|
+
<button
|
|
318
|
+
onClick={onUnresolve}
|
|
319
|
+
style={{
|
|
320
|
+
marginLeft: "8px",
|
|
321
|
+
padding: "0",
|
|
322
|
+
border: "none",
|
|
323
|
+
background: "transparent",
|
|
324
|
+
color: styles?.accentColor ?? "#3b82f6",
|
|
325
|
+
cursor: "pointer",
|
|
326
|
+
textDecoration: "underline",
|
|
327
|
+
}}
|
|
328
|
+
>
|
|
329
|
+
Reopen
|
|
330
|
+
</button>
|
|
331
|
+
)}
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypingIndicator Component
|
|
3
|
+
*
|
|
4
|
+
* Shows who is currently typing in a thread.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useEffect } from "react";
|
|
8
|
+
import { useComments } from "./CommentsProvider";
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export interface TypingIndicatorProps {
|
|
15
|
+
/** User IDs currently typing */
|
|
16
|
+
users: string[];
|
|
17
|
+
/** Max number of names to show before "and X others" */
|
|
18
|
+
maxNamesToShow?: number;
|
|
19
|
+
/** CSS class name */
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Component
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Animated typing indicator showing who is typing.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* ```tsx
|
|
32
|
+
* const typingUsers = useQuery(api.comments.getTypingUsers, { threadId });
|
|
33
|
+
*
|
|
34
|
+
* <TypingIndicator users={typingUsers.map(u => u.userId)} />
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function TypingIndicator({
|
|
38
|
+
users,
|
|
39
|
+
maxNamesToShow = 3,
|
|
40
|
+
className = "",
|
|
41
|
+
}: TypingIndicatorProps) {
|
|
42
|
+
const { resolveUser } = useComments();
|
|
43
|
+
const [userNames, setUserNames] = useState<Record<string, string>>({});
|
|
44
|
+
|
|
45
|
+
// Resolve user names
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (resolveUser) {
|
|
48
|
+
users.forEach(async (userId) => {
|
|
49
|
+
if (!userNames[userId]) {
|
|
50
|
+
const info = await resolveUser(userId);
|
|
51
|
+
setUserNames((prev) => ({ ...prev, [userId]: info.name }));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}, [users, resolveUser, userNames]);
|
|
56
|
+
|
|
57
|
+
if (users.length === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Format the typing message
|
|
62
|
+
const names = users.map((id) => userNames[id] ?? id);
|
|
63
|
+
let message: string;
|
|
64
|
+
|
|
65
|
+
if (names.length === 1) {
|
|
66
|
+
message = `${names[0]} is typing`;
|
|
67
|
+
} else if (names.length <= maxNamesToShow) {
|
|
68
|
+
const lastN = names[names.length - 1];
|
|
69
|
+
const restNames = names.slice(0, -1).join(", ");
|
|
70
|
+
message = `${restNames} and ${lastN} are typing`;
|
|
71
|
+
} else {
|
|
72
|
+
const shown = names.slice(0, maxNamesToShow).join(", ");
|
|
73
|
+
const remaining = names.length - maxNamesToShow;
|
|
74
|
+
message = `${shown} and ${remaining} others are typing`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
className={`typing-indicator ${className}`}
|
|
80
|
+
style={{
|
|
81
|
+
display: "flex",
|
|
82
|
+
alignItems: "center",
|
|
83
|
+
gap: "8px",
|
|
84
|
+
fontSize: "13px",
|
|
85
|
+
color: "#6b7280",
|
|
86
|
+
fontStyle: "italic",
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
{/* Animated dots */}
|
|
90
|
+
<span
|
|
91
|
+
style={{
|
|
92
|
+
display: "inline-flex",
|
|
93
|
+
gap: "2px",
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<span
|
|
97
|
+
style={{
|
|
98
|
+
width: "4px",
|
|
99
|
+
height: "4px",
|
|
100
|
+
borderRadius: "50%",
|
|
101
|
+
backgroundColor: "#9ca3af",
|
|
102
|
+
animation: "typingDot 1.4s infinite ease-in-out",
|
|
103
|
+
animationDelay: "0s",
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
<span
|
|
107
|
+
style={{
|
|
108
|
+
width: "4px",
|
|
109
|
+
height: "4px",
|
|
110
|
+
borderRadius: "50%",
|
|
111
|
+
backgroundColor: "#9ca3af",
|
|
112
|
+
animation: "typingDot 1.4s infinite ease-in-out",
|
|
113
|
+
animationDelay: "0.2s",
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
<span
|
|
117
|
+
style={{
|
|
118
|
+
width: "4px",
|
|
119
|
+
height: "4px",
|
|
120
|
+
borderRadius: "50%",
|
|
121
|
+
backgroundColor: "#9ca3af",
|
|
122
|
+
animation: "typingDot 1.4s infinite ease-in-out",
|
|
123
|
+
animationDelay: "0.4s",
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
</span>
|
|
127
|
+
<span>{message}</span>
|
|
128
|
+
|
|
129
|
+
{/* Inline styles for animation */}
|
|
130
|
+
<style>{`
|
|
131
|
+
@keyframes typingDot {
|
|
132
|
+
0%, 80%, 100% {
|
|
133
|
+
transform: scale(1);
|
|
134
|
+
opacity: 0.5;
|
|
135
|
+
}
|
|
136
|
+
40% {
|
|
137
|
+
transform: scale(1.2);
|
|
138
|
+
opacity: 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
`}</style>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comments Component - React UI
|
|
3
|
+
*
|
|
4
|
+
* Drop-in React components for the comments system.
|
|
5
|
+
* These components provide a fully managed UI for comments,
|
|
6
|
+
* threads, reactions, and typing indicators.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
CommentsProvider,
|
|
11
|
+
useComments,
|
|
12
|
+
type CommentsContextValue,
|
|
13
|
+
type CommentsStyles,
|
|
14
|
+
type CommentsProviderProps,
|
|
15
|
+
} from "./CommentsProvider";
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
Comments,
|
|
19
|
+
type CommentsProps,
|
|
20
|
+
} from "./Comments";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
Thread,
|
|
24
|
+
type ThreadProps,
|
|
25
|
+
} from "./Thread";
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
Comment,
|
|
29
|
+
type CommentProps,
|
|
30
|
+
} from "./Comment";
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
AddComment,
|
|
34
|
+
type AddCommentProps,
|
|
35
|
+
} from "./AddComment";
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
ReactionPicker,
|
|
39
|
+
type ReactionPickerProps,
|
|
40
|
+
} from "./ReactionPicker";
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
TypingIndicator,
|
|
44
|
+
type TypingIndicatorProps,
|
|
45
|
+
} from "./TypingIndicator";
|