Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/chat/resolved-thread-panel.tsx
14422 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020-2026 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
Read-only "this thread is resolved" panel that replaces the reply input on
8
a resolved (LaTeX collaborative-TODO) thread. Shows who resolved it and
9
when, plus a "Start new chat thread" affordance that — after the user
10
confirms the target file/line — calls the editor's `insertChatMarker`.
11
12
The panel is editor-agnostic at the UI layer: it discovers the marker
13
target via `frameTreeActions.previewMarkerInsertion()` and then fires
14
`frameTreeActions.insertChatMarker()` on confirm. Editors that don't
15
provide those methods just hide the button (defensive — only LaTeX wires
16
this in for now).
17
*/
18
19
import { useState } from "react";
20
import { Button, Popconfirm } from "antd";
21
22
import { redux, useTypedRedux } from "@cocalc/frontend/app-framework";
23
import { Icon } from "@cocalc/frontend/components";
24
import { COLORS } from "@cocalc/util/theme";
25
26
import type { ChatActions } from "./actions";
27
28
interface Props {
29
actions: ChatActions;
30
account_id: string;
31
at: string; // ISO timestamp
32
}
33
34
export function ResolvedThreadPanel({ actions, account_id, at }: Props) {
35
const userMap = useTypedRedux("users", "user_map");
36
const meAccountId = redux.getStore("account")?.get_account_id();
37
38
const resolvedAt = new Date(at);
39
const resolvedDateText = isNaN(resolvedAt.valueOf())
40
? at
41
: resolvedAt.toLocaleString();
42
43
let resolverName = "Someone";
44
if (account_id && account_id === meAccountId) {
45
resolverName = "You";
46
} else if (account_id && userMap != null) {
47
const u = userMap.get(account_id);
48
const first = u?.get("first_name") ?? "";
49
const last = u?.get("last_name") ?? "";
50
const full = `${first} ${last}`.trim();
51
if (full) resolverName = full;
52
}
53
54
const [target, setTarget] = useState<{ path: string; line: number } | null>(
55
null,
56
);
57
const editorActions: any = actions.frameTreeActions;
58
const canStartNew =
59
typeof editorActions?.previewMarkerInsertion === "function" &&
60
typeof editorActions?.insertChatMarker === "function";
61
62
const handleOpenStartNew = () => {
63
if (!canStartNew) return;
64
try {
65
setTarget(editorActions.previewMarkerInsertion());
66
} catch {
67
setTarget(null);
68
}
69
};
70
const handleConfirmStartNew = () => {
71
if (!canStartNew) return;
72
void editorActions.insertChatMarker({});
73
setTarget(null);
74
};
75
76
return (
77
<div
78
style={{
79
margin: "5px 10px",
80
padding: "12px 16px",
81
background: COLORS.GRAY_LL,
82
border: `1px solid ${COLORS.GRAY_L}`,
83
borderRadius: 6,
84
color: COLORS.GRAY_DD,
85
display: "flex",
86
alignItems: "center",
87
gap: 12,
88
}}
89
>
90
<Icon
91
name="check-circle"
92
style={{ color: COLORS.GRAY_M, fontSize: "1.1em" }}
93
/>
94
<div style={{ flex: 1 }}>
95
<div style={{ fontWeight: 500 }}>This chat is resolved.</div>
96
<div style={{ fontSize: "0.9em", color: COLORS.GRAY_M }}>
97
Resolved by {resolverName} on {resolvedDateText}. Replies are disabled
98
— start a new thread to continue the discussion at a fresh location.
99
</div>
100
</div>
101
{canStartNew && (
102
<Popconfirm
103
title="Start a new chat thread?"
104
description={
105
<div style={{ maxWidth: 320 }}>
106
{target == null ? (
107
<span>
108
No active editor pane. Click into a <code>.tex</code> file
109
first so the new marker has a target.
110
</span>
111
) : (
112
<span>
113
This will insert a new <code>% chat:</code> marker in{" "}
114
<code>{target.path}</code> at line {target.line + 1} and open
115
a fresh chat thread there.
116
</span>
117
)}
118
</div>
119
}
120
okText={target == null ? undefined : "Insert marker"}
121
cancelText="Cancel"
122
okButtonProps={{ disabled: target == null }}
123
onConfirm={handleConfirmStartNew}
124
onOpenChange={(open) => {
125
if (open) handleOpenStartNew();
126
}}
127
placement="topRight"
128
>
129
<Button type="primary" size="small">
130
<Icon name="comment" /> Start new chat thread
131
</Button>
132
</Popconfirm>
133
)}
134
</div>
135
);
136
}
137
138