@@ -37,6 +37,8 @@ export interface CommentThreadProps {
3737 onDeleteComment ?: ( commentId : string ) => void ;
3838 /** Callback when thread is resolved/reopened */
3939 onResolve ?: ( resolved : boolean ) => void ;
40+ /** Callback when a reaction is toggled */
41+ onReaction ?: ( commentId : string , emoji : string ) => void ;
4042 /** Whether the thread is resolved */
4143 resolved ?: boolean ;
4244 /** Additional className */
@@ -192,6 +194,48 @@ const styles = {
192194 cursor : 'pointer' ,
193195 padding : 0 ,
194196 } ,
197+ sortSelect : {
198+ background : 'none' ,
199+ border : '1px solid #e2e8f0' ,
200+ borderRadius : '4px' ,
201+ padding : '2px 6px' ,
202+ fontSize : '11px' ,
203+ color : '#64748b' ,
204+ cursor : 'pointer' ,
205+ outline : 'none' ,
206+ } ,
207+ reactionBar : {
208+ display : 'flex' ,
209+ gap : '4px' ,
210+ marginTop : '4px' ,
211+ flexWrap : 'wrap' as const ,
212+ } ,
213+ reactionBtn : {
214+ background : 'none' ,
215+ border : '1px solid #e2e8f0' ,
216+ borderRadius : '12px' ,
217+ padding : '1px 6px' ,
218+ fontSize : '12px' ,
219+ cursor : 'pointer' ,
220+ display : 'inline-flex' ,
221+ alignItems : 'center' ,
222+ gap : '2px' ,
223+ lineHeight : '1.5' ,
224+ } ,
225+ reactionBtnActive : {
226+ backgroundColor : '#eff6ff' ,
227+ borderColor : '#93c5fd' ,
228+ } ,
229+ reactionPicker : {
230+ background : 'none' ,
231+ border : '1px solid #e2e8f0' ,
232+ borderRadius : '12px' ,
233+ padding : '1px 6px' ,
234+ fontSize : '12px' ,
235+ cursor : 'pointer' ,
236+ color : '#94a3b8' ,
237+ lineHeight : '1.5' ,
238+ } ,
195239 inputArea : {
196240 display : 'flex' ,
197241 gap : '8px' ,
@@ -281,6 +325,7 @@ export function CommentThread({
281325 onEditComment,
282326 onDeleteComment,
283327 onResolve,
328+ onReaction,
284329 resolved = false ,
285330 className,
286331} : CommentThreadProps ) : React . ReactElement {
@@ -290,6 +335,7 @@ export function CommentThread({
290335 const [ editValue , setEditValue ] = useState ( '' ) ;
291336 const [ mentionQuery , setMentionQuery ] = useState < string | null > ( null ) ;
292337 const [ mentionIndex , setMentionIndex ] = useState ( 0 ) ;
338+ const [ sortOrder , setSortOrder ] = useState < 'newest' | 'oldest' > ( 'oldest' ) ;
293339 const inputRef = useRef < HTMLTextAreaElement > ( null ) ;
294340
295341 const filteredMentions = useMemo ( ( ) => {
@@ -390,8 +436,14 @@ export function CommentThread({
390436 } , [ filteredMentions . length , mentionIndex ] ) ;
391437
392438 const rootComments = useMemo (
393- ( ) => comments . filter ( c => ! c . parentId ) ,
394- [ comments ] ,
439+ ( ) => {
440+ const roots = comments . filter ( c => ! c . parentId ) ;
441+ if ( sortOrder === 'newest' ) {
442+ return [ ...roots ] . sort ( ( a , b ) => new Date ( b . createdAt ) . getTime ( ) - new Date ( a . createdAt ) . getTime ( ) ) ;
443+ }
444+ return roots ;
445+ } ,
446+ [ comments , sortOrder ] ,
395447 ) ;
396448 const replies = useMemo (
397449 ( ) => comments . filter ( c => c . parentId ) ,
@@ -446,12 +498,39 @@ export function CommentThread({
446498 } , 'Cancel' ) ,
447499 )
448500 : React . createElement ( 'div' , { style : styles . content } , renderContent ( comment . content ) ) ,
501+ // Reactions display
502+ ! isEditing && comment . reactions && Object . keys ( comment . reactions ) . length > 0 && React . createElement ( 'div' , { style : styles . reactionBar } ,
503+ Object . entries ( comment . reactions ) . map ( ( [ emoji , userIds ] ) =>
504+ React . createElement ( 'button' , {
505+ key : emoji ,
506+ style : {
507+ ...styles . reactionBtn ,
508+ ...( userIds . includes ( currentUser . id ) ? styles . reactionBtnActive : { } ) ,
509+ } ,
510+ onClick : ( ) => onReaction ?.( comment . id , emoji ) ,
511+ title : userIds . length === 1 ? '1 reaction' : `${ userIds . length } reactions` ,
512+ } , `${ emoji } ${ userIds . length } ` ) ,
513+ ) ,
514+ onReaction && React . createElement ( 'button' , {
515+ style : styles . reactionPicker ,
516+ onClick : ( ) => onReaction ( comment . id , '👍' ) ,
517+ title : 'Add reaction' ,
518+ } , '+' ) ,
519+ ) ,
449520 // Actions
450521 ! isEditing && React . createElement ( 'div' , { style : styles . actions } ,
451522 React . createElement ( 'button' , {
452523 style : styles . actionBtn ,
453524 onClick : ( ) => setReplyTo ( comment . id ) ,
454525 } , 'Reply' ) ,
526+ onReaction && React . createElement ( 'button' , {
527+ style : styles . actionBtn ,
528+ onClick : ( ) => onReaction ( comment . id , '👍' ) ,
529+ } , '👍' ) ,
530+ onReaction && React . createElement ( 'button' , {
531+ style : styles . actionBtn ,
532+ onClick : ( ) => onReaction ( comment . id , '❤️' ) ,
533+ } , '❤️' ) ,
455534 isOwner && onEditComment && React . createElement ( 'button' , {
456535 style : styles . actionBtn ,
457536 onClick : ( ) => handleEdit ( comment . id ) ,
@@ -476,10 +555,21 @@ export function CommentThread({
476555 `${ comments . length } comment${ comments . length !== 1 ? 's' : '' } ` ,
477556 resolved ? ' · Resolved' : '' ,
478557 ) ,
479- onResolve && React . createElement ( 'button' , {
480- style : styles . resolveBtn ,
481- onClick : ( ) => onResolve ( ! resolved ) ,
482- } , resolved ? 'Reopen' : 'Resolve' ) ,
558+ React . createElement ( 'div' , { style : { display : 'flex' , gap : '6px' , alignItems : 'center' } } ,
559+ React . createElement ( 'select' , {
560+ value : sortOrder ,
561+ onChange : ( e : React . ChangeEvent < HTMLSelectElement > ) => setSortOrder ( e . target . value as 'newest' | 'oldest' ) ,
562+ style : styles . sortSelect ,
563+ 'aria-label' : 'Sort comments' ,
564+ } ,
565+ React . createElement ( 'option' , { value : 'oldest' } , 'Oldest' ) ,
566+ React . createElement ( 'option' , { value : 'newest' } , 'Newest' ) ,
567+ ) ,
568+ onResolve && React . createElement ( 'button' , {
569+ style : styles . resolveBtn ,
570+ onClick : ( ) => onResolve ( ! resolved ) ,
571+ } , resolved ? 'Reopen' : 'Resolve' ) ,
572+ ) ,
483573 ) ,
484574 // Comments list
485575 React . createElement ( 'div' , { style : styles . commentList } ,
0 commit comments