@object-ui/collaboration 3.0.2 → 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.
@@ -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;AAiOD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,gBAAqB,EACrB,YAAY,EACZ,aAAa,EACb,eAAe,EACf,SAAS,EACT,QAAgB,EAChB,SAAS,GACV,EAAE,kBAAkB,GAAG,KAAK,CAAC,YAAY,CAuQzC"}
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"}
@@ -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(() => comments.filter(c => !c.parentId), [comments]);
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'), isOwner && onEditComment && React.createElement('button', {
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' : ''), onResolve && React.createElement('button', {
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';
@@ -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.2",
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.2"
29
+ "@object-ui/types": "3.1.0"
30
30
  },
31
31
  "devDependencies": {
32
- "@types/react": "19.2.13",
32
+ "@types/react": "19.2.14",
33
33
  "react": "19.2.4",
34
34
  "typescript": "^5.9.3",
35
35
  "vitest": "^4.0.18"