CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/chat/utils.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { throttle } from "lodash";
7
import { redux } from "@cocalc/frontend/app-framework";
8
import { original_path } from "@cocalc/util/misc";
9
import type {
10
ChatMessageTyped,
11
MentionList,
12
ChatMessages,
13
ChatMessage,
14
} from "./types";
15
import { is_date as isDate } from "@cocalc/util/misc";
16
17
export const INPUT_HEIGHT = "125px";
18
19
export const USER_MENTION_MARKUP =
20
'<span class="user-mention" account-id=__id__ >@__display__</span>';
21
22
const USER_MENTION_MARKUP_WITHOUT_PLACEHOLDERS =
23
'<span class="user-mention" account-id= ></span>';
24
25
const SINGLE_MENTION_OFFSET = USER_MENTION_MARKUP_WITHOUT_PLACEHOLDERS.length;
26
27
/*
28
Given plain text which looks like
29
```
30
@person name you need to do this.
31
```
32
`cursor_plain_text_index` in that text,
33
and `mentions` from react-mentions,
34
35
return the cursor position in the backing text which looks like
36
```
37
<span class-name="user-mention" account-id= 72583e2b-3ea3-431c-892f-2b9616e6754e >@person name</span> you need to do this.
38
```
39
*/
40
export function compute_cursor_offset_position(
41
cursor_plain_text_index: number,
42
mentions: MentionList,
43
) {
44
let index_offset = 0;
45
let usuable_cursor_index = cursor_plain_text_index;
46
const mention_array = mentions.toJS() as any;
47
48
for (let i = 0; i < mention_array.length; i++) {
49
const current_mention = mention_array[i];
50
const { id, display, index, plainTextIndex } = current_mention;
51
const mention_offset = index - plainTextIndex;
52
53
if (cursor_plain_text_index <= plainTextIndex) {
54
// Cursor is in front of this mention. ie. " asdfas |@jim" where | is the cursor
55
index_offset = mention_offset;
56
break;
57
} else if (cursor_plain_text_index >= plainTextIndex + display.length) {
58
if (i == mention_array.length - 1) {
59
// Cursor is after last mention.
60
index_offset = mention_offset + id.length + SINGLE_MENTION_OFFSET;
61
}
62
} else if (cursor_plain_text_index > plainTextIndex + display.length / 2) {
63
usuable_cursor_index = plainTextIndex + display.length;
64
if (i == mention_array.length - 1) {
65
// Cursor is inside the second half of the last mention.
66
index_offset = mention_offset + id.length + SINGLE_MENTION_OFFSET;
67
}
68
} else if (cursor_plain_text_index <= plainTextIndex + display.length / 2) {
69
// Cursor is inside the first half of this mention
70
usuable_cursor_index = plainTextIndex;
71
index_offset = mention_offset;
72
break;
73
}
74
}
75
return index_offset + usuable_cursor_index;
76
}
77
78
export function newest_content(message: ChatMessageTyped): string {
79
const history = message.get("history");
80
return history?.first()?.get("content") ?? "";
81
}
82
83
export function sender_is_viewer(
84
account_id: string,
85
message: ChatMessageTyped,
86
): boolean {
87
return account_id == message.get("sender_id");
88
}
89
90
export function message_colors(
91
account_id: string,
92
message: ChatMessageTyped,
93
): {
94
background: string;
95
color: string;
96
message_class: string;
97
lighten?: { color: string };
98
} {
99
if (sender_is_viewer(account_id, message)) {
100
return {
101
background: "#46b1f6",
102
color: "#fff",
103
message_class: "smc-message-from-viewer",
104
};
105
} else {
106
return {
107
background: "#f8f8f8",
108
color: "#000",
109
lighten: { color: "#888" },
110
message_class: "smc-message-from-other",
111
};
112
}
113
}
114
115
export function is_editing(
116
message: ChatMessageTyped,
117
account_id: string,
118
): boolean {
119
return message.get("editing")?.has(account_id);
120
}
121
122
export const markChatAsReadIfUnseen: (
123
project_id: string,
124
path: string,
125
) => void = throttle((project_id: string, path: string) => {
126
const info = redux
127
?.getStore("file_use")
128
?.get_file_info(project_id, original_path(path));
129
if (info == null || info.is_unseenchat) {
130
// only mark chat as read if it is unseen
131
const actions = redux?.getActions("file_use");
132
if (actions == null) return;
133
actions.mark_file(project_id, path, "read");
134
actions.mark_file(project_id, path, "chatseen");
135
}
136
}, 3000);
137
138
export function getSelectedHashtagsSearch(hashtags): {
139
selectedHashtags: Set<string>;
140
selectedHashtagsSearch: string;
141
} {
142
const X = new Set<string>([]);
143
if (hashtags == null)
144
return { selectedHashtags: X, selectedHashtagsSearch: "" };
145
for (const [key] of hashtags) {
146
if (hashtags.get(key) == 1) {
147
// only care about visible hashtags
148
X.add(key);
149
}
150
}
151
return {
152
selectedHashtags: X,
153
selectedHashtagsSearch: X.size > 0 ? " #" + Array.from(X).join(" #") : "",
154
};
155
}
156
157
export function getRootMessage({
158
message,
159
messages,
160
}: {
161
message: ChatMessage;
162
messages: ChatMessages;
163
}): ChatMessageTyped | undefined {
164
const { reply_to, date } = message;
165
// we can't find the original message, if there is no reply_to
166
if (!reply_to) {
167
// the msssage itself is the root
168
return messages.get(`${new Date(date).valueOf()}`);
169
} else {
170
// All messages in a thread have the same reply_to, which points to the root.
171
return messages.get(`${new Date(reply_to).valueOf()}`);
172
}
173
}
174
175
export function getReplyToRoot({
176
message,
177
messages,
178
}: {
179
message: ChatMessage;
180
messages: ChatMessages;
181
}): Date | undefined {
182
const root = getRootMessage({ message, messages });
183
const date = root?.get("date");
184
// date is a "Date" object, but we're just double checking it is not a string by accident
185
return date ? new Date(date) : undefined;
186
}
187
188
export function getThreadRootDate({
189
date,
190
messages,
191
}: {
192
date: number;
193
messages?: ChatMessages;
194
}): number {
195
if (messages == null) {
196
return 0;
197
}
198
const message = messages.get(`${date}`)?.toJS();
199
if (message == null) {
200
return 0;
201
}
202
const d = getReplyToRoot({ message, messages });
203
return d?.valueOf() ?? 0;
204
}
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
}
226
227