1
1
/*
2
2
3
- Find all messages that match a given collection of filters.
3
+ Find all threads that match a given collection of filters.
4
4
5
+ NOTE: chat uses every imaginable way to store a timestamp at once,
6
+ which is the may source of weirdness in the code below... Beware.
5
7
*/
6
8
7
9
import type { ChatMessages , ChatMessageTyped , MessageHistory } from "./types" ;
8
10
import { search_match , search_split } from "@cocalc/util/misc" ;
9
11
import { List } from "immutable" ;
10
12
import type { TypedMap } from "@cocalc/frontend/app-framework" ;
11
13
import { webapp_client } from "@cocalc/frontend/webapp-client" ;
14
+ import LRU from "lru-cache" ;
12
15
13
16
export function filterMessages ( {
14
17
messages,
@@ -20,44 +23,108 @@ export function filterMessages({
20
23
filter ?: string ;
21
24
filterRecentH ?: number ;
22
25
} ) {
23
- let messages0 = messages ;
26
+ filter = filter ?. trim ( ) ;
27
+
28
+ if ( ! ( filter || ( typeof filterRecentH === "number" && filterRecentH > 0 ) ) ) {
29
+ // no filters -- typical special case; waste now time.
30
+ return messages ;
31
+ }
32
+ const searchData = getSearchData ( messages ) ;
33
+ let matchingRootTimes : Set < string > ;
24
34
if ( filter ) {
35
+ matchingRootTimes = new Set < string > ( ) ;
25
36
const searchTerms = search_split ( filter ) ;
26
- messages0 = messages0 . filter ( ( message ) =>
27
- searchMatches ( message , searchTerms ) ,
28
- ) ;
37
+ for ( const rootTime in searchData ) {
38
+ const { content } = searchData [ rootTime ] ;
39
+ if ( search_match ( content , searchTerms ) ) {
40
+ matchingRootTimes . add ( rootTime ) ;
41
+ }
42
+ }
43
+ } else {
44
+ matchingRootTimes = new Set ( Object . keys ( searchData ) ) ;
29
45
}
30
-
31
46
if ( typeof filterRecentH === "number" && filterRecentH > 0 ) {
47
+ // remove anything from matchingRootTimes that doesn't match
32
48
const now = webapp_client . server_time ( ) . getTime ( ) ;
33
49
const cutoff = now - filterRecentH * 1000 * 60 * 60 ;
34
- messages0 = messages0 . filter ( ( message ) => {
35
- const date = message . get ( "date" ) . getTime ( ) ;
36
- return date >= cutoff ;
37
- } ) ;
50
+ const x = new Set < string > ( ) ;
51
+ for ( const rootTime of matchingRootTimes ) {
52
+ const { newestTime } = searchData [ rootTime ] ;
53
+ if ( newestTime >= cutoff ) {
54
+ x . add ( rootTime ) ;
55
+ }
56
+ }
57
+ matchingRootTimes = x ;
38
58
}
39
59
40
- if ( messages0 . size == 0 ) {
41
- // nothing matches
42
- return messages0 ;
43
- }
44
-
45
- // Next, we expand to include all threads containing any matching messages.
46
- // First find the roots of all matching threads:
47
- const roots = new Set < string > ( ) ;
48
- for ( const [ _ , message ] of messages0 ) {
49
- roots . add ( message . get ( "reply_to" ) ?? message . get ( "date" ) . toISOString ( ) ) ;
50
- }
60
+ // Finally take all messages in all threads that have root in matchingRootTimes.
51
61
// Return all messages in these threads
52
- return messages . filter ( ( message ) => roots . has ( message . get ( "reply_to" ) ?? message . get ( "date" ) . toISOString ( ) ) ) ;
62
+ // @ts -ignore -- immutable js typing seems wrong for filter
63
+ const matchingThreads = messages . filter ( ( message , time ) => {
64
+ const reply_to = message . get ( "reply_to" ) ; // iso string if defined
65
+ let rootTime : string ;
66
+ if ( reply_to != null ) {
67
+ rootTime = `${ new Date ( reply_to ) . valueOf ( ) } ` ;
68
+ } else {
69
+ rootTime = time ;
70
+ }
71
+ return matchingRootTimes . has ( rootTime ) ;
72
+ } ) ;
73
+
74
+ return matchingThreads ;
53
75
}
54
76
55
77
// NOTE: I removed search including send name, since that would
56
- // be slower and of questionable value.
57
- export function searchMatches ( message : ChatMessageTyped , searchTerms ) : boolean {
78
+ // be slower and of questionable value. Maybe we want to add it back?
79
+ // A dropdown listing people might be better though, similar to the
80
+ // time filter.
81
+ function getContent ( message : ChatMessageTyped ) : string {
58
82
const first = message . get ( "history" , List ( ) ) . first ( ) as
59
83
| TypedMap < MessageHistory >
60
84
| undefined ;
61
- if ( first == null ) return false ;
62
- return search_match ( first . get ( "content" , "" ) , searchTerms ) ;
85
+ return first ?. get ( "content" ) ?? "" ;
86
+ }
87
+
88
+ // Make a map
89
+ // thread root timestamp --> {content:string; newest_message:Date}
90
+ // We can then use this to find the thread root timestamps that match the entire search
91
+
92
+ type SearchData = {
93
+ // time in ms but as string
94
+ // newestTime in ms as actual number (suitable to compare)
95
+ [ rootTime : string ] : { content : string ; newestTime : number } ;
96
+ } ;
97
+
98
+ const cache = new LRU < ChatMessages , SearchData > ( { max : 25 } ) ;
99
+
100
+ function getSearchData ( messages ) : SearchData {
101
+ if ( cache . has ( messages ) ) {
102
+ return cache . get ( messages ) ! ;
103
+ }
104
+ const data : SearchData = { } ;
105
+ for ( const [ time , message ] of messages ) {
106
+ let rootTime : string ;
107
+ if ( message . get ( "reply_to" ) ) {
108
+ // non-root in thread
109
+ rootTime = `${ new Date ( message . get ( "reply_to" ) ) . valueOf ( ) } ` ;
110
+ } else {
111
+ // new root thread
112
+ rootTime = time ;
113
+ }
114
+ const messageTime = parseFloat ( time ) ;
115
+ const content = getContent ( message ) ;
116
+ if ( data [ rootTime ] == null ) {
117
+ data [ rootTime ] = {
118
+ content,
119
+ newestTime : messageTime ,
120
+ } ;
121
+ } else {
122
+ data [ rootTime ] . content += "\n" + content ;
123
+ if ( data [ rootTime ] . newestTime < messageTime ) {
124
+ data [ rootTime ] . newestTime = messageTime ;
125
+ }
126
+ }
127
+ }
128
+ cache . set ( messages , data ) ;
129
+ return data ;
63
130
}
0 commit comments