@object-ui/collaboration 3.0.3 → 3.1.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/dist/CommentThread.d.ts +5 -1
- package/dist/CommentThread.d.ts.map +1 -1
- package/dist/CommentThread.js +84 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/useCommentSearch.d.ts +33 -0
- package/dist/useCommentSearch.d.ts.map +1 -0
- package/dist/useCommentSearch.js +62 -0
- package/dist/useMentionNotifications.d.ts +40 -0
- package/dist/useMentionNotifications.d.ts.map +1 -0
- package/dist/useMentionNotifications.js +49 -0
- package/package.json +3 -3
package/dist/CommentThread.d.ts
CHANGED
|
@@ -46,6 +46,10 @@ export interface CommentThreadProps {
|
|
|
46
46
|
onDeleteComment?: (commentId: string) => void;
|
|
47
47
|
/** Callback when thread is resolved/reopened */
|
|
48
48
|
onResolve?: (resolved: boolean) => void;
|
|
49
|
+
/** Callback when a reaction is toggled */
|
|
50
|
+
onReaction?: (commentId: string, emoji: string) => void;
|
|
51
|
+
/** Callback when @mentions are detected — for notification delivery (email/push) */
|
|
52
|
+
onMentionNotify?: (mentionedUserIds: string[], commentContent: string) => void;
|
|
49
53
|
/** Whether the thread is resolved */
|
|
50
54
|
resolved?: boolean;
|
|
51
55
|
/** Additional className */
|
|
@@ -57,5 +61,5 @@ export interface CommentThreadProps {
|
|
|
57
61
|
* Renders a list of comments with author avatars, timestamps,
|
|
58
62
|
* reply functionality, and an @mention suggestions popup.
|
|
59
63
|
*/
|
|
60
|
-
export declare function CommentThread({ threadId, comments, currentUser, mentionableUsers, onAddComment, onEditComment, onDeleteComment, onResolve, resolved, className, }: CommentThreadProps): React.ReactElement;
|
|
64
|
+
export declare function CommentThread({ threadId, comments, currentUser, mentionableUsers, onAddComment, onEditComment, onDeleteComment, onResolve, onReaction, onMentionNotify, resolved, className, }: CommentThreadProps): React.ReactElement;
|
|
61
65
|
//# sourceMappingURL=CommentThread.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommentThread.d.ts","sourceRoot":"","sources":["../src/CommentThread.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA4D,MAAM,OAAO,CAAC;AAEjF,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kBAAkB;IACjC,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,mBAAmB;IACnB,WAAW,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,oCAAoC;IACpC,gBAAgB,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACnE,4CAA4C;IAC5C,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAChF,wCAAwC;IACxC,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,yCAAyC;IACzC,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,gDAAgD;IAChD,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2BAA2B;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"CommentThread.d.ts","sourceRoot":"","sources":["../src/CommentThread.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA4D,MAAM,OAAO,CAAC;AAEjF,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kBAAkB;IACjC,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,mBAAmB;IACnB,WAAW,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,oCAAoC;IACpC,gBAAgB,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACnE,4CAA4C;IAC5C,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAChF,wCAAwC;IACxC,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,yCAAyC;IACzC,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,gDAAgD;IAChD,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD,oFAAoF;IACpF,eAAe,CAAC,EAAE,CAAC,gBAAgB,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/E,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2BAA2B;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA2QD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,gBAAqB,EACrB,YAAY,EACZ,aAAa,EACb,eAAe,EACf,SAAS,EACT,UAAU,EACV,eAAe,EACf,QAAgB,EAChB,SAAS,GACV,EAAE,kBAAkB,GAAG,KAAK,CAAC,YAAY,CA0TzC"}
|
package/dist/CommentThread.js
CHANGED
|
@@ -152,6 +152,48 @@ const styles = {
|
|
|
152
152
|
cursor: 'pointer',
|
|
153
153
|
padding: 0,
|
|
154
154
|
},
|
|
155
|
+
sortSelect: {
|
|
156
|
+
background: 'none',
|
|
157
|
+
border: '1px solid #e2e8f0',
|
|
158
|
+
borderRadius: '4px',
|
|
159
|
+
padding: '2px 6px',
|
|
160
|
+
fontSize: '11px',
|
|
161
|
+
color: '#64748b',
|
|
162
|
+
cursor: 'pointer',
|
|
163
|
+
outline: 'none',
|
|
164
|
+
},
|
|
165
|
+
reactionBar: {
|
|
166
|
+
display: 'flex',
|
|
167
|
+
gap: '4px',
|
|
168
|
+
marginTop: '4px',
|
|
169
|
+
flexWrap: 'wrap',
|
|
170
|
+
},
|
|
171
|
+
reactionBtn: {
|
|
172
|
+
background: 'none',
|
|
173
|
+
border: '1px solid #e2e8f0',
|
|
174
|
+
borderRadius: '12px',
|
|
175
|
+
padding: '1px 6px',
|
|
176
|
+
fontSize: '12px',
|
|
177
|
+
cursor: 'pointer',
|
|
178
|
+
display: 'inline-flex',
|
|
179
|
+
alignItems: 'center',
|
|
180
|
+
gap: '2px',
|
|
181
|
+
lineHeight: '1.5',
|
|
182
|
+
},
|
|
183
|
+
reactionBtnActive: {
|
|
184
|
+
backgroundColor: '#eff6ff',
|
|
185
|
+
borderColor: '#93c5fd',
|
|
186
|
+
},
|
|
187
|
+
reactionPicker: {
|
|
188
|
+
background: 'none',
|
|
189
|
+
border: '1px solid #e2e8f0',
|
|
190
|
+
borderRadius: '12px',
|
|
191
|
+
padding: '1px 6px',
|
|
192
|
+
fontSize: '12px',
|
|
193
|
+
cursor: 'pointer',
|
|
194
|
+
color: '#94a3b8',
|
|
195
|
+
lineHeight: '1.5',
|
|
196
|
+
},
|
|
155
197
|
inputArea: {
|
|
156
198
|
display: 'flex',
|
|
157
199
|
gap: '8px',
|
|
@@ -230,13 +272,14 @@ function renderContent(content) {
|
|
|
230
272
|
* Renders a list of comments with author avatars, timestamps,
|
|
231
273
|
* reply functionality, and an @mention suggestions popup.
|
|
232
274
|
*/
|
|
233
|
-
export function CommentThread({ threadId, comments, currentUser, mentionableUsers = [], onAddComment, onEditComment, onDeleteComment, onResolve, resolved = false, className, }) {
|
|
275
|
+
export function CommentThread({ threadId, comments, currentUser, mentionableUsers = [], onAddComment, onEditComment, onDeleteComment, onResolve, onReaction, onMentionNotify, resolved = false, className, }) {
|
|
234
276
|
const [inputValue, setInputValue] = useState('');
|
|
235
277
|
const [replyTo, setReplyTo] = useState(null);
|
|
236
278
|
const [editingId, setEditingId] = useState(null);
|
|
237
279
|
const [editValue, setEditValue] = useState('');
|
|
238
280
|
const [mentionQuery, setMentionQuery] = useState(null);
|
|
239
281
|
const [mentionIndex, setMentionIndex] = useState(0);
|
|
282
|
+
const [sortOrder, setSortOrder] = useState('oldest');
|
|
240
283
|
const inputRef = useRef(null);
|
|
241
284
|
const filteredMentions = useMemo(() => {
|
|
242
285
|
if (mentionQuery === null)
|
|
@@ -280,10 +323,14 @@ export function CommentThread({ threadId, comments, currentUser, mentionableUser
|
|
|
280
323
|
return;
|
|
281
324
|
const mentions = parseMentions(trimmed, mentionableUsers);
|
|
282
325
|
onAddComment(trimmed, mentions, replyTo ?? undefined);
|
|
326
|
+
// Trigger notification delivery for mentioned users
|
|
327
|
+
if (mentions.length > 0 && onMentionNotify) {
|
|
328
|
+
onMentionNotify(mentions, trimmed);
|
|
329
|
+
}
|
|
283
330
|
setInputValue('');
|
|
284
331
|
setReplyTo(null);
|
|
285
332
|
setMentionQuery(null);
|
|
286
|
-
}, [inputValue, onAddComment, mentionableUsers, replyTo]);
|
|
333
|
+
}, [inputValue, onAddComment, mentionableUsers, replyTo, onMentionNotify]);
|
|
287
334
|
const handleKeyDown = useCallback((e) => {
|
|
288
335
|
if (mentionQuery !== null && filteredMentions.length > 0) {
|
|
289
336
|
if (e.key === 'ArrowDown') {
|
|
@@ -328,7 +375,13 @@ export function CommentThread({ threadId, comments, currentUser, mentionableUser
|
|
|
328
375
|
setMentionIndex(Math.max(0, filteredMentions.length - 1));
|
|
329
376
|
}
|
|
330
377
|
}, [filteredMentions.length, mentionIndex]);
|
|
331
|
-
const rootComments = useMemo(() =>
|
|
378
|
+
const rootComments = useMemo(() => {
|
|
379
|
+
const roots = comments.filter(c => !c.parentId);
|
|
380
|
+
if (sortOrder === 'newest') {
|
|
381
|
+
return [...roots].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
382
|
+
}
|
|
383
|
+
return roots;
|
|
384
|
+
}, [comments, sortOrder]);
|
|
332
385
|
const replies = useMemo(() => comments.filter(c => c.parentId), [comments]);
|
|
333
386
|
const renderComment = (comment, isReply = false) => {
|
|
334
387
|
const isEditing = editingId === comment.id;
|
|
@@ -367,11 +420,31 @@ export function CommentThread({ threadId, comments, currentUser, mentionableUser
|
|
|
367
420
|
style: { ...styles.actionBtn },
|
|
368
421
|
}, 'Cancel'))
|
|
369
422
|
: React.createElement('div', { style: styles.content }, renderContent(comment.content)),
|
|
423
|
+
// Reactions display
|
|
424
|
+
!isEditing && comment.reactions && Object.keys(comment.reactions).length > 0 && React.createElement('div', { style: styles.reactionBar }, Object.entries(comment.reactions).map(([emoji, userIds]) => React.createElement('button', {
|
|
425
|
+
key: emoji,
|
|
426
|
+
style: {
|
|
427
|
+
...styles.reactionBtn,
|
|
428
|
+
...(userIds.includes(currentUser.id) ? styles.reactionBtnActive : {}),
|
|
429
|
+
},
|
|
430
|
+
onClick: () => onReaction?.(comment.id, emoji),
|
|
431
|
+
title: userIds.length === 1 ? '1 reaction' : `${userIds.length} reactions`,
|
|
432
|
+
}, `${emoji} ${userIds.length}`)), onReaction && React.createElement('button', {
|
|
433
|
+
style: styles.reactionPicker,
|
|
434
|
+
onClick: () => onReaction(comment.id, '👍'),
|
|
435
|
+
title: 'Add thumbs up',
|
|
436
|
+
}, '+')),
|
|
370
437
|
// Actions
|
|
371
438
|
!isEditing && React.createElement('div', { style: styles.actions }, React.createElement('button', {
|
|
372
439
|
style: styles.actionBtn,
|
|
373
440
|
onClick: () => setReplyTo(comment.id),
|
|
374
|
-
}, 'Reply'),
|
|
441
|
+
}, 'Reply'), onReaction && React.createElement('button', {
|
|
442
|
+
style: styles.actionBtn,
|
|
443
|
+
onClick: () => onReaction(comment.id, '👍'),
|
|
444
|
+
}, '👍'), onReaction && React.createElement('button', {
|
|
445
|
+
style: styles.actionBtn,
|
|
446
|
+
onClick: () => onReaction(comment.id, '❤️'),
|
|
447
|
+
}, '❤️'), isOwner && onEditComment && React.createElement('button', {
|
|
375
448
|
style: styles.actionBtn,
|
|
376
449
|
onClick: () => handleEdit(comment.id),
|
|
377
450
|
}, 'Edit'), isOwner && onDeleteComment && React.createElement('button', {
|
|
@@ -385,10 +458,15 @@ export function CommentThread({ threadId, comments, currentUser, mentionableUser
|
|
|
385
458
|
'data-thread-id': threadId,
|
|
386
459
|
},
|
|
387
460
|
// Header
|
|
388
|
-
React.createElement('div', { style: styles.header }, React.createElement('span', null, `${comments.length} comment${comments.length !== 1 ? 's' : ''}`, resolved ? ' · Resolved' : ''),
|
|
461
|
+
React.createElement('div', { style: styles.header }, React.createElement('span', null, `${comments.length} comment${comments.length !== 1 ? 's' : ''}`, resolved ? ' · Resolved' : ''), React.createElement('div', { style: { display: 'flex', gap: '6px', alignItems: 'center' } }, React.createElement('select', {
|
|
462
|
+
value: sortOrder,
|
|
463
|
+
onChange: (e) => setSortOrder(e.target.value),
|
|
464
|
+
style: styles.sortSelect,
|
|
465
|
+
'aria-label': 'Sort comments',
|
|
466
|
+
}, React.createElement('option', { value: 'oldest' }, 'Oldest'), React.createElement('option', { value: 'newest' }, 'Newest')), onResolve && React.createElement('button', {
|
|
389
467
|
style: styles.resolveBtn,
|
|
390
468
|
onClick: () => onResolve(!resolved),
|
|
391
|
-
}, resolved ? 'Reopen' : 'Resolve')),
|
|
469
|
+
}, resolved ? 'Reopen' : 'Resolve'))),
|
|
392
470
|
// Comments list
|
|
393
471
|
React.createElement('div', { style: styles.commentList }, rootComments.map(comment => React.createElement(React.Fragment, { key: comment.id }, renderComment(comment), replies
|
|
394
472
|
.filter(r => r.parentId === comment.id)
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export { useRealtimeSubscription, type RealtimeConfig, type ConnectionState, typ
|
|
|
22
22
|
export { usePresence, createPresenceUpdater, type PresenceUser, type PresenceConfig, type PresenceResult, } from './usePresence';
|
|
23
23
|
export { useConflictResolution, type VersionEntry, type ConflictInfo, type ConflictResolutionResult, } from './useConflictResolution';
|
|
24
24
|
export { CommentThread, type Comment, type CommentThreadProps, } from './CommentThread';
|
|
25
|
+
export { useMentionNotifications, type MentionNotificationsConfig, type MentionNotificationsResult, } from './useMentionNotifications';
|
|
26
|
+
export { useCommentSearch, type CommentSearchConfig, type CommentSearchReturn, } from './useCommentSearch';
|
|
25
27
|
export { LiveCursors, type LiveCursorsProps, } from './LiveCursors';
|
|
26
28
|
export { PresenceAvatars, type PresenceAvatarsProps, } from './PresenceAvatars';
|
|
27
29
|
export type { CollaborationPresence, CollaborationOperation, CollaborationConfig, } from '@object-ui/types';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,uBAAuB,EACvB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,qBAAqB,EACrB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,wBAAwB,GAC9B,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,aAAa,EACb,KAAK,OAAO,EACZ,KAAK,kBAAkB,GACxB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,WAAW,EACX,KAAK,gBAAgB,GACtB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC;AAG3B,YAAY,EACV,qBAAqB,EACrB,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,uBAAuB,EACvB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,qBAAqB,EACrB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,wBAAwB,GAC9B,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,aAAa,EACb,KAAK,OAAO,EACZ,KAAK,kBAAkB,GACxB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,uBAAuB,EACvB,KAAK,0BAA0B,EAC/B,KAAK,0BAA0B,GAChC,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,gBAAgB,EAChB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,GACzB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,WAAW,EACX,KAAK,gBAAgB,GACtB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC;AAG3B,YAAY,EACV,qBAAqB,EACrB,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -22,5 +22,7 @@ export { useRealtimeSubscription, } from './useRealtimeSubscription';
|
|
|
22
22
|
export { usePresence, createPresenceUpdater, } from './usePresence';
|
|
23
23
|
export { useConflictResolution, } from './useConflictResolution';
|
|
24
24
|
export { CommentThread, } from './CommentThread';
|
|
25
|
+
export { useMentionNotifications, } from './useMentionNotifications';
|
|
26
|
+
export { useCommentSearch, } from './useCommentSearch';
|
|
25
27
|
export { LiveCursors, } from './LiveCursors';
|
|
26
28
|
export { PresenceAvatars, } from './PresenceAvatars';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
import type { CommentEntry, CommentSearchResult } from '@object-ui/types';
|
|
9
|
+
export interface CommentSearchConfig {
|
|
10
|
+
/** All comments across records, each must have objectName and recordId set */
|
|
11
|
+
comments: CommentEntry[];
|
|
12
|
+
}
|
|
13
|
+
export interface CommentSearchReturn {
|
|
14
|
+
/** Current search query */
|
|
15
|
+
query: string;
|
|
16
|
+
/** Update the search query */
|
|
17
|
+
setQuery: (query: string) => void;
|
|
18
|
+
/** Filtered/matched results */
|
|
19
|
+
results: CommentSearchResult[];
|
|
20
|
+
/** Whether a search is active */
|
|
21
|
+
isSearching: boolean;
|
|
22
|
+
/** Clear the search */
|
|
23
|
+
clearSearch: () => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Hook for searching comments across all records.
|
|
27
|
+
*
|
|
28
|
+
* Accepts a flat list of CommentEntry items (each should have objectName
|
|
29
|
+
* and recordId set) and provides a search interface that returns matching
|
|
30
|
+
* results with highlighted snippets.
|
|
31
|
+
*/
|
|
32
|
+
export declare function useCommentSearch({ comments }: CommentSearchConfig): CommentSearchReturn;
|
|
33
|
+
//# sourceMappingURL=useCommentSearch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCommentSearch.d.ts","sourceRoot":"","sources":["../src/useCommentSearch.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE1E,MAAM,WAAW,mBAAmB;IAClC,8EAA8E;IAC9E,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,+BAA+B;IAC/B,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,iCAAiC;IACjC,WAAW,EAAE,OAAO,CAAC;IACrB,uBAAuB;IACvB,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB;AAiBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,QAAQ,EAAE,EAAE,mBAAmB,GAAG,mBAAmB,CAkCvF"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
9
|
+
/**
|
|
10
|
+
* Build a highlighted snippet around the first match of `query` in `text`.
|
|
11
|
+
* Returns the match wrapped in <mark> tags for display.
|
|
12
|
+
*/
|
|
13
|
+
function buildHighlight(text, query) {
|
|
14
|
+
if (!query)
|
|
15
|
+
return text;
|
|
16
|
+
const idx = text.toLowerCase().indexOf(query.toLowerCase());
|
|
17
|
+
if (idx === -1)
|
|
18
|
+
return text;
|
|
19
|
+
const start = Math.max(0, idx - 30);
|
|
20
|
+
const end = Math.min(text.length, idx + query.length + 30);
|
|
21
|
+
const before = start > 0 ? '…' : '';
|
|
22
|
+
const after = end < text.length ? '…' : '';
|
|
23
|
+
return `${before}${text.slice(start, end)}${after}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Hook for searching comments across all records.
|
|
27
|
+
*
|
|
28
|
+
* Accepts a flat list of CommentEntry items (each should have objectName
|
|
29
|
+
* and recordId set) and provides a search interface that returns matching
|
|
30
|
+
* results with highlighted snippets.
|
|
31
|
+
*/
|
|
32
|
+
export function useCommentSearch({ comments }) {
|
|
33
|
+
const [query, setQuery] = useState('');
|
|
34
|
+
const isSearching = query.trim().length > 0;
|
|
35
|
+
const results = useMemo(() => {
|
|
36
|
+
const trimmed = query.trim().toLowerCase();
|
|
37
|
+
if (!trimmed)
|
|
38
|
+
return [];
|
|
39
|
+
return comments
|
|
40
|
+
.filter(c => {
|
|
41
|
+
const textMatch = c.text.toLowerCase().includes(trimmed);
|
|
42
|
+
const authorMatch = c.author.toLowerCase().includes(trimmed);
|
|
43
|
+
return textMatch || authorMatch;
|
|
44
|
+
})
|
|
45
|
+
.map(c => ({
|
|
46
|
+
comment: c,
|
|
47
|
+
objectName: c.objectName ?? '',
|
|
48
|
+
recordId: c.recordId ?? '',
|
|
49
|
+
highlight: buildHighlight(c.text, trimmed),
|
|
50
|
+
}));
|
|
51
|
+
}, [query, comments]);
|
|
52
|
+
const clearSearch = useCallback(() => {
|
|
53
|
+
setQuery('');
|
|
54
|
+
}, []);
|
|
55
|
+
return {
|
|
56
|
+
query,
|
|
57
|
+
setQuery,
|
|
58
|
+
results,
|
|
59
|
+
isSearching,
|
|
60
|
+
clearSearch,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
import type { MentionNotification } from '@object-ui/types';
|
|
9
|
+
export interface MentionNotificationsConfig {
|
|
10
|
+
/** Current user ID to filter notifications for */
|
|
11
|
+
currentUserId: string;
|
|
12
|
+
/** Initial notifications */
|
|
13
|
+
initialNotifications?: MentionNotification[];
|
|
14
|
+
/** Callback when a notification should be delivered (email/push) */
|
|
15
|
+
onDeliver?: (notification: MentionNotification) => void | Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export interface MentionNotificationsResult {
|
|
18
|
+
/** All notifications for the current user */
|
|
19
|
+
notifications: MentionNotification[];
|
|
20
|
+
/** Unread notifications count */
|
|
21
|
+
unreadCount: number;
|
|
22
|
+
/** Add a new mention notification (triggered when @mention is detected) */
|
|
23
|
+
addNotification: (notification: MentionNotification) => void;
|
|
24
|
+
/** Mark a notification as read */
|
|
25
|
+
markAsRead: (notificationId: string) => void;
|
|
26
|
+
/** Mark all notifications as read */
|
|
27
|
+
markAllAsRead: () => void;
|
|
28
|
+
/** Dismiss/remove a notification */
|
|
29
|
+
dismiss: (notificationId: string) => void;
|
|
30
|
+
/** Clear all notifications */
|
|
31
|
+
clearAll: () => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Hook for managing @mention notifications with delivery support.
|
|
35
|
+
*
|
|
36
|
+
* Tracks mention notifications for the current user and provides
|
|
37
|
+
* callbacks for delivery via email/push channels.
|
|
38
|
+
*/
|
|
39
|
+
export declare function useMentionNotifications({ currentUserId, initialNotifications, onDeliver, }: MentionNotificationsConfig): MentionNotificationsResult;
|
|
40
|
+
//# sourceMappingURL=useMentionNotifications.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMentionNotifications.d.ts","sourceRoot":"","sources":["../src/useMentionNotifications.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE5D,MAAM,WAAW,0BAA0B;IACzC,kDAAkD;IAClD,aAAa,EAAE,MAAM,CAAC;IACtB,4BAA4B;IAC5B,oBAAoB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC7C,oEAAoE;IACpE,SAAS,CAAC,EAAE,CAAC,YAAY,EAAE,mBAAmB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzE;AAED,MAAM,WAAW,0BAA0B;IACzC,6CAA6C;IAC7C,aAAa,EAAE,mBAAmB,EAAE,CAAC;IACrC,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,eAAe,EAAE,CAAC,YAAY,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAC7D,kCAAkC;IAClC,UAAU,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,qCAAqC;IACrC,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,oCAAoC;IACpC,OAAO,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,aAAa,EACb,oBAAyB,EACzB,SAAS,GACV,EAAE,0BAA0B,GAAG,0BAA0B,CAiDzD"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
9
|
+
/**
|
|
10
|
+
* Hook for managing @mention notifications with delivery support.
|
|
11
|
+
*
|
|
12
|
+
* Tracks mention notifications for the current user and provides
|
|
13
|
+
* callbacks for delivery via email/push channels.
|
|
14
|
+
*/
|
|
15
|
+
export function useMentionNotifications({ currentUserId, initialNotifications = [], onDeliver, }) {
|
|
16
|
+
const [notifications, setNotifications] = useState(initialNotifications.filter(n => n.recipientId === currentUserId));
|
|
17
|
+
const unreadCount = useMemo(() => notifications.filter(n => !n.read).length, [notifications]);
|
|
18
|
+
const addNotification = useCallback((notification) => {
|
|
19
|
+
if (notification.recipientId !== currentUserId)
|
|
20
|
+
return;
|
|
21
|
+
setNotifications(prev => {
|
|
22
|
+
if (prev.some(n => n.id === notification.id))
|
|
23
|
+
return prev;
|
|
24
|
+
return [notification, ...prev];
|
|
25
|
+
});
|
|
26
|
+
onDeliver?.(notification);
|
|
27
|
+
}, [currentUserId, onDeliver]);
|
|
28
|
+
const markAsRead = useCallback((notificationId) => {
|
|
29
|
+
setNotifications(prev => prev.map(n => (n.id === notificationId ? { ...n, read: true } : n)));
|
|
30
|
+
}, []);
|
|
31
|
+
const markAllAsRead = useCallback(() => {
|
|
32
|
+
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
|
|
33
|
+
}, []);
|
|
34
|
+
const dismiss = useCallback((notificationId) => {
|
|
35
|
+
setNotifications(prev => prev.filter(n => n.id !== notificationId));
|
|
36
|
+
}, []);
|
|
37
|
+
const clearAll = useCallback(() => {
|
|
38
|
+
setNotifications([]);
|
|
39
|
+
}, []);
|
|
40
|
+
return {
|
|
41
|
+
notifications,
|
|
42
|
+
unreadCount,
|
|
43
|
+
addNotification,
|
|
44
|
+
markAsRead,
|
|
45
|
+
markAllAsRead,
|
|
46
|
+
dismiss,
|
|
47
|
+
clearAll,
|
|
48
|
+
};
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/collaboration",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Real-time collaboration for Object UI with presence tracking, live cursors, conflict resolution, and comment threads.",
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"react": "^18.0.0 || ^19.0.0"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@object-ui/types": "3.0
|
|
29
|
+
"@object-ui/types": "3.1.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@types/react": "19.2.
|
|
32
|
+
"@types/react": "19.2.14",
|
|
33
33
|
"react": "19.2.4",
|
|
34
34
|
"typescript": "^5.9.3",
|
|
35
35
|
"vitest": "^4.0.18"
|