Improced BBCode support
parent
e94a8a3b4f
commit
08968b3d54
11
ChangeLog.md
11
ChangeLog.md
|
@ -1,4 +1,15 @@
|
|||
# Changelog:
|
||||
* **07.12.20**
|
||||
- Fixed the Markdown to BBCode transpiler falsely emitting empty lines
|
||||
- Fixed invalid BBCode escaping
|
||||
- Added proper BBCode support for lazy close tags
|
||||
- Improved the URL detecting and replace support (Reduced false positives and proper protocol checking)
|
||||
- Adding support for list bb codes and background color
|
||||
- Fixed some minor BBCode parser bugs
|
||||
- Fixed BBCode inline code style
|
||||
- URL tags can not contain any other tags
|
||||
- Correctly parsing the "lazy close tag" `[/]`
|
||||
|
||||
* **05.12.20**
|
||||
- Fixed the webclient for Firefox in incognito mode
|
||||
|
||||
|
|
|
@ -3962,8 +3962,7 @@
|
|||
"decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
|
||||
"dev": true
|
||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
|
||||
},
|
||||
"decompress-response": {
|
||||
"version": "3.3.0",
|
||||
|
@ -12297,8 +12296,7 @@
|
|||
"strict-uri-encode": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
|
||||
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
|
||||
"dev": true
|
||||
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
|
@ -13511,6 +13509,26 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"url-knife": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/url-knife/-/url-knife-3.1.3.tgz",
|
||||
"integrity": "sha512-6GXFPWBHtUBCSzkbJwirszFzL6It73p+KDKejzRO1CEAETrt0uN7WXKihTiHbzaY94nCcXZpqEm+pKkshe2+/A==",
|
||||
"requires": {
|
||||
"query-string": "^5.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"query-string": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz",
|
||||
"integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==",
|
||||
"requires": {
|
||||
"decode-uri-component": "^0.2.0",
|
||||
"object-assign": "^4.1.0",
|
||||
"strict-uri-encode": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"url-parse-lax": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
|
||||
|
|
|
@ -109,6 +109,7 @@
|
|||
"sdp-transform": "^2.14.0",
|
||||
"simplebar-react": "^2.2.0",
|
||||
"twemoji": "^13.0.0",
|
||||
"url-knife": "^3.1.3",
|
||||
"webcrypto-liner": "^1.2.3",
|
||||
"webrtc-adapter": "^7.5.1"
|
||||
}
|
||||
|
|
|
@ -232,30 +232,33 @@ export class Group {
|
|||
}
|
||||
|
||||
log(message: string, ...optionalParams: any[]) : this {
|
||||
if(!this.enabled)
|
||||
if(!this.enabled) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if(!this.initialized) {
|
||||
if(this.mode == GroupMode.NATIVE) {
|
||||
if(this._collapsed && console.groupCollapsed)
|
||||
if(this._collapsed && console.groupCollapsed) {
|
||||
console.groupCollapsed(this.name, ...this.optionalParams);
|
||||
else
|
||||
} else {
|
||||
console.group(this.name, ...this.optionalParams);
|
||||
}
|
||||
} else {
|
||||
this._log_prefix = " ";
|
||||
let parent = this.owner;
|
||||
while(parent) {
|
||||
if(parent.mode == GroupMode.PREFIX)
|
||||
if(parent.mode == GroupMode.PREFIX) {
|
||||
this._log_prefix = this._log_prefix + parent._log_prefix;
|
||||
else
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.initialized = true;
|
||||
}
|
||||
if(this.mode == GroupMode.NATIVE)
|
||||
if(this.mode == GroupMode.NATIVE) {
|
||||
logDirect(this.level, message, ...optionalParams);
|
||||
else {
|
||||
} else {
|
||||
logDirect(this.level, "[%s] " + this._log_prefix + message, category_mapping.get(this.category), ...optionalParams);
|
||||
}
|
||||
return this;
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
:global {
|
||||
.xbbcode-tag {
|
||||
&.xbbcode-tag-hr {
|
||||
border: none;
|
||||
border-top: .125em solid #555;
|
||||
|
||||
margin-top: .1em;
|
||||
margin-bottom: .1em;
|
||||
}
|
||||
|
||||
&.xbbcode-tag-table {
|
||||
th, td {
|
||||
border-color: var(--chat-message-table-border);
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: var(--chat-message-table-row-background);
|
||||
}
|
||||
|
||||
tr:nth-child(2n) {
|
||||
background-color: var(--chat-message-table-row-even-background);
|
||||
}
|
||||
}
|
||||
|
||||
&.xbbcode-tag-bg-color {
|
||||
padding: 0 .25em;
|
||||
line-height: 1em;
|
||||
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.xbbcode-tag-quote {
|
||||
border-color: var(--chat-message-quote-border);
|
||||
padding-left: .5em;
|
||||
color: var(--chat-message-quote);
|
||||
}
|
||||
|
||||
&.xbbcode-tag-img {
|
||||
padding: .25em;
|
||||
border-radius: .25em;
|
||||
|
||||
overflow: hidden;
|
||||
max-width: 20em;
|
||||
max-height: 20em;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,15 +4,16 @@ import {rendererHTML, rendererReact, rendererText} from "tc-shared/text/bbcode/r
|
|||
import {parse as parseBBCode} from "vendor/xbbcode/parser";
|
||||
import {fixupJQueryUrlTags} from "tc-shared/text/bbcode/url";
|
||||
import {fixupJQueryImageTags} from "tc-shared/text/bbcode/image";
|
||||
import "./bbcode.scss";
|
||||
|
||||
export const escapeBBCode = (text: string) => text.replace(/([\[\]])/g, "\\$1");
|
||||
export const escapeBBCode = (text: string) => text.replace(/(\[)/g, "\\$1");
|
||||
|
||||
export const allowedBBCodes = [
|
||||
"b", "big",
|
||||
"i", "italic",
|
||||
"u", "underlined",
|
||||
"s", "strikethrough",
|
||||
"color",
|
||||
"color", "bgcolor",
|
||||
"url",
|
||||
"code",
|
||||
"i-code", "icode",
|
||||
|
@ -22,7 +23,9 @@ export const allowedBBCodes = [
|
|||
"left", "l", "center", "c", "right", "r",
|
||||
|
||||
"ul", "ol", "list",
|
||||
"ulist", "olist",
|
||||
"li",
|
||||
"*",
|
||||
|
||||
"table",
|
||||
"tr", "td", "th",
|
||||
|
@ -37,7 +40,7 @@ export interface BBCodeRenderOptions {
|
|||
convertSingleUrls: boolean;
|
||||
}
|
||||
|
||||
const yt_url_regex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/;
|
||||
const youtubeUrlRegex = /^((?: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 */
|
||||
|
@ -53,7 +56,7 @@ function preprocessMessage(message: string, settings: BBCodeRenderOptions) : str
|
|||
|
||||
single_url_yt:
|
||||
{
|
||||
const result = raw_url.match(yt_url_regex);
|
||||
const result = raw_url.match(youtubeUrlRegex);
|
||||
if(!result) break single_url_yt;
|
||||
|
||||
return "[yt]https://www.youtube.com/watch?v=" + result[5] + "[/yt]";
|
||||
|
|
|
@ -13,13 +13,13 @@
|
|||
&.inlineCode {
|
||||
display: inline-block;
|
||||
|
||||
> .hljs {
|
||||
padding: 0 .25em!important;
|
||||
}
|
||||
|
||||
white-space: pre-wrap;
|
||||
margin: 0 0 -0.1em;
|
||||
vertical-align: bottom;
|
||||
|
||||
> :global(.hljs) {
|
||||
padding: 0 .25em!important;
|
||||
}
|
||||
}
|
||||
&.code {
|
||||
word-wrap: normal;
|
||||
|
|
|
@ -1,70 +1,74 @@
|
|||
//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 "../log";
|
||||
import {LogCategory} from "../log";
|
||||
import UrlKnife from 'url-knife';
|
||||
import {Settings, settings} from "../settings";
|
||||
import {renderMarkdownAsBBCode} from "../text/markdown";
|
||||
import {escapeBBCode} from "../text/bbcode";
|
||||
import { tr } from "tc-shared/i18n/localize";
|
||||
|
||||
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]";
|
||||
}
|
||||
interface UrlKnifeUrl {
|
||||
value: {
|
||||
url: string,
|
||||
},
|
||||
area: string,
|
||||
index: {
|
||||
start: number,
|
||||
end: number
|
||||
}
|
||||
}
|
||||
|
||||
return message || words.join(" ");
|
||||
}
|
||||
|
||||
export function preprocessChatMessageForSend(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;
|
||||
function bbcodeLinkUrls(message: string) : string {
|
||||
const urls: UrlKnifeUrl[] = UrlKnife.TextArea.extractAllUrls(message, {
|
||||
'ip_v4' : true,
|
||||
'ip_v6' : false,
|
||||
'localhost' : false,
|
||||
'intranet' : true
|
||||
});
|
||||
|
||||
/* we want to go through the urls from the back to the front */
|
||||
urls.sort((a, b) => b.index.start - a.index.start);
|
||||
for(const url of urls) {
|
||||
const prefix = message.substr(0, url.index.start);
|
||||
const suffix = message.substr(url.index.end);
|
||||
const urlPath = message.substring(url.index.start, url.index.end);
|
||||
let bbcodeUrl;
|
||||
|
||||
let colonIndex = urlPath.indexOf(":");
|
||||
if(colonIndex === -1 || colonIndex + 2 < urlPath.length || urlPath[colonIndex + 1] !== "/" || urlPath[colonIndex + 2] !== "/") {
|
||||
bbcodeUrl = "[url=https://" + urlPath + "]" + urlPath + "[/url]";
|
||||
} else {
|
||||
bbcodeUrl = "[url]" + urlPath + "[/url]";
|
||||
}
|
||||
|
||||
if(escape_bb)
|
||||
message = escapeBBCode(message);
|
||||
|
||||
if(process_url)
|
||||
message = process_urls(message);
|
||||
message = prefix + bbcodeUrl + suffix;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
export function preprocessChatMessageForSend(message: string) : string {
|
||||
const processUrls = settings.static_global(Settings.KEY_CHAT_TAG_URLS);
|
||||
const parseMarkdown = settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN);
|
||||
const escapeBBCodes = !settings.static_global(Settings.KEY_CHAT_ENABLE_BBCODE);
|
||||
|
||||
if(parseMarkdown) {
|
||||
return renderMarkdownAsBBCode(message, text => {
|
||||
if(escapeBBCodes) {
|
||||
text = escapeBBCode(text);
|
||||
}
|
||||
|
||||
if(processUrls) {
|
||||
text = bbcodeLinkUrls(text);
|
||||
}
|
||||
|
||||
return text;
|
||||
});
|
||||
} else {
|
||||
if(escapeBBCodes) {
|
||||
message = escapeBBCode(message);
|
||||
}
|
||||
|
||||
if(processUrls) {
|
||||
message = bbcodeLinkUrls(message);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,14 @@
|
|||
import * as log from "../log";
|
||||
import {LogCategory} from "../log";
|
||||
import {LogCategory, logTrace} from "../log";
|
||||
import {
|
||||
CodeToken, Env, FenceToken, HeadingOpenToken,
|
||||
CodeToken,
|
||||
Env,
|
||||
FenceToken,
|
||||
HeadingOpenToken,
|
||||
ImageToken,
|
||||
LinkOpenToken, Options,
|
||||
ParagraphOpenToken,
|
||||
LinkOpenToken,
|
||||
Options,
|
||||
ParagraphCloseToken,
|
||||
SubToken,
|
||||
SupToken,
|
||||
TextToken,
|
||||
|
@ -12,21 +16,20 @@ import {
|
|||
} from "remarkable/lib";
|
||||
import {escapeBBCode} from "../text/bbcode";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
|
||||
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),
|
||||
"text": (renderer: MD2BBCodeRenderer, token: TextToken) => {
|
||||
renderer.currentLineCount += token.content.split("\n").length;
|
||||
return renderer.options().textProcessor(token.content);
|
||||
},
|
||||
"softbreak": () => "\n",
|
||||
"hardbreak": () => "\n",
|
||||
|
||||
"paragraph_open": (renderer: MD2BBCodeRenderer, token: ParagraphOpenToken) => {
|
||||
debugger;
|
||||
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": () => "",
|
||||
"paragraph_open": () => "",
|
||||
"paragraph_close": (_, token: ParagraphCloseToken) => token.tight ? "" : "[br]",
|
||||
|
||||
"strong_open": () => "[b]",
|
||||
"strong_close": () => "[/b]",
|
||||
|
@ -70,7 +73,10 @@ export class MD2BBCodeRenderer {
|
|||
"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]",
|
||||
"image": (renderer: MD2BBCodeRenderer, token: ImageToken) => {
|
||||
renderer.currentLineCount += 1;
|
||||
return "[img=" + (token.src) + "]" + (token.alt || token.src) + "[/img]";
|
||||
},
|
||||
|
||||
//footnote_ref
|
||||
|
||||
|
@ -96,14 +102,26 @@ export class MD2BBCodeRenderer {
|
|||
};
|
||||
|
||||
private _options;
|
||||
last_paragraph: Token;
|
||||
currentLineCount: number;
|
||||
|
||||
reset() {
|
||||
this._options = undefined;
|
||||
this.currentLineCount = 0;
|
||||
}
|
||||
|
||||
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].lines?.length) {
|
||||
while(this.currentLineCount < tokens[index].lines[0]) {
|
||||
this.currentLineCount += 1;
|
||||
result += "[br]";
|
||||
}
|
||||
}
|
||||
|
||||
if (tokens[index].type === 'inline') {
|
||||
/* we're just ignoring the inline fact */
|
||||
result += this.render((tokens[index] as any).children, options, env);
|
||||
|
@ -124,10 +142,7 @@ export class MD2BBCodeRenderer {
|
|||
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;
|
||||
return renderer(this, token);
|
||||
}
|
||||
|
||||
options() : any {
|
||||
|
@ -135,17 +150,19 @@ export class MD2BBCodeRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
const md2bbCodeRenderer = new MD2BBCodeRenderer();
|
||||
const remarkableRenderer = new Remarkable("full", {
|
||||
typographer: true
|
||||
});
|
||||
remarkableRenderer.renderer = new MD2BBCodeRenderer() as any;
|
||||
remarkableRenderer.renderer = 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);
|
||||
|
||||
md2bbCodeRenderer.reset();
|
||||
|
||||
let result = remarkableRenderer.render(message);
|
||||
if(result.endsWith("\n"))
|
||||
result = result.substr(0, result.length - 1);
|
||||
logTrace(LogCategory.CHAT, tr("Markdown render result:\n%s"), result);
|
||||
return result;
|
||||
}
|
|
@ -19,10 +19,11 @@ export function htmlEscape(message: string) : string[] {
|
|||
}
|
||||
|
||||
export function formatElement(object: any, escape_html: boolean = true) : JQuery[] {
|
||||
if($.isArray(object)) {
|
||||
if(Array.isArray(object)) {
|
||||
let result = [];
|
||||
for(let element of object)
|
||||
for(let element of object) {
|
||||
result.push(...formatElement(element, escape_html));
|
||||
}
|
||||
return result;
|
||||
} else if(typeof(object) == "string") {
|
||||
if(object.length == 0) return [];
|
||||
|
|
|
@ -275,91 +275,91 @@ export enum FrameContent {
|
|||
|
||||
export class Frame {
|
||||
readonly handle: ConnectionHandler;
|
||||
private _info_frame: InfoFrame;
|
||||
private _html_tag: JQuery;
|
||||
private _container_info: JQuery;
|
||||
private _container_chat: JQuery;
|
||||
private infoFrame: InfoFrame;
|
||||
private htmlTag: JQuery;
|
||||
private containerInfo: JQuery;
|
||||
private containerChannelChat: JQuery;
|
||||
private _content_type: FrameContent;
|
||||
|
||||
private _client_info: ClientInfo;
|
||||
private _music_info: MusicInfo;
|
||||
private _channel_conversations: ConversationManager;
|
||||
private _private_conversations: PrivateConversationManager;
|
||||
private clientInfo: ClientInfo;
|
||||
private musicInfo: MusicInfo;
|
||||
private channelConversations: ConversationManager;
|
||||
private privateConversations: PrivateConversationManager;
|
||||
|
||||
constructor(handle: ConnectionHandler) {
|
||||
this.handle = handle;
|
||||
|
||||
this._content_type = FrameContent.NONE;
|
||||
this._info_frame = new InfoFrame(this);
|
||||
this._private_conversations = new PrivateConversationManager(handle);
|
||||
this._channel_conversations = new ConversationManager(handle);
|
||||
this._client_info = new ClientInfo(this);
|
||||
this._music_info = new MusicInfo(this);
|
||||
this.infoFrame = new InfoFrame(this);
|
||||
this.privateConversations = new PrivateConversationManager(handle);
|
||||
this.channelConversations = new ConversationManager(handle);
|
||||
this.clientInfo = new ClientInfo(this);
|
||||
this.musicInfo = new MusicInfo(this);
|
||||
|
||||
this._build_html_tag();
|
||||
this.show_channel_conversations();
|
||||
this.info_frame().update_chat_counter();
|
||||
}
|
||||
|
||||
html_tag() : JQuery { return this._html_tag; }
|
||||
info_frame() : InfoFrame { return this._info_frame; }
|
||||
html_tag() : JQuery { return this.htmlTag; }
|
||||
info_frame() : InfoFrame { return this.infoFrame; }
|
||||
|
||||
content_type() : FrameContent { return this._content_type; }
|
||||
|
||||
destroy() {
|
||||
this._html_tag && this._html_tag.remove();
|
||||
this._html_tag = undefined;
|
||||
this.htmlTag && this.htmlTag.remove();
|
||||
this.htmlTag = undefined;
|
||||
|
||||
this._info_frame && this._info_frame.destroy();
|
||||
this._info_frame = undefined;
|
||||
this.infoFrame && this.infoFrame.destroy();
|
||||
this.infoFrame = undefined;
|
||||
|
||||
this._client_info && this._client_info.destroy();
|
||||
this._client_info = undefined;
|
||||
this.clientInfo && this.clientInfo.destroy();
|
||||
this.clientInfo = undefined;
|
||||
|
||||
this._music_info && this._music_info.destroy();
|
||||
this._music_info = undefined;
|
||||
this.musicInfo && this.musicInfo.destroy();
|
||||
this.musicInfo = undefined;
|
||||
|
||||
this._private_conversations && this._private_conversations.destroy();
|
||||
this._private_conversations = undefined;
|
||||
this.privateConversations && this.privateConversations.destroy();
|
||||
this.privateConversations = undefined;
|
||||
|
||||
this._channel_conversations && this._channel_conversations.destroy();
|
||||
this._channel_conversations = undefined;
|
||||
this.channelConversations && this.channelConversations.destroy();
|
||||
this.channelConversations = undefined;
|
||||
|
||||
this._container_info && this._container_info.remove();
|
||||
this._container_info = undefined;
|
||||
this.containerInfo && this.containerInfo.remove();
|
||||
this.containerInfo = undefined;
|
||||
|
||||
this._container_chat && this._container_chat.remove();
|
||||
this._container_chat = undefined;
|
||||
this.containerChannelChat && this.containerChannelChat.remove();
|
||||
this.containerChannelChat = undefined;
|
||||
}
|
||||
|
||||
private _build_html_tag() {
|
||||
this._html_tag = $("#tmpl_frame_chat").renderTag();
|
||||
this._container_info = this._html_tag.find(".container-info");
|
||||
this._container_chat = this._html_tag.find(".container-chat");
|
||||
this.htmlTag = $("#tmpl_frame_chat").renderTag();
|
||||
this.containerInfo = this.htmlTag.find(".container-info");
|
||||
this.containerChannelChat = this.htmlTag.find(".container-chat");
|
||||
|
||||
this._info_frame.html_tag().appendTo(this._container_info);
|
||||
this.infoFrame.html_tag().appendTo(this.containerInfo);
|
||||
}
|
||||
|
||||
|
||||
private_conversations() : PrivateConversationManager {
|
||||
return this._private_conversations;
|
||||
return this.privateConversations;
|
||||
}
|
||||
|
||||
channel_conversations() : ConversationManager {
|
||||
return this._channel_conversations;
|
||||
return this.channelConversations;
|
||||
}
|
||||
|
||||
client_info() : ClientInfo {
|
||||
return this._client_info;
|
||||
return this.clientInfo;
|
||||
}
|
||||
|
||||
music_info() : MusicInfo {
|
||||
return this._music_info;
|
||||
return this.musicInfo;
|
||||
}
|
||||
|
||||
private _clear() {
|
||||
this._content_type = FrameContent.NONE;
|
||||
this._container_chat.children().detach();
|
||||
this.containerChannelChat.children().detach();
|
||||
}
|
||||
|
||||
show_private_conversations() {
|
||||
|
@ -368,9 +368,9 @@ export class Frame {
|
|||
|
||||
this._clear();
|
||||
this._content_type = FrameContent.PRIVATE_CHAT;
|
||||
this._container_chat.append(this._private_conversations.htmlTag);
|
||||
this._private_conversations.handlePanelShow();
|
||||
this._info_frame.set_mode(InfoFrameMode.PRIVATE_CHAT);
|
||||
this.containerChannelChat.append(this.privateConversations.htmlTag);
|
||||
this.privateConversations.handlePanelShow();
|
||||
this.infoFrame.set_mode(InfoFrameMode.PRIVATE_CHAT);
|
||||
}
|
||||
|
||||
show_channel_conversations() {
|
||||
|
@ -379,36 +379,36 @@ export class Frame {
|
|||
|
||||
this._clear();
|
||||
this._content_type = FrameContent.CHANNEL_CHAT;
|
||||
this._container_chat.append(this._channel_conversations.htmlTag);
|
||||
this._channel_conversations.handlePanelShow();
|
||||
this.containerChannelChat.append(this.channelConversations.htmlTag);
|
||||
this.channelConversations.handlePanelShow();
|
||||
|
||||
this._info_frame.set_mode(InfoFrameMode.CHANNEL_CHAT);
|
||||
this.infoFrame.set_mode(InfoFrameMode.CHANNEL_CHAT);
|
||||
}
|
||||
|
||||
show_client_info(client: ClientEntry) {
|
||||
this._client_info.set_current_client(client);
|
||||
this._info_frame.set_mode(InfoFrameMode.CLIENT_INFO); /* specially needs an update here to update the conversation button */
|
||||
this.clientInfo.set_current_client(client);
|
||||
this.infoFrame.set_mode(InfoFrameMode.CLIENT_INFO); /* specially needs an update here to update the conversation button */
|
||||
|
||||
if(this._content_type === FrameContent.CLIENT_INFO)
|
||||
return;
|
||||
|
||||
this._client_info.previous_frame_content = this._content_type;
|
||||
this.clientInfo.previous_frame_content = this._content_type;
|
||||
this._clear();
|
||||
this._content_type = FrameContent.CLIENT_INFO;
|
||||
this._container_chat.append(this._client_info.html_tag());
|
||||
this.containerChannelChat.append(this.clientInfo.html_tag());
|
||||
}
|
||||
|
||||
show_music_player(client: MusicClientEntry) {
|
||||
this._music_info.set_current_bot(client);
|
||||
this.musicInfo.set_current_bot(client);
|
||||
|
||||
if(this._content_type === FrameContent.MUSIC_BOT)
|
||||
return;
|
||||
|
||||
this._info_frame.set_mode(InfoFrameMode.MUSIC_BOT);
|
||||
this._music_info.previous_frame_content = this._content_type;
|
||||
this.infoFrame.set_mode(InfoFrameMode.MUSIC_BOT);
|
||||
this.musicInfo.previous_frame_content = this._content_type;
|
||||
this._clear();
|
||||
this._content_type = FrameContent.MUSIC_BOT;
|
||||
this._container_chat.append(this._music_info.html_tag());
|
||||
this.containerChannelChat.append(this.musicInfo.html_tag());
|
||||
}
|
||||
|
||||
set_content(type: FrameContent) {
|
||||
|
@ -422,7 +422,7 @@ export class Frame {
|
|||
else {
|
||||
this._clear();
|
||||
this._content_type = FrameContent.NONE;
|
||||
this._info_frame.set_mode(InfoFrameMode.NONE);
|
||||
this.infoFrame.set_mode(InfoFrameMode.NONE);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,8 +31,9 @@ const EmojiButton = (props: { events: Registry<ChatBoxEvents> }) => {
|
|||
const refContainer = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
if(!shown)
|
||||
if(!shown) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clickListener = (event: MouseEvent) => {
|
||||
let target = event.target as HTMLElement;
|
||||
|
@ -90,8 +91,9 @@ const nodeToText = (element: Node) => {
|
|||
return '\n';
|
||||
}
|
||||
|
||||
if(element.children.length > 0)
|
||||
if(element.children.length > 0) {
|
||||
return [...element.childNodes].map(nodeToText).join("");
|
||||
}
|
||||
|
||||
return typeof(element.innerText) === "string" ? element.innerText : "";
|
||||
} else {
|
||||
|
@ -139,14 +141,14 @@ const TextInput = (props: { events: Registry<ChatBoxEvents>, enabled?: boolean,
|
|||
const clipboard = event.clipboardData || (window as any).clipboardData;
|
||||
if(!clipboard) return;
|
||||
|
||||
const raw_text = clipboard.getData('text/plain');
|
||||
const rawText = clipboard.getData('text/plain');
|
||||
const selection = window.getSelection();
|
||||
if (!selection.rangeCount)
|
||||
return false;
|
||||
|
||||
let htmlXML = clipboard.getData('text/html');
|
||||
if(!htmlXML) {
|
||||
pasteTextTransformElement.textContent = raw_text;
|
||||
pasteTextTransformElement.textContent = rawText;
|
||||
htmlXML = pasteTextTransformElement.innerHTML;
|
||||
}
|
||||
|
||||
|
@ -159,18 +161,22 @@ const TextInput = (props: { events: Registry<ChatBoxEvents>, enabled?: boolean,
|
|||
{
|
||||
let prefix_length = 0, suffix_length = 0;
|
||||
{
|
||||
for (let i = 0; i < raw_text.length; i++)
|
||||
if (raw_text.charAt(i) === '\n')
|
||||
for (let i = 0; i < rawText.length; i++) {
|
||||
if (rawText.charAt(i) === '\n') {
|
||||
prefix_length++;
|
||||
else if (raw_text.charAt(i) !== '\r')
|
||||
} else if (rawText.charAt(i) !== '\r') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = raw_text.length - 1; i >= 0; i++)
|
||||
if (raw_text.charAt(i) === '\n')
|
||||
for (let i = rawText.length - 1; i >= 0; i++) {
|
||||
if (rawText.charAt(i) === '\n') {
|
||||
suffix_length++;
|
||||
else if (raw_text.charAt(i) !== '\r')
|
||||
} else if (rawText.charAt(i) !== '\r') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data = data.replace(/^[\n\r]+|[\n\r]+$/g, '');
|
||||
data = "\n".repeat(prefix_length) + data + "\n".repeat(suffix_length);
|
||||
|
@ -186,14 +192,16 @@ const TextInput = (props: { events: Registry<ChatBoxEvents>, enabled?: boolean,
|
|||
|
||||
const inputEmpty = refInput.current.innerText.trim().length === 0;
|
||||
if(event.key === "Enter" && !event.shiftKey) {
|
||||
if(inputEmpty)
|
||||
if(inputEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = refInput.current.innerText;
|
||||
props.events.fire("action_submit_message", { message: text });
|
||||
history.current.push(text);
|
||||
while(history.current.length > 10)
|
||||
while(history.current.length > 10) {
|
||||
history.current.pop_front();
|
||||
}
|
||||
|
||||
refInput.current.innerText = "";
|
||||
setHistoryIndex(-1);
|
||||
|
@ -221,15 +229,17 @@ const TextInput = (props: { events: Registry<ChatBoxEvents>, enabled?: boolean,
|
|||
|
||||
props.events.reactUse("action_request_focus", () => refInput.current?.focus());
|
||||
props.events.reactUse("notify_typing", () => {
|
||||
if(typeof typingTimeout.current === "number")
|
||||
if(typeof typingTimeout.current === "number") {
|
||||
return;
|
||||
}
|
||||
|
||||
typingTimeout.current = setTimeout(() => typingTimeout.current = undefined, 1000);
|
||||
});
|
||||
props.events.reactUse("action_insert_text", event => {
|
||||
refInput.current.innerHTML = refInput.current.innerHTML + event.text;
|
||||
if(event.focus)
|
||||
if(event.focus) {
|
||||
refInput.current.focus();
|
||||
}
|
||||
});
|
||||
props.events.reactUse("action_set_enabled", event => {
|
||||
setEnabled(event.enabled);
|
||||
|
@ -273,8 +283,9 @@ const MarkdownFormatHelper = () => {
|
|||
const [ visible, setVisible ] = useState(settings.global(Settings.KEY_CHAT_ENABLE_MARKDOWN));
|
||||
|
||||
settings.events.reactUse("notify_setting_changed", event => {
|
||||
if(event.setting !== Settings.KEY_CHAT_ENABLE_MARKDOWN.key)
|
||||
if(event.setting !== Settings.KEY_CHAT_ENABLE_MARKDOWN.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
setVisible(settings.global(Settings.KEY_CHAT_ENABLE_MARKDOWN));
|
||||
});
|
||||
|
@ -323,7 +334,8 @@ export class ChatBox extends React.Component<ChatBoxProperties, ChatBoxState> {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<ChatBoxProperties>, prevState: Readonly<ChatBoxState>, snapshot?: any): void {
|
||||
if(prevState.enabled !== this.state.enabled)
|
||||
if(prevState.enabled !== this.state.enabled) {
|
||||
this.events.fire_react("action_set_enabled", { enabled: this.state.enabled });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -298,34 +298,6 @@ html:root {
|
|||
margin-bottom: .1em;
|
||||
}
|
||||
|
||||
table {
|
||||
th, td {
|
||||
border-color: var(--chat-message-table-border);
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: var(--chat-message-table-row-background);
|
||||
}
|
||||
|
||||
tr:nth-child(2n) {
|
||||
background-color: var(--chat-message-table-row-even-background);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.xbbcode-tag-img) {
|
||||
padding: .25em;
|
||||
border-radius: .25em;
|
||||
|
||||
overflow: hidden;
|
||||
max-width: 20em;
|
||||
max-height: 20em;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.chat-emoji) {
|
||||
height: 1.1em;
|
||||
width: 1.1em;
|
||||
|
@ -341,12 +313,6 @@ html:root {
|
|||
margin-bottom: .1em;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.xbbcode-tag-quote) {
|
||||
border-color: var(--chat-message-quote-border);
|
||||
padding-left: .5em;
|
||||
color: var(--chat-message-quote);
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
|
|
|
@ -7,64 +7,79 @@ import {
|
|||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {ContextDivider} from "tc-shared/ui/react-elements/ContextDivider";
|
||||
import {ConversationPanel} from "tc-shared/ui/frames/side/ConversationUI";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useContext, useEffect, useState} from "react";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import {AvatarRenderer} from "tc-shared/ui/react-elements/Avatar";
|
||||
import {TimestampRenderer} from "tc-shared/ui/react-elements/TimestampRenderer";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {getGlobalAvatarManagerFactory} from "tc-shared/file/Avatars";
|
||||
|
||||
const cssStyle = require("./PrivateConversationUI.scss");
|
||||
const kTypingTimeout = 5000;
|
||||
|
||||
const ConversationEntryInfo = React.memo((props: { events: Registry<PrivateConversationUIEvents>, chatId: string, initialNickname: string, lastMessage: number }) => {
|
||||
const HandlerIdContext = React.createContext<string>(undefined);
|
||||
const EventContext = React.createContext<Registry<PrivateConversationUIEvents>>(undefined);
|
||||
|
||||
const ConversationEntryInfo = React.memo((props: { chatId: string, initialNickname: string, lastMessage: number }) => {
|
||||
const events = useContext(EventContext);
|
||||
|
||||
const [ nickname, setNickname ] = useState(props.initialNickname);
|
||||
const [ lastMessage, setLastMessage ] = useState(props.lastMessage);
|
||||
const [ typingTimestamp, setTypingTimestamp ] = useState(0);
|
||||
|
||||
props.events.reactUse("notify_partner_name_changed", event => {
|
||||
if(event.chatId !== props.chatId)
|
||||
events.reactUse("notify_partner_name_changed", event => {
|
||||
if(event.chatId !== props.chatId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setNickname(event.name);
|
||||
});
|
||||
|
||||
props.events.reactUse("notify_partner_changed", event => {
|
||||
if(event.chatId !== props.chatId)
|
||||
events.reactUse("notify_partner_changed", event => {
|
||||
if(event.chatId !== props.chatId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setNickname(event.name);
|
||||
});
|
||||
|
||||
props.events.reactUse("notify_chat_event", event => {
|
||||
if(event.chatId !== props.chatId)
|
||||
events.reactUse("notify_chat_event", event => {
|
||||
if(event.chatId !== props.chatId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(event.event.type === "message") {
|
||||
if(event.event.timestamp > lastMessage)
|
||||
if(event.event.timestamp > lastMessage) {
|
||||
setLastMessage(event.event.timestamp);
|
||||
}
|
||||
|
||||
if(!event.event.isOwnMessage)
|
||||
if(!event.event.isOwnMessage) {
|
||||
setTypingTimestamp(0);
|
||||
}
|
||||
} else if(event.event.type === "partner-action" || event.event.type === "local-action") {
|
||||
setTypingTimestamp(0);
|
||||
}
|
||||
});
|
||||
|
||||
props.events.reactUse("notify_conversation_state", event => {
|
||||
if(event.chatId !== props.chatId || event.state !== "normal")
|
||||
events.reactUse("notify_conversation_state", event => {
|
||||
if(event.chatId !== props.chatId || event.state !== "normal") {
|
||||
return;
|
||||
}
|
||||
|
||||
let lastStatedMessage = event.events
|
||||
.filter(e => e.type === "message")
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
.last()?.timestamp;
|
||||
if(typeof lastStatedMessage === "number" && (typeof lastMessage === "undefined" || lastStatedMessage > lastMessage))
|
||||
|
||||
if(typeof lastStatedMessage === "number" && (typeof lastMessage === "undefined" || lastStatedMessage > lastMessage)) {
|
||||
setLastMessage(lastStatedMessage);
|
||||
}
|
||||
});
|
||||
|
||||
props.events.reactUse("notify_partner_typing", event => {
|
||||
if(event.chatId !== props.chatId)
|
||||
events.reactUse("notify_partner_typing", event => {
|
||||
if(event.chatId !== props.chatId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTypingTimestamp(Date.now());
|
||||
});
|
||||
|
@ -100,17 +115,18 @@ const ConversationEntryInfo = React.memo((props: { events: Registry<PrivateConve
|
|||
</div>
|
||||
);
|
||||
});
|
||||
const ConversationEntryUnreadMarker = React.memo((props: { events: Registry<PrivateConversationUIEvents>, chatId: string, initialUnread: boolean }) => {
|
||||
const ConversationEntryUnreadMarker = React.memo((props: { chatId: string, initialUnread: boolean }) => {
|
||||
const events = useContext(EventContext);
|
||||
const [ unread, setUnread ] = useState(props.initialUnread);
|
||||
|
||||
props.events.reactUse("notify_unread_timestamp_changed", event => {
|
||||
events.reactUse("notify_unread_timestamp_changed", event => {
|
||||
if(event.chatId !== props.chatId)
|
||||
return;
|
||||
|
||||
setUnread(event.timestamp !== undefined);
|
||||
});
|
||||
|
||||
props.events.reactUse("notify_chat_event", event => {
|
||||
events.reactUse("notify_chat_event", event => {
|
||||
if(event.chatId !== props.chatId || !event.triggerUnread)
|
||||
return;
|
||||
|
||||
|
@ -123,10 +139,13 @@ const ConversationEntryUnreadMarker = React.memo((props: { events: Registry<Priv
|
|||
return <div key={"unread-marker"} className={cssStyle.unread} />;
|
||||
});
|
||||
|
||||
const ConversationEntry = React.memo((props: { events: Registry<PrivateConversationUIEvents>, info: PrivateConversationInfo, selected: boolean, connection: ConnectionHandler }) => {
|
||||
const ConversationEntry = React.memo((props: { info: PrivateConversationInfo, selected: boolean }) => {
|
||||
const events = useContext(EventContext);
|
||||
const handlerId = useContext(HandlerIdContext);
|
||||
|
||||
const [ clientId, setClientId ] = useState(props.info.clientId);
|
||||
|
||||
props.events.reactUse("notify_partner_changed", event => {
|
||||
events.reactUse("notify_partner_changed", event => {
|
||||
if(event.chatId !== props.info.chatId)
|
||||
return;
|
||||
|
||||
|
@ -134,37 +153,41 @@ const ConversationEntry = React.memo((props: { events: Registry<PrivateConversat
|
|||
setClientId(event.clientId);
|
||||
});
|
||||
|
||||
const avatarFactory = getGlobalAvatarManagerFactory().getManager(handlerId);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cssStyle.conversationEntry + " " + (props.selected ? cssStyle.selected : "")}
|
||||
onClick={() => props.events.fire("action_select_chat", { chatId: props.info.chatId })}
|
||||
onClick={() => events.fire("action_select_chat", { chatId: props.info.chatId })}
|
||||
>
|
||||
<div className={cssStyle.containerAvatar}>
|
||||
<AvatarRenderer className={cssStyle.avatar} avatar={props.connection.fileManager.avatars.resolveClientAvatar({ id: clientId, database_id: 0, clientUniqueId: props.info.uniqueId })} />
|
||||
<ConversationEntryUnreadMarker chatId={props.info.chatId} events={props.events} initialUnread={props.info.unreadMessages} />
|
||||
<AvatarRenderer className={cssStyle.avatar} avatar={avatarFactory.resolveClientAvatar({ id: clientId, database_id: 0, clientUniqueId: props.info.uniqueId })} />
|
||||
<ConversationEntryUnreadMarker chatId={props.info.chatId} initialUnread={props.info.unreadMessages} />
|
||||
</div>
|
||||
<ConversationEntryInfo events={props.events} chatId={props.info.chatId} initialNickname={props.info.nickname} lastMessage={props.info.lastMessage} />
|
||||
<ConversationEntryInfo chatId={props.info.chatId} initialNickname={props.info.nickname} lastMessage={props.info.lastMessage} />
|
||||
<div className={cssStyle.close} onClick={() => {
|
||||
props.events.fire("action_close_chat", { chatId: props.info.chatId });
|
||||
events.fire("action_close_chat", { chatId: props.info.chatId });
|
||||
}} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const OpenConversationsPanel = React.memo((props: { events: Registry<PrivateConversationUIEvents>, connection: ConnectionHandler }) => {
|
||||
const OpenConversationsPanel = React.memo(() => {
|
||||
const events = useContext(EventContext);
|
||||
|
||||
const [ conversations, setConversations ] = useState<PrivateConversationInfo[] | "loading">(() => {
|
||||
props.events.fire("query_private_conversations");
|
||||
events.fire("query_private_conversations");
|
||||
return "loading";
|
||||
});
|
||||
|
||||
const [ selected, setSelected ] = useState("unselected");
|
||||
|
||||
props.events.reactUse("notify_private_conversations", event => {
|
||||
events.reactUse("notify_private_conversations", event => {
|
||||
setConversations(event.conversations);
|
||||
setSelected(event.selected);
|
||||
});
|
||||
|
||||
props.events.reactUse("notify_selected_chat", event => {
|
||||
events.reactUse("notify_selected_chat", event => {
|
||||
setSelected(event.chatId);
|
||||
});
|
||||
|
||||
|
@ -184,10 +207,8 @@ const OpenConversationsPanel = React.memo((props: { events: Registry<PrivateConv
|
|||
} else {
|
||||
content = conversations.map(e => <ConversationEntry
|
||||
key={"c-" + e.chatId}
|
||||
events={props.events}
|
||||
info={e}
|
||||
selected={e.chatId === selected}
|
||||
connection={props.connection}
|
||||
/>)
|
||||
}
|
||||
|
||||
|
@ -200,8 +221,12 @@ const OpenConversationsPanel = React.memo((props: { events: Registry<PrivateConv
|
|||
|
||||
|
||||
export const PrivateConversationsPanel = (props: { events: Registry<PrivateConversationUIEvents>, handler: ConnectionHandler }) => (
|
||||
<HandlerIdContext.Provider value={props.handler.handlerId}>
|
||||
<EventContext.Provider value={props.events}>
|
||||
<ContextDivider id={"seperator-conversation-list-messages"} direction={"horizontal"} defaultValue={25} separatorClassName={cssStyle.divider}>
|
||||
<OpenConversationsPanel events={props.events} connection={props.handler} />
|
||||
<OpenConversationsPanel />
|
||||
<ConversationPanel events={props.events as any} handlerId={props.handler.handlerId} noFirstMessageOverlay={true} messagesDeletable={false} />
|
||||
</ContextDivider>
|
||||
</EventContext.Provider>
|
||||
</HandlerIdContext.Provider>
|
||||
);
|
|
@ -0,0 +1,41 @@
|
|||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {ChannelEntry} from "tc-shared/tree/Channel";
|
||||
import {ChannelEditableProperty} from "tc-shared/ui/modal/channel-edit/Definitions";
|
||||
|
||||
const spawnChannelEditNew = (connection: ConnectionHandler, channel: ChannelEntry) => {
|
||||
};
|
||||
|
||||
class ChannelEditController {
|
||||
private readonly connection: ConnectionHandler;
|
||||
private readonly channel: ChannelEntry;
|
||||
|
||||
constructor() {
|
||||
this.getChannelProperty("sortingOrder");
|
||||
}
|
||||
|
||||
getChannelProperty<T extends keyof ChannelEditableProperty>(property: T) : ChannelEditableProperty[T] {
|
||||
|
||||
const properties = this.channel.properties;
|
||||
|
||||
/*
|
||||
switch (property) {
|
||||
case "name":
|
||||
return properties.channel_name;
|
||||
|
||||
case "phoneticName":
|
||||
return properties.channel_name_phonetic;
|
||||
|
||||
case "topic":
|
||||
return properties.channel_topic;
|
||||
|
||||
case "description":
|
||||
return properties.channel_description;
|
||||
|
||||
case "password":
|
||||
break;
|
||||
|
||||
}
|
||||
*/
|
||||
return undefined;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
export interface ChannelEditableProperty {
|
||||
"name": string,
|
||||
"sortingOrder": { previousChannelId: number, availableChannels: { channelName: string, channelId: number }[] | undefined },
|
||||
/*
|
||||
"phoneticName": string,
|
||||
"talkPower": number,
|
||||
"password": { state: "set", password?: string } | { state: "clear" },
|
||||
"topic": string,
|
||||
"description": string,
|
||||
"type": "default" | "permanent" | "semi-permanent" | "temporary",
|
||||
"maxUsers": "unlimited" | number,
|
||||
"maxFamilyUsers": "unlimited" | "inherited" | number,
|
||||
"codec": { type: number, quality: number },
|
||||
"deleteDelay": number,
|
||||
"encryptedVoiceData": number
|
||||
*/
|
||||
}
|
||||
|
||||
export interface ChannelPropertyPermission {
|
||||
name: boolean,
|
||||
password: { editable: boolean, enforced: boolean },
|
||||
talkPower: boolean,
|
||||
sortingOrder: boolean,
|
||||
topic: boolean,
|
||||
description: boolean,
|
||||
channelType: {
|
||||
permanent: boolean,
|
||||
semipermanent: boolean,
|
||||
temporary: boolean,
|
||||
default: boolean
|
||||
},
|
||||
maxUsers: boolean,
|
||||
maxFamilyUsers: boolean,
|
||||
codec: {
|
||||
opusVoice: boolean,
|
||||
opusMusic: boolean
|
||||
},
|
||||
deleteDelay: {
|
||||
editable: boolean,
|
||||
maxDelay: number,
|
||||
},
|
||||
encryptVoiceData: boolean
|
||||
}
|
||||
|
||||
export interface ChannelPropertyStatus {
|
||||
name: boolean,
|
||||
password: boolean
|
||||
}
|
||||
|
||||
export interface ChannelEditEvents {
|
||||
change_property: {
|
||||
property: keyof ChannelEditableProperty
|
||||
value: ChannelEditableProperty[keyof ChannelEditableProperty]
|
||||
},
|
||||
|
||||
query_property: {
|
||||
property: keyof ChannelEditableProperty
|
||||
},
|
||||
query_property_permission: {
|
||||
permission: keyof ChannelPropertyPermission
|
||||
}
|
||||
|
||||
notify_property: {
|
||||
property: keyof ChannelEditableProperty
|
||||
value: ChannelEditableProperty[keyof ChannelEditableProperty]
|
||||
},
|
||||
notify_property_permission: {
|
||||
permission: keyof ChannelPropertyPermission
|
||||
value: ChannelPropertyPermission[keyof ChannelPropertyPermission]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.containerGeneral {
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||
import * as React from "react";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {ChannelEditableProperty, ChannelEditEvents} from "tc-shared/ui/modal/channel-edit/Definitions";
|
||||
import {useContext, useState} from "react";
|
||||
import {BoxedInputField} from "tc-shared/ui/react-elements/InputField";
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
|
||||
const EventContext = React.createContext<Registry<ChannelEditEvents>>(undefined);
|
||||
const ChangesApplying = React.createContext(false);
|
||||
|
||||
const kPropertyLoading = "loading";
|
||||
|
||||
function useProperty<T extends keyof ChannelEditableProperty>(property: T) : {
|
||||
originalValue: ChannelEditableProperty[T],
|
||||
currentValue: ChannelEditableProperty[T],
|
||||
setCurrentValue: (value: ChannelEditableProperty[T]) => void
|
||||
} | typeof kPropertyLoading {
|
||||
const events = useContext(EventContext);
|
||||
|
||||
const [ value, setValue ] = useState(() => {
|
||||
events.fire("query_property", { property: property });
|
||||
return kPropertyLoading;
|
||||
});
|
||||
|
||||
events.reactUse("notify_property", event => {
|
||||
if(event.property !== property) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(event.value as any);
|
||||
}, undefined, []);
|
||||
|
||||
return kPropertyLoading;
|
||||
}
|
||||
|
||||
const ChannelName = () => {
|
||||
const changesApplying = useContext(ChangesApplying);
|
||||
const property = useProperty("name");
|
||||
|
||||
return (
|
||||
<BoxedInputField
|
||||
disabled={changesApplying || property === kPropertyLoading}
|
||||
value={property === kPropertyLoading ? null : property.currentValue}
|
||||
placeholder={property === kPropertyLoading ? tr("loading") : tr("Channel name")}
|
||||
onInput={newValue => property !== kPropertyLoading && property.setCurrentValue(newValue)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const GeneralContainer = () => {
|
||||
return (
|
||||
<div className={cssStyle.containerGeneral}>
|
||||
<ChannelName />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export class ChannelEditModal extends InternalModal {
|
||||
private readonly channelExists: number;
|
||||
|
||||
renderBody(): React.ReactElement {
|
||||
return (<>
|
||||
<GeneralContainer />
|
||||
</>);
|
||||
}
|
||||
|
||||
title(): string | React.ReactElement {
|
||||
return <Translatable key={"create"}>Create channel</Translatable>;
|
||||
}
|
||||
}
|
|
@ -1 +1 @@
|
|||
Subproject commit df9aada506995d35787098694d1ed2e2e2b5a619
|
||||
Subproject commit b884828db27025abf0802a015b2fa46bf2c2e44c
|
Loading…
Reference in New Issue