Skip to content

Commit d4ce947

Browse files
authored
Merge pull request #7598 from sagemathinc/chat-improve-today-filter
frontend/chat: improve "today" filtering and search
2 parents a8d7590 + 16d6f06 commit d4ce947

File tree

8 files changed

+202
-50
lines changed

8 files changed

+202
-50
lines changed

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

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@ import {
2222
import { VisibleMDLG } from "@cocalc/frontend/components";
2323
import useVirtuosoScrollHook from "@cocalc/frontend/components/virtuoso-scroll-hook";
2424
import { HashtagBar } from "@cocalc/frontend/editors/task-editor/hashtag-bar";
25+
import { webapp_client } from "@cocalc/frontend/webapp-client";
2526
import {
2627
cmp,
28+
hoursToTimeIntervalHuman,
2729
parse_hashtags,
2830
search_match,
2931
search_split,
3032
} from "@cocalc/util/misc";
31-
import { webapp_client } from "../webapp-client";
3233
import { ChatActions, getRootMessage } from "./actions";
3334
import Composing from "./composing";
3435
import Message from "./message";
@@ -78,7 +79,7 @@ export function ChatLog(props: Readonly<Props>) {
7879
actions.setState({ scrollToBottom: undefined });
7980
}, [scrollToBottom]);
8081

81-
const today = useRedux(["today"], project_id, path);
82+
const filterRecentH = useRedux(["filterRecentH"], project_id, path);
8283
const user_map = useTypedRedux("users", "user_map");
8384
const account_id = useTypedRedux("account", "account_id");
8485
const { dates: sortedDates, numFolded } = useMemo<{
@@ -89,10 +90,10 @@ export function ChatLog(props: Readonly<Props>) {
8990
messages,
9091
search,
9192
account_id,
92-
today,
93+
filterRecentH,
9394
);
9495
return { dates, numFolded };
95-
}, [messages, search, project_id, path, today]);
96+
}, [messages, search, project_id, path, filterRecentH]);
9697

9798
const visibleHashtags = useMemo(() => {
9899
let X = immutableSet<string>([]);
@@ -115,13 +116,15 @@ export function ChatLog(props: Readonly<Props>) {
115116
scrollToBottomRef.current = (force?: boolean) => {
116117
if (manualScrollRef.current && !force) return;
117118
manualScrollRef.current = false;
118-
virtuosoRef.current?.scrollToIndex({ index: 99999999999999999999 });
119+
virtuosoRef.current?.scrollToIndex({ index: Number.MAX_SAFE_INTEGER });
119120
// sometimes scrolling to bottom is requested before last entry added,
120121
// so we do it again in the next render loop. This seems needed mainly
121122
// for side chat when there is little vertical space.
122123
setTimeout(
123124
() =>
124-
virtuosoRef.current?.scrollToIndex({ index: 99999999999999999999 }),
125+
virtuosoRef.current?.scrollToIndex({
126+
index: Number.MAX_SAFE_INTEGER,
127+
}),
125128
0,
126129
);
127130
};
@@ -151,7 +154,7 @@ export function ChatLog(props: Readonly<Props>) {
151154
<NotShowing
152155
num={messages.size - numFolded - sortedDates.length}
153156
search={search}
154-
today={today}
157+
filterRecentH={filterRecentH}
155158
/>
156159
)}
157160
<Virtuoso
@@ -167,7 +170,10 @@ export function ChatLog(props: Readonly<Props>) {
167170
}
168171

169172
const is_thread = isThread(messages, message);
170-
const is_folded = isFolded(messages, message, account_id);
173+
// if we search for a message, we treat all threads as unfolded
174+
const force_unfold = !!search;
175+
const is_folded =
176+
!force_unfold && isFolded(messages, message, account_id);
171177
const is_thread_body = message.get("reply_to") != null;
172178

173179
return (
@@ -185,6 +191,7 @@ export function ChatLog(props: Readonly<Props>) {
185191
actions={actions}
186192
is_thread={is_thread}
187193
is_folded={is_folded}
194+
force_unfold={force_unfold}
188195
is_thread_body={is_thread_body}
189196
is_prev_sender={isPrevMessageSender(
190197
index,
@@ -304,7 +311,7 @@ export function getSortedDates(
304311
messages: ChatMessages,
305312
search?: string,
306313
account_id?: string,
307-
today?: boolean,
314+
filterRecentH?: number,
308315
): { dates: string[]; numFolded: number } {
309316
let numFolded = 0;
310317
let m = messages;
@@ -315,8 +322,9 @@ export function getSortedDates(
315322
m = m.filter((message) => searchMatches(message, searchTerms));
316323
}
317324

318-
if (today) {
319-
const cutoff = webapp_client.server_time().getTime() - 1000 * 24 * 60 * 60;
325+
if (typeof filterRecentH === "number" && filterRecentH > 0) {
326+
const now = webapp_client.server_time().getTime();
327+
const cutoff = now - filterRecentH * 1000 * 60 * 60;
320328
m = m.filter((msg) => {
321329
const date = msg.get("date").getTime();
322330
return date >= cutoff;
@@ -327,13 +335,16 @@ export function getSortedDates(
327335
for (const [date, message] of m) {
328336
if (message == null) continue;
329337

330-
const is_thread = isThread(messages, message);
331-
const is_folded = isFolded(messages, message, account_id);
332-
const is_thread_body = message.get("reply_to") != null;
333-
const folded = is_thread && is_folded && is_thread_body;
334-
if (folded) {
335-
numFolded++;
336-
continue;
338+
// If we search for a message, we treat all threads as unfolded
339+
if (!search) {
340+
const is_thread = isThread(messages, message);
341+
const is_folded = isFolded(messages, message, account_id);
342+
const is_thread_body = message.get("reply_to") != null;
343+
const folded = is_thread && is_folded && is_thread_body;
344+
if (folded) {
345+
numFolded++;
346+
continue;
347+
}
337348
}
338349

339350
const reply_to = message.get("reply_to");
@@ -384,8 +395,18 @@ export function getUserName(userMap, accountId: string): string {
384395
return account.get("first_name", "") + " " + account.get("last_name", "");
385396
}
386397

387-
function NotShowing({ num, search, today }) {
398+
interface NotShowingProps {
399+
num: number;
400+
search: string;
401+
filterRecentH: number;
402+
}
403+
404+
function NotShowing({ num, search, filterRecentH }: NotShowingProps) {
388405
if (num <= 0) return null;
406+
407+
const timespan =
408+
filterRecentH > 0 ? hoursToTimeIntervalHuman(filterRecentH) : null;
409+
389410
return (
390411
<Alert
391412
style={{ margin: "0" }}
@@ -399,8 +420,10 @@ function NotShowing({ num, search, today }) {
399420
{search.trim()
400421
? ` that do not match search for '${search.trim()}'`
401422
: ""}
402-
{today
403-
? ` ${search.trim() ? "and" : "that"} were not sent today`
423+
{timespan
424+
? ` ${
425+
search.trim() ? "and" : "that"
426+
} were not sent in the past ${timespan}`
404427
: ""}
405428
.
406429
</b>

src/packages/frontend/chat/chatroom.tsx

Lines changed: 109 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
44
*/
55

6-
import { Button, Checkbox } from "antd";
6+
import { Button, Divider, Input, Select, Tooltip } from "antd";
77
import { debounce } from "lodash";
88
import { useDebounce } from "use-debounce";
99

@@ -21,6 +21,7 @@ import {
2121
useEffect,
2222
useRedux,
2323
useRef,
24+
useState,
2425
} from "@cocalc/frontend/app-framework";
2526
import {
2627
Icon,
@@ -34,7 +35,7 @@ import SelectComputeServerForFile from "@cocalc/frontend/compute/select-server-f
3435
import StaticMarkdown from "@cocalc/frontend/editors/slate/static-markdown";
3536
import { SaveButton } from "@cocalc/frontend/frame-editors/frame-tree/save-button";
3637
import { sanitize_html_safe } from "@cocalc/frontend/misc";
37-
import { history_path } from "@cocalc/util/misc";
38+
import { history_path, hoursToTimeIntervalHuman } from "@cocalc/util/misc";
3839
import { ChatActions } from "./actions";
3940
import { ChatLog } from "./chat-log";
4041
import ChatInput from "./input";
@@ -43,6 +44,8 @@ import { SubmitMentionsFn } from "./types";
4344
import { INPUT_HEIGHT, markChatAsReadIfUnseen } from "./utils";
4445
import VideoChatButton from "./video/launch-button";
4546

47+
const FILTER_RECENT_NONE = { value: 0, label: "All" } as const;
48+
4649
const PREVIEW_STYLE: React.CSSProperties = {
4750
background: "#f5f5f5",
4851
fontSize: "14px",
@@ -95,7 +98,9 @@ export const ChatRoom: React.FC<Props> = ({ project_id, path }) => {
9598

9699
const search = useRedux(["search"], project_id, path);
97100
const messages = useRedux(["messages"], project_id, path);
98-
const today: boolean = useRedux(["today"], project_id, path);
101+
const filterRecentH: number = useRedux(["filterRecentH"], project_id, path);
102+
const [filterRecentHCustom, setFilterRecentHCustom] = useState<string>("");
103+
const [filterRecentOpen, setFilterRecentOpen] = useState<boolean>(false);
99104
const llm_cost_room: [number, number] = useRedux(
100105
["llm_cost_room"],
101106
project_id,
@@ -294,8 +299,10 @@ export const ChatRoom: React.FC<Props> = ({ project_id, path }) => {
294299
default_value={search}
295300
on_change={debounce(
296301
(value) => actions.setState({ search: value }),
297-
250,
302+
150,
303+
{ leading: false, trailing: true },
298304
)}
305+
status={!search ? undefined : "warning"}
299306
style={{
300307
margin: 0,
301308
width: "100%",
@@ -308,6 +315,93 @@ export const ChatRoom: React.FC<Props> = ({ project_id, path }) => {
308315
);
309316
}
310317

318+
function isValidFilterRecentCustom(): boolean {
319+
const v = parseFloat(filterRecentHCustom);
320+
return isFinite(v) && v >= 0;
321+
}
322+
323+
function renderFilterRecent() {
324+
return (
325+
<Select
326+
open={filterRecentOpen}
327+
onDropdownVisibleChange={(v) => setFilterRecentOpen(v)}
328+
value={filterRecentH}
329+
status={filterRecentH > 0 ? "warning" : undefined}
330+
allowClear
331+
onClear={() => {
332+
actions.setState({ filterRecentH: 0 });
333+
setFilterRecentHCustom("");
334+
}}
335+
style={{ width: 120 }}
336+
popupMatchSelectWidth={false}
337+
onSelect={(val: number) => actions.setState({ filterRecentH: val })}
338+
options={[
339+
FILTER_RECENT_NONE,
340+
...[1, 6, 12, 24, 48, 24 * 7, 14 * 24, 28 * 24].map((value) => {
341+
const label = hoursToTimeIntervalHuman(value);
342+
return { value, label };
343+
}),
344+
]}
345+
labelRender={({ label, value }) => {
346+
if (!label) {
347+
if (isValidFilterRecentCustom()) {
348+
value = parseFloat(filterRecentHCustom);
349+
label = hoursToTimeIntervalHuman(value);
350+
} else {
351+
({ label, value } = FILTER_RECENT_NONE);
352+
}
353+
}
354+
return (
355+
<Tooltip
356+
title={
357+
value === 0
358+
? `All messages.`
359+
: `Only messages sent in the past ${label}.`
360+
}
361+
>
362+
{label}
363+
</Tooltip>
364+
);
365+
}}
366+
dropdownRender={(menu) => (
367+
<>
368+
{menu}
369+
<Divider style={{ margin: "8px 0" }} />
370+
<Input
371+
placeholder="Number of hours"
372+
allowClear
373+
value={filterRecentHCustom}
374+
status={
375+
filterRecentHCustom == "" || isValidFilterRecentCustom()
376+
? undefined
377+
: "error"
378+
}
379+
onChange={debounce(
380+
(e: React.ChangeEvent<HTMLInputElement>) => {
381+
const v = e.target.value;
382+
setFilterRecentHCustom(v);
383+
const val = parseFloat(v);
384+
if (isFinite(val) && val >= 0) {
385+
actions.setState({ filterRecentH: val });
386+
} else if (v == "") {
387+
actions.setState({
388+
filterRecentH: FILTER_RECENT_NONE.value,
389+
});
390+
}
391+
},
392+
150,
393+
{ leading: true, trailing: true },
394+
)}
395+
onKeyDown={(e) => e.stopPropagation()}
396+
onPressEnter={() => setFilterRecentOpen(false)}
397+
addonAfter={<span style={{ paddingLeft: "5px" }}>hours</span>}
398+
/>
399+
</>
400+
)}
401+
/>
402+
);
403+
}
404+
311405
function render_button_row() {
312406
return (
313407
<Row style={{ marginLeft: 0, marginRight: 0 }}>
@@ -348,17 +442,17 @@ export const ChatRoom: React.FC<Props> = ({ project_id, path }) => {
348442
</Button>
349443
)}
350444
</Col>
351-
<Col xs={3} md={3} style={{ padding: "2px", display: "flex" }}>
352-
<div style={{ marginTop: "5px" }}>
353-
<Checkbox
354-
checked={today}
355-
onChange={() => {
356-
actions.setState({ today: !today });
357-
}}
358-
>
359-
Today
360-
</Checkbox>
361-
</div>
445+
<Col
446+
xs={3}
447+
md={3}
448+
style={{
449+
padding: "2px",
450+
display: "flex",
451+
verticalAlign: "center",
452+
gap: "5px",
453+
}}
454+
>
455+
{renderFilterRecent()}
362456
{render_search()}
363457
</Col>
364458
</Row>

0 commit comments

Comments
 (0)