mirror of
https://github.com/discourse/discourse.git
synced 2024-11-30 13:45:59 +08:00
363 lines
12 KiB
JavaScript
363 lines
12 KiB
JavaScript
import { performEmojiUnescape } from "pretty-text/emoji";
|
||
import I18n from "discourse-i18n";
|
||
|
||
let customMarkdownCookFn;
|
||
|
||
const chatTranscriptRule = {
|
||
tag: "chat",
|
||
|
||
replace: function (state, tagInfo, content) {
|
||
// shouldn't really happen but we don't want to break rendering if it does
|
||
if (!customMarkdownCookFn) {
|
||
return;
|
||
}
|
||
|
||
const options = state.md.options.discourse;
|
||
const [username, messageIdStart, messageTimeStart] =
|
||
(tagInfo.attrs.quote && tagInfo.attrs.quote.split(";")) || [];
|
||
const reactions = tagInfo.attrs.reactions;
|
||
const multiQuote = !!tagInfo.attrs.multiQuote;
|
||
const noLink = !!tagInfo.attrs.noLink;
|
||
const channelName = tagInfo.attrs.channel;
|
||
const channelId = tagInfo.attrs.channelId;
|
||
const threadId = tagInfo.attrs.threadId;
|
||
const threadTitle = tagInfo.attrs.threadTitle;
|
||
const channelLink = channelId
|
||
? options.getURL(`/chat/c/-/${channelId}`)
|
||
: null;
|
||
|
||
if (!username || !messageIdStart || !messageTimeStart) {
|
||
return;
|
||
}
|
||
|
||
let wrapperDivToken = state.push("div_chat_transcript_wrap", "div", 1);
|
||
|
||
if (channelName && multiQuote) {
|
||
let metaDivToken = state.push("div_chat_transcript_meta", "div", 1);
|
||
metaDivToken.attrs = [["class", "chat-transcript-meta"]];
|
||
const channelToken = state.push("html_inline", "", 0);
|
||
|
||
const unescapedChannelName = performEmojiUnescape(channelName, {
|
||
getURL: options.getURL,
|
||
emojiSet: options.emojiSet,
|
||
emojiCDNUrl: options.emojiCDNUrl,
|
||
enableEmojiShortcuts: options.enableEmojiShortcuts,
|
||
inlineEmoji: options.inlineEmoji,
|
||
lazy: true,
|
||
});
|
||
|
||
channelToken.content = I18n.t("chat.quote.original_channel", {
|
||
channel: unescapedChannelName,
|
||
channelLink,
|
||
});
|
||
state.push("div_chat_transcript_meta", "div", -1);
|
||
}
|
||
|
||
if (threadId) {
|
||
state.push("details_chat_transcript_wrap_open", "details", 1);
|
||
state.push("summary_chat_transcript_open", "summary", 1);
|
||
|
||
const threadToken = state.push("div_thread_open", "div", 1);
|
||
threadToken.attrs = [["class", "chat-transcript-thread"]];
|
||
|
||
const threadHeaderToken = state.push("div_thread_header_open", "div", 1);
|
||
threadHeaderToken.attrs = [["class", "chat-transcript-thread-header"]];
|
||
|
||
const thread_svg = state.push("svg_thread_header_open", "svg", 1);
|
||
thread_svg.block = false;
|
||
thread_svg.attrs = [
|
||
["class", "fa d-icon d-icon-discourse-threads svg-icon svg-node"],
|
||
];
|
||
state.push(thread_svg);
|
||
let thread_use = state.push("use_svg_thread_open", "use", 1);
|
||
thread_use.block = false;
|
||
thread_use.attrs = [["href", "#discourse-threads"]];
|
||
state.push(thread_use);
|
||
state.push(state.push("use_svg_thread_close", "use", -1));
|
||
state.push(state.push("svg_thread_header_close", "svg", -1));
|
||
|
||
const threadTitleContainerToken = state.push(
|
||
"span_thread_title_open",
|
||
"span",
|
||
1
|
||
);
|
||
threadTitleContainerToken.attrs = [
|
||
["class", "chat-transcript-thread-header__title"],
|
||
];
|
||
|
||
const threadTitleToken = state.push("html_inline", "", 0);
|
||
const unescapedThreadTitle = performEmojiUnescape(threadTitle, {
|
||
getURL: options.getURL,
|
||
emojiSet: options.emojiSet,
|
||
emojiCDNUrl: options.emojiCDNUrl,
|
||
enableEmojiShortcuts: options.enableEmojiShortcuts,
|
||
inlineEmoji: options.inlineEmoji,
|
||
lazy: true,
|
||
});
|
||
threadTitleToken.content = unescapedThreadTitle
|
||
? unescapedThreadTitle
|
||
: I18n.t("chat.quote.default_thread_title");
|
||
|
||
state.push("span_thread_title_close", "span", -1);
|
||
|
||
state.push("div_thread_header_close", "div", -1);
|
||
}
|
||
|
||
let wrapperClasses = ["chat-transcript"];
|
||
|
||
if (tagInfo.attrs.chained) {
|
||
wrapperClasses.push("chat-transcript-chained");
|
||
}
|
||
|
||
wrapperDivToken.attrs = [["class", wrapperClasses.join(" ")]];
|
||
wrapperDivToken.attrs.push(["data-message-id", messageIdStart]);
|
||
wrapperDivToken.attrs.push(["data-username", username]);
|
||
wrapperDivToken.attrs.push(["data-datetime", messageTimeStart]);
|
||
|
||
if (reactions) {
|
||
wrapperDivToken.attrs.push(["data-reactions", reactions]);
|
||
}
|
||
|
||
if (channelName) {
|
||
wrapperDivToken.attrs.push(["data-channel-name", channelName]);
|
||
}
|
||
|
||
if (channelId) {
|
||
wrapperDivToken.attrs.push(["data-channel-id", channelId]);
|
||
}
|
||
|
||
let userDivToken = state.push("div_chat_transcript_user", "div", 1);
|
||
userDivToken.attrs = [["class", "chat-transcript-user"]];
|
||
|
||
// start: user avatar
|
||
let avatarDivToken = state.push(
|
||
"div_chat_transcript_user_avatar",
|
||
"div",
|
||
1
|
||
);
|
||
avatarDivToken.attrs = [["class", "chat-transcript-user-avatar"]];
|
||
|
||
// server-side, we need to lookup the avatar from the username
|
||
let avatarImg;
|
||
if (options.lookupAvatar) {
|
||
avatarImg = options.lookupAvatar(username);
|
||
}
|
||
if (avatarImg) {
|
||
const avatarImgToken = state.push("html_inline", "", 0);
|
||
avatarImgToken.content = avatarImg;
|
||
}
|
||
|
||
state.push("div_chat_transcript_user_avatar", "div", -1);
|
||
// end: user avatar
|
||
|
||
// start: username
|
||
let usernameDivToken = state.push("div_chat_transcript_username", "div", 1);
|
||
usernameDivToken.attrs = [["class", "chat-transcript-username"]];
|
||
|
||
let displayName;
|
||
if (options.formatUsername) {
|
||
displayName = options.formatUsername(username);
|
||
} else {
|
||
displayName = username;
|
||
}
|
||
|
||
const usernameToken = state.push("html_inline", "", 0);
|
||
usernameToken.content = displayName;
|
||
|
||
state.push("div_chat_transcript_username", "div", -1);
|
||
// end: username
|
||
|
||
// start: time + link to message
|
||
let datetimeDivToken = state.push("div_chat_transcript_datetime", "div", 1);
|
||
datetimeDivToken.attrs = [["class", "chat-transcript-datetime"]];
|
||
|
||
// for some cases, like archiving, we don't want the link to the
|
||
// chat message because it will just result in a 404
|
||
// also handles the case where the quote doesn’t contain
|
||
// enough data to build a valid channel/message link
|
||
if (noLink || !channelLink) {
|
||
let spanToken = state.push("span_open", "span", 1);
|
||
spanToken.attrs = [["title", messageTimeStart]];
|
||
|
||
spanToken.block = false;
|
||
if (channelName && !multiQuote) {
|
||
let channelLinkToken = state.push("link_open", "a", 1);
|
||
channelLinkToken.attrs = [
|
||
["class", "chat-transcript-channel"],
|
||
["href", channelLink],
|
||
];
|
||
let inlineTextToken = state.push("html_inline", "", 0);
|
||
inlineTextToken.content = `#${channelName}`;
|
||
channelLinkToken = state.push("link_close", "a", -1);
|
||
channelLinkToken.block = false;
|
||
}
|
||
spanToken = state.push("span_close", "span", -1);
|
||
spanToken.block = false;
|
||
} else {
|
||
let linkToken = state.push("link_open", "a", 1);
|
||
linkToken.attrs = [
|
||
["href", `${channelLink}/${messageIdStart}`],
|
||
["title", messageTimeStart],
|
||
];
|
||
|
||
linkToken.block = false;
|
||
linkToken = state.push("link_close", "a", -1);
|
||
linkToken.block = false;
|
||
}
|
||
|
||
state.push("div_chat_transcript_datetime", "div", -1);
|
||
// end: time + link to message
|
||
|
||
// start: channel link for !multiQuote
|
||
if (channelName && !multiQuote) {
|
||
let channelLinkToken = state.push("link_open", "a", 1);
|
||
channelLinkToken.attrs = [
|
||
["class", "chat-transcript-channel"],
|
||
["href", channelLink],
|
||
];
|
||
let inlineTextToken = state.push("html_inline", "", 0);
|
||
inlineTextToken.content = `#${channelName}`;
|
||
channelLinkToken = state.push("link_close", "a", -1);
|
||
channelLinkToken.block = false;
|
||
}
|
||
// end: channel link for !multiQuote
|
||
|
||
state.push("div_chat_transcript_user", "div", -1);
|
||
|
||
let messagesToken = state.push("div_chat_transcript_messages", "div", 1);
|
||
messagesToken.attrs = [["class", "chat-transcript-messages"]];
|
||
|
||
if (threadId) {
|
||
const regex = /\[chat/i;
|
||
const match = regex.exec(content);
|
||
|
||
if (match) {
|
||
const threadToken = state.push("html_raw", "", 1);
|
||
|
||
threadToken.content = customMarkdownCookFn(
|
||
content.substring(0, match.index)
|
||
);
|
||
state.push("html_raw", "", -1);
|
||
state.push("div_thread_close", "div", -1);
|
||
state.push("summary_chat_transcript_close", "summary", -1);
|
||
const token = state.push("html_raw", "", 1);
|
||
|
||
token.content = customMarkdownCookFn(content.substring(match.index));
|
||
state.push("html_raw", "", -1);
|
||
state.push("details_chat_transcript_wrap_close", "details", -1);
|
||
}
|
||
} else {
|
||
// rendering chat message content with limited markdown rule subset
|
||
const token = state.push("html_raw", "", 1);
|
||
|
||
token.content = customMarkdownCookFn(content);
|
||
state.push("html_raw", "", -1);
|
||
}
|
||
|
||
if (reactions) {
|
||
let emojiHtmlCache = {};
|
||
let reactionsToken = state.push(
|
||
"div_chat_transcript_reactions",
|
||
"div",
|
||
1
|
||
);
|
||
reactionsToken.attrs = [["class", "chat-transcript-reactions"]];
|
||
|
||
reactions.split(";").forEach((reaction) => {
|
||
const split = reaction.split(":");
|
||
const emoji = split[0];
|
||
const usernames = split[1].split(",");
|
||
|
||
const reactToken = state.push("div_chat_transcript_reaction", "div", 1);
|
||
reactToken.attrs = [["class", "chat-transcript-reaction"]];
|
||
const emojiToken = state.push("html_inline", "", 0);
|
||
if (!emojiHtmlCache[emoji]) {
|
||
emojiHtmlCache[emoji] = performEmojiUnescape(`:${emoji}:`, {
|
||
getURL: options.getURL,
|
||
emojiSet: options.emojiSet,
|
||
emojiCDNUrl: options.emojiCDNUrl,
|
||
enableEmojiShortcuts: options.enableEmojiShortcuts,
|
||
inlineEmoji: options.inlineEmoji,
|
||
lazy: true,
|
||
});
|
||
}
|
||
emojiToken.content = `${
|
||
emojiHtmlCache[emoji]
|
||
} ${usernames.length.toString()}`;
|
||
state.push("div_chat_transcript_reaction", "div", -1);
|
||
});
|
||
state.push("div_chat_transcript_reactions", "div", -1);
|
||
}
|
||
|
||
state.push("div_chat_transcript_messages", "div", -1);
|
||
state.push("div_chat_transcript_wrap", "div", -1);
|
||
return true;
|
||
},
|
||
};
|
||
|
||
export function setup(helper) {
|
||
helper.allowList([
|
||
"svg[class=fa d-icon d-icon-discourse-threads svg-icon svg-node]",
|
||
"use[href=#discourse-threads]",
|
||
"div[class=chat-transcript]",
|
||
"details[class=chat-transcript]",
|
||
"div[class=chat-transcript chat-transcript-chained]",
|
||
"details[class=chat-transcript chat-transcript-chained]",
|
||
"div.chat-transcript-meta",
|
||
"div.chat-transcript-user",
|
||
"div.chat-transcript-username",
|
||
"div.chat-transcript-user-avatar",
|
||
"div.chat-transcript-messages",
|
||
"div.chat-transcript-datetime",
|
||
"div.chat-transcript-reactions",
|
||
"div.chat-transcript-reaction",
|
||
"span[title]",
|
||
"div[data-message-id]",
|
||
"div[data-channel-name]",
|
||
"div[data-channel-id]",
|
||
"div[data-username]",
|
||
"div[data-datetime]",
|
||
"a.chat-transcript-channel",
|
||
"div.chat-transcript-thread",
|
||
"div.chat-transcript-thread-header",
|
||
"span.chat-transcript-thread-header__title",
|
||
]);
|
||
|
||
helper.registerOptions((opts, siteSettings) => {
|
||
opts.features["chat-transcript"] = !!siteSettings.chat_enabled;
|
||
});
|
||
|
||
helper.registerPlugin((md) => {
|
||
if (md.options.discourse.features["chat-transcript"]) {
|
||
md.block.bbcode.ruler.push("chat-transcript", chatTranscriptRule);
|
||
}
|
||
});
|
||
|
||
helper.buildCookFunction((opts, generateCookFunction) => {
|
||
if (!opts.discourse.additionalOptions?.chat) {
|
||
return;
|
||
}
|
||
|
||
const chatAdditionalOpts = opts.discourse.additionalOptions.chat;
|
||
|
||
// we need to be able to quote images from chat, but the image rule is usually
|
||
// banned for chat messages
|
||
const markdownItRules =
|
||
chatAdditionalOpts.limited_pretty_text_markdown_rules.concat("image");
|
||
|
||
generateCookFunction(
|
||
{
|
||
featuresOverride: chatAdditionalOpts.limited_pretty_text_features,
|
||
markdownItRules,
|
||
hashtagLookup: opts.discourse.hashtagLookup,
|
||
hashtagTypesInPriorityOrder:
|
||
chatAdditionalOpts.hashtag_configurations["chat-composer"],
|
||
hashtagIcons: opts.discourse.hashtagIcons,
|
||
},
|
||
(customCookFn) => {
|
||
customMarkdownCookFn = customCookFn;
|
||
}
|
||
);
|
||
});
|
||
}
|