Skip to content

Commit 9a876a3

Browse files
committed
chat: implement action to jump to message with given date, clearing seach or unfolding threads, as needed
1 parent 86f2ce8 commit 9a876a3

File tree

7 files changed

+137
-57
lines changed

7 files changed

+137
-57
lines changed

src/packages/frontend/chat/actions.ts

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,10 @@ export class ChatActions extends Actions<ChatState> {
104104
handleSyncDBChange({ changes, store: this.store, syncdb: this.syncdb });
105105
};
106106

107-
foldThread = (reply_to: Date, messageIndex?: number) => {
108-
if (this.syncdb == null) return;
107+
toggleFoldThread = (reply_to: Date, messageIndex?: number) => {
108+
if (this.syncdb == null) {
109+
return;
110+
}
109111
const account_id = this.redux.getStore("account").get_account_id();
110112
const cur = this.syncdb.get_one({ event: "chat", date: reply_to });
111113
const folding = cur?.get("folding") ?? List([]);
@@ -122,7 +124,7 @@ export class ChatActions extends Actions<ChatState> {
122124
this.syncdb.commit();
123125

124126
if (folded && messageIndex != null) {
125-
this.scrollToBottom(messageIndex);
127+
this.scrollToIndex(messageIndex);
126128
}
127129
};
128130

@@ -216,7 +218,7 @@ export class ChatActions extends Actions<ChatState> {
216218
?.getIn([`${reply_to.valueOf()}`, "folding"])
217219
?.includes(sender_id)
218220
) {
219-
this.foldThread(reply_to);
221+
this.toggleFoldThread(reply_to);
220222
}
221223
}
222224

@@ -483,18 +485,42 @@ export class ChatActions extends Actions<ChatState> {
483485
// if date is given, scrolls to the bottom of the chat *thread*
484486
// that starts with that date.
485487
// safe to call after closing actions.
486-
scrollToBottom = (index: number = -1) => {
487-
if (this.syncdb == null) return;
488-
// this triggers scroll behavior in the chat-log component.
489-
// no-op, but necessary to trigger a change
488+
clearScrollRequest = () => {
490489
this.frameTreeActions.set_frame_data({
491490
id: this.frameId,
492-
scrollToBottom: null,
491+
scrollToIndex: null,
492+
scrollToDate: null,
493493
});
494+
};
495+
scrollToIndex = (index: number = -1) => {
496+
if (this.syncdb == null) return;
497+
// we first clear, then set it, since scroll to needs to
498+
// work even if it is the same as last time.
499+
// TODO: alternatively, we could get a reference
500+
// to virtuoso and directly control things from here.
501+
this.clearScrollRequest();
502+
setTimeout(() => {
503+
this.frameTreeActions.set_frame_data({
504+
id: this.frameId,
505+
scrollToIndex: index,
506+
scrollToDate: null,
507+
});
508+
}, 1);
509+
};
510+
511+
scrollToBottom = () => {
512+
this.scrollToIndex(Number.MAX_SAFE_INTEGER);
513+
};
514+
515+
scrollToDate = (date: number | Date | string) => {
516+
this.clearScrollRequest();
494517
setTimeout(() => {
495518
this.frameTreeActions.set_frame_data({
496519
id: this.frameId,
497-
scrollToBottom: index,
520+
// string version of ms since epoch, which is the key
521+
// in the messages immutable Map
522+
scrollToDate: `${new Date(date).valueOf()}`,
523+
scrollToIndex: null,
498524
});
499525
}, 1);
500526
};
@@ -516,7 +542,12 @@ export class ChatActions extends Actions<ChatState> {
516542
const path = this.store.get("path") + ".md";
517543
const project_id = this.store.get("project_id");
518544
if (project_id == null) return;
519-
const { dates } = getSortedDates(messages, this.store.get("search"));
545+
const account_id = this.redux.getStore("account").get_account_id();
546+
const { dates } = getSortedDates(
547+
messages,
548+
this.store.get("search"),
549+
account_id,
550+
);
520551
const v: string[] = [];
521552
for (const date of dates) {
522553
const message = messages.get(date);
@@ -996,7 +1027,7 @@ export class ChatActions extends Actions<ChatState> {
9961027
tag: `chat:summarize`,
9971028
noNotification: true,
9981029
});
999-
this.scrollToBottom();
1030+
this.scrollToIndex();
10001031
}
10011032
};
10021033

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

Lines changed: 81 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import Composing from "./composing";
2727
import Message from "./message";
2828
import type { ChatMessageTyped, ChatMessages, Mode } from "./types";
2929
import { getSelectedHashtagsSearch, newest_content } from "./utils";
30-
import { getRootMessage } from "./utils";
30+
import { getRootMessage, getThreadRootDate } from "./utils";
3131
import { DivTempHeight } from "@cocalc/frontend/jupyter/cell-list";
3232
import { filterMessages } from "./filter-messages";
3333

@@ -43,7 +43,9 @@ interface Props {
4343
filterRecentH?;
4444
selectedHashtags;
4545
disableFilters?: boolean;
46-
scrollToBottom?: null | number | undefined;
46+
scrollToIndex?: null | number | undefined;
47+
// scrollToDate = string ms from epoch
48+
scrollToDate?: null | undefined | string;
4749
}
4850

4951
export function ChatLog({
@@ -58,7 +60,8 @@ export function ChatLog({
5860
filterRecentH,
5961
selectedHashtags: selectedHashtags0,
6062
disableFilters,
61-
scrollToBottom,
63+
scrollToIndex,
64+
scrollToDate,
6265
}: Props) {
6366
const messages = useRedux(["messages"], project_id, path) as ChatMessages;
6467
const llm_cost_reply: [number, number] = useRedux(
@@ -71,22 +74,7 @@ export function ChatLog({
7174
const { selectedHashtags, selectedHashtagsSearch } = useMemo(() => {
7275
return getSelectedHashtagsSearch(selectedHashtags0);
7376
}, [selectedHashtags0]);
74-
const search = search0 + " " + selectedHashtagsSearch;
75-
76-
useEffect(() => {
77-
scrollToBottomRef?.current?.(true);
78-
}, [search]);
79-
80-
useEffect(() => {
81-
if (scrollToBottom == null) return;
82-
if (scrollToBottom == -1) {
83-
scrollToBottomRef?.current?.(true);
84-
} else {
85-
// console.log({ scrollToBottom }, " -- not implemented");
86-
virtuosoRef.current?.scrollToIndex({ index: scrollToBottom });
87-
}
88-
actions.setState({ scrollToBottom: undefined });
89-
}, [scrollToBottom]);
77+
const search = (search0 + " " + selectedHashtagsSearch).trim();
9078

9179
const user_map = useTypedRedux("users", "user_map");
9280
const account_id = useTypedRedux("account", "account_id");
@@ -97,7 +85,7 @@ export function ChatLog({
9785
const { dates, numFolded } = getSortedDates(
9886
messages,
9987
search,
100-
account_id,
88+
account_id!,
10189
filterRecentH,
10290
);
10391
// TODO: This is an ugly hack because I'm tired and need to finish this.
@@ -113,6 +101,68 @@ export function ChatLog({
113101
return { dates, numFolded };
114102
}, [messages, search, project_id, path, filterRecentH]);
115103

104+
useEffect(() => {
105+
scrollToBottomRef?.current?.(true);
106+
}, [search]);
107+
108+
useEffect(() => {
109+
if (scrollToIndex == null) {
110+
return;
111+
}
112+
if (scrollToIndex == -1) {
113+
scrollToBottomRef?.current?.(true);
114+
} else {
115+
virtuosoRef.current?.scrollToIndex({ index: scrollToIndex });
116+
}
117+
actions.clearScrollRequest();
118+
}, [scrollToIndex]);
119+
120+
useEffect(() => {
121+
if (scrollToDate == null) {
122+
return;
123+
}
124+
// linear search, which should be fine given that this is not a tight inner loop
125+
const index = sortedDates.indexOf(scrollToDate);
126+
if (index == -1) {
127+
// didn't find it?
128+
const message = messages.get(scrollToDate);
129+
if (message == null) {
130+
// the message really doesn't exist. Weird. Give up.
131+
actions.clearScrollRequest();
132+
return;
133+
}
134+
let tryAgain = false;
135+
// we clear all filters and ALSO make sure
136+
// if message is in a folded thread, then that thread is not folded.
137+
if (account_id && isFolded(messages, message, account_id)) {
138+
// this actually unfolds it, since it was folded.
139+
const date = new Date(
140+
getThreadRootDate({ date: parseFloat(scrollToDate), messages }),
141+
);
142+
actions.toggleFoldThread(date);
143+
tryAgain = true;
144+
}
145+
if (messages.size > sortedDates.length && (search || filterRecentH)) {
146+
// there was a search, so clear it just to be sure -- it could still hide
147+
// the folded threaded
148+
actions.clearAllFilters();
149+
tryAgain = true;
150+
}
151+
if (tryAgain) {
152+
// we have to wait a while for full re-render to happen
153+
setTimeout(() => {
154+
actions.scrollToDate(parseFloat(scrollToDate));
155+
}, 10);
156+
} else {
157+
// totally give up
158+
actions.clearScrollRequest();
159+
}
160+
return;
161+
}
162+
virtuosoRef.current?.scrollToIndex({ index });
163+
actions.clearScrollRequest();
164+
}, [scrollToDate]);
165+
116166
const visibleHashtags = useMemo(() => {
117167
let X = immutableSet<string>([]);
118168
if (disableFilters) {
@@ -137,17 +187,14 @@ export function ChatLog({
137187
scrollToBottomRef.current = (force?: boolean) => {
138188
if (manualScrollRef.current && !force) return;
139189
manualScrollRef.current = false;
140-
virtuosoRef.current?.scrollToIndex({ index: Number.MAX_SAFE_INTEGER });
190+
const doScroll = () =>
191+
virtuosoRef.current?.scrollToIndex({ index: Number.MAX_SAFE_INTEGER });
192+
193+
doScroll();
141194
// sometimes scrolling to bottom is requested before last entry added,
142195
// so we do it again in the next render loop. This seems needed mainly
143196
// for side chat when there is little vertical space.
144-
setTimeout(
145-
() =>
146-
virtuosoRef.current?.scrollToIndex({
147-
index: Number.MAX_SAFE_INTEGER,
148-
}),
149-
0,
150-
);
197+
setTimeout(doScroll, 1);
151198
};
152199
}, [scrollToBottomRef != null]);
153200

@@ -256,9 +303,11 @@ function isThread(messages: ChatMessages, message: ChatMessageTyped) {
256303
function isFolded(
257304
messages: ChatMessages,
258305
message: ChatMessageTyped,
259-
account_id?: string,
306+
account_id: string,
260307
) {
261-
if (account_id == null) return false;
308+
if (account_id == null) {
309+
return false;
310+
}
262311
const rootMsg = getRootMessage({ message: message.toJS(), messages });
263312
return rootMsg?.get("folding")?.includes(account_id) ?? false;
264313
}
@@ -271,8 +320,8 @@ function isFolded(
271320
// It was very easy to sort these before reply_to, which complicates things.
272321
export function getSortedDates(
273322
messages: ChatMessages,
274-
search?: string,
275-
account_id?: string,
323+
search: string | undefined,
324+
account_id: string,
276325
filterRecentH?: number,
277326
): { dates: string[]; numFolded: number } {
278327
let numFolded = 0;

src/packages/frontend/chat/chatroom.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ export function ChatRoom({
9090
const search = desc.get("data-search") ?? "";
9191
const filterRecentH: number = desc.get("data-filterRecentH") ?? 0;
9292
const selectedHashtags = desc.get("data-selectedHashtags");
93-
const scrollToBottom = desc.get("data-scrollToBottom") ?? null;
93+
const scrollToIndex = desc.get("data-scrollToIndex") ?? null;
94+
const scrollToDate = desc.get("data-scrollToDate") ?? null;
9495

9596
const messages = useEditor("messages");
9697
const [filterRecentHCustom, setFilterRecentHCustom] = useState<string>("");
@@ -288,7 +289,8 @@ export function ChatRoom({
288289
search={search}
289290
filterRecentH={filterRecentH}
290291
selectedHashtags={selectedHashtags}
291-
scrollToBottom={scrollToBottom}
292+
scrollToIndex={scrollToIndex}
293+
scrollToDate={scrollToDate}
292294
/>
293295
{render_preview_message()}
294296
</div>

src/packages/frontend/chat/message.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ export default function Message(props: Readonly<Props>) {
693693
reply = replyMentionsRef.current?.() ?? replyMessageRef.current;
694694
}
695695
props.actions.sendReply({ message: message.toJS(), reply });
696-
props.actions.scrollToBottom(props.index);
696+
props.actions.scrollToIndex(props.index);
697697
}
698698

699699
function renderComposeReply() {
@@ -852,7 +852,7 @@ export default function Message(props: Readonly<Props>) {
852852
type="text"
853853
icon={<Icon name="down-circle-o" />}
854854
onClick={() =>
855-
props.actions?.foldThread(message.get("date"), props.index)
855+
props.actions?.toggleFoldThread(message.get("date"), props.index)
856856
}
857857
>
858858
<Text type="secondary">Unfold</Text>
@@ -887,7 +887,7 @@ export default function Message(props: Readonly<Props>) {
887887
type="text"
888888
style={style}
889889
onClick={() =>
890-
props.actions?.foldThread(message.get("date"), props.index)
890+
props.actions?.toggleFoldThread(message.get("date"), props.index)
891891
}
892892
icon={
893893
<Icon

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ export default function SideChat({
4848
const input: string = useRedux(["input"], project_id, path);
4949
const search = desc?.get("data-search") ?? "";
5050
const selectedHashtags = desc?.get("data-selectedHashtags");
51-
const scrollToBottom = desc.get("data-scrollToBottom") ?? null;
51+
const scrollToIndex = desc.get("data-scrollToIndex") ?? null;
52+
const scrollToDate = desc.get("data-scrollToIDate") ?? null;
5253
const addCollab: boolean = useRedux(["add_collab"], project_id, path);
5354
const is_uploading = useRedux(["is_uploading"], project_id, path);
5455
const project_map = useTypedRedux("projects", "project_map");
@@ -179,7 +180,8 @@ export default function SideChat({
179180
search={search}
180181
selectedHashtags={selectedHashtags}
181182
disableFilters={disableFilters}
182-
scrollToBottom={scrollToBottom}
183+
scrollToIndex={scrollToIndex}
184+
scrollToDate={scrollToDate}
183185
/>
184186
</div>
185187

src/packages/frontend/chat/store.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,6 @@ export interface ChatState {
2929
has_unsaved_changes: boolean;
3030
unsent_user_mentions: MentionList;
3131
is_uploading: boolean;
32-
// whenever this changes and is defined, do a scroll.
33-
// scrollToBottom = 0 -- scroll to the bottom
34-
// scrollToBottom = ms since epoch -- scroll to the bottom of that thread
35-
scrollToBottom?: number | null;
3632
filterRecentH: number;
3733
llm_cost_room?: [number, number] | null;
3834
llm_cost_reply?: [number, number] | null;

src/packages/frontend/frame-editors/chat-editor/actions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,10 @@ export class Actions extends CodeEditorActions<ChatEditorState> {
131131
}
132132

133133
scrollToBottom = (frameId) => {
134-
this.getChatActions(frameId)?.scrollToBottom();
134+
this.getChatActions(frameId)?.scrollToIndex(-1);
135135
};
136136

137137
scrollToTop = (frameId) => {
138-
this.getChatActions(frameId)?.scrollToBottom(0);
138+
this.getChatActions(frameId)?.scrollToIndex(0);
139139
};
140140
}

0 commit comments

Comments
 (0)