Skip to content

Commit 481a4e9

Browse files
committed
change chat search to be "by thread" instead of "by message"
1 parent db7ef00 commit 481a4e9

File tree

4 files changed

+119
-32
lines changed

4 files changed

+119
-32
lines changed

src/packages/frontend/chat/README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
# Chat
22

33
WARNING: like all development docs, don't trust anything technical in
4-
this file; instead, only trust the code itself! Nobody ever looks at
4+
this file; instead, only trust the code itself! Nobody ever looks at
55
docs like this, except people very new to the codebase, hence they tend
66
to just maximize confusion.
77

8+
## Timestamps
9+
10+
Note: There are a couple of ways to represent a time in Javascript:
11+
12+
- iso string
13+
- ms since epoch as a number
14+
- string version of ms since epoch
15+
- Date object
16+
17+
The data structures for chat have somehow evolved since that
18+
crazy Sage Days by the Ocean in WA to use all of these at once, which is
19+
confusing and annoying. Be careful!
20+
821
## Overview
922

1023
CoCalc has two chat views.
@@ -59,4 +72,3 @@ date : Date Object
5972
history : immutable.List of immutable.Maps
6073
editing : immutable.Map
6174
```
62-

src/packages/frontend/chat/chat-log.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export function ChatLog({
160160
{messages != null && (
161161
<NotShowing
162162
num={messages.size - numFolded - sortedDates.length}
163+
showing={sortedDates.length}
163164
search={search}
164165
filterRecentH={filterRecentH}
165166
actions={actions}
@@ -343,17 +344,24 @@ interface NotShowingProps {
343344
search: string;
344345
filterRecentH: number;
345346
actions;
347+
showing;
346348
}
347349

348-
function NotShowing({ num, search, filterRecentH, actions }: NotShowingProps) {
350+
function NotShowing({
351+
num,
352+
search,
353+
filterRecentH,
354+
actions,
355+
showing,
356+
}: NotShowingProps) {
349357
if (num <= 0) return null;
350358

351359
const timespan =
352360
filterRecentH > 0 ? hoursToTimeIntervalHuman(filterRecentH) : null;
353361

354362
return (
355363
<Alert
356-
style={{ marginBottom: "5px" }}
364+
style={{ margin: "5px" }}
357365
showIcon
358366
type="warning"
359367
message={
@@ -370,7 +378,7 @@ function NotShowing({ num, search, filterRecentH, actions }: NotShowingProps) {
370378
search.trim() ? "and" : "that"
371379
} were not sent in the past ${timespan}`
372380
: ""}
373-
.
381+
. Showing {showing} {plural(showing, "message")}.
374382
</b>
375383
<Button
376384
onClick={() => {
Lines changed: 93 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
/*
22
3-
Find all messages that match a given collection of filters.
3+
Find all threads that match a given collection of filters.
44
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.
57
*/
68

79
import type { ChatMessages, ChatMessageTyped, MessageHistory } from "./types";
810
import { search_match, search_split } from "@cocalc/util/misc";
911
import { List } from "immutable";
1012
import type { TypedMap } from "@cocalc/frontend/app-framework";
1113
import { webapp_client } from "@cocalc/frontend/webapp-client";
14+
import LRU from "lru-cache";
1215

1316
export function filterMessages({
1417
messages,
@@ -20,44 +23,108 @@ export function filterMessages({
2023
filter?: string;
2124
filterRecentH?: number;
2225
}) {
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>;
2434
if (filter) {
35+
matchingRootTimes = new Set<string>();
2536
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));
2945
}
30-
3146
if (typeof filterRecentH === "number" && filterRecentH > 0) {
47+
// remove anything from matchingRootTimes that doesn't match
3248
const now = webapp_client.server_time().getTime();
3349
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;
3858
}
3959

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.
5161
// 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;
5375
}
5476

5577
// 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 {
5882
const first = message.get("history", List()).first() as
5983
| TypedMap<MessageHistory>
6084
| 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;
63130
}

src/packages/frontend/chat/filter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default function Filter({ actions, search, style }) {
2626
<Tooltip
2727
title={
2828
!value
29-
? "Show only threads containing at least one message that matches this search. Use /re/ for a regular expression, quotes, and dashes to negate."
29+
? "Show only threads that match this search. Use /re/ for a regular expression, quotes, and dashes to negate."
3030
: undefined
3131
}
3232
>

0 commit comments

Comments
 (0)