@@ -77,11 +77,16 @@ export function ChatLog({
77
77
78
78
const user_map = useTypedRedux ( "users" , "user_map" ) ;
79
79
const account_id = useTypedRedux ( "account" , "account_id" ) ;
80
- const { dates : sortedDates , numFolded } = useMemo < {
80
+ const {
81
+ dates : sortedDates ,
82
+ numFolded,
83
+ numChildren,
84
+ } = useMemo < {
81
85
dates : string [ ] ;
82
86
numFolded : number ;
87
+ numChildren ;
83
88
} > ( ( ) => {
84
- const { dates, numFolded } = getSortedDates (
89
+ const { dates, numFolded, numChildren } = getSortedDates (
85
90
messages ,
86
91
search ,
87
92
account_id ! ,
@@ -97,7 +102,7 @@ export function ChatLog({
97
102
: new Date ( parseFloat ( dates [ dates . length - 1 ] ) ) ,
98
103
) ;
99
104
} , 1 ) ;
100
- return { dates, numFolded } ;
105
+ return { dates, numFolded, numChildren } ;
101
106
} , [ messages , search , project_id , path , filterRecentH ] ) ;
102
107
103
108
useEffect ( ( ) => {
@@ -237,6 +242,7 @@ export function ChatLog({
237
242
manualScrollRef,
238
243
mode,
239
244
selectedDate,
245
+ numChildren,
240
246
} }
241
247
/>
242
248
< Composing
@@ -283,21 +289,14 @@ function isPrevMessageSender(
283
289
) ;
284
290
}
285
291
286
- function isThread ( messages : ChatMessages , message : ChatMessageTyped ) {
292
+ function isThread (
293
+ message : ChatMessageTyped ,
294
+ numChildren : { [ date : number ] : number } ,
295
+ ) {
287
296
if ( message . get ( "reply_to" ) != null ) {
288
297
return true ;
289
298
}
290
-
291
- // TODO/WARNING!!! This is a linear search
292
- // through all messages to decide if a message is the root of a thread.
293
- // This is VERY BAD and must to be redone at some point, since we call isThread
294
- // on all messages (in getSortedDates), making that algorithm O(n^2),
295
- // which is hideous as the number of messages scales. Instead one must
296
- // use a proper data structure (or even a cache) to track this once
297
- // and for all. It's more complicated but everything needs to be at
298
- // most O(n).
299
- const s = message . get ( "date" ) . toISOString ( ) ;
300
- return messages . some ( ( m ) => m . get ( "reply_to" ) === s ) ;
299
+ return ( numChildren [ message . get ( "date" ) . valueOf ( ) ] ?? 0 ) > 0 ;
301
300
}
302
301
303
302
function isFolded (
@@ -323,24 +322,45 @@ export function getSortedDates(
323
322
search : string | undefined ,
324
323
account_id : string ,
325
324
filterRecentH ?: number ,
326
- ) : { dates : string [ ] ; numFolded : number } {
325
+ ) : {
326
+ dates : string [ ] ;
327
+ numFolded : number ;
328
+ numChildren : { [ date : number ] : number } ;
329
+ } {
327
330
let numFolded = 0 ;
328
331
let m = messages ;
329
332
if ( m == null ) {
330
- return { dates : [ ] , numFolded : 0 } ;
333
+ return {
334
+ dates : [ ] ,
335
+ numFolded : 0 ,
336
+ numChildren : { } ,
337
+ } ;
331
338
}
332
339
340
+ // we assume filterMessages contains complete threads. It does
341
+ // right now, but that's an assumption in this function.
333
342
m = filterMessages ( { messages : m , filter : search , filterRecentH } ) ;
334
343
344
+ // Do a linear pass through all messages to divide into threads, so that
345
+ // getSortedDates is O(n) instead of O(n^2) !
346
+ const numChildren : { [ date : number ] : number } = { } ;
347
+ for ( const [ _ , message ] of m ) {
348
+ const parent = message . get ( "reply_to" ) ;
349
+ if ( parent != null ) {
350
+ const d = new Date ( parent ) . valueOf ( ) ;
351
+ numChildren [ d ] = ( numChildren [ d ] ?? 0 ) + 1 ;
352
+ }
353
+ }
354
+
335
355
const v : [ date : number , reply_to : number | undefined ] [ ] = [ ] ;
336
356
for ( const [ date , message ] of m ) {
337
357
if ( message == null ) continue ;
338
358
339
359
// If we search for a message, we treat all threads as unfolded
340
360
if ( ! search ) {
341
- const is_thread = isThread ( messages , message ) ;
342
- const is_folded = isFolded ( messages , message , account_id ) ;
343
- const is_thread_body = message . get ( "reply_to" ) != null ;
361
+ const is_thread = isThread ( message , numChildren ) ;
362
+ const is_folded = is_thread && isFolded ( messages , message , account_id ) ;
363
+ const is_thread_body = is_thread && message . get ( "reply_to" ) != null ;
344
364
const folded = is_thread && is_folded && is_thread_body ;
345
365
if ( folded ) {
346
366
numFolded ++ ;
@@ -356,7 +376,7 @@ export function getSortedDates(
356
376
}
357
377
v . sort ( cmpMessages ) ;
358
378
const dates = v . map ( ( z ) => `${ z [ 0 ] } ` ) ;
359
- return { dates, numFolded } ;
379
+ return { dates, numFolded, numChildren } ;
360
380
}
361
381
362
382
/*
@@ -465,6 +485,7 @@ export function MessageList({
465
485
manualScrollRef,
466
486
mode,
467
487
selectedDate,
488
+ numChildren,
468
489
} : {
469
490
messages ;
470
491
account_id ;
@@ -481,6 +502,7 @@ export function MessageList({
481
502
costEstimate ?;
482
503
manualScrollRef ?;
483
504
selectedDate ?: string ;
505
+ numChildren ?;
484
506
} ) {
485
507
const virtuosoHeightsRef = useRef < { [ index : number ] : number } > ( { } ) ;
486
508
const virtuosoScroll = useVirtuosoScrollHook ( {
@@ -510,9 +532,13 @@ export function MessageList({
510
532
return < div style = { { height : "1px" } } /> ;
511
533
}
512
534
513
- const is_thread = isThread ( messages , message ) ;
514
- const is_folded = isFolded ( messages , message , account_id ) ;
515
- const is_thread_body = message . get ( "reply_to" ) != null ;
535
+ // only do threading if numChildren is defined. It's not defined,
536
+ // e.g., when viewing past versions via TimeTravel.
537
+ const is_thread = numChildren != null && isThread ( message , numChildren ) ;
538
+ // optimization: only threads can be folded, so don't waste time
539
+ // checking on folding state if it isn't a thread.
540
+ const is_folded = is_thread && isFolded ( messages , message , account_id ) ;
541
+ const is_thread_body = is_thread && message . get ( "reply_to" ) != null ;
516
542
const h = virtuosoHeightsRef . current [ index ] ;
517
543
518
544
return (
0 commit comments