some general project restructuring
parent
3816700703
commit
873aecbc2e
|
@ -52,7 +52,7 @@ function load_script_url(url: string) : Promise<void> {
|
||||||
document.getElementById("scripts").appendChild(script_tag);
|
document.getElementById("scripts").appendChild(script_tag);
|
||||||
|
|
||||||
script_tag.src = config.baseUrl + url;
|
script_tag.src = config.baseUrl + url;
|
||||||
})).then(result => {
|
})).then(() => {
|
||||||
/* cleanup memory */
|
/* cleanup memory */
|
||||||
_script_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
|
_script_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
|
||||||
return _script_promises[url];
|
return _script_promises[url];
|
||||||
|
|
|
@ -1,445 +1,3 @@
|
||||||
import {Settings, settings} from "tc-shared/settings";
|
|
||||||
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
|
||||||
import {spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
|
|
||||||
import {copy_to_clipboard} from "tc-shared/utils/helpers";
|
|
||||||
import * as loader from "tc-loader";
|
|
||||||
import * as image_preview from "./ui/frames/image_preview"
|
|
||||||
import * as DOMPurify from "dompurify";
|
|
||||||
|
|
||||||
import {parse as parseBBCode} from "vendor/xbbcode/parser";
|
|
||||||
|
|
||||||
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
|
||||||
import HTMLRenderer from "vendor/xbbcode/renderer/html";
|
|
||||||
import TextRenderer from "vendor/xbbcode/renderer/text";
|
|
||||||
|
|
||||||
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
|
||||||
import {TagElement, TextElement} from "vendor/xbbcode/elements";
|
|
||||||
import * as React from "react";
|
|
||||||
import {XBBCodeRenderer} from "vendor/xbbcode/react";
|
|
||||||
|
|
||||||
import * as emojiRegex from "emoji-regex";
|
|
||||||
import * as hljs from 'highlight.js/lib/core';
|
|
||||||
import '!style-loader!css-loader!highlight.js/styles/darcula.css';
|
|
||||||
import {tra} from "tc-shared/i18n/localize";
|
|
||||||
|
|
||||||
const emojiRegexInstance = (emojiRegex as any)() as RegExp;
|
|
||||||
|
|
||||||
const registerLanguage = (name, language: Promise<any>) => {
|
|
||||||
language.then(lan => hljs.registerLanguage(name, lan)).catch(error => {
|
|
||||||
console.warn("Failed to load language %s (%o)", name, error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
registerLanguage("javascript", import("highlight.js/lib/languages/javascript"));
|
|
||||||
registerLanguage("actionscript", import("highlight.js/lib/languages/actionscript"));
|
|
||||||
registerLanguage("armasm", import("highlight.js/lib/languages/armasm"));
|
|
||||||
registerLanguage("basic", import("highlight.js/lib/languages/basic"));
|
|
||||||
registerLanguage("c-like", import("highlight.js/lib/languages/c-like"));
|
|
||||||
registerLanguage("c", import("highlight.js/lib/languages/c"));
|
|
||||||
registerLanguage("cmake", import("highlight.js/lib/languages/cmake"));
|
|
||||||
registerLanguage("coffeescript", import("highlight.js/lib/languages/coffeescript"));
|
|
||||||
registerLanguage("cpp", import("highlight.js/lib/languages/cpp"));
|
|
||||||
registerLanguage("csharp", import("highlight.js/lib/languages/csharp"));
|
|
||||||
registerLanguage("css", import("highlight.js/lib/languages/css"));
|
|
||||||
registerLanguage("dart", import("highlight.js/lib/languages/dart"));
|
|
||||||
registerLanguage("delphi", import("highlight.js/lib/languages/delphi"));
|
|
||||||
registerLanguage("dockerfile", import("highlight.js/lib/languages/dockerfile"));
|
|
||||||
registerLanguage("elixir", import("highlight.js/lib/languages/elixir"));
|
|
||||||
registerLanguage("erlang", import("highlight.js/lib/languages/erlang"));
|
|
||||||
registerLanguage("fortran", import("highlight.js/lib/languages/fortran"));
|
|
||||||
registerLanguage("go", import("highlight.js/lib/languages/go"));
|
|
||||||
registerLanguage("groovy", import("highlight.js/lib/languages/groovy"));
|
|
||||||
registerLanguage("ini", import("highlight.js/lib/languages/ini"));
|
|
||||||
registerLanguage("java", import("highlight.js/lib/languages/java"));
|
|
||||||
registerLanguage("javascript", import("highlight.js/lib/languages/javascript"));
|
|
||||||
registerLanguage("json", import("highlight.js/lib/languages/json"));
|
|
||||||
registerLanguage("kotlin", import("highlight.js/lib/languages/kotlin"));
|
|
||||||
registerLanguage("latex", import("highlight.js/lib/languages/latex"));
|
|
||||||
registerLanguage("lua", import("highlight.js/lib/languages/lua"));
|
|
||||||
registerLanguage("makefile", import("highlight.js/lib/languages/makefile"));
|
|
||||||
registerLanguage("markdown", import("highlight.js/lib/languages/markdown"));
|
|
||||||
registerLanguage("mathematica", import("highlight.js/lib/languages/mathematica"));
|
|
||||||
registerLanguage("matlab", import("highlight.js/lib/languages/matlab"));
|
|
||||||
registerLanguage("objectivec", import("highlight.js/lib/languages/objectivec"));
|
|
||||||
registerLanguage("perl", import("highlight.js/lib/languages/perl"));
|
|
||||||
registerLanguage("php", import("highlight.js/lib/languages/php"));
|
|
||||||
registerLanguage("plaintext", import("highlight.js/lib/languages/plaintext"));
|
|
||||||
registerLanguage("powershell", import("highlight.js/lib/languages/powershell"));
|
|
||||||
registerLanguage("protobuf", import("highlight.js/lib/languages/protobuf"));
|
|
||||||
registerLanguage("python", import("highlight.js/lib/languages/python"));
|
|
||||||
registerLanguage("ruby", import("highlight.js/lib/languages/ruby"));
|
|
||||||
registerLanguage("rust", import("highlight.js/lib/languages/rust"));
|
|
||||||
registerLanguage("scala", import("highlight.js/lib/languages/scala"));
|
|
||||||
registerLanguage("shell", import("highlight.js/lib/languages/shell"));
|
|
||||||
registerLanguage("sql", import("highlight.js/lib/languages/sql"));
|
|
||||||
registerLanguage("swift", import("highlight.js/lib/languages/swift"));
|
|
||||||
registerLanguage("typescript", import("highlight.js/lib/languages/typescript"));
|
|
||||||
registerLanguage("vbnet", import("highlight.js/lib/languages/vbnet"));
|
|
||||||
registerLanguage("vbscript", import("highlight.js/lib/languages/vbscript"));
|
|
||||||
registerLanguage("x86asm", import("highlight.js/lib/languages/x86asm"));
|
|
||||||
registerLanguage("xml", import("highlight.js/lib/languages/xml"));
|
|
||||||
registerLanguage("yaml", import("highlight.js/lib/languages/yaml"));
|
|
||||||
|
|
||||||
const rendererText = new TextRenderer();
|
|
||||||
const rendererReact = new ReactRenderer();
|
|
||||||
const rendererHTML = new HTMLRenderer(rendererReact);
|
|
||||||
|
|
||||||
export namespace bbcode {
|
|
||||||
const yt_url_regex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/;
|
|
||||||
|
|
||||||
export const allowedBBCodes = [
|
|
||||||
"b", "big",
|
|
||||||
"i", "italic",
|
|
||||||
"u", "underlined",
|
|
||||||
"s", "strikethrough",
|
|
||||||
"color",
|
|
||||||
"url",
|
|
||||||
"code",
|
|
||||||
"i-code", "icode",
|
|
||||||
"sub", "sup",
|
|
||||||
"size",
|
|
||||||
"hr", "br",
|
|
||||||
"left", "l", "center", "c", "right", "r",
|
|
||||||
|
|
||||||
"ul", "ol", "list",
|
|
||||||
"li",
|
|
||||||
|
|
||||||
"table",
|
|
||||||
"tr", "td", "th",
|
|
||||||
|
|
||||||
"yt", "youtube",
|
|
||||||
"img",
|
|
||||||
|
|
||||||
"quote"
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface FormatSettings {
|
|
||||||
is_chat_message?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function preprocessMessage(message: string, fsettings?: FormatSettings) {
|
|
||||||
fsettings = fsettings || {};
|
|
||||||
|
|
||||||
single_url_parse:
|
|
||||||
if(fsettings.is_chat_message) {
|
|
||||||
/* try if its only one url */
|
|
||||||
const raw_url = message.replace(/\[url(=\S+)?](\S+)\[\/url]/, "$2");
|
|
||||||
let url: URL;
|
|
||||||
try {
|
|
||||||
url = new URL(raw_url);
|
|
||||||
} catch(error) {
|
|
||||||
break single_url_parse;
|
|
||||||
}
|
|
||||||
|
|
||||||
single_url_yt:
|
|
||||||
{
|
|
||||||
const result = raw_url.match(yt_url_regex);
|
|
||||||
if(!result) break single_url_yt;
|
|
||||||
|
|
||||||
return "[yt]https://www.youtube.com/watch?v=" + result[5] + "[/yt]";
|
|
||||||
}
|
|
||||||
|
|
||||||
single_url_image:
|
|
||||||
{
|
|
||||||
const ext_index = url.pathname.lastIndexOf(".");
|
|
||||||
if(ext_index == -1) break single_url_image;
|
|
||||||
|
|
||||||
const ext_name = url.pathname.substr(ext_index + 1).toLowerCase();
|
|
||||||
if([
|
|
||||||
"jpeg", "jpg",
|
|
||||||
"png", "bmp", "gif",
|
|
||||||
"tiff", "pdf", "svg"
|
|
||||||
].findIndex(e => e === ext_name) == -1) break single_url_image;
|
|
||||||
|
|
||||||
return "[img]" + message + "[/img]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function format(message: string, fsettings?: FormatSettings) : JQuery[] {
|
|
||||||
message = preprocessMessage(message, fsettings);
|
|
||||||
const result = parseBBCode(message, {
|
|
||||||
tag_whitelist: allowedBBCodes
|
|
||||||
});
|
|
||||||
|
|
||||||
let html = result.map(e => rendererHTML.render(e)).join("");
|
|
||||||
/* FIXME: TODO or remove JQuery renderer
|
|
||||||
if(settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES))
|
|
||||||
html = twemoji.parse(html);
|
|
||||||
*/
|
|
||||||
|
|
||||||
const container = $.spawn("div") as JQuery;
|
|
||||||
container[0].innerHTML = html;
|
|
||||||
|
|
||||||
/* fixup some listeners */
|
|
||||||
container.find("a")
|
|
||||||
.attr('target', "_blank")
|
|
||||||
.on('contextmenu', event => {
|
|
||||||
if(event.isDefaultPrevented())
|
|
||||||
return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
spawnUrlContextMenu(event.pageX, event.pageY, $(event.target).attr("href"));
|
|
||||||
});
|
|
||||||
|
|
||||||
container.find("img").on('load', event => load_image(event.target as HTMLImageElement));
|
|
||||||
|
|
||||||
return [container.contents() as JQuery];
|
|
||||||
//return result.root_tag.content.map(e => e.build_html()).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 ? "inline" : "") + "block").html(entry == "" && idx != 0 ? " " : entry));
|
|
||||||
}
|
|
||||||
|
|
||||||
function spawnUrlContextMenu(pageX: number, pageY: number, target: string) {
|
|
||||||
contextmenu.spawn_context_menu(pageX, pageY, {
|
|
||||||
callback: () => {
|
|
||||||
const win = window.open(target, '_blank');
|
|
||||||
win.focus();
|
|
||||||
},
|
|
||||||
name: tr("Open URL"),
|
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
|
||||||
icon_class: "client-browse-addon-online"
|
|
||||||
}, {
|
|
||||||
callback: () => {
|
|
||||||
//TODO
|
|
||||||
},
|
|
||||||
name: tr("Open URL in Browser"),
|
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
|
||||||
visible: __build.target === "client" && false // Currently not possible
|
|
||||||
}, contextmenu.Entry.HR(), {
|
|
||||||
callback: () => copy_to_clipboard(target),
|
|
||||||
name: tr("Copy URL to clipboard"),
|
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
|
||||||
icon_class: "client-copy"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function load_image(entry: HTMLImageElement) {
|
|
||||||
if(!entry.hasAttribute("x-image-url"))
|
|
||||||
return;
|
|
||||||
|
|
||||||
const url = decodeURIComponent(entry.getAttribute("x-image-url") || "");
|
|
||||||
entry.removeAttribute("x-image-url");
|
|
||||||
|
|
||||||
let proxiedURL;
|
|
||||||
try {
|
|
||||||
const parsedURL = new URL(url);
|
|
||||||
if(parsedURL.hostname === "cdn.discordapp.com") {
|
|
||||||
proxiedURL = url;
|
|
||||||
}
|
|
||||||
} catch (e) { }
|
|
||||||
|
|
||||||
if(!proxiedURL) {
|
|
||||||
proxiedURL = "https://images.weserv.nl/?url=" + encodeURIComponent(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.onload = undefined;
|
|
||||||
entry.src = proxiedURL;
|
|
||||||
|
|
||||||
const parent = $(entry.parentElement);
|
|
||||||
parent.on('contextmenu', event => {
|
|
||||||
contextmenu.spawn_context_menu(event.pageX, event.pageY, {
|
|
||||||
callback: () => {
|
|
||||||
const win = window.open(url, '_blank');
|
|
||||||
win.focus();
|
|
||||||
},
|
|
||||||
name: tr("Open image in browser"),
|
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
|
||||||
icon_class: "client-browse-addon-online"
|
|
||||||
}, contextmenu.Entry.HR(), {
|
|
||||||
callback: () => copy_to_clipboard(url),
|
|
||||||
name: tr("Copy image URL to clipboard"),
|
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
|
||||||
icon_class: "client-copy"
|
|
||||||
})
|
|
||||||
});
|
|
||||||
parent.css("cursor", "pointer").on('click', () => image_preview.preview_image(proxiedURL, url));
|
|
||||||
}
|
|
||||||
|
|
||||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
|
||||||
name: "XBBCode code tag init",
|
|
||||||
function: async () => {
|
|
||||||
let reactId = 0;
|
|
||||||
|
|
||||||
/* override default parser */
|
|
||||||
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
|
||||||
tags(): string | string[] {
|
|
||||||
return ["code", "icode", "i-code"];
|
|
||||||
}
|
|
||||||
|
|
||||||
render(element: TagElement): React.ReactNode {
|
|
||||||
const klass = element.tagNormalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code";
|
|
||||||
const language = (element.options || "").replace("\"", "'").toLowerCase();
|
|
||||||
|
|
||||||
let lines = rendererText.renderContent(element).join("").split("\n");
|
|
||||||
if(lines.length > 1) {
|
|
||||||
if(lines[0].length === 0)
|
|
||||||
lines = lines.slice(1);
|
|
||||||
|
|
||||||
if(lines[lines.length - 1]?.length === 0)
|
|
||||||
lines = lines.slice(0, lines.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: HighlightJSResult;
|
|
||||||
|
|
||||||
const detectedLanguage = hljs.getLanguage(language);
|
|
||||||
if(detectedLanguage)
|
|
||||||
result = hljs.highlight(detectedLanguage.name, lines.join("\n"), true);
|
|
||||||
else
|
|
||||||
result = hljs.highlightAuto(lines.join("\n"));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<pre key={"er-" + ++reactId} className={klass}>
|
|
||||||
<code
|
|
||||||
className={"hljs"}
|
|
||||||
title={tra("{} code", result.language || tr("general"))}
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: DOMPurify.sanitize(result.value)
|
|
||||||
}}
|
|
||||||
onContextMenu={event => {
|
|
||||||
event.preventDefault();
|
|
||||||
spawn_context_menu(event.pageX, event.pageY, {
|
|
||||||
callback: () => copy_to_clipboard(lines.join("\n")),
|
|
||||||
name: tr("Copy code"),
|
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
|
||||||
icon_class: "client-copy"
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</pre>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const regexUrl = /^(?:[a-zA-Z]{1,16}):(?:\/{1,3}|\\)[-a-zA-Z0-9:;,@#%&()~_?+=\/\\.]*$/g;
|
|
||||||
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
|
||||||
render(element: TagElement, renderer: ReactRenderer): React.ReactNode {
|
|
||||||
let target;
|
|
||||||
if (!element.options)
|
|
||||||
target = rendererText.render(element);
|
|
||||||
else
|
|
||||||
target = element.options;
|
|
||||||
|
|
||||||
regexUrl.lastIndex = 0;
|
|
||||||
if (!regexUrl.test(target))
|
|
||||||
target = '#';
|
|
||||||
|
|
||||||
/* TODO: Implement client URLs */
|
|
||||||
return <a key={"er-" + ++reactId} className={"xbbcode xbbcode-tag-url"} href={target} target={"_blank"} onContextMenu={event => {
|
|
||||||
event.preventDefault();
|
|
||||||
spawnUrlContextMenu(event.pageX, event.pageY, target);
|
|
||||||
}}>
|
|
||||||
{renderer.renderContent(element)}
|
|
||||||
</a>;
|
|
||||||
}
|
|
||||||
|
|
||||||
tags(): string | string[] {
|
|
||||||
return "url";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const regexImage = /^(?:https?):(?:\/{1,3}|\\)[-a-zA-Z0-9:;,@#%&()~_?+=\/\\.]*$/g;
|
|
||||||
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
|
||||||
tags(): string | string[] {
|
|
||||||
return ["img", "image"];
|
|
||||||
}
|
|
||||||
|
|
||||||
render(element: TagElement): React.ReactNode {
|
|
||||||
let target;
|
|
||||||
let content = rendererText.render(element);
|
|
||||||
if (!element.options) {
|
|
||||||
target = content;
|
|
||||||
} else
|
|
||||||
target = element.options;
|
|
||||||
|
|
||||||
regexImage.lastIndex = 0;
|
|
||||||
if (!regexImage.test(target))
|
|
||||||
return <React.Fragment key={"er-" + ++reactId}>{"[img]" + content + "[/img]"}</React.Fragment>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={"er-" + ++reactId} className={"xbbcode-tag-img"}>
|
|
||||||
<img src={"img/loading_image.svg"} onLoad={event => load_image(event.currentTarget)} x-image-url={encodeURIComponent(target)} title={target} alt={target} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function toCodePoint(unicodeSurrogates) {
|
|
||||||
let r = [],
|
|
||||||
c = 0,
|
|
||||||
p = 0,
|
|
||||||
i = 0;
|
|
||||||
while (i < unicodeSurrogates.length) {
|
|
||||||
c = unicodeSurrogates.charCodeAt(i++);
|
|
||||||
if (p) {
|
|
||||||
r.push((0x10000 + ((p - 0xD800) << 10) + (c - 0xDC00)).toString(16));
|
|
||||||
p = 0;
|
|
||||||
} else if (0xD800 <= c && c <= 0xDBFF) {
|
|
||||||
p = c;
|
|
||||||
} else {
|
|
||||||
r.push(c.toString(16));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r.join("-");
|
|
||||||
}
|
|
||||||
|
|
||||||
const U200D = String.fromCharCode(0x200D);
|
|
||||||
const UFE0Fg = /\uFE0F/g;
|
|
||||||
function grabTheRightIcon(rawText) {
|
|
||||||
// if variant is present as \uFE0F
|
|
||||||
return toCodePoint(rawText.indexOf(U200D) < 0 ?
|
|
||||||
rawText.replace(UFE0Fg, '') :
|
|
||||||
rawText
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
rendererReact.setTextRenderer(new class extends ElementRenderer<TextElement, React.ReactNode> {
|
|
||||||
render(element: TextElement, renderer: ReactRenderer): React.ReactNode {
|
|
||||||
if(!settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES))
|
|
||||||
return element.text();
|
|
||||||
|
|
||||||
let text = element.text();
|
|
||||||
emojiRegexInstance.lastIndex = 0;
|
|
||||||
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
let lastIndex = 0;
|
|
||||||
while(true) {
|
|
||||||
let match = emojiRegexInstance.exec(text);
|
|
||||||
|
|
||||||
const rawText = text.substring(lastIndex, match?.index);
|
|
||||||
if(rawText)
|
|
||||||
result.push(renderer.renderAsText(rawText, false));
|
|
||||||
|
|
||||||
if(!match)
|
|
||||||
break;
|
|
||||||
|
|
||||||
let hash = grabTheRightIcon(match[0]);
|
|
||||||
result.push(<img key={"er-" + ++reactId} draggable={false} src={"https://twemoji.maxcdn.com/v/12.1.2/72x72/" + hash + ".png"} alt={match[0]} className={"chat-emoji"} />);
|
|
||||||
lastIndex = match.index + match[0].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
tags(): string | string[] { return undefined; }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
priority: 10
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BBCodeChatMessage = (props: { message: string }) => (
|
|
||||||
<XBBCodeRenderer options={{ tag_whitelist: bbcode.allowedBBCodes }} renderer={rendererReact}>
|
|
||||||
{bbcode.preprocessMessage(props.message, { is_chat_message: true })}
|
|
||||||
</XBBCodeRenderer>
|
|
||||||
);
|
|
||||||
|
|
||||||
export function sanitize_text(text: string) : string {
|
|
||||||
return $(DOMPurify.sanitize("<a>" + text + "</a>", {
|
|
||||||
})).text();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatDate(secs: number) : string {
|
export function formatDate(secs: number) : string {
|
||||||
let years = Math.floor(secs / (60 * 60 * 24 * 365));
|
let years = Math.floor(secs / (60 * 60 * 24 * 365));
|
||||||
let days = Math.floor(secs / (60 * 60 * 24)) % 365;
|
let days = Math.floor(secs / (60 * 60 * 24)) % 365;
|
||||||
|
|
|
@ -15,12 +15,13 @@ import {
|
||||||
} from "tc-shared/ui/client";
|
} from "tc-shared/ui/client";
|
||||||
import {ChannelEntry} from "tc-shared/ui/channel";
|
import {ChannelEntry} from "tc-shared/ui/channel";
|
||||||
import {ConnectionHandler, ConnectionState, DisconnectReason, ViewReasonId} from "tc-shared/ConnectionHandler";
|
import {ConnectionHandler, ConnectionState, DisconnectReason, ViewReasonId} from "tc-shared/ConnectionHandler";
|
||||||
import {bbcode_chat, formatMessage} from "tc-shared/ui/frames/chat";
|
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||||
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
||||||
import {spawnPoke} from "tc-shared/ui/modal/ModalPoke";
|
import {spawnPoke} from "tc-shared/ui/modal/ModalPoke";
|
||||||
import {AbstractCommandHandler, AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
|
import {AbstractCommandHandler, AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
|
||||||
import {batch_updates, BatchUpdateType, flush_batched_updates} from "tc-shared/ui/react-elements/ReactComponentBase";
|
import {batch_updates, BatchUpdateType, flush_batched_updates} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||||
import {OutOfViewClient} from "tc-shared/ui/frames/side/PrivateConversationManager";
|
import {OutOfViewClient} from "tc-shared/ui/frames/side/PrivateConversationManager";
|
||||||
|
import {renderBBCodeAsJQuery} from "tc-shared/text/bbcode";
|
||||||
|
|
||||||
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
|
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
|
||||||
constructor(connection: AbstractServerConnection) {
|
constructor(connection: AbstractServerConnection) {
|
||||||
|
@ -218,7 +219,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
if(properties.virtualserver_hostmessage || properties.virtualserver_hostmessage_mode == 3)
|
if(properties.virtualserver_hostmessage || properties.virtualserver_hostmessage_mode == 3)
|
||||||
createModal({
|
createModal({
|
||||||
header: tr("Host message"),
|
header: tr("Host message"),
|
||||||
body: bbcode_chat(properties.virtualserver_hostmessage),
|
body: renderBBCodeAsJQuery(properties.virtualserver_hostmessage, { convertSingleUrls: false }),
|
||||||
footer: undefined
|
footer: undefined
|
||||||
}).open();
|
}).open();
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
//Used by CertAccept popup
|
//Used by CertAccept popup
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
||||||
|
function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
||||||
|
|
||||||
interface Array<T> {
|
interface Array<T> {
|
||||||
remove(elem?: T): boolean;
|
remove(elem?: T): boolean;
|
||||||
last?(): T;
|
last?(): T;
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
import {XBBCodeRenderer} from "vendor/xbbcode/react";
|
||||||
|
import * as React from "react";
|
||||||
|
import {rendererHTML, rendererReact} from "tc-shared/text/bbcode/renderer";
|
||||||
|
import {parse as parseBBCode} from "vendor/xbbcode/parser";
|
||||||
|
import {fixupJQueryUrlTags} from "tc-shared/text/bbcode/url";
|
||||||
|
import {fixupJQueryImageTags} from "tc-shared/text/bbcode/image";
|
||||||
|
|
||||||
|
export const escapeBBCode = (text: string) => text.replace(/([\[\]])/g, "\\$1");
|
||||||
|
|
||||||
|
export const allowedBBCodes = [
|
||||||
|
"b", "big",
|
||||||
|
"i", "italic",
|
||||||
|
"u", "underlined",
|
||||||
|
"s", "strikethrough",
|
||||||
|
"color",
|
||||||
|
"url",
|
||||||
|
"code",
|
||||||
|
"i-code", "icode",
|
||||||
|
"sub", "sup",
|
||||||
|
"size",
|
||||||
|
"hr", "br",
|
||||||
|
"left", "l", "center", "c", "right", "r",
|
||||||
|
|
||||||
|
"ul", "ol", "list",
|
||||||
|
"li",
|
||||||
|
|
||||||
|
"table",
|
||||||
|
"tr", "td", "th",
|
||||||
|
|
||||||
|
"yt", "youtube",
|
||||||
|
"img",
|
||||||
|
|
||||||
|
"quote"
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface BBCodeRenderOptions {
|
||||||
|
convertSingleUrls: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const yt_url_regex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/;
|
||||||
|
|
||||||
|
function preprocessMessage(message: string, settings: BBCodeRenderOptions) : string {
|
||||||
|
/* try if its only one url */
|
||||||
|
single_url_parse:
|
||||||
|
if(settings.convertSingleUrls) {
|
||||||
|
const raw_url = message.replace(/\[url(=\S+)?](\S+)\[\/url]/, "$2");
|
||||||
|
let url: URL;
|
||||||
|
try {
|
||||||
|
url = new URL(raw_url);
|
||||||
|
} catch(error) {
|
||||||
|
break single_url_parse;
|
||||||
|
}
|
||||||
|
|
||||||
|
single_url_yt:
|
||||||
|
{
|
||||||
|
const result = raw_url.match(yt_url_regex);
|
||||||
|
if(!result) break single_url_yt;
|
||||||
|
|
||||||
|
return "[yt]https://www.youtube.com/watch?v=" + result[5] + "[/yt]";
|
||||||
|
}
|
||||||
|
|
||||||
|
single_url_image:
|
||||||
|
{
|
||||||
|
const ext_index = url.pathname.lastIndexOf(".");
|
||||||
|
if(ext_index == -1) break single_url_image;
|
||||||
|
|
||||||
|
const ext_name = url.pathname.substr(ext_index + 1).toLowerCase();
|
||||||
|
if([
|
||||||
|
"jpeg", "jpg",
|
||||||
|
"png", "bmp", "gif",
|
||||||
|
"tiff", "pdf", "svg"
|
||||||
|
].findIndex(e => e === ext_name) == -1) break single_url_image;
|
||||||
|
|
||||||
|
return "[img]" + message + "[/img]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BBCodeRenderer = (props: { message: string, settings: BBCodeRenderOptions }) => (
|
||||||
|
<XBBCodeRenderer options={{ tag_whitelist: allowedBBCodes }} renderer={rendererReact}>
|
||||||
|
{preprocessMessage(props.message, props.settings)}
|
||||||
|
</XBBCodeRenderer>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export function renderBBCodeAsJQuery(message: string, settings: BBCodeRenderOptions) : JQuery[] {
|
||||||
|
const result = parseBBCode(preprocessMessage(message, settings), {
|
||||||
|
tag_whitelist: allowedBBCodes
|
||||||
|
});
|
||||||
|
|
||||||
|
let html = result.map(e => rendererHTML.render(e)).join("");
|
||||||
|
|
||||||
|
const container = $.spawn("div") as JQuery;
|
||||||
|
container[0].innerHTML = html;
|
||||||
|
|
||||||
|
/* fixup some listeners */
|
||||||
|
fixupJQueryUrlTags(container);
|
||||||
|
fixupJQueryImageTags(container);
|
||||||
|
|
||||||
|
return [container.contents() as JQuery];
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
import * as loader from "tc-loader";
|
||||||
|
import { rendererReact } from "tc-shared/text/bbcode/renderer";
|
||||||
|
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
||||||
|
import {TextElement} from "vendor/xbbcode/elements";
|
||||||
|
import * as React from "react";
|
||||||
|
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
||||||
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
|
|
||||||
|
import * as emojiRegex from "emoji-regex";
|
||||||
|
|
||||||
|
const emojiRegexInstance = (emojiRegex as any)() as RegExp;
|
||||||
|
|
||||||
|
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
name: "XBBCode emoji init",
|
||||||
|
function: async () => {
|
||||||
|
let reactId = 0;
|
||||||
|
|
||||||
|
function toCodePoint(unicodeSurrogates) {
|
||||||
|
let r = [],
|
||||||
|
c = 0,
|
||||||
|
p = 0,
|
||||||
|
i = 0;
|
||||||
|
while (i < unicodeSurrogates.length) {
|
||||||
|
c = unicodeSurrogates.charCodeAt(i++);
|
||||||
|
if (p) {
|
||||||
|
r.push((0x10000 + ((p - 0xD800) << 10) + (c - 0xDC00)).toString(16));
|
||||||
|
p = 0;
|
||||||
|
} else if (0xD800 <= c && c <= 0xDBFF) {
|
||||||
|
p = c;
|
||||||
|
} else {
|
||||||
|
r.push(c.toString(16));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.join("-");
|
||||||
|
}
|
||||||
|
|
||||||
|
const U200D = String.fromCharCode(0x200D);
|
||||||
|
const UFE0Fg = /\uFE0F/g;
|
||||||
|
function grabTheRightIcon(rawText) {
|
||||||
|
// if variant is present as \uFE0F
|
||||||
|
return toCodePoint(rawText.indexOf(U200D) < 0 ?
|
||||||
|
rawText.replace(UFE0Fg, '') :
|
||||||
|
rawText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
rendererReact.setTextRenderer(new class extends ElementRenderer<TextElement, React.ReactNode> {
|
||||||
|
render(element: TextElement, renderer: ReactRenderer): React.ReactNode {
|
||||||
|
if(!settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES))
|
||||||
|
return element.text();
|
||||||
|
|
||||||
|
let text = element.text();
|
||||||
|
emojiRegexInstance.lastIndex = 0;
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
let lastIndex = 0;
|
||||||
|
while(true) {
|
||||||
|
let match = emojiRegexInstance.exec(text);
|
||||||
|
|
||||||
|
const rawText = text.substring(lastIndex, match?.index);
|
||||||
|
if(rawText)
|
||||||
|
result.push(renderer.renderAsText(rawText, false));
|
||||||
|
|
||||||
|
if(!match)
|
||||||
|
break;
|
||||||
|
|
||||||
|
let hash = grabTheRightIcon(match[0]);
|
||||||
|
result.push(<img key={"er-" + ++reactId} draggable={false} src={"https://twemoji.maxcdn.com/v/12.1.2/72x72/" + hash + ".png"} alt={match[0]} className={"chat-emoji"} />);
|
||||||
|
lastIndex = match.index + match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags(): string | string[] { return undefined; }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
priority: 10
|
||||||
|
});
|
|
@ -0,0 +1,126 @@
|
||||||
|
import * as hljs from "highlight.js/lib/core";
|
||||||
|
import * as loader from "tc-loader";
|
||||||
|
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
||||||
|
import {TagElement} from "vendor/xbbcode/elements";
|
||||||
|
import * as React from "react";
|
||||||
|
import {tra} from "tc-shared/i18n/localize";
|
||||||
|
import * as DOMPurify from "dompurify";
|
||||||
|
import {copy_to_clipboard} from "tc-shared/utils/helpers";
|
||||||
|
import {rendererReact, rendererText} from "tc-shared/text/bbcode/renderer";
|
||||||
|
import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
|
||||||
|
|
||||||
|
import '!style-loader!css-loader!highlight.js/styles/darcula.css';
|
||||||
|
|
||||||
|
const registerLanguage = (name, language: Promise<any>) => {
|
||||||
|
language.then(lan => hljs.registerLanguage(name, lan)).catch(error => {
|
||||||
|
console.warn("Failed to load language %s (%o)", name, error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
registerLanguage("javascript", import("highlight.js/lib/languages/javascript"));
|
||||||
|
registerLanguage("actionscript", import("highlight.js/lib/languages/actionscript"));
|
||||||
|
registerLanguage("armasm", import("highlight.js/lib/languages/armasm"));
|
||||||
|
registerLanguage("basic", import("highlight.js/lib/languages/basic"));
|
||||||
|
registerLanguage("c-like", import("highlight.js/lib/languages/c-like"));
|
||||||
|
registerLanguage("c", import("highlight.js/lib/languages/c"));
|
||||||
|
registerLanguage("cmake", import("highlight.js/lib/languages/cmake"));
|
||||||
|
registerLanguage("coffeescript", import("highlight.js/lib/languages/coffeescript"));
|
||||||
|
registerLanguage("cpp", import("highlight.js/lib/languages/cpp"));
|
||||||
|
registerLanguage("csharp", import("highlight.js/lib/languages/csharp"));
|
||||||
|
registerLanguage("css", import("highlight.js/lib/languages/css"));
|
||||||
|
registerLanguage("dart", import("highlight.js/lib/languages/dart"));
|
||||||
|
registerLanguage("delphi", import("highlight.js/lib/languages/delphi"));
|
||||||
|
registerLanguage("dockerfile", import("highlight.js/lib/languages/dockerfile"));
|
||||||
|
registerLanguage("elixir", import("highlight.js/lib/languages/elixir"));
|
||||||
|
registerLanguage("erlang", import("highlight.js/lib/languages/erlang"));
|
||||||
|
registerLanguage("fortran", import("highlight.js/lib/languages/fortran"));
|
||||||
|
registerLanguage("go", import("highlight.js/lib/languages/go"));
|
||||||
|
registerLanguage("groovy", import("highlight.js/lib/languages/groovy"));
|
||||||
|
registerLanguage("ini", import("highlight.js/lib/languages/ini"));
|
||||||
|
registerLanguage("java", import("highlight.js/lib/languages/java"));
|
||||||
|
registerLanguage("javascript", import("highlight.js/lib/languages/javascript"));
|
||||||
|
registerLanguage("json", import("highlight.js/lib/languages/json"));
|
||||||
|
registerLanguage("kotlin", import("highlight.js/lib/languages/kotlin"));
|
||||||
|
registerLanguage("latex", import("highlight.js/lib/languages/latex"));
|
||||||
|
registerLanguage("lua", import("highlight.js/lib/languages/lua"));
|
||||||
|
registerLanguage("makefile", import("highlight.js/lib/languages/makefile"));
|
||||||
|
registerLanguage("markdown", import("highlight.js/lib/languages/markdown"));
|
||||||
|
registerLanguage("mathematica", import("highlight.js/lib/languages/mathematica"));
|
||||||
|
registerLanguage("matlab", import("highlight.js/lib/languages/matlab"));
|
||||||
|
registerLanguage("objectivec", import("highlight.js/lib/languages/objectivec"));
|
||||||
|
registerLanguage("perl", import("highlight.js/lib/languages/perl"));
|
||||||
|
registerLanguage("php", import("highlight.js/lib/languages/php"));
|
||||||
|
registerLanguage("plaintext", import("highlight.js/lib/languages/plaintext"));
|
||||||
|
registerLanguage("powershell", import("highlight.js/lib/languages/powershell"));
|
||||||
|
registerLanguage("protobuf", import("highlight.js/lib/languages/protobuf"));
|
||||||
|
registerLanguage("python", import("highlight.js/lib/languages/python"));
|
||||||
|
registerLanguage("ruby", import("highlight.js/lib/languages/ruby"));
|
||||||
|
registerLanguage("rust", import("highlight.js/lib/languages/rust"));
|
||||||
|
registerLanguage("scala", import("highlight.js/lib/languages/scala"));
|
||||||
|
registerLanguage("shell", import("highlight.js/lib/languages/shell"));
|
||||||
|
registerLanguage("sql", import("highlight.js/lib/languages/sql"));
|
||||||
|
registerLanguage("swift", import("highlight.js/lib/languages/swift"));
|
||||||
|
registerLanguage("typescript", import("highlight.js/lib/languages/typescript"));
|
||||||
|
registerLanguage("vbnet", import("highlight.js/lib/languages/vbnet"));
|
||||||
|
registerLanguage("vbscript", import("highlight.js/lib/languages/vbscript"));
|
||||||
|
registerLanguage("x86asm", import("highlight.js/lib/languages/x86asm"));
|
||||||
|
registerLanguage("xml", import("highlight.js/lib/languages/xml"));
|
||||||
|
registerLanguage("yaml", import("highlight.js/lib/languages/yaml"));
|
||||||
|
|
||||||
|
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
name: "XBBCode highlight init",
|
||||||
|
function: async () => {
|
||||||
|
let reactId = 0;
|
||||||
|
|
||||||
|
/* override default parser */
|
||||||
|
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
||||||
|
tags(): string | string[] {
|
||||||
|
return ["code", "icode", "i-code"];
|
||||||
|
}
|
||||||
|
|
||||||
|
render(element: TagElement): React.ReactNode {
|
||||||
|
const klass = element.tagNormalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code";
|
||||||
|
const language = (element.options || "").replace("\"", "'").toLowerCase();
|
||||||
|
|
||||||
|
let lines = rendererText.renderContent(element).join("").split("\n");
|
||||||
|
if(lines.length > 1) {
|
||||||
|
if(lines[0].length === 0)
|
||||||
|
lines = lines.slice(1);
|
||||||
|
|
||||||
|
if(lines[lines.length - 1]?.length === 0)
|
||||||
|
lines = lines.slice(0, lines.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: HighlightJSResult;
|
||||||
|
|
||||||
|
const detectedLanguage = hljs.getLanguage(language);
|
||||||
|
if(detectedLanguage)
|
||||||
|
result = hljs.highlight(detectedLanguage.name, lines.join("\n"), true);
|
||||||
|
else
|
||||||
|
result = hljs.highlightAuto(lines.join("\n"));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre key={"hrc-" + ++reactId} className={klass}>
|
||||||
|
<code
|
||||||
|
className={"hljs"}
|
||||||
|
title={tra("{} code", result.language || tr("general"))}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(result.value)
|
||||||
|
}}
|
||||||
|
onContextMenu={event => {
|
||||||
|
event.preventDefault();
|
||||||
|
spawn_context_menu(event.pageX, event.pageY, {
|
||||||
|
callback: () => copy_to_clipboard(lines.join("\n")),
|
||||||
|
name: tr("Copy code"),
|
||||||
|
type: MenuEntryType.ENTRY,
|
||||||
|
icon_class: "client-copy"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
priority: 10
|
||||||
|
});
|
|
@ -0,0 +1,89 @@
|
||||||
|
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
||||||
|
import {TagElement} from "vendor/xbbcode/elements";
|
||||||
|
import * as React from "react";
|
||||||
|
import * as loader from "tc-loader";
|
||||||
|
import {rendererReact, rendererText} from "tc-shared/text/bbcode/renderer";
|
||||||
|
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
||||||
|
import {copy_to_clipboard} from "tc-shared/utils/helpers";
|
||||||
|
import * as image_preview from "tc-shared/ui/frames/image_preview";
|
||||||
|
|
||||||
|
const regexImage = /^(?:https?):(?:\/{1,3}|\\)[-a-zA-Z0-9:;,@#%&()~_?+=\/\\.]*$/g;
|
||||||
|
|
||||||
|
function loadImageForElement(element: HTMLImageElement) {
|
||||||
|
if(!element.hasAttribute("x-image-url"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const url = decodeURIComponent(element.getAttribute("x-image-url") || "");
|
||||||
|
element.removeAttribute("x-image-url");
|
||||||
|
|
||||||
|
let proxiedURL;
|
||||||
|
try {
|
||||||
|
const parsedURL = new URL(url);
|
||||||
|
if(parsedURL.hostname === "cdn.discordapp.com") {
|
||||||
|
proxiedURL = url;
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
|
if(!proxiedURL) {
|
||||||
|
proxiedURL = "https://images.weserv.nl/?url=" + encodeURIComponent(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
element.onload = undefined;
|
||||||
|
element.src = proxiedURL;
|
||||||
|
|
||||||
|
const parent = $(element.parentElement);
|
||||||
|
parent.on('contextmenu', event => {
|
||||||
|
contextmenu.spawn_context_menu(event.pageX, event.pageY, {
|
||||||
|
callback: () => {
|
||||||
|
const win = window.open(url, '_blank');
|
||||||
|
win.focus();
|
||||||
|
},
|
||||||
|
name: tr("Open image in browser"),
|
||||||
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
icon_class: "client-browse-addon-online"
|
||||||
|
}, contextmenu.Entry.HR(), {
|
||||||
|
callback: () => copy_to_clipboard(url),
|
||||||
|
name: tr("Copy image URL to clipboard"),
|
||||||
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
icon_class: "client-copy"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
parent.css("cursor", "pointer").on('click', () => image_preview.preview_image(proxiedURL, url));
|
||||||
|
}
|
||||||
|
|
||||||
|
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
name: "XBBCode emoji init",
|
||||||
|
function: async () => {
|
||||||
|
let reactId = 0;
|
||||||
|
|
||||||
|
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
||||||
|
tags(): string | string[] {
|
||||||
|
return ["img", "image"];
|
||||||
|
}
|
||||||
|
|
||||||
|
render(element: TagElement): React.ReactNode {
|
||||||
|
let target;
|
||||||
|
let content = rendererText.render(element);
|
||||||
|
if (!element.options) {
|
||||||
|
target = content;
|
||||||
|
} else
|
||||||
|
target = element.options;
|
||||||
|
|
||||||
|
regexImage.lastIndex = 0;
|
||||||
|
if (!regexImage.test(target))
|
||||||
|
return <React.Fragment key={"er-" + ++reactId}>{"[img]" + content + "[/img]"}</React.Fragment>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={"irc-" + ++reactId} className={"xbbcode-tag-img"}>
|
||||||
|
<img src={"img/loading_image.svg"} onLoad={event => loadImageForElement(event.currentTarget)} x-image-url={encodeURIComponent(target)} title={target} alt={target} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
priority: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
export function fixupJQueryImageTags(container: JQuery) {
|
||||||
|
container.find("img").on('load', event => loadImageForElement(event.target as HTMLImageElement));
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import TextRenderer from "vendor/xbbcode/renderer/text";
|
||||||
|
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
||||||
|
import HTMLRenderer from "vendor/xbbcode/renderer/html";
|
||||||
|
|
||||||
|
import "./emoji";
|
||||||
|
import "./highlight";
|
||||||
|
|
||||||
|
export const rendererText = new TextRenderer();
|
||||||
|
export const rendererReact = new ReactRenderer();
|
||||||
|
export const rendererHTML = new HTMLRenderer(rendererReact);
|
|
@ -0,0 +1,78 @@
|
||||||
|
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
||||||
|
import {copy_to_clipboard} from "tc-shared/utils/helpers";
|
||||||
|
import * as loader from "tc-loader";
|
||||||
|
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
||||||
|
import {TagElement} from "vendor/xbbcode/elements";
|
||||||
|
import * as React from "react";
|
||||||
|
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
||||||
|
import {rendererReact, rendererText} from "tc-shared/text/bbcode/renderer";
|
||||||
|
|
||||||
|
function spawnUrlContextMenu(pageX: number, pageY: number, target: string) {
|
||||||
|
contextmenu.spawn_context_menu(pageX, pageY, {
|
||||||
|
callback: () => {
|
||||||
|
const win = window.open(target, '_blank');
|
||||||
|
win.focus();
|
||||||
|
},
|
||||||
|
name: tr("Open URL"),
|
||||||
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
icon_class: "client-browse-addon-online"
|
||||||
|
}, {
|
||||||
|
callback: () => {
|
||||||
|
//TODO
|
||||||
|
},
|
||||||
|
name: tr("Open URL in Browser"),
|
||||||
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
visible: __build.target === "client" && false // Currently not possible
|
||||||
|
}, contextmenu.Entry.HR(), {
|
||||||
|
callback: () => copy_to_clipboard(target),
|
||||||
|
name: tr("Copy URL to clipboard"),
|
||||||
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
icon_class: "client-copy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
name: "XBBCode code tag init",
|
||||||
|
function: async () => {
|
||||||
|
let reactId = 0;
|
||||||
|
|
||||||
|
const regexUrl = /^(?:[a-zA-Z]{1,16}):(?:\/{1,3}|\\)[-a-zA-Z0-9:;,@#%&()~_?+=\/\\.]*$/g;
|
||||||
|
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
||||||
|
render(element: TagElement, renderer: ReactRenderer): React.ReactNode {
|
||||||
|
let target;
|
||||||
|
if (!element.options)
|
||||||
|
target = rendererText.render(element);
|
||||||
|
else
|
||||||
|
target = element.options;
|
||||||
|
|
||||||
|
regexUrl.lastIndex = 0;
|
||||||
|
if (!regexUrl.test(target))
|
||||||
|
target = '#';
|
||||||
|
|
||||||
|
/* TODO: Implement client URLs */
|
||||||
|
return <a key={"er-" + ++reactId} className={"xbbcode xbbcode-tag-url"} href={target} target={"_blank"} onContextMenu={event => {
|
||||||
|
event.preventDefault();
|
||||||
|
spawnUrlContextMenu(event.pageX, event.pageY, target);
|
||||||
|
}}>
|
||||||
|
{renderer.renderContent(element)}
|
||||||
|
</a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags(): string | string[] {
|
||||||
|
return "url";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
priority: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export function fixupJQueryUrlTags(container: JQuery) {
|
||||||
|
container.find("a").on('contextmenu', event => {
|
||||||
|
if(event.isDefaultPrevented())
|
||||||
|
return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
spawnUrlContextMenu(event.pageX, event.pageY, $(event.target).attr("href"));
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
//https://regex101.com/r/YQbfcX/2
|
||||||
|
//static readonly URL_REGEX = /^(?<hostname>([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?<path>(?:[^\s?]+)?)(?:\?(?<query>\S+))?)?$/gm;
|
||||||
|
import * as log from "tc-shared/log";
|
||||||
|
import {LogCategory} from "tc-shared/log";
|
||||||
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
|
import {renderMarkdownAsBBCode} from "tc-shared/text/markdown";
|
||||||
|
import {escapeBBCode} from "tc-shared/text/bbcode";
|
||||||
|
|
||||||
|
const URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm;
|
||||||
|
function process_urls(message: string) : string {
|
||||||
|
const words = message.split(/[ \n]/);
|
||||||
|
for(let index = 0; index < words.length; index++) {
|
||||||
|
const flag_escaped = words[index].startsWith('!');
|
||||||
|
const unescaped = flag_escaped ? words[index].substr(1) : words[index];
|
||||||
|
|
||||||
|
_try:
|
||||||
|
try {
|
||||||
|
const url = new URL(unescaped);
|
||||||
|
log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url);
|
||||||
|
if(url.protocol !== 'http:' && url.protocol !== 'https:')
|
||||||
|
break _try;
|
||||||
|
if(flag_escaped) {
|
||||||
|
message = undefined;
|
||||||
|
words[index] = unescaped;
|
||||||
|
} else {
|
||||||
|
message = undefined;
|
||||||
|
words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]";
|
||||||
|
}
|
||||||
|
} catch(e) { /* word isn't an url */ }
|
||||||
|
|
||||||
|
if(unescaped.match(URL_REGEX)) {
|
||||||
|
if(flag_escaped) {
|
||||||
|
message = undefined;
|
||||||
|
words[index] = unescaped;
|
||||||
|
} else {
|
||||||
|
message = undefined;
|
||||||
|
words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return message || words.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function preprocess_chat_message(message: string) : string {
|
||||||
|
const process_url = settings.static_global(Settings.KEY_CHAT_TAG_URLS);
|
||||||
|
const parse_markdown = settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN);
|
||||||
|
const escape_bb = !settings.static_global(Settings.KEY_CHAT_ENABLE_BBCODE);
|
||||||
|
|
||||||
|
if(parse_markdown) {
|
||||||
|
return renderMarkdownAsBBCode(message, text => {
|
||||||
|
if(escape_bb)
|
||||||
|
text = escapeBBCode(text);
|
||||||
|
|
||||||
|
if(process_url)
|
||||||
|
text = process_urls(text);
|
||||||
|
|
||||||
|
return text;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(escape_bb)
|
||||||
|
message = escapeBBCode(message);
|
||||||
|
|
||||||
|
if(process_url)
|
||||||
|
message = process_urls(message);
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
import * as log from "tc-shared/log";
|
||||||
|
import {LogCategory} from "tc-shared/log";
|
||||||
|
import {
|
||||||
|
CodeToken, Env, FenceToken, HeadingOpenToken,
|
||||||
|
ImageToken,
|
||||||
|
LinkOpenToken, Options,
|
||||||
|
ParagraphOpenToken,
|
||||||
|
SubToken,
|
||||||
|
SupToken,
|
||||||
|
TextToken,
|
||||||
|
Token
|
||||||
|
} from "remarkable/lib";
|
||||||
|
import {escapeBBCode} from "tc-shared/text/BBCodeHelper";
|
||||||
|
const { Remarkable } = require("remarkable");
|
||||||
|
|
||||||
|
export class MD2BBCodeRenderer {
|
||||||
|
private static renderers: {[key: string]:(renderer: MD2BBCodeRenderer, token: Token) => string} = {
|
||||||
|
"text": (renderer: MD2BBCodeRenderer, token: TextToken) => renderer.options().textProcessor(token.content),
|
||||||
|
"softbreak": () => "\n",
|
||||||
|
"hardbreak": () => "\n",
|
||||||
|
|
||||||
|
"paragraph_open": (renderer: MD2BBCodeRenderer, token: ParagraphOpenToken) => {
|
||||||
|
const last_line = !renderer.last_paragraph || !renderer.last_paragraph.lines ? 0 : renderer.last_paragraph.lines[1];
|
||||||
|
const lines = token.lines[0] - last_line;
|
||||||
|
return [...new Array(lines)].map(() => "[br]").join("");
|
||||||
|
},
|
||||||
|
"paragraph_close": () => "",
|
||||||
|
|
||||||
|
"strong_open": () => "[b]",
|
||||||
|
"strong_close": () => "[/b]",
|
||||||
|
|
||||||
|
"em_open": () => "[i]",
|
||||||
|
"em_close": () => "[/i]",
|
||||||
|
|
||||||
|
"del_open": () => "[s]",
|
||||||
|
"del_close": () => "[/s]",
|
||||||
|
|
||||||
|
"sup": (renderer: MD2BBCodeRenderer, token: SupToken) => "[sup]" + renderer.options().textProcessor(token.content) + "[/sup]",
|
||||||
|
"sub": (renderer: MD2BBCodeRenderer, token: SubToken) => "[sub]" + renderer.options().textProcessor(token.content) + "[/sub]",
|
||||||
|
|
||||||
|
"bullet_list_open": () => "[ul]",
|
||||||
|
"bullet_list_close": () => "[/ul]",
|
||||||
|
|
||||||
|
"ordered_list_open": () => "[ol]",
|
||||||
|
"ordered_list_close": () => "[/ol]",
|
||||||
|
|
||||||
|
"list_item_open": () => "[li]",
|
||||||
|
"list_item_close": () => "[/li]",
|
||||||
|
|
||||||
|
"table_open": () => "[table]",
|
||||||
|
"table_close": () => "[/table]",
|
||||||
|
|
||||||
|
"thead_open": () => "",
|
||||||
|
"thead_close": () => "",
|
||||||
|
|
||||||
|
"tbody_open": () => "",
|
||||||
|
"tbody_close": () => "",
|
||||||
|
|
||||||
|
"tr_open": () => "[tr]",
|
||||||
|
"tr_close": () => "[/tr]",
|
||||||
|
|
||||||
|
"th_open": (renderer: MD2BBCodeRenderer, token: any) => "[th" + (token.align ? ("=" + token.align) : "") + "]",
|
||||||
|
"th_close": () => "[/th]",
|
||||||
|
|
||||||
|
"td_open": () => "[td]",
|
||||||
|
"td_close": () => "[/td]",
|
||||||
|
|
||||||
|
"link_open": (renderer: MD2BBCodeRenderer, token: LinkOpenToken) => "[url" + (token.href ? ("=" + token.href) : "") + "]",
|
||||||
|
"link_close": () => "[/url]",
|
||||||
|
|
||||||
|
"image": (renderer: MD2BBCodeRenderer, token: ImageToken) => "[img=" + (token.src) + "]" + (token.alt || token.src) + "[/img]",
|
||||||
|
|
||||||
|
//footnote_ref
|
||||||
|
|
||||||
|
//"content": "==Marked text==",
|
||||||
|
//mark_open
|
||||||
|
//mark_close
|
||||||
|
|
||||||
|
//++Inserted text++
|
||||||
|
"ins_open": () => "[u]",
|
||||||
|
"ins_close": () => "[/u]",
|
||||||
|
|
||||||
|
"code": (renderer: MD2BBCodeRenderer, token: CodeToken) => "[i-code]" + escapeBBCode(token.content) + "[/i-code]",
|
||||||
|
"fence": (renderer: MD2BBCodeRenderer, token: FenceToken) => "[code" + (token.params ? ("=" + token.params) : "") + "]" + escapeBBCode(token.content) + "[/code]",
|
||||||
|
|
||||||
|
"heading_open": (renderer: MD2BBCodeRenderer, token: HeadingOpenToken) => "[size=" + (9 - Math.min(4, token.hLevel)) + "]",
|
||||||
|
"heading_close": () => "[/size][hr]",
|
||||||
|
|
||||||
|
"hr": () => "[hr]",
|
||||||
|
|
||||||
|
//> Experience real-time editing with Remarkable!
|
||||||
|
"blockquote_open": () => "[quote]",
|
||||||
|
"blockquote_close": () => "[/quote]"
|
||||||
|
};
|
||||||
|
|
||||||
|
private _options;
|
||||||
|
last_paragraph: Token;
|
||||||
|
|
||||||
|
render(tokens: Token[], options: Options, env: Env): string {
|
||||||
|
this.last_paragraph = undefined;
|
||||||
|
this._options = options;
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
for(let index = 0; index < tokens.length; index++) {
|
||||||
|
if (tokens[index].type === 'inline') {
|
||||||
|
/* we're just ignoring the inline fact */
|
||||||
|
result += this.render((tokens[index] as any).children, options, env);
|
||||||
|
} else {
|
||||||
|
result += this.renderToken(tokens[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._options = undefined;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderToken(token: Token) {
|
||||||
|
log.debug(LogCategory.GENERAL, tr("Render Markdown token: %o"), token);
|
||||||
|
const renderer = MD2BBCodeRenderer.renderers[token.type];
|
||||||
|
if(typeof(renderer) === "undefined") {
|
||||||
|
log.warn(LogCategory.CHAT, tr("Missing markdown to bbcode renderer for token %s: %o"), token.type, token);
|
||||||
|
return 'content' in token ? this.options().textProcessor(token.content) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = renderer(this, token);
|
||||||
|
if(token.type === "paragraph_open")
|
||||||
|
this.last_paragraph = token;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
options() : any {
|
||||||
|
return this._options;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const remarkableRenderer = new Remarkable("full", {
|
||||||
|
typographer: true
|
||||||
|
});
|
||||||
|
remarkableRenderer.renderer = new MD2BBCodeRenderer() as any;
|
||||||
|
remarkableRenderer.inline.ruler.disable([ 'newline', 'autolink' ]);
|
||||||
|
|
||||||
|
export function renderMarkdownAsBBCode(message: string, textProcessor: (text: string) => string) : string {
|
||||||
|
remarkableRenderer.set({ textProcessor: textProcessor } as any);
|
||||||
|
|
||||||
|
let result = remarkableRenderer.render(message);
|
||||||
|
if(result.endsWith("\n"))
|
||||||
|
result = result.substr(0, result.length - 1);
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -1,10 +1,7 @@
|
||||||
import {LogCategory} from "tc-shared/log";
|
import {LogCategory} from "tc-shared/log";
|
||||||
import {settings, Settings} from "tc-shared/settings";
|
import {settings, Settings} from "tc-shared/settings";
|
||||||
import * as log from "tc-shared/log";
|
import * as log from "tc-shared/log";
|
||||||
import {bbcode} from "tc-shared/MessageFormatter";
|
|
||||||
import * as loader from "tc-loader";
|
import * as loader from "tc-loader";
|
||||||
import { XBBCodeRenderer } from "vendor/xbbcode/react";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export enum ChatType {
|
export enum ChatType {
|
||||||
GENERAL,
|
GENERAL,
|
||||||
|
@ -137,14 +134,6 @@ export function formatMessageString(pattern: string, ...args: string[]) : string
|
||||||
return result.join("");
|
return result.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Remove this (only legacy)
|
|
||||||
export function bbcode_chat(message: string) : JQuery[] {
|
|
||||||
return bbcode.format(message, {
|
|
||||||
is_chat_message: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export namespace network {
|
export namespace network {
|
||||||
export const KB = 1024;
|
export const KB = 1024;
|
||||||
export const MB = 1024 * KB;
|
export const MB = 1024 * KB;
|
||||||
|
|
|
@ -10,9 +10,6 @@ import {MusicInfo} from "tc-shared/ui/frames/side/music_info";
|
||||||
import {ConversationManager} from "tc-shared/ui/frames/side/ConversationManager";
|
import {ConversationManager} from "tc-shared/ui/frames/side/ConversationManager";
|
||||||
import {PrivateConversationManager} from "tc-shared/ui/frames/side/PrivateConversationManager";
|
import {PrivateConversationManager} from "tc-shared/ui/frames/side/PrivateConversationManager";
|
||||||
|
|
||||||
declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
|
||||||
declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
|
||||||
|
|
||||||
export enum InfoFrameMode {
|
export enum InfoFrameMode {
|
||||||
NONE = "none",
|
NONE = "none",
|
||||||
CHANNEL_CHAT = "channel_chat",
|
CHANNEL_CHAT = "channel_chat",
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import {tra} from "tc-shared/i18n/localize";
|
import {tra, traj} from "tc-shared/i18n/localize";
|
||||||
import {PermissionInfo} from "tc-shared/permission/PermissionManager";
|
import {PermissionInfo} from "tc-shared/permission/PermissionManager";
|
||||||
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
|
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
|
||||||
import * as htmltags from "tc-shared/ui/htmltags";
|
import * as htmltags from "tc-shared/ui/htmltags";
|
||||||
import {bbcode_chat, format_time, formatMessage} from "tc-shared/ui/frames/chat";
|
import {format_time, formatMessage} from "tc-shared/ui/frames/chat";
|
||||||
import {formatDate} from "tc-shared/MessageFormatter";
|
import {formatDate} from "tc-shared/MessageFormatter";
|
||||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
|
import {BBCodeRenderOptions, renderBBCodeAsJQuery} from "tc-shared/text/bbcode";
|
||||||
|
|
||||||
export enum Type {
|
export enum Type {
|
||||||
CONNECTION_BEGIN = "connection_begin",
|
CONNECTION_BEGIN = "connection_begin",
|
||||||
|
@ -259,7 +260,7 @@ export interface TypeInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageBuilderOptions = {};
|
export type MessageBuilderOptions = {};
|
||||||
export type MessageBuilder<T extends keyof TypeInfo> = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined;
|
export type MessageBuilder<T extends keyof TypeInfo> = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | string | undefined;
|
||||||
|
|
||||||
export const MessageBuilders: {[key: string]: MessageBuilder<any>} = {
|
export const MessageBuilders: {[key: string]: MessageBuilder<any>} = {
|
||||||
"error_custom": (data: event.ErrorCustom) => {
|
"error_custom": (data: event.ErrorCustom) => {
|
||||||
|
@ -513,12 +514,16 @@ MessageBuilders["client_view_leave"] = (data: event.ClientLeave) => {
|
||||||
return [$.spawn("div").addClass("log-error").text("Invalid view leave reason id (" + data.reason + ")")];
|
return [$.spawn("div").addClass("log-error").text("Invalid view leave reason id (" + data.reason + ")")];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bbcodeRenderOptions: BBCodeRenderOptions = {
|
||||||
|
convertSingleUrls: false
|
||||||
|
};
|
||||||
|
|
||||||
MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage) => {
|
MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage) => {
|
||||||
return bbcode_chat("[color=green]" + data.message + "[/color]");
|
return renderBBCodeAsJQuery("[color=green]" + data.message + "[/color]", bbcodeRenderOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
MessageBuilders["server_host_message"] = (data: event.WelcomeMessage) => {
|
MessageBuilders["server_host_message"] = (data: event.WelcomeMessage) => {
|
||||||
return bbcode_chat("[color=green]" + data.message + "[/color]");
|
return renderBBCodeAsJQuery("[color=green]" + data.message + "[/color]", bbcodeRenderOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged) => {
|
MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged) => {
|
||||||
|
@ -557,14 +562,14 @@ MessageBuilders["server_banned"] = (data: event.ServerBanned) => {
|
||||||
const time = data.time == 0 ? tr("ever") : format_time(data.time * 1000, tr("one second"));
|
const time = data.time == 0 ? tr("ever") : format_time(data.time * 1000, tr("one second"));
|
||||||
if(data.invoker.client_id > 0) {
|
if(data.invoker.client_id > 0) {
|
||||||
if(data.message)
|
if(data.message)
|
||||||
result = tra("You've been banned from the server by {0} for {1}. Reason: {2}", client_tag(data.invoker), time, data.message);
|
result = traj("You've been banned from the server by {0} for {1}. Reason: {2}", client_tag(data.invoker), time, data.message);
|
||||||
else
|
else
|
||||||
result = tra("You've been banned from the server by {0} for {1}.", client_tag(data.invoker), time);
|
result = traj("You've been banned from the server by {0} for {1}.", client_tag(data.invoker), time);
|
||||||
} else {
|
} else {
|
||||||
if(data.message)
|
if(data.message)
|
||||||
result = tra("You've been banned from the server for {0}. Reason: {1}", time, data.message);
|
result = traj("You've been banned from the server for {0}. Reason: {1}", time, data.message);
|
||||||
else
|
else
|
||||||
result = tra("You've been banned from the server for {0}.", time);
|
result = traj("You've been banned from the server for {0}.", time);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.map(e => e.addClass("log-error"));
|
return result.map(e => e.addClass("log-error"));
|
||||||
|
|
|
@ -66,8 +66,9 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(this.activeClient instanceof ClientEntry) {
|
if(this.activeClient instanceof ClientEntry) {
|
||||||
|
this.activeClient.setUnread(false); /* clear the unread flag */
|
||||||
|
|
||||||
if(client instanceof ClientEntry) {
|
if(client instanceof ClientEntry) {
|
||||||
this.activeClient.setUnread(false); /* "transfer" the unread flag */
|
|
||||||
this.registerChatEvent({
|
this.registerChatEvent({
|
||||||
type: "partner-instance-changed",
|
type: "partner-instance-changed",
|
||||||
oldClient: this.activeClient.clientNickName(),
|
oldClient: this.activeClient.clientNickName(),
|
||||||
|
@ -87,7 +88,7 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
hasUnreadMessages() : boolean {
|
hasUnreadMessages() : boolean {
|
||||||
return false;
|
return this.unreadTimestamp !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleIncomingMessage(client: ClientEntry | OutOfViewClient, isOwnMessage: boolean, message: ChatMessage) {
|
handleIncomingMessage(client: ClientEntry | OutOfViewClient, isOwnMessage: boolean, message: ChatMessage) {
|
||||||
|
@ -245,6 +246,14 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
|
||||||
this.sendMessageSendingEnabled(this.lastClientInfo.clientId !== 0);
|
this.sendMessageSendingEnabled(this.lastClientInfo.clientId !== 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setUnreadTimestamp(timestamp: number | undefined) {
|
||||||
|
super.setUnreadTimestamp(timestamp);
|
||||||
|
|
||||||
|
/* TODO: Move this somehow to the client itself? */
|
||||||
|
if(this.activeClient instanceof ClientEntry)
|
||||||
|
this.activeClient.setUnread(timestamp === undefined);
|
||||||
|
}
|
||||||
|
|
||||||
protected canClientAccessChat(): boolean {
|
protected canClientAccessChat(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,215 +1,10 @@
|
||||||
import * as log from "tc-shared/log";
|
import * as log from "tc-shared/log";
|
||||||
import {LogCategory} from "tc-shared/log";
|
import {LogCategory} from "tc-shared/log";
|
||||||
import {Settings, settings} from "tc-shared/settings";
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
const { Remarkable } = require("remarkable");
|
|
||||||
console.error(Remarkable);
|
|
||||||
|
|
||||||
const escapeBBCode = (text: string) => text.replace(/([\[\]])/g, "\\$1");
|
const escapeBBCode = (text: string) => text.replace(/([\[\]])/g, "\\$1");
|
||||||
|
|
||||||
export namespace helpers {
|
export namespace helpers {
|
||||||
//https://regex101.com/r/YQbfcX/2
|
|
||||||
//static readonly URL_REGEX = /^(?<hostname>([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?<path>(?:[^\s?]+)?)(?:\?(?<query>\S+))?)?$/gm;
|
|
||||||
const URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm;
|
|
||||||
function process_urls(message: string) : string {
|
|
||||||
const words = message.split(/[ \n]/);
|
|
||||||
for(let index = 0; index < words.length; index++) {
|
|
||||||
const flag_escaped = words[index].startsWith('!');
|
|
||||||
const unescaped = flag_escaped ? words[index].substr(1) : words[index];
|
|
||||||
|
|
||||||
_try:
|
|
||||||
try {
|
|
||||||
const url = new URL(unescaped);
|
|
||||||
log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url);
|
|
||||||
if(url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
||||||
break _try;
|
|
||||||
if(flag_escaped) {
|
|
||||||
message = undefined;
|
|
||||||
words[index] = unescaped;
|
|
||||||
} else {
|
|
||||||
message = undefined;
|
|
||||||
words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]";
|
|
||||||
}
|
|
||||||
} catch(e) { /* word isn't an url */ }
|
|
||||||
|
|
||||||
if(unescaped.match(URL_REGEX)) {
|
|
||||||
if(flag_escaped) {
|
|
||||||
message = undefined;
|
|
||||||
words[index] = unescaped;
|
|
||||||
} else {
|
|
||||||
message = undefined;
|
|
||||||
words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return message || words.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MD2BBCodeRenderer {
|
|
||||||
private static renderers: {[key: string]:(renderer: MD2BBCodeRenderer, token: Remarkable.Token) => string} = {
|
|
||||||
"text": (renderer: MD2BBCodeRenderer, token: Remarkable.TextToken) => renderer.options().process_url ? process_urls(renderer.maybe_escape_bb(token.content)) : renderer.maybe_escape_bb(token.content),
|
|
||||||
"softbreak": () => "\n",
|
|
||||||
"hardbreak": () => "\n",
|
|
||||||
|
|
||||||
"paragraph_open": (renderer: MD2BBCodeRenderer, token: Remarkable.ParagraphOpenToken) => {
|
|
||||||
const last_line = !renderer.last_paragraph || !renderer.last_paragraph.lines ? 0 : renderer.last_paragraph.lines[1];
|
|
||||||
const lines = token.lines[0] - last_line;
|
|
||||||
return [...new Array(lines)].map(() => "[br]").join("");
|
|
||||||
},
|
|
||||||
"paragraph_close": () => "",
|
|
||||||
|
|
||||||
"strong_open": () => "[b]",
|
|
||||||
"strong_close": () => "[/b]",
|
|
||||||
|
|
||||||
"em_open": () => "[i]",
|
|
||||||
"em_close": () => "[/i]",
|
|
||||||
|
|
||||||
"del_open": () => "[s]",
|
|
||||||
"del_close": () => "[/s]",
|
|
||||||
|
|
||||||
"sup": (renderer: MD2BBCodeRenderer, token: Remarkable.SupToken) => "[sup]" + renderer.maybe_escape_bb(token.content) + "[/sup]",
|
|
||||||
"sub": (renderer: MD2BBCodeRenderer, token: Remarkable.SubToken) => "[sub]" + renderer.maybe_escape_bb(token.content) + "[/sub]",
|
|
||||||
|
|
||||||
"bullet_list_open": () => "[ul]",
|
|
||||||
"bullet_list_close": () => "[/ul]",
|
|
||||||
|
|
||||||
"ordered_list_open": () => "[ol]",
|
|
||||||
"ordered_list_close": () => "[/ol]",
|
|
||||||
|
|
||||||
"list_item_open": () => "[li]",
|
|
||||||
"list_item_close": () => "[/li]",
|
|
||||||
|
|
||||||
"table_open": () => "[table]",
|
|
||||||
"table_close": () => "[/table]",
|
|
||||||
|
|
||||||
"thead_open": () => "",
|
|
||||||
"thead_close": () => "",
|
|
||||||
|
|
||||||
"tbody_open": () => "",
|
|
||||||
"tbody_close": () => "",
|
|
||||||
|
|
||||||
"tr_open": () => "[tr]",
|
|
||||||
"tr_close": () => "[/tr]",
|
|
||||||
|
|
||||||
"th_open": (renderer: MD2BBCodeRenderer, token: any) => "[th" + (token.align ? ("=" + token.align) : "") + "]",
|
|
||||||
"th_close": () => "[/th]",
|
|
||||||
|
|
||||||
"td_open": () => "[td]",
|
|
||||||
"td_close": () => "[/td]",
|
|
||||||
|
|
||||||
"link_open": (renderer: MD2BBCodeRenderer, token: Remarkable.LinkOpenToken) => "[url" + (token.href ? ("=" + token.href) : "") + "]",
|
|
||||||
"link_close": () => "[/url]",
|
|
||||||
|
|
||||||
"image": (renderer: MD2BBCodeRenderer, token: Remarkable.ImageToken) => "[img=" + (token.src) + "]" + (token.alt || token.src) + "[/img]",
|
|
||||||
|
|
||||||
//footnote_ref
|
|
||||||
|
|
||||||
//"content": "==Marked text==",
|
|
||||||
//mark_open
|
|
||||||
//mark_close
|
|
||||||
|
|
||||||
//++Inserted text++
|
|
||||||
"ins_open": () => "[u]",
|
|
||||||
"ins_close": () => "[/u]",
|
|
||||||
|
|
||||||
"code": (renderer: MD2BBCodeRenderer, token: Remarkable.CodeToken) => "[i-code]" + escapeBBCode(token.content) + "[/i-code]",
|
|
||||||
"fence": (renderer: MD2BBCodeRenderer, token: Remarkable.FenceToken) => "[code" + (token.params ? ("=" + token.params) : "") + "]" + escapeBBCode(token.content) + "[/code]",
|
|
||||||
|
|
||||||
"heading_open": (renderer: MD2BBCodeRenderer, token: Remarkable.HeadingOpenToken) => "[size=" + (9 - Math.min(4, token.hLevel)) + "]",
|
|
||||||
"heading_close": () => "[/size][hr]",
|
|
||||||
|
|
||||||
"hr": () => "[hr]",
|
|
||||||
|
|
||||||
//> Experience real-time editing with Remarkable!
|
|
||||||
"blockquote_open": () => "[quote]",
|
|
||||||
"blockquote_close": () => "[/quote]"
|
|
||||||
};
|
|
||||||
|
|
||||||
private _options;
|
|
||||||
last_paragraph: Remarkable.Token;
|
|
||||||
|
|
||||||
render(tokens: Remarkable.Token[], options: Remarkable.Options, env: Remarkable.Env): string {
|
|
||||||
this.last_paragraph = undefined;
|
|
||||||
this._options = options;
|
|
||||||
let result = '';
|
|
||||||
|
|
||||||
//TODO: Escape BB-Codes
|
|
||||||
for(let index = 0; index < tokens.length; index++) {
|
|
||||||
if (tokens[index].type === 'inline') {
|
|
||||||
/* we're just ignoring the inline fact */
|
|
||||||
result += this.render((tokens[index] as any).children, options, env);
|
|
||||||
} else {
|
|
||||||
result += this.renderToken(tokens[index], index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._options = undefined;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderToken(token: Remarkable.Token, index: number) {
|
|
||||||
log.debug(LogCategory.GENERAL, tr("Render Markdown token: %o"), token);
|
|
||||||
const renderer = MD2BBCodeRenderer.renderers[token.type];
|
|
||||||
if(typeof(renderer) === "undefined") {
|
|
||||||
log.warn(LogCategory.CHAT, tr("Missing markdown to bbcode renderer for token %s: %o"), token.type, token);
|
|
||||||
return 'content' in token ? token.content : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = renderer(this, token);
|
|
||||||
if(token.type === "paragraph_open")
|
|
||||||
this.last_paragraph = token;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
options() : any {
|
|
||||||
return this._options;
|
|
||||||
}
|
|
||||||
|
|
||||||
maybe_escape_bb(text: string) {
|
|
||||||
if(this._options.escape_bb)
|
|
||||||
return escapeBBCode(text);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const remarkableRenderer = new Remarkable("full", {
|
|
||||||
typographer: true
|
|
||||||
});
|
|
||||||
remarkableRenderer.renderer = new MD2BBCodeRenderer() as any;
|
|
||||||
remarkableRenderer.inline.ruler.disable([ 'newline', 'autolink' ]);
|
|
||||||
|
|
||||||
function process_markdown(message: string, options: {
|
|
||||||
process_url?: boolean,
|
|
||||||
escape_bb?: boolean
|
|
||||||
}) : string {
|
|
||||||
|
|
||||||
remarkableRenderer.set({
|
|
||||||
process_url: !!options.process_url,
|
|
||||||
escape_bb: !!options.escape_bb
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
let result: string = remarkableRenderer.render(message);
|
|
||||||
if(result.endsWith("\n"))
|
|
||||||
result = result.substr(0, result.length - 1);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function preprocess_chat_message(message: string) : string {
|
|
||||||
const process_url = settings.static_global(Settings.KEY_CHAT_TAG_URLS);
|
|
||||||
const parse_markdown = settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN);
|
|
||||||
const escape_bb = !settings.static_global(Settings.KEY_CHAT_ENABLE_BBCODE);
|
|
||||||
|
|
||||||
if(parse_markdown)
|
|
||||||
return process_markdown(message, {
|
|
||||||
process_url: process_url,
|
|
||||||
escape_bb: escape_bb
|
|
||||||
});
|
|
||||||
|
|
||||||
if(escape_bb)
|
|
||||||
message = escapeBBCode(message);
|
|
||||||
return process_url ? process_urls(message) : message;
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace date {
|
export namespace date {
|
||||||
export function same_day(a: number | Date, b: number | Date) {
|
export function same_day(a: number | Date, b: number | Date) {
|
||||||
a = a instanceof Date ? a : new Date(a);
|
a = a instanceof Date ? a : new Date(a);
|
||||||
|
|
|
@ -7,9 +7,6 @@ import * as image_preview from "../image_preview";
|
||||||
import {format} from "tc-shared/ui/frames/side/chat_helper";
|
import {format} from "tc-shared/ui/frames/side/chat_helper";
|
||||||
import * as i18nc from "tc-shared/i18n/country";
|
import * as i18nc from "tc-shared/i18n/country";
|
||||||
|
|
||||||
declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
|
||||||
declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
|
||||||
|
|
||||||
export class ClientInfo {
|
export class ClientInfo {
|
||||||
readonly handle: Frame;
|
readonly handle: Frame;
|
||||||
private _html_tag: JQuery;
|
private _html_tag: JQuery;
|
||||||
|
|
|
@ -9,8 +9,6 @@ import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal";
|
||||||
import * as log from "tc-shared/log";
|
import * as log from "tc-shared/log";
|
||||||
import * as image_preview from "../image_preview";
|
import * as image_preview from "../image_preview";
|
||||||
|
|
||||||
declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
|
||||||
|
|
||||||
interface LoadedSongData {
|
interface LoadedSongData {
|
||||||
description: string;
|
description: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
import {createModal, Modal} from "tc-shared/ui/elements/Modal";
|
import {createModal, Modal} from "tc-shared/ui/elements/Modal";
|
||||||
import * as htmltags from "tc-shared/ui/htmltags";
|
import * as htmltags from "tc-shared/ui/htmltags";
|
||||||
import {bbcode_chat} from "tc-shared/ui/frames/chat";
|
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
|
import {renderBBCodeAsJQuery} from "tc-shared/text/bbcode";
|
||||||
|
|
||||||
let global_modal: PokeModal;
|
let global_modal: PokeModal;
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ class PokeModal {
|
||||||
}))).appendTo(container);
|
}))).appendTo(container);
|
||||||
if(message) {
|
if(message) {
|
||||||
$.spawn("div").addClass("text").text(tr("pokes you:")).appendTo(container);
|
$.spawn("div").addClass("text").text(tr("pokes you:")).appendTo(container);
|
||||||
$.spawn("div").addClass("poke-message").append(...bbcode_chat(message)).appendTo(container);
|
$.spawn("div").addClass("poke-message").append(...renderBBCodeAsJQuery(message, { convertSingleUrls: false })).appendTo(container);
|
||||||
} else {
|
} else {
|
||||||
$.spawn("div").addClass("text").text(tr("pokes you.")).appendTo(container);
|
$.spawn("div").addClass("text").text(tr("pokes you.")).appendTo(container);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue