Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/chat/utils.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { throttle } from "lodash";6import { redux } from "@cocalc/frontend/app-framework";7import { original_path } from "@cocalc/util/misc";8import type {9ChatMessageTyped,10MentionList,11ChatMessages,12ChatMessage,13} from "./types";14import { is_date as isDate } from "@cocalc/util/misc";1516export const INPUT_HEIGHT = "125px";1718export const USER_MENTION_MARKUP =19'<span class="user-mention" account-id=__id__ >@__display__</span>';2021const USER_MENTION_MARKUP_WITHOUT_PLACEHOLDERS =22'<span class="user-mention" account-id= ></span>';2324const SINGLE_MENTION_OFFSET = USER_MENTION_MARKUP_WITHOUT_PLACEHOLDERS.length;2526/*27Given plain text which looks like28```29@person name you need to do this.30```31`cursor_plain_text_index` in that text,32and `mentions` from react-mentions,3334return the cursor position in the backing text which looks like35```36<span class-name="user-mention" account-id= 72583e2b-3ea3-431c-892f-2b9616e6754e >@person name</span> you need to do this.37```38*/39export function compute_cursor_offset_position(40cursor_plain_text_index: number,41mentions: MentionList,42) {43let index_offset = 0;44let usuable_cursor_index = cursor_plain_text_index;45const mention_array = mentions.toJS() as any;4647for (let i = 0; i < mention_array.length; i++) {48const current_mention = mention_array[i];49const { id, display, index, plainTextIndex } = current_mention;50const mention_offset = index - plainTextIndex;5152if (cursor_plain_text_index <= plainTextIndex) {53// Cursor is in front of this mention. ie. " asdfas |@jim" where | is the cursor54index_offset = mention_offset;55break;56} else if (cursor_plain_text_index >= plainTextIndex + display.length) {57if (i == mention_array.length - 1) {58// Cursor is after last mention.59index_offset = mention_offset + id.length + SINGLE_MENTION_OFFSET;60}61} else if (cursor_plain_text_index > plainTextIndex + display.length / 2) {62usuable_cursor_index = plainTextIndex + display.length;63if (i == mention_array.length - 1) {64// Cursor is inside the second half of the last mention.65index_offset = mention_offset + id.length + SINGLE_MENTION_OFFSET;66}67} else if (cursor_plain_text_index <= plainTextIndex + display.length / 2) {68// Cursor is inside the first half of this mention69usuable_cursor_index = plainTextIndex;70index_offset = mention_offset;71break;72}73}74return index_offset + usuable_cursor_index;75}7677export function newest_content(message: ChatMessageTyped): string {78const history = message.get("history");79return history?.first()?.get("content") ?? "";80}8182export function sender_is_viewer(83account_id: string,84message: ChatMessageTyped,85): boolean {86return account_id == message.get("sender_id");87}8889export function message_colors(90account_id: string,91message: ChatMessageTyped,92): {93background: string;94color: string;95message_class: string;96lighten?: { color: string };97} {98if (sender_is_viewer(account_id, message)) {99return {100background: "#46b1f6",101color: "#fff",102message_class: "smc-message-from-viewer",103};104} else {105return {106background: "#f8f8f8",107color: "#000",108lighten: { color: "#888" },109message_class: "smc-message-from-other",110};111}112}113114export function is_editing(115message: ChatMessageTyped,116account_id: string,117): boolean {118return message.get("editing")?.has(account_id);119}120121export const markChatAsReadIfUnseen: (122project_id: string,123path: string,124) => void = throttle((project_id: string, path: string) => {125const info = redux126?.getStore("file_use")127?.get_file_info(project_id, original_path(path));128if (info == null || info.is_unseenchat) {129// only mark chat as read if it is unseen130const actions = redux?.getActions("file_use");131if (actions == null) return;132actions.mark_file(project_id, path, "read");133actions.mark_file(project_id, path, "chatseen");134}135}, 3000);136137export function getSelectedHashtagsSearch(hashtags): {138selectedHashtags: Set<string>;139selectedHashtagsSearch: string;140} {141const X = new Set<string>([]);142if (hashtags == null)143return { selectedHashtags: X, selectedHashtagsSearch: "" };144for (const [key] of hashtags) {145if (hashtags.get(key) == 1) {146// only care about visible hashtags147X.add(key);148}149}150return {151selectedHashtags: X,152selectedHashtagsSearch: X.size > 0 ? " #" + Array.from(X).join(" #") : "",153};154}155156export function getRootMessage({157message,158messages,159}: {160message: ChatMessage;161messages: ChatMessages;162}): ChatMessageTyped | undefined {163const { reply_to, date } = message;164// we can't find the original message, if there is no reply_to165if (!reply_to) {166// the msssage itself is the root167return messages.get(`${new Date(date).valueOf()}`);168} else {169// All messages in a thread have the same reply_to, which points to the root.170return messages.get(`${new Date(reply_to).valueOf()}`);171}172}173174export function getReplyToRoot({175message,176messages,177}: {178message: ChatMessage;179messages: ChatMessages;180}): Date | undefined {181const root = getRootMessage({ message, messages });182const date = root?.get("date");183// date is a "Date" object, but we're just double checking it is not a string by accident184return date ? new Date(date) : undefined;185}186187export function getThreadRootDate({188date,189messages,190}: {191date: number;192messages?: ChatMessages;193}): number {194if (messages == null) {195return 0;196}197const message = messages.get(`${date}`)?.toJS();198if (message == null) {199return 0;200}201const d = getReplyToRoot({ message, messages });202return d?.valueOf() ?? 0;203}204205// Use heuristics to try to turn "date", whatever it might be,206// into a string representation of the number of ms since the207// epoch.208const floatRegex = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;209export function toMsString(date): string {210if (isDate(date)) {211return `${date.valueOf()}`;212}213214switch (typeof date) {215case "number":216return `${date}`;217case "string":218if (floatRegex.test(date)) {219return `${parseInt(date)}`;220}221default:222return `${new Date(date).valueOf()}`;223}224}225226227