Skip to content

Commit 7ed8e5d

Browse files
committed
chatrooms: fragment urls working
- side chat not implemented yet
1 parent 3ecb232 commit 7ed8e5d

File tree

10 files changed

+117
-15
lines changed

10 files changed

+117
-15
lines changed

src/packages/frontend/chat/actions.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ import type {
5252
} from "./types";
5353
import { history_path } from "@cocalc/util/misc";
5454
import { initFromSyncDB, handleSyncDBChange, processSyncDBObj } from "./sync";
55-
import { getReplyToRoot, getThreadRootDate } from "./utils";
55+
import { getReplyToRoot, getThreadRootDate, toMsString } from "./utils";
56+
import Fragment from "@cocalc/frontend/misc/fragment-id";
5657

5758
const MAX_CHATSTREAM = 10;
5859

@@ -512,14 +513,18 @@ export class ChatActions extends Actions<ChatState> {
512513
this.scrollToIndex(Number.MAX_SAFE_INTEGER);
513514
};
514515

515-
scrollToDate = (date: number | Date | string) => {
516+
scrollToDate = (date) => {
516517
this.clearScrollRequest();
518+
this.frameTreeActions.set_frame_data({
519+
id: this.frameId,
520+
fragmentId: toMsString(date),
521+
});
517522
setTimeout(() => {
518523
this.frameTreeActions.set_frame_data({
519524
id: this.frameId,
520525
// string version of ms since epoch, which is the key
521526
// in the messages immutable Map
522-
scrollToDate: `${new Date(date).valueOf()}`,
527+
scrollToDate: toMsString(date),
523528
scrollToIndex: null,
524529
});
525530
}, 1);
@@ -1091,6 +1096,16 @@ export class ChatActions extends Actions<ChatState> {
10911096
selectedHashtags,
10921097
});
10931098
};
1099+
1100+
setFragment = (date?) => {
1101+
if (!date) {
1102+
Fragment.clear();
1103+
} else {
1104+
const fragmentId = toMsString(date);
1105+
Fragment.set({ chat: fragmentId });
1106+
this.frameTreeActions.set_frame_data({ id: this.frameId, fragmentId });
1107+
}
1108+
};
10941109
}
10951110

10961111
// We strip out any cased version of the string @chatgpt and also all mentions.

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ interface Props {
4646
scrollToIndex?: null | number | undefined;
4747
// scrollToDate = string ms from epoch
4848
scrollToDate?: null | undefined | string;
49+
selectedDate?: string;
4950
}
5051

5152
export function ChatLog({
@@ -62,6 +63,7 @@ export function ChatLog({
6263
disableFilters,
6364
scrollToIndex,
6465
scrollToDate,
66+
selectedDate,
6567
}: Props) {
6668
const messages = useRedux(["messages"], project_id, path) as ChatMessages;
6769
const llm_cost_reply: [number, number] = useRedux(
@@ -237,6 +239,7 @@ export function ChatLog({
237239
llm_cost_reply,
238240
manualScrollRef,
239241
mode,
242+
selectedDate,
240243
}}
241244
/>
242245
<Composing
@@ -464,6 +467,7 @@ export function MessageList({
464467
llm_cost_reply,
465468
manualScrollRef,
466469
mode,
470+
selectedDate,
467471
}: {
468472
messages;
469473
account_id;
@@ -479,6 +483,7 @@ export function MessageList({
479483
actions?;
480484
llm_cost_reply?;
481485
manualScrollRef?;
486+
selectedDate?: string;
482487
}) {
483488
const virtuosoHeightsRef = useRef<{ [index: number]: number }>({});
484489
const virtuosoScroll = useVirtuosoScrollHook({
@@ -528,6 +533,7 @@ export function MessageList({
528533
account_id={account_id}
529534
user_map={user_map}
530535
message={message}
536+
selected={date == selectedDate}
531537
project_id={project_id}
532538
path={path}
533539
font_size={fontSize}

src/packages/frontend/chat/chatroom.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export function ChatRoom({
9292
const selectedHashtags = desc.get("data-selectedHashtags");
9393
const scrollToIndex = desc.get("data-scrollToIndex") ?? null;
9494
const scrollToDate = desc.get("data-scrollToDate") ?? null;
95+
const fragmentId = desc.get("data-fragmentId") ?? null;
9596

9697
const messages = useEditor("messages");
9798
const [filterRecentHCustom, setFilterRecentHCustom] = useState<string>("");
@@ -291,6 +292,7 @@ export function ChatRoom({
291292
selectedHashtags={selectedHashtags}
292293
scrollToIndex={scrollToIndex}
293294
scrollToDate={scrollToDate}
295+
selectedDate={fragmentId}
294296
/>
295297
{render_preview_message()}
296298
</div>

src/packages/frontend/chat/message.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ const AVATAR_MARGIN_LEFTRIGHT = "15px";
100100
interface Props {
101101
index: number;
102102
actions?: ChatActions;
103-
104103
get_user_name: (account_id?: string) => string;
105104
messages;
106105
message: ChatMessageTyped;
@@ -126,6 +125,8 @@ interface Props {
126125
is_thread_body: boolean;
127126

128127
llm_cost_reply?: [number, number] | null;
128+
129+
selected?: boolean;
129130
}
130131

131132
export default function Message(props: Readonly<Props>) {
@@ -139,6 +140,7 @@ export default function Message(props: Readonly<Props>) {
139140
mode,
140141
project_id,
141142
font_size,
143+
selected,
142144
} = props;
143145

144146
const showAISummarize = redux
@@ -410,8 +412,11 @@ export default function Message(props: Readonly<Props>) {
410412
marginTop,
411413
borderRadius,
412414
fontSize: font_size,
413-
padding: "9px",
414-
...(mode === "sidechat" ? { marginLeft: "5px", marginRight: "5px" } : {}),
415+
padding: selected ? "6px" : "9px",
416+
...(mode === "sidechat"
417+
? { marginLeft: "5px", marginRight: "5px" }
418+
: undefined),
419+
...(selected ? { border: "3px solid #66bb6a" } : undefined),
415420
} as const;
416421

417422
const mainXS = mode === "standalone" ? 20 : 22;
@@ -422,7 +427,12 @@ export default function Message(props: Readonly<Props>) {
422427

423428
return (
424429
<Col key={1} xs={mainXS}>
425-
<div style={{ display: "flex" }}>
430+
<div
431+
style={{ display: "flex" }}
432+
onClick={() => {
433+
props.actions?.setFragment(message.get("date"));
434+
}}
435+
>
426436
{!props.is_prev_sender &&
427437
!is_viewers_message &&
428438
message.get("sender_id") ? (
@@ -468,6 +478,24 @@ export default function Message(props: Readonly<Props>) {
468478
<Icon name="thumbs-up" />
469479
</Button>
470480
)}{" "}
481+
<Tooltip title="Select this message. Copy the browser URL to link to this message.">
482+
<Button
483+
onClick={() => {
484+
props.actions?.setFragment(message.get("date"));
485+
}}
486+
size="small"
487+
type={"text"}
488+
style={{
489+
marginRight: "5px",
490+
float: "right",
491+
marginTop: "-4px",
492+
color: is_viewers_message ? "white" : "#888",
493+
fontSize: "12px",
494+
}}
495+
>
496+
<Icon name="link" />
497+
</Button>
498+
</Tooltip>
471499
</span>
472500
)}
473501
{!isEditing && (

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export default function SideChat({
5050
const selectedHashtags = desc?.get("data-selectedHashtags");
5151
const scrollToIndex = desc.get("data-scrollToIndex") ?? null;
5252
const scrollToDate = desc.get("data-scrollToIDate") ?? null;
53+
const fragmentId = desc.get("data-fragmentId") ?? null;
5354
const addCollab: boolean = useRedux(["add_collab"], project_id, path);
5455
const is_uploading = useRedux(["is_uploading"], project_id, path);
5556
const project_map = useTypedRedux("projects", "project_map");
@@ -182,6 +183,7 @@ export default function SideChat({
182183
disableFilters={disableFilters}
183184
scrollToIndex={scrollToIndex}
184185
scrollToDate={scrollToDate}
186+
selectedDate={fragmentId}
185187
/>
186188
</div>
187189

src/packages/frontend/chat/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
ChatMessages,
1313
ChatMessage,
1414
} from "./types";
15+
import { is_date as isDate } from "@cocalc/util/misc";
1516

1617
export const INPUT_HEIGHT = "125px";
1718

@@ -201,3 +202,24 @@ export function getThreadRootDate({
201202
const d = getReplyToRoot({ message, messages });
202203
return d?.valueOf() ?? 0;
203204
}
205+
206+
// Use heuristics to try to turn "date", whatever it might be,
207+
// into a string representation of the number of ms since the
208+
// epoch.
209+
const floatRegex = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;
210+
export function toMsString(date): string {
211+
if (isDate(date)) {
212+
return `${date.valueOf()}`;
213+
}
214+
215+
switch (typeof date) {
216+
case "number":
217+
return `${date}`;
218+
case "string":
219+
if (floatRegex.test(date)) {
220+
return `${parseInt(date)}`;
221+
}
222+
default:
223+
return `${new Date(date).valueOf()}`;
224+
}
225+
}

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
import { handleSyncDBChange, initFromSyncDB } from "@cocalc/frontend/chat/sync";
2222
import { redux_name } from "@cocalc/frontend/app-framework";
2323
import { aux_file } from "@cocalc/util/misc";
24+
import type { FragmentId } from "@cocalc/frontend/misc/fragment-id";
2425

2526
const FRAME_TYPE = "chatroom";
2627

@@ -137,4 +138,30 @@ export class Actions extends CodeEditorActions<ChatEditorState> {
137138
scrollToTop = (frameId) => {
138139
this.getChatActions(frameId)?.scrollToIndex(0);
139140
};
141+
142+
gotoFragment = async (fragmentId: FragmentId) => {
143+
const { chat } = fragmentId as any;
144+
if (!chat) {
145+
return;
146+
}
147+
const frameId = await this.waitUntilFrameReady({
148+
type: FRAME_TYPE,
149+
});
150+
if (!frameId) {
151+
return;
152+
}
153+
const actions = this.getChatActions(frameId);
154+
if (actions == null) {
155+
return;
156+
}
157+
// if id is an iso string, just pass that in; otherwise, it could be a string
158+
// repr of ms since epoch and in that case we have to convert it to a number
159+
actions.scrollToDate(chat);
160+
// do it again since above scrollTo will be wrong if frame just opened, since
161+
// new chat frames typically scroll to bottom on initial render.
162+
// TODO: we could obviously do better here!
163+
for (const d of [5, 50, 500]) {
164+
setTimeout(() => actions.scrollToDate(chat), d);
165+
}
166+
};
140167
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ export class JupyterEditorActions extends BaseActions<JupyterEditorState> {
398398
this.close_recently_focused_frame_of_type("introspect");
399399
}
400400

401-
async gotoFragment(fragmentId: FragmentId) {
401+
gotoFragment = async (fragmentId: FragmentId) => {
402402
const frameId = await this.waitUntilFrameReady({
403403
type: "jupyter_cell_notebook",
404404
syncdoc: this.jupyter_actions.syncdb,
@@ -463,7 +463,7 @@ export class JupyterEditorActions extends BaseActions<JupyterEditorState> {
463463
}
464464
return;
465465
}
466-
}
466+
};
467467

468468
languageModelGetText(
469469
frameId: string,

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -455,8 +455,8 @@ export class Actions<T extends State = State> extends BaseActions<T | State> {
455455
obj.page =
456456
frameId == null
457457
? this.defaultPageId()
458-
: pages.get((this._get_frame_node(frameId)?.get("page") ?? 1) - 1) ??
459-
this.defaultPageId();
458+
: (pages.get((this._get_frame_node(frameId)?.get("page") ?? 1) - 1) ??
459+
this.defaultPageId());
460460
}
461461

462462
// Remove certain fields that never ever make no sense for a new element
@@ -1318,7 +1318,7 @@ export class Actions<T extends State = State> extends BaseActions<T | State> {
13181318
this.zoom100(frameId);
13191319
}
13201320

1321-
async gotoFragment(fragmentId: FragmentId) {
1321+
gotoFragment = async (fragmentId: FragmentId) => {
13221322
// console.log("gotoFragment ", fragmentId);
13231323
const frameId = await this.waitUntilFrameReady({
13241324
type: this.mainFrameType,
@@ -1341,7 +1341,7 @@ export class Actions<T extends State = State> extends BaseActions<T | State> {
13411341
this.scrollElementIntoView(id, frameId);
13421342
return;
13431343
}
1344-
}
1344+
};
13451345

13461346
public async show_table_of_contents(
13471347
_id: string | undefined = undefined,

src/packages/frontend/misc/fragment-id.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { debounce } from "lodash";
88
import { IS_EMBEDDED } from "@cocalc/frontend/client/handle-target";
99

1010
interface Chat {
11-
chat?: boolean; // if true, fragment refers to message in side chat for the named path.
11+
chat?: string; // fragment refers to ms since epoch of chat message
1212
}
1313

1414
interface Anchor extends Chat {
@@ -35,7 +35,7 @@ export function isPageFragment(x: any): x is Page {
3535
return typeof x?.page === "string";
3636
}
3737

38-
export type FragmentId = Line | Id | Page | Anchor;
38+
export type FragmentId = Chat | Line | Id | Page | Anchor;
3939

4040
namespace Fragment {
4141
// set is debounced so you can call it as frequently as you want...

0 commit comments

Comments
 (0)