2021-02-10 16:46:29 +01:00
|
|
|
import ClipboardJS from "clipboard";
|
2021-04-04 00:02:34 +02:00
|
|
|
import {isValid, parseISO} from "date-fns";
|
2021-03-11 05:43:45 +01:00
|
|
|
import $ from "jquery";
|
2024-01-03 08:02:47 +01:00
|
|
|
import assert from "minimalistic-assert";
|
2020-08-01 03:43:15 +02:00
|
|
|
|
2021-02-10 16:46:29 +01:00
|
|
|
import copy_code_button from "../templates/copy_code_button.hbs";
|
2021-03-03 12:18:35 +01:00
|
|
|
import render_markdown_timestamp from "../templates/markdown_timestamp.hbs";
|
2021-02-10 16:46:29 +01:00
|
|
|
import view_code_in_playground from "../templates/view_code_in_playground.hbs";
|
2020-07-28 01:48:47 +02:00
|
|
|
|
2021-03-16 23:38:59 +01:00
|
|
|
import * as blueslip from "./blueslip";
|
2023-09-23 10:07:32 +02:00
|
|
|
import {show_copied_confirmation} from "./copied_tooltip";
|
2021-04-13 06:51:54 +02:00
|
|
|
import {$t, $t_html} from "./i18n";
|
2023-11-03 15:46:14 +01:00
|
|
|
import * as message_store from "./message_store";
|
2024-01-03 08:02:47 +01:00
|
|
|
import type {Message} from "./message_store";
|
2021-02-10 16:46:29 +01:00
|
|
|
import * as people from "./people";
|
2021-04-13 08:43:03 +02:00
|
|
|
import * as realm_playground from "./realm_playground";
|
2023-11-03 15:46:14 +01:00
|
|
|
import * as rows from "./rows";
|
2021-02-28 00:37:11 +01:00
|
|
|
import * as rtl from "./rtl";
|
2023-05-18 15:53:21 +02:00
|
|
|
import * as sub_store from "./sub_store";
|
2021-02-28 01:14:36 +01:00
|
|
|
import * as timerender from "./timerender";
|
2021-02-28 00:42:57 +01:00
|
|
|
import * as user_groups from "./user_groups";
|
2021-07-28 16:00:58 +02:00
|
|
|
import {user_settings} from "./user_settings";
|
2023-09-14 05:20:31 +02:00
|
|
|
import * as util from "./util";
|
2020-08-20 16:11:08 +02:00
|
|
|
|
2020-05-21 06:17:24 +02:00
|
|
|
/*
|
|
|
|
rendered_markdown
|
|
|
|
|
2020-08-11 01:47:44 +02:00
|
|
|
This module provides a single function 'update_elements' to
|
2020-05-21 06:17:24 +02:00
|
|
|
update any renamed users/streams/groups etc. and other
|
|
|
|
dynamic parts of our rendered messages.
|
|
|
|
|
2020-08-11 01:47:49 +02:00
|
|
|
Use this module wherever some Markdown rendered content
|
2020-05-21 06:17:24 +02:00
|
|
|
is being displayed.
|
|
|
|
*/
|
|
|
|
|
2024-01-03 08:02:47 +01:00
|
|
|
export function get_user_id_for_mention_button(elem: HTMLElement): "*" | number | undefined {
|
2020-07-15 01:29:15 +02:00
|
|
|
const user_id_string = $(elem).attr("data-user-id");
|
2020-08-11 01:47:49 +02:00
|
|
|
// Handle legacy Markdown that was rendered before we cut
|
2020-05-21 00:53:14 +02:00
|
|
|
// over to using data-user-id.
|
2020-07-15 01:29:15 +02:00
|
|
|
const email = $(elem).attr("data-user-email");
|
2020-05-21 00:53:14 +02:00
|
|
|
|
|
|
|
if (user_id_string === "*" || email === "*") {
|
|
|
|
return "*";
|
|
|
|
}
|
|
|
|
|
|
|
|
if (user_id_string) {
|
2020-10-07 09:17:30 +02:00
|
|
|
return Number.parseInt(user_id_string, 10);
|
2020-05-21 00:53:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (email) {
|
|
|
|
// Will return undefined if there's no match
|
|
|
|
const user = people.get_by_email(email);
|
|
|
|
if (user) {
|
|
|
|
return user.user_id;
|
|
|
|
}
|
|
|
|
}
|
2020-09-24 07:50:36 +02:00
|
|
|
return undefined;
|
2020-05-21 00:53:14 +02:00
|
|
|
}
|
|
|
|
|
2024-01-03 08:02:47 +01:00
|
|
|
function get_user_group_id_for_mention_button(elem: HTMLElement): number | undefined {
|
2020-07-15 01:29:15 +02:00
|
|
|
const user_group_id = $(elem).attr("data-user-group-id");
|
2020-05-21 00:53:14 +02:00
|
|
|
|
|
|
|
if (user_group_id) {
|
2020-10-07 09:17:30 +02:00
|
|
|
return Number.parseInt(user_group_id, 10);
|
2020-05-21 00:53:14 +02:00
|
|
|
}
|
|
|
|
|
2020-09-24 07:50:36 +02:00
|
|
|
return undefined;
|
2020-05-21 00:53:14 +02:00
|
|
|
}
|
|
|
|
|
2024-01-03 08:02:47 +01:00
|
|
|
function get_message_for_message_content($content: JQuery): Message | undefined {
|
2023-11-23 17:17:37 +01:00
|
|
|
// TODO: This selector is designed to exclude drafts/scheduled
|
|
|
|
// messages. Arguably those settings should be unconditionally
|
|
|
|
// marked with user-mention-me, but rows.id doesn't support
|
|
|
|
// those elements, and we should address that quirk for
|
|
|
|
// mentions holistically.
|
|
|
|
const $message_row = $content.closest(".message_row");
|
|
|
|
if (!$message_row.length || $message_row.closest(".overlay-message-row").length) {
|
|
|
|
// There's no containing message when rendering a preview.
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
const message_id = rows.id($message_row);
|
2024-01-03 08:02:47 +01:00
|
|
|
assert(message_id !== undefined);
|
2023-11-23 17:17:37 +01:00
|
|
|
return message_store.get(message_id);
|
|
|
|
}
|
|
|
|
|
2020-05-21 04:15:27 +02:00
|
|
|
// Helper function to update a mentioned user's name.
|
2024-01-03 08:02:47 +01:00
|
|
|
export function set_name_in_mention_element(
|
|
|
|
element: HTMLElement,
|
|
|
|
name: string,
|
|
|
|
user_id?: number,
|
|
|
|
): void {
|
2023-09-14 17:09:41 +02:00
|
|
|
if (user_id !== undefined && people.should_add_guest_user_indicator(user_id)) {
|
|
|
|
let display_text;
|
|
|
|
if (!$(element).hasClass("silent")) {
|
|
|
|
display_text = $t({defaultMessage: "@{name} (guest)"}, {name});
|
|
|
|
} else {
|
|
|
|
display_text = $t({defaultMessage: "{name} (guest)"}, {name});
|
|
|
|
}
|
|
|
|
$(element).text(display_text);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
if ($(element).hasClass("silent")) {
|
2020-05-21 04:15:27 +02:00
|
|
|
$(element).text(name);
|
|
|
|
} else {
|
|
|
|
$(element).text("@" + name);
|
|
|
|
}
|
2021-02-10 16:46:29 +01:00
|
|
|
}
|
2020-05-21 04:15:27 +02:00
|
|
|
|
2024-01-03 08:02:47 +01:00
|
|
|
export const update_elements = ($content: JQuery): void => {
|
2020-05-21 00:53:14 +02:00
|
|
|
// Set the rtl class if the text has an rtl direction
|
2022-01-25 11:36:19 +01:00
|
|
|
if (rtl.get_direction($content.text()) === "rtl") {
|
|
|
|
$content.addClass("rtl");
|
2020-05-21 00:53:14 +02:00
|
|
|
}
|
|
|
|
|
2023-09-14 05:20:31 +02:00
|
|
|
if (util.is_client_safari()) {
|
|
|
|
// Without this video thumbnail doesn't load on Safari.
|
2024-01-03 08:02:47 +01:00
|
|
|
$content.find<HTMLMediaElement>(".message_inline_video video").each(function () {
|
2023-09-14 05:20:31 +02:00
|
|
|
// On Safari, one needs to manually load video elements.
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/load
|
|
|
|
this.load();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-11-23 17:17:37 +01:00
|
|
|
// personal and stream wildcard mentions
|
2024-01-03 08:02:47 +01:00
|
|
|
$content.find(".user-mention").each(function (): void {
|
2020-05-21 00:53:14 +02:00
|
|
|
const user_id = get_user_id_for_mention_button(this);
|
2023-11-23 17:17:37 +01:00
|
|
|
const message = get_message_for_message_content($content);
|
2020-05-21 00:53:14 +02:00
|
|
|
// We give special highlights to the mention buttons
|
|
|
|
// that refer to the current user.
|
2023-11-23 17:17:37 +01:00
|
|
|
if (user_id === "*" && message && message.stream_wildcard_mentioned) {
|
|
|
|
$(this).addClass("user-mention-me");
|
2020-05-21 00:53:14 +02:00
|
|
|
}
|
2023-11-23 17:17:37 +01:00
|
|
|
if (
|
2024-01-03 08:02:47 +01:00
|
|
|
user_id !== undefined &&
|
2023-11-23 17:17:37 +01:00
|
|
|
user_id !== "*" &&
|
|
|
|
people.is_my_user_id(user_id) &&
|
|
|
|
message &&
|
|
|
|
message.mentioned_me_directly
|
|
|
|
) {
|
|
|
|
$(this).addClass("user-mention-me");
|
|
|
|
}
|
|
|
|
|
2020-05-21 00:53:14 +02:00
|
|
|
if (user_id && user_id !== "*" && !$(this).find(".highlight").length) {
|
2023-12-01 12:18:25 +01:00
|
|
|
// If it's a mention of a specific user, edit the mention
|
|
|
|
// text to show the user's current name, assuming that
|
|
|
|
// you're not searching for text inside the highlight.
|
2023-08-02 20:04:25 +02:00
|
|
|
const person = people.maybe_get_user_by_id(user_id, true);
|
2023-12-01 12:18:25 +01:00
|
|
|
if (person === undefined || person.is_inaccessible_user) {
|
2020-05-21 00:53:14 +02:00
|
|
|
// Note that person might be undefined in some
|
2023-12-01 12:18:25 +01:00
|
|
|
// unpleasant corner cases involving data import
|
|
|
|
// or when guest users cannot access all users in
|
|
|
|
// the organization.
|
|
|
|
//
|
|
|
|
// In these cases, the best we can do is leave the
|
|
|
|
// existing name in the existing mention pill
|
|
|
|
// HTML. Clicking on the pill will show the the
|
|
|
|
// "Unknown user" popover.
|
|
|
|
if (person === undefined) {
|
|
|
|
people.add_inaccessible_user(user_id);
|
|
|
|
}
|
|
|
|
return;
|
2020-05-21 00:53:14 +02:00
|
|
|
}
|
2023-12-01 12:18:25 +01:00
|
|
|
|
|
|
|
set_name_in_mention_element(this, person.full_name, user_id);
|
2020-05-21 00:53:14 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-01-03 08:02:47 +01:00
|
|
|
$content.find(".topic-mention").each(function (): void {
|
2023-11-23 17:17:37 +01:00
|
|
|
const message = get_message_for_message_content($content);
|
2023-11-03 15:46:14 +01:00
|
|
|
|
2023-11-23 17:17:37 +01:00
|
|
|
if (message && message.topic_wildcard_mentioned) {
|
2023-11-03 15:46:14 +01:00
|
|
|
$(this).addClass("user-mention-me");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-01-03 08:02:47 +01:00
|
|
|
$content.find(".user-group-mention").each(function (): void {
|
2020-05-21 00:53:14 +02:00
|
|
|
const user_group_id = get_user_group_id_for_mention_button(this);
|
2021-08-03 18:21:15 +02:00
|
|
|
let user_group;
|
2024-01-03 08:02:47 +01:00
|
|
|
if (user_group_id === undefined) {
|
|
|
|
blueslip.info("Rendered undefined user group");
|
|
|
|
return;
|
|
|
|
}
|
2021-08-03 18:21:15 +02:00
|
|
|
try {
|
|
|
|
user_group = user_groups.get_user_group_from_id(user_group_id);
|
|
|
|
} catch {
|
2020-05-21 00:53:14 +02:00
|
|
|
// This is a user group the current user doesn't have
|
|
|
|
// data on. This can happen when user groups are
|
|
|
|
// deleted.
|
2023-04-24 15:57:45 +02:00
|
|
|
blueslip.info("Rendered unexpected user group", {user_group_id});
|
2020-05-21 00:53:14 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const my_user_id = people.my_current_user_id();
|
|
|
|
// Mark user group you're a member of.
|
2022-05-05 10:04:57 +02:00
|
|
|
if (user_groups.is_direct_member_of(my_user_id, user_group_id)) {
|
2020-07-15 01:29:15 +02:00
|
|
|
$(this).addClass("user-mention-me");
|
2020-05-21 00:53:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (user_group_id && !$(this).find(".highlight").length) {
|
|
|
|
// Edit the mention to show the current name for the
|
|
|
|
// user group, if its not in search.
|
2021-05-16 21:01:53 +02:00
|
|
|
set_name_in_mention_element(this, user_group.name);
|
2020-05-21 00:53:14 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-01-03 08:02:47 +01:00
|
|
|
$content.find("a.stream").each(function (): void {
|
|
|
|
const stream_id_string = $(this).attr("data-stream-id");
|
|
|
|
if (stream_id_string !== undefined) {
|
|
|
|
const stream_id = Number.parseInt(stream_id_string, 10);
|
|
|
|
if (stream_id && !$(this).find(".highlight").length) {
|
|
|
|
// Display the current name for stream if it is not
|
|
|
|
// being displayed in search highlight.
|
|
|
|
const stream_name = sub_store.maybe_get_stream_name(stream_id);
|
|
|
|
if (stream_name !== undefined) {
|
|
|
|
// If the stream has been deleted,
|
|
|
|
// sub_store.maybe_get_stream_name might return
|
|
|
|
// undefined. Otherwise, display the current stream name.
|
|
|
|
$(this).text("#" + stream_name);
|
|
|
|
}
|
2020-05-21 00:53:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-01-03 08:02:47 +01:00
|
|
|
$content.find("a.stream-topic").each(function (): void {
|
|
|
|
const stream_id_string = $(this).attr("data-stream-id");
|
|
|
|
if (stream_id_string !== undefined) {
|
|
|
|
const stream_id = Number.parseInt(stream_id_string, 10);
|
|
|
|
if (stream_id && !$(this).find(".highlight").length) {
|
|
|
|
// Display the current name for stream if it is not
|
|
|
|
// being displayed in search highlight.
|
|
|
|
const stream_name = sub_store.maybe_get_stream_name(stream_id);
|
|
|
|
if (stream_name !== undefined) {
|
|
|
|
// If the stream has been deleted,
|
|
|
|
// sub_store.maybe_get_stream_name might return
|
|
|
|
// undefined. Otherwise, display the current stream name.
|
|
|
|
const text = $(this).text();
|
|
|
|
$(this).text("#" + stream_name + text.slice(text.indexOf(" > ")));
|
|
|
|
}
|
2020-05-21 00:53:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-01-03 08:02:47 +01:00
|
|
|
$content.find("time").each(function (): void {
|
2018-02-13 00:33:36 +01:00
|
|
|
// Populate each timestamp span with mentioned time
|
2022-02-24 21:15:43 +01:00
|
|
|
// in user's local time zone.
|
2020-07-15 01:29:15 +02:00
|
|
|
const time_str = $(this).attr("datetime");
|
2020-06-18 01:32:24 +02:00
|
|
|
if (time_str === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-09-29 22:20:46 +02:00
|
|
|
const timestamp = parseISO(time_str);
|
|
|
|
if (isValid(timestamp)) {
|
2021-03-03 12:18:35 +01:00
|
|
|
const rendered_timestamp = render_markdown_timestamp({
|
2022-03-02 23:06:33 +01:00
|
|
|
text: timerender.format_markdown_time(timestamp),
|
2021-03-03 12:18:35 +01:00
|
|
|
});
|
|
|
|
$(this).html(rendered_timestamp);
|
2018-02-13 00:33:36 +01:00
|
|
|
} else {
|
2020-07-06 17:17:31 +02:00
|
|
|
// This shouldn't happen. If it does, we're very interested in debugging it.
|
2023-04-24 15:57:45 +02:00
|
|
|
blueslip.error("Could not parse datetime supplied by backend", {time_str});
|
2018-02-13 00:33:36 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-01-03 08:02:47 +01:00
|
|
|
$content.find("span.timestamp-error").each(function (): void {
|
|
|
|
const match_array = /^Invalid time format: (.*)$/.exec($(this).text());
|
|
|
|
assert(match_array !== null);
|
|
|
|
const [, time_str] = match_array;
|
2021-04-13 06:51:54 +02:00
|
|
|
const text = $t(
|
|
|
|
{defaultMessage: "Invalid time format: {timestamp}"},
|
|
|
|
{timestamp: time_str},
|
|
|
|
);
|
2020-07-06 17:07:44 +02:00
|
|
|
$(this).text(text);
|
|
|
|
});
|
|
|
|
|
2024-01-03 08:02:47 +01:00
|
|
|
$content.find("div.spoiler-header").each(function (): void {
|
2020-07-06 17:11:58 +02:00
|
|
|
// If a spoiler block has no header content, it should have a default header.
|
|
|
|
// We do this client side to allow for i18n by the client.
|
2020-07-22 03:39:41 +02:00
|
|
|
if ($(this).html().trim().length === 0) {
|
2021-04-13 05:48:41 +02:00
|
|
|
$(this).append(`<p>${$t_html({defaultMessage: "Spoiler"})}</p>`);
|
2020-04-04 22:14:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Add the expand/collapse button to spoiler blocks
|
2020-07-15 00:34:28 +02:00
|
|
|
const toggle_button_html =
|
|
|
|
'<span class="spoiler-button" aria-expanded="false"><span class="spoiler-arrow"></span></span>';
|
2020-04-04 22:14:34 +02:00
|
|
|
$(this).prepend(toggle_button_html);
|
|
|
|
});
|
|
|
|
|
2023-10-25 15:28:25 +02:00
|
|
|
// Display the view-code-in-playground and the copy-to-clipboard button inside the div.codehilite element,
|
|
|
|
// and add a `zulip-code-block` class to it to detect it easily in `copy_and_paste.js`.
|
2024-01-03 08:02:47 +01:00
|
|
|
$content.find("div.codehilite").each(function (): void {
|
2020-09-06 09:00:45 +02:00
|
|
|
const $codehilite = $(this);
|
|
|
|
const $pre = $codehilite.find("pre");
|
|
|
|
const fenced_code_lang = $codehilite.data("code-language");
|
|
|
|
if (fenced_code_lang !== undefined) {
|
2021-05-09 22:29:53 +02:00
|
|
|
const playground_info =
|
|
|
|
realm_playground.get_playground_info_for_languages(fenced_code_lang);
|
2020-09-06 09:00:45 +02:00
|
|
|
if (playground_info !== undefined) {
|
|
|
|
// If a playground is configured for this language,
|
|
|
|
// offer to view the code in that playground. When
|
|
|
|
// there are multiple playgrounds, we display a
|
|
|
|
// popover listing the options.
|
2021-04-13 06:51:54 +02:00
|
|
|
let title = $t({defaultMessage: "View in playground"});
|
2022-01-25 11:36:19 +01:00
|
|
|
const $view_in_playground_button = $(view_code_in_playground());
|
|
|
|
$pre.prepend($view_in_playground_button);
|
2020-09-06 09:00:45 +02:00
|
|
|
if (playground_info.length === 1) {
|
2021-04-13 06:51:54 +02:00
|
|
|
title = $t(
|
|
|
|
{defaultMessage: "View in {playground_name}"},
|
|
|
|
{playground_name: playground_info[0].name},
|
|
|
|
);
|
2020-09-06 09:00:45 +02:00
|
|
|
} else {
|
2022-01-25 11:36:19 +01:00
|
|
|
$view_in_playground_button.attr("aria-haspopup", "true");
|
2020-09-06 09:00:45 +02:00
|
|
|
}
|
2022-01-25 11:36:19 +01:00
|
|
|
$view_in_playground_button.attr("data-tippy-content", title);
|
|
|
|
$view_in_playground_button.attr("aria-label", title);
|
2020-09-06 09:00:45 +02:00
|
|
|
}
|
|
|
|
}
|
2022-01-25 11:36:19 +01:00
|
|
|
const $copy_button = $(copy_code_button());
|
|
|
|
$pre.prepend($copy_button);
|
2023-02-21 16:45:12 +01:00
|
|
|
const clipboard = new ClipboardJS($copy_button[0], {
|
2020-08-20 16:11:08 +02:00
|
|
|
text(copy_element) {
|
|
|
|
return $(copy_element).siblings("code").text();
|
|
|
|
},
|
|
|
|
});
|
2023-02-21 16:45:12 +01:00
|
|
|
clipboard.on("success", () => {
|
|
|
|
show_copied_confirmation($copy_button[0]);
|
|
|
|
});
|
2023-10-25 15:28:25 +02:00
|
|
|
$codehilite.addClass("zulip-code-block");
|
2020-08-20 16:11:08 +02:00
|
|
|
});
|
|
|
|
|
2020-05-21 00:53:14 +02:00
|
|
|
// Display emoji (including realm emoji) as text if
|
2021-07-28 16:00:58 +02:00
|
|
|
// user_settings.emojiset is 'text'.
|
|
|
|
if (user_settings.emojiset === "text") {
|
2024-01-03 08:02:47 +01:00
|
|
|
$content.find(".emoji").replaceWith(function (): string {
|
2020-05-21 00:53:14 +02:00
|
|
|
const text = $(this).attr("title");
|
|
|
|
return ":" + text + ":";
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|