diff --git a/src/client/webapp/elements/context-menu-conn.ts b/src/client/webapp/elements/context-menu-conn.tsx
similarity index 64%
rename from src/client/webapp/elements/context-menu-conn.ts
rename to src/client/webapp/elements/context-menu-conn.tsx
index 5de910d..30ffbe7 100644
--- a/src/client/webapp/elements/context-menu-conn.ts
+++ b/src/client/webapp/elements/context-menu-conn.tsx
@@ -1,3 +1,5 @@
+import React from 'react';
+import ReactHelper from './require/react-helper.js';
import ElementsUtil from './require/elements-util.js';
import BaseElements from './require/base-elements.js';
@@ -9,20 +11,20 @@ import CombinedGuild from '../guild-combined.js';
export default function createConnectionContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild) {
const statuses = [ 'online', 'away', 'busy', 'invisible' ];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- let content: any[] = [
- { class: 'item personalize', content: [
- { class: 'icon', content: { tag: 'img', src: './img/pencil-icon.png' } },
- { content: 'Personalize' }
- ] },
- { class: 'item-spacer' }
+ let content: JSX.Element[] = [
+
,
+
];
- content = content.concat(statuses.map(status => {
- return { class: 'item ' + status, content: [
- { class: 'status-circle' },
- { class: 'status-text', content: status }
- ] };
- }));
- const element = BaseElements.createContextMenu(document, { class: 'member-context', content: content });
+ content = content.concat(statuses.map(status => (
+
+ )));
+ const element = BaseElements.createContextMenu(document, {content}
);
q.$$$(element, '.personalize').addEventListener('click', async () => {
element.removeSelf();
diff --git a/src/client/webapp/elements/context-menu-guild-title.ts b/src/client/webapp/elements/context-menu-guild-title.tsx
similarity index 70%
rename from src/client/webapp/elements/context-menu-guild-title.ts
rename to src/client/webapp/elements/context-menu-guild-title.tsx
index efbe45d..5d7376f 100644
--- a/src/client/webapp/elements/context-menu-guild-title.ts
+++ b/src/client/webapp/elements/context-menu-guild-title.tsx
@@ -16,30 +16,37 @@ import createCreateChannelOverlay from './overlay-create-channel';
import createTokenLogOverlay from './overlay-token-log';
import CombinedGuild from '../guild-combined';
-export default function createGuildTitleContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild): HTMLElement {
+import React from 'react';
+import ReactHelper from './require/react-helper';
+
+export default function createGuildTitleContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild): Element {
if (ui.activeConnection === null) {
LOG.warn('no active connection when creating guild title context menu');
- return q.create({}) as HTMLElement;
+ return ReactHelper.createElementFromJSX();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- const menuItems: any[] = [];
+ const menuItems: JSX.Element[] = [];
if (ui.activeConnection.privileges.includes('modify_profile')) {
- menuItems.push({ class: 'item guild-settings', content: [
- { class: 'icon', content: BaseElements.Q_COG },
- 'Guild Settings'
- ] });
+ menuItems.push(
+
+
{BaseElements.COG}
+
Guild Settings
+
+ );
}
if (ui.activeConnection.privileges.includes('modify_channels')) {
if (ui.activeConnection.privileges.includes('modify_profile')) {
- menuItems.push({ class: 'item-spacer' });
+ menuItems.push();
}
- menuItems.push({ class: 'item create-channel', content: [
- { class: 'icon', content: BaseElements.Q_CREATE },
- 'Create Channel'
- ] });
+ menuItems.push(
+
+
{BaseElements.CREATE}
+
Create Channel
+
+ );
}
if (ui.activeConnection.privileges.includes('modify_members')) {
@@ -47,21 +54,25 @@ export default function createGuildTitleContextMenu(document: Document, q: Q, ui
ui.activeConnection.privileges.includes('modify_profile') ||
ui.activeConnection.privileges.includes('modify_channels')
) {
- menuItems.push({ class: 'item-spacer' });
+ menuItems.push();
}
- menuItems.push({ class: 'item create-invite-token', content: [
- { class: 'icon', content: BaseElements.Q_TOKEN },
- 'Create Invite Token'
- ] });
- menuItems.push({ class: 'item token-log', content: [
- { class: 'icon', content: BaseElements.Q_TOKEN },
- 'Token Log'
- ] });
+ menuItems.push(
+
+
{BaseElements.TOKEN}
+
Create Invite Token
+
+ );
+ menuItems.push(
+
+
{BaseElements.TOKEN}
+
Token Log
+
+ );
}
- const element = BaseElements.createContextMenu(document, {
- class: 'guild-title-context', content: menuItems
- });
+ const element = BaseElements.createContextMenu(document, (
+ {menuItems}
+ ));
if (ui.activeConnection.privileges.includes('modify_profile')) {
q.$$$(element, '.item.guild-settings').addEventListener('click', async () => {
diff --git a/src/client/webapp/elements/context-menu-guild.ts b/src/client/webapp/elements/context-menu-guild.tsx
similarity index 80%
rename from src/client/webapp/elements/context-menu-guild.ts
rename to src/client/webapp/elements/context-menu-guild.tsx
index d3eabb1..d87845c 100644
--- a/src/client/webapp/elements/context-menu-guild.ts
+++ b/src/client/webapp/elements/context-menu-guild.tsx
@@ -10,12 +10,14 @@ import UI from '../ui';
import GuildsManager from '../guilds-manager';
import CombinedGuild from '../guild-combined';
+import React from 'react';
+
export default function createGuildContextMenu(document: Document, q: Q, ui: UI, guildsManager: GuildsManager, guild: CombinedGuild) {
- const element = BaseElements.createContextMenu(document, {
- class: 'guild-context', content: [
- { class: 'item red leave-guild', content: 'Leave Guild' }
- ]
- });
+ const element = BaseElements.createContextMenu(document, (
+
+ ));
q.$$$(element, '.leave-guild').addEventListener('click', async () => {
element.removeSelf();
diff --git a/src/client/webapp/elements/context-menu-img.ts b/src/client/webapp/elements/context-menu-img.tsx
similarity index 83%
rename from src/client/webapp/elements/context-menu-img.ts
rename to src/client/webapp/elements/context-menu-img.tsx
index 91bdee2..2bcb917 100644
--- a/src/client/webapp/elements/context-menu-img.ts
+++ b/src/client/webapp/elements/context-menu-img.tsx
@@ -10,6 +10,8 @@ import ElementsUtil from './require/elements-util';
import Q from '../q-module';
import CombinedGuild from '../guild-combined';
+import React from 'react';
+
export default function createImageContextMenu(
document: Document,
q: Q,
@@ -21,10 +23,12 @@ export default function createImageContextMenu(
isPreview: boolean
): HTMLElement {
// TODO: try/catch around sharp?
- const contextMenu = BaseElements.createContextMenu(document, { class: 'image', content: [
- { class: 'item copy-image', content: 'Copy Image' + (isPreview ? ' Preview' : '') },
- { class: 'item save-image', content: 'Save Image' + (isPreview ? ' Preview' : '') },
- ] });
+ const contextMenu = BaseElements.createContextMenu(document, (
+
+
Copy Image{isPreview ? ' Preview' : ''}
+
Save Image{isPreview ? ' Preview' : ''}
+
+ ));
q.$$$(contextMenu, '.copy-image').addEventListener('click', async () => {
q.$$$(contextMenu, '.copy-image').innerText = 'Copying...';
let nativeImage: electron.NativeImage;
diff --git a/src/client/webapp/elements/error-indicator.ts b/src/client/webapp/elements/error-indicator.tsx
similarity index 78%
rename from src/client/webapp/elements/error-indicator.ts
rename to src/client/webapp/elements/error-indicator.tsx
index 251218c..1c257ac 100644
--- a/src/client/webapp/elements/error-indicator.ts
+++ b/src/client/webapp/elements/error-indicator.tsx
@@ -5,6 +5,8 @@ import Q from '../q-module';
const LOG = Logger.create(__filename, electronConsole);
import ElementsUtil from './require/elements-util';
+import React from 'react';
+import ReactHelper from './require/react-helper';
export interface CreateErrorIndicatorProps {
container: HTMLElement;
@@ -16,19 +18,19 @@ export interface CreateErrorIndicatorProps {
}
// resolveFunc and rejectFunc should be the resolve/reject functions from the withPotentialError promise
-export default function createErrorIndicator(q: Q, props: CreateErrorIndicatorProps): HTMLElement {
+export default function createErrorIndicator(q: Q, props: CreateErrorIndicatorProps): Element {
props.classes = props.classes ?? [];
const { container, classes, message, taskFunc, resolveFunc, rejectFunc } = props;
- const element = q.create({
- class: [ 'error-indicator', ...classes ], content: [
- { tag: 'img', src: './img/error.png', alt: 'error' },
- { content: [
- { content: message },
- { class: 'retry-button', content: 'Try Again' }
- ] }
- ]
- }) as HTMLElement;
+ const element = ReactHelper.createElementFromJSX(
+
+
+
+
{message}
+
Try Again
+
+
+ );
const observer = new MutationObserver(() => {
if (element.parentElement == null) {
diff --git a/src/client/webapp/elements/events-guild-title.ts b/src/client/webapp/elements/events-guild-title.ts
index 546aa74..f6ce845 100644
--- a/src/client/webapp/elements/events-guild-title.ts
+++ b/src/client/webapp/elements/events-guild-title.ts
@@ -13,7 +13,7 @@ export default function bindAddGuildTitleEvents(document: Document, q: Q, ui: UI
!ui.activeConnection.privileges.includes('modify_members')
) return;
- const contextMenu = createGuildTitleContextMenu(document, q, ui, ui.activeGuild);
+ const contextMenu = createGuildTitleContextMenu(document, q, ui, ui.activeGuild) as HTMLElement;
document.body.appendChild(contextMenu);
ElementsUtil.alignContextElement(contextMenu, q.$('#guild-name-container'), { top: 'bottom', centerX: 'centerX' });
});
diff --git a/src/client/webapp/elements/guild-list-guild.ts b/src/client/webapp/elements/guild-list-guild.tsx
similarity index 90%
rename from src/client/webapp/elements/guild-list-guild.ts
rename to src/client/webapp/elements/guild-list-guild.tsx
index 97076bb..562035e 100644
--- a/src/client/webapp/elements/guild-list-guild.ts
+++ b/src/client/webapp/elements/guild-list-guild.tsx
@@ -14,11 +14,16 @@ import createGuildContextMenu from './context-menu-guild';
import GuildsManager from '../guilds-manager';
import CombinedGuild from '../guild-combined';
-export default function createGuildListGuild(document: Document, q: Q, ui: UI, guildsManager: GuildsManager, guild: CombinedGuild) {
- const element = q.create({ class: 'guild', 'data-id': guild.id, 'meta-name': guild.id, content: [
- { class: 'pill' },
- { tag: 'img', src: './img/loading.svg', alt: 'guild' }, // src is set later by script.js
- ] }) as HTMLElement;
+import React from 'react';
+import ReactHelper from './require/react-helper';
+
+export default function createGuildListGuild(document: Document, q: Q, ui: UI, guildsManager: GuildsManager, guild: CombinedGuild): Element {
+ const element = ReactHelper.createElementFromJSX(
+
+
+
+
+ ) as HTMLElement;
// Hover over for name + connection info
(async () => {
diff --git a/src/client/webapp/elements/message.ts b/src/client/webapp/elements/message.ts
index e32f0f1..ce9051d 100644
--- a/src/client/webapp/elements/message.ts
+++ b/src/client/webapp/elements/message.ts
@@ -8,8 +8,9 @@ import createResourceMessageContinued from './msg-res-cont';
import createTextMessage from './msg-txt';
import createTextMessageContinued from './msg-txt-cont';
-export default function createMessage(document: Document, q: Q, guild: CombinedGuild, message: Message, lastMessage: Message | null): HTMLElement {
- let element: HTMLElement;
+// TODO: This is probably best as a react class
+export default function createMessage(document: Document, q: Q, guild: CombinedGuild, message: Message, lastMessage: Message | null): Element {
+ let element: Element;
if (message.hasResource()) {
if (message.isImageResource()) {
if (message.isContinued(lastMessage)) {
diff --git a/src/client/webapp/elements/msg-img-res-cont.ts b/src/client/webapp/elements/msg-img-res-cont.tsx
similarity index 72%
rename from src/client/webapp/elements/msg-img-res-cont.ts
rename to src/client/webapp/elements/msg-img-res-cont.tsx
index 0896489..c19849c 100644
--- a/src/client/webapp/elements/msg-img-res-cont.ts
+++ b/src/client/webapp/elements/msg-img-res-cont.tsx
@@ -14,19 +14,26 @@ import createImageOverlay from './overlay-image';
import createImageContextMenu from './context-menu-img';
import CombinedGuild from '../guild-combined';
-export default function createImageResourceMessageContinued(document: Document, q: Q, guild: CombinedGuild, message: Message): HTMLElement {
+import React from 'react';
+import ReactHelper from './require/react-helper';
+
+export default function createImageResourceMessageContinued(document: Document, q: Q, guild: CombinedGuild, message: Message): Element {
if (!message.resourceId || !message.resourcePreviewId || !message.resourceName) {
throw new ShouldNeverHappenError('Message is not a resource message');
}
- const element = q.create({ class: 'message continued', 'data-id': message.id, 'meta-member-id': message.member.id, 'data-guild-id': guild.id, content: [
- { class: 'timestamp', content: moment(message.sent).format('HH:mm') },
- { class: 'right', content: [
- { class: 'content image', style: `width: ${message.previewWidth}px; height: ${message.previewHeight}px;`, content:
- { tag: 'img', src: './img/loading.svg', alt: message.resourceName } }, // src will be replaced later
- { class: 'content text', content: ElementsUtil.parseMessageText(message.text ?? '') }
- ] }
- ] }) as HTMLElement;
+ const element = ReactHelper.createElementFromJSX(
+
+
{moment(message.sent).format('HH:mm')}
+
+
+
+
+
{ElementsUtil.parseMessageText(message.text ?? '')}
+
+
+ );
+
q.$$$(element, '.content.image').addEventListener('click', () => {
document.body.appendChild(createImageOverlay(document, q, guild, message.resourceId as string, message.resourceName as string));
});
@@ -51,5 +58,6 @@ export default function createImageResourceMessageContinued(document: Document,
(q.$$$(element, '.content.image img') as HTMLImageElement).src = './img/error.png';
}
})();
+
return element;
}
diff --git a/src/client/webapp/elements/msg-img-res.ts b/src/client/webapp/elements/msg-img-res.tsx
similarity index 69%
rename from src/client/webapp/elements/msg-img-res.ts
rename to src/client/webapp/elements/msg-img-res.tsx
index ca7937d..b28bdc8 100644
--- a/src/client/webapp/elements/msg-img-res.ts
+++ b/src/client/webapp/elements/msg-img-res.tsx
@@ -14,7 +14,10 @@ import createImageOverlay from './overlay-image';
import createImageContextMenu from './context-menu-img';
import CombinedGuild from '../guild-combined';
-export default function createImageResourceMessage(document: Document, q: Q, guild: CombinedGuild, message: Message) {
+import React from 'react';
+import ReactHelper from './require/react-helper';
+
+export default function createImageResourceMessage(document: Document, q: Q, guild: CombinedGuild, message: Message): Element {
if (!message.resourceId || !message.resourcePreviewId || !message.resourceName) {
throw new ShouldNeverHappenError('Message is not a resource message');
}
@@ -38,19 +41,25 @@ export default function createImageResourceMessage(document: Document, q: Q, gui
};
}
- const nameStyle = memberInfo.roleColor != null ? 'color: ' + memberInfo.roleColor : '';
- const element = q.create({ class: 'message', 'data-id': message.id, 'meta-member-id': message.member.id, 'data-guild-id': guild.id, content: [
- { class: 'member-avatar', content: { tag: 'img', src: './img/loading.svg', alt: memberInfo.displayName } },
- { class: 'right', content: [
- { class: 'header', content: [
- { class: 'member-name', style: nameStyle, content: memberInfo.displayName },
- { class: 'timestamp', content: moment(message.sent).calendar(ElementsUtil.calendarFormats) }
- ] },
- { class: 'content image', style: `width: ${message.previewWidth}px; height: ${message.previewHeight}px;`, content:
- { tag: 'img', src: './img/loading.svg', alt: message.resourceName } }, // src will be replaced later
- { class: 'content text', content: ElementsUtil.parseMessageText(message.text ?? '') }
- ] }
- ] }) as HTMLElement;
+ const nameStyle = memberInfo.roleColor != null ? { color: memberInfo.roleColor } : {};
+ const element = ReactHelper.createElementFromJSX(
+
+
+
+
+
+
+
{memberInfo.displayName}
+
{moment(message.sent).calendar(ElementsUtil.calendarFormats)}
+
+
+
+
+
{ElementsUtil.parseMessageText(message.text ?? '')}
+
+
+ );
+
q.$$$(element, '.content.image').addEventListener('click', (e) => {
document.body.appendChild(createImageOverlay(document, q, guild, message.resourceId as string, message.resourceName as string));
});
diff --git a/src/client/webapp/elements/msg-res-cont.ts b/src/client/webapp/elements/msg-res-cont.tsx
similarity index 62%
rename from src/client/webapp/elements/msg-res-cont.ts
rename to src/client/webapp/elements/msg-res-cont.tsx
index 1275df3..72eb150 100644
--- a/src/client/webapp/elements/msg-res-cont.ts
+++ b/src/client/webapp/elements/msg-res-cont.tsx
@@ -5,24 +5,30 @@ import Q from "../q-module";
import ElementsUtil from "./require/elements-util";
-export default function createResourceMessageContinued(q: Q, guild: CombinedGuild, message: Message): HTMLElement {
+import React from 'react';
+import ReactHelper from './require/react-helper';
+
+export default function createResourceMessageContinued(q: Q, guild: CombinedGuild, message: Message): Element {
if (!message.resourceId || !message.resourceName) {
throw new ShouldNeverHappenError('Message is not a resource message');
}
- const element = q.create({ class: 'message continued', 'data-id': message.id, 'meta-member-id': message.member.id, 'data-guild-id': guild.id, content: [
- { class: 'timestamp', content: moment(message.sent).format('HH:mm') },
- { class: 'right', content: [
- { class: 'content resource', content: [
- { tag: 'img', class: 'icon', src: './img/file-icon.png' }, // TODO: SVG based on content-type
- { class: 'text', content: [
- { class: 'filename', content: message.resourceName },
- { class: 'download-status', content: 'Click to Download' },
- ] }
- ] },
- { class: 'content text', content: ElementsUtil.parseMessageText(message.text ?? '') }
- ] }
- ] }) as HTMLElement;
+ const element = ReactHelper.createElementFromJSX(
+
+
{moment(message.sent).format('HH:mm')}
+
+
+
+
+
{message.resourceName}
+
Click to Download
+
+
+
{ElementsUtil.parseMessageText(message.text ?? '')}
+
+
+ );
+
q.$$$(element, '.resource').addEventListener('click', ElementsUtil.createDownloadListener({
guild: guild, resourceId: message.resourceId, resourceName: message.resourceName,
downloadStartFunc: () => {
diff --git a/src/client/webapp/elements/msg-res.ts b/src/client/webapp/elements/msg-res.tsx
similarity index 62%
rename from src/client/webapp/elements/msg-res.ts
rename to src/client/webapp/elements/msg-res.tsx
index 07f9082..1665c04 100644
--- a/src/client/webapp/elements/msg-res.ts
+++ b/src/client/webapp/elements/msg-res.tsx
@@ -5,7 +5,10 @@ import Q from '../q-module';
import ElementsUtil from './require/elements-util';
-export default function createResourceMessage(q: Q, guild: CombinedGuild, message: Message): HTMLElement {
+import React from 'react';
+import ReactHelper from './require/react-helper';
+
+export default function createResourceMessage(q: Q, guild: CombinedGuild, message: Message): Element {
if (!message.resourceId || !message.resourceName) {
throw new ShouldNeverHappenError('Message is not a resource message');
}
@@ -29,24 +32,29 @@ export default function createResourceMessage(q: Q, guild: CombinedGuild, messag
};
}
- const nameStyle = memberInfo.roleColor ? 'color: ' + memberInfo.roleColor : '';
- const element = q.create({ class: 'message', 'data-id': message.id, 'meta-member-id': message.member.id, 'data-guild-id': guild.id, content: [
- { class: 'member-avatar', content: { tag: 'img', src: './img/loading.svg', alt: memberInfo.displayName } },
- { class: 'right', content: [
- { class: 'header', content: [
- { class: 'member-name', style: nameStyle, content: memberInfo.displayName },
- { class: 'timestamp', content: moment(message.sent).calendar(ElementsUtil.calendarFormats) }
- ] },
- { class: 'content resource', content: [
- { tag: 'img', class: 'icon', src: './img/file-icon.png' }, // TODO: SVG based on content-type
- { class: 'text', content: [
- { class: 'filename', content: message.resourceName },
- { class: 'download-status', content: 'Click to Download' },
- ] }
- ] },
- { class: 'content text', content: ElementsUtil.parseMessageText(message.text ?? '') }
- ] }
- ] }) as HTMLElement;
+ const nameStyle = memberInfo.roleColor != null ? { color: memberInfo.roleColor } : {};
+ const element = ReactHelper.createElementFromJSX(
+
+
+
+
+
+
+
{memberInfo.displayName}
+
{moment(message.sent).calendar(ElementsUtil.calendarFormats)}
+
+
+
+
+
{message.resourceName}
+
Click to Download
+
+
+
{ElementsUtil.parseMessageText(message.text ?? '')}
+
+
+ );
+
q.$$$(element, '.resource').addEventListener('click', ElementsUtil.createDownloadListener({
guild: guild, resourceId: message.resourceId, resourceName: message.resourceName,
downloadStartFunc: () => {
diff --git a/src/client/webapp/elements/msg-txt-cont.ts b/src/client/webapp/elements/msg-txt-cont.ts
deleted file mode 100644
index bf6aa32..0000000
--- a/src/client/webapp/elements/msg-txt-cont.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import moment from 'moment';
-import { Message } from '../data-types';
-import CombinedGuild from '../guild-combined';
-import Q from '../q-module.js';
-
-import ElementsUtil from './require/elements-util.js';
-
-export default function createTextMessageContinued(q: Q, guild: CombinedGuild, message: Message): HTMLElement {
- return q.create({ class: 'message continued', 'data-id': message.id, 'meta-member-id': message.member.id, 'data-guild-id': guild.id, content: [
- { class: 'timestamp', content: moment(message.sent).format('HH:mm') },
- { class: 'right', content: [
- { class: 'content text', content: ElementsUtil.parseMessageText(message.text ?? '') }
- ] }
- ] }) as HTMLElement;
-}
diff --git a/src/client/webapp/elements/msg-txt-cont.tsx b/src/client/webapp/elements/msg-txt-cont.tsx
new file mode 100644
index 0000000..0d01840
--- /dev/null
+++ b/src/client/webapp/elements/msg-txt-cont.tsx
@@ -0,0 +1,20 @@
+import moment from 'moment';
+import { Message } from '../data-types';
+import CombinedGuild from '../guild-combined';
+import Q from '../q-module.js';
+
+import ElementsUtil from './require/elements-util.js';
+
+import React from 'react';
+import ReactHelper from './require/react-helper';
+
+export default function createTextMessageContinued(q: Q, guild: CombinedGuild, message: Message): Element {
+ return ReactHelper.createElementFromJSX(
+
+
{moment(message.sent).format('HH:mm')}
+
+
{ElementsUtil.parseMessageText(message.text ?? '')}
+
+
+ );
+}
diff --git a/src/client/webapp/elements/msg-txt.ts b/src/client/webapp/elements/msg-txt.tsx
similarity index 59%
rename from src/client/webapp/elements/msg-txt.ts
rename to src/client/webapp/elements/msg-txt.tsx
index 95d2314..ad0ddba 100644
--- a/src/client/webapp/elements/msg-txt.ts
+++ b/src/client/webapp/elements/msg-txt.tsx
@@ -6,7 +6,10 @@ import { Message, Member, IDummyTextMessage } from '../data-types';
import Q from '../q-module';
import CombinedGuild from '../guild-combined';
-export default function createTextMessage(q: Q, guild: CombinedGuild, message: Message | IDummyTextMessage): HTMLElement {
+import React from 'react';
+import ReactHelper from './require/react-helper';
+
+export default function createTextMessage(q: Q, guild: CombinedGuild, message: Message | IDummyTextMessage): Element {
let memberInfo: {
roleColor: string | null,
displayName: string,
@@ -34,17 +37,21 @@ export default function createTextMessage(q: Q, guild: CombinedGuild, message: M
};
}
- const nameStyle = memberInfo.roleColor ? 'color: ' + memberInfo.roleColor : '';
- const element = q.create({ class: 'message', 'data-id': message.id, 'meta-member-id': message.member.id, 'data-guild-id': guild.id, content: [
- { class: 'member-avatar', content: { tag: 'img', src: './img/loading.svg', alt: memberInfo.displayName } },
- { class: 'right', content: [
- { class: 'header', content: [
- { class: 'member-name', style: nameStyle, content: memberInfo.displayName },
- { class: 'timestamp', content: moment(message.sent).calendar(ElementsUtil.calendarFormats) }
- ] },
- { class: 'content text', content: ElementsUtil.parseMessageText(message.text || '') }
- ] }
- ] }) as HTMLElement;
+ const nameStyle = memberInfo.roleColor != null ? { color: memberInfo.roleColor } : {};
+ const element = ReactHelper.createElementFromJSX(
+
+
+
+
+
+
+
{memberInfo.displayName}
+
{moment(message.sent).calendar(ElementsUtil.calendarFormats)}
+
+
{ElementsUtil.parseMessageText(message.text ?? '')}
+
+
+ );
(async () => {
(q.$$$(element, '.member-avatar img') as HTMLImageElement).src =
await ElementsUtil.getImageBufferFromResourceFailSoftly(guild, memberInfo.avatarResourceId);
diff --git a/src/client/webapp/elements/require/base-elements.tsx b/src/client/webapp/elements/require/base-elements.tsx
index 1e0258e..a71e3e3 100644
--- a/src/client/webapp/elements/require/base-elements.tsx
+++ b/src/client/webapp/elements/require/base-elements.tsx
@@ -13,8 +13,9 @@ import ElementsUtil from './elements-util';
import { Channel } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import Q from '../../q-module';
+import ReactHelper from './react-helper';
-interface HTMLElementWithRemoveSelf extends HTMLElement {
+export interface HTMLElementWithRemoveSelf extends HTMLElement {
removeSelf: (() => void);
}
@@ -158,12 +159,26 @@ export default class BaseElements {
]
}
+ static TOKEN = (
+
+ );
static Q_TOKEN = {
ns: 'http://www.w3.org/2000/svg', tag: 'svg', width: 16, height: 16, viewBox: '0 0 16 16', content:
{ ns: 'http://www.w3.org/2000/svg', tag: 'path', fill: 'currentColor', 'fill-rule': 'evenodd', 'clip-rule': 'evenodd',
d: 'M 7.9160156 0 A 8.0000004 8.0000004 0 0 0 1.8710938 2.8574219 A 8.0000004 8.0000004 0 0 0 2.8574219 14.128906 A 8.0000004 8.0000004 0 0 0 14.128906 13.142578 A 8.0000004 8.0000004 0 0 0 13.142578 1.8710938 A 8.0000004 8.0000004 0 0 0 7.9160156 0 z M 5.2324219 3.2851562 A 2.4000002 2.4000002 0 0 1 6.7851562 3.8476562 A 2.4000002 2.4000002 0 0 1 7.6054688 6.1015625 L 13.367188 10.9375 L 12.595703 11.857422 L 10.90625 12.003906 L 10.445312 11.619141 L 10.373047 10.773438 L 9.9121094 10.388672 L 9.0664062 10.462891 L 8.6074219 10.076172 L 8.5332031 9.2304688 L 8.0742188 8.8457031 L 7.2285156 8.9199219 L 6.0625 7.9414062 A 2.4000002 2.4000002 0 0 1 3.6992188 7.5253906 A 2.4000002 2.4000002 0 0 1 3.4042969 4.1425781 A 2.4000002 2.4000002 0 0 1 5.2324219 3.2851562 z' }
}
+ static CREATE = (
+
+ );
static Q_CREATE = {
ns: 'http://www.w3.org/2000/svg', tag: 'svg', width: 16, height: 16, viewBox: '0 0 16 16', content:
{ ns: 'http://www.w3.org/2000/svg', tag: 'path', fill: 'currentColor', 'fill-rule': 'evenodd', 'clip-rule': 'evenodd',
@@ -210,20 +225,26 @@ export default class BaseElements {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- static createContextMenu(document: Document, content: any): HTMLElementWithRemoveSelf {
- const q = new Q(document);
-
+ static createContextMenu(document: Document, content: JSX.Element): HTMLElementWithRemoveSelf {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- const element = q.create({ class: 'context', content: { class: 'menu', content: content }}) as any;
+ const element = ReactHelper.createElementFromJSX(
+
+ ) as HTMLElementWithRemoveSelf;
+
element.addEventListener('mousedown', (e: Event) => {
e.stopPropagation(); // stop the bubble
});
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
element.removeSelf = () => {
if (element.parentElement) {
element.parentElement.removeChild(element);
}
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
document.body.removeEventListener('mousedown', element.removeSelf);
};
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
document.body.addEventListener('mousedown', element.removeSelf);
return element as HTMLElementWithRemoveSelf;
}
diff --git a/src/client/webapp/elements/require/elements-util.ts b/src/client/webapp/elements/require/elements-util.tsx
similarity index 89%
rename from src/client/webapp/elements/require/elements-util.ts
rename to src/client/webapp/elements/require/elements-util.tsx
index 0e8f7e7..3b822d3 100644
--- a/src/client/webapp/elements/require/elements-util.ts
+++ b/src/client/webapp/elements/require/elements-util.tsx
@@ -5,7 +5,6 @@ import * as electron from 'electron';
import * as electronRemote from '@electron/remote';
const electronConsole = electronRemote.getGlobal('console') as Console;
-
import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
@@ -16,6 +15,8 @@ import Globals from '../../globals';
import CombinedGuild from '../../guild-combined';
import { ShouldNeverHappenError } from '../../data-types';
+import React from 'react';
+
// TODO: pass-through Globals in init function
// alignment: {
// centerY: 'top'
@@ -34,6 +35,13 @@ interface IHTMLElementWithRemovalType extends HTMLElement {
manualRemoval?: boolean;
}
+interface SimpleQElement {
+ tag: 'span',
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ content: (SimpleQElement | string)[],
+ class: string | null
+}
+
interface CreateDownloadListenerProps {
downloadBuff?: Buffer;
guild?: CombinedGuild;
@@ -115,19 +123,19 @@ export default class ElementsUtil {
}
}
- // creates spans to format the text (all in q.js element markup)
+ // creates spans to format the text (all in q.js element markup that gets converted to jsx [aka hypergaming])
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- static parseMessageText(text: string): any {
+ static parseMessageText(text: string): JSX.Element {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- const obj = { tag: 'span', content: [] as any[], class: null };
+ const obj: SimpleQElement = { tag: 'span', content: [] as (SimpleQElement | string)[], class: null };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- const stack: any[] = [ obj ];
+ const stack: SimpleQElement[] = [ obj ];
let idx = 0;
function makeEscape(regex: RegExp, len: number, str: string): { matcher: RegExp, response: ((i: number) => void)} { // function for readability
return {
matcher: regex,
response: (i: number) => {
- const top = stack[stack.length - 1];
+ const top = stack[stack.length - 1] as SimpleQElement;
if (idx != i) top.content.push(text.substring(idx, i));
top.content.push(str);
idx = i + len;
@@ -138,13 +146,14 @@ export default class ElementsUtil {
return {
matcher: regex,
response: (i: number) => {
- const top = stack[stack.length - 1];
+ const top = stack[stack.length - 1] as SimpleQElement;
if (idx != i) top.content.push(text.substring(idx, i));
if (top.class == cls) { // italic ends
// TODO: optimise out empty elements
stack.pop();
} else { // italic begins
- const obj = { tag: 'span', class: cls, content: [] }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const obj: SimpleQElement = { tag: 'span', class: cls, content: [] as any[] }
top.content.push(obj);
stack.push(obj);
}
@@ -175,13 +184,23 @@ export default class ElementsUtil {
}
if (!matched && idx != text.length) {
// Add any remaining content
- const top = stack[stack.length - 1];
+ const top = stack[stack.length - 1] as SimpleQElement;
top.content.push(text.substr(idx));
idx = text.length;
}
}
+
+ function createReactElement(qjsObj: SimpleQElement | string): JSX.Element | string {
+ if (typeof qjsObj === 'string') {
+ return qjsObj
+ } else {
+ const content = qjsObj.content.map(qjsElement => createReactElement(qjsElement));
+ return React.createElement(qjsObj.tag, { className: qjsObj.class }, content);
+ }
+
+ }
- return obj;
+ return createReactElement(obj) as JSX.Element; // since the first layer will always have a tag
}
// NOTE: both elements must be within the document or this function will not work
diff --git a/src/client/webapp/elements/require/react-helper.ts b/src/client/webapp/elements/require/react-helper.ts
index 7b41d4a..a206362 100644
--- a/src/client/webapp/elements/require/react-helper.ts
+++ b/src/client/webapp/elements/require/react-helper.ts
@@ -1,4 +1,3 @@
-import React from "react";
import ReactDOMServer from "react-dom/server";
import { ShouldNeverHappenError } from "../../data-types";
@@ -6,7 +5,7 @@ import { ShouldNeverHappenError } from "../../data-types";
export default class ReactHelper {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- static createElementFromJSX(element: React.ReactElement>): Element {
+ static createElementFromJSX(element: JSX.Element): Element {
// See also: https://www.codegrepper.com/code-examples/javascript/convert+a+string+to+html+element+in+js
const htmlString = ReactDOMServer.renderToStaticMarkup(element);
const parser = new DOMParser();
diff --git a/src/client/webapp/ui.ts b/src/client/webapp/ui.ts
index ac7ccfe..9e8e340 100644
--- a/src/client/webapp/ui.ts
+++ b/src/client/webapp/ui.ts
@@ -34,7 +34,7 @@ export default class UI {
public messagePairsGuild: CombinedGuild | null = null;
public messagePairsChannel: Channel | { id: string } | null = null;
- public messagePairs = new Map(); // messageId -> { message: Message, element: HTMLElement }
+ public messagePairs = new Map(); // messageId -> { message: Message, element: HTMLElement }
private document: Document;
private q: Q;
@@ -311,7 +311,7 @@ export default class UI {
}
}
- public async setChannelsErrorIndicator(guild: CombinedGuild, errorIndicatorElement: HTMLElement): Promise {
+ public async setChannelsErrorIndicator(guild: CombinedGuild, errorIndicatorElement: Element): Promise {
await this.lockChannels(guild, () => {
Q.clearChildren(this.q.$('#channel-list'));
this.q.$('#channel-list').appendChild(errorIndicatorElement);
@@ -381,7 +381,7 @@ export default class UI {
const newStyle = member.roleColor ? 'color: ' + member.roleColor : null;
const newName = member.displayName;
// the extra query selectors may be overkill
- for (const messageElement of this.q.$$(`.message[meta-member-id="${member.id}"]`)) {
+ for (const messageElement of this.q.$$(`.message[data-member-id="${member.id}"]`)) {
const nameElement = this.q.$$$_(messageElement, '.member-name');
if (nameElement) { // continued messages will still show up but need to be skipped
if (newStyle) nameElement.setAttribute('style', newStyle);
@@ -396,19 +396,19 @@ export default class UI {
await this.addMembers(guild, members, { clear: true });
}
- public async setMembersErrorIndicator(guild: CombinedGuild, errorIndicatorElement: HTMLElement): Promise {
+ public async setMembersErrorIndicator(guild: CombinedGuild, errorIndicatorElement: Element): Promise {
await this.lockMembers(guild, () => {
Q.clearChildren(this.q.$('#guild-members'));
this.q.$('#guild-members').appendChild(errorIndicatorElement);
});
}
- public getTopMessagePair(): { message: Message, element: HTMLElement } | null {
+ public getTopMessagePair(): { message: Message, element: Element } | null {
const element = this.q.$$('#channel-feed .message')[0];
return element && this.messagePairs.get(element.getAttribute('data-id')) || null;
}
- public getBottomMessagePair(): { message: Message, element: HTMLElement } | null {
+ public getBottomMessagePair(): { message: Message, element: Element } | null {
const messageElements = this.q.$$('#channel-feed .message');
const element = messageElements[messageElements.length - 1];
return element && this.messagePairs.get(element.getAttribute('data-id')) || null;
@@ -688,19 +688,19 @@ export default class UI {
}
}
- public async addMessagesErrorIndicatorBefore(guild: CombinedGuild, channel: Channel, errorIndicatorElement: HTMLElement): Promise {
+ public async addMessagesErrorIndicatorBefore(guild: CombinedGuild, channel: Channel, errorIndicatorElement: Element): Promise {
await this.lockMessages(guild, channel, () => {
this.q.$('#channel-feed').prepend(errorIndicatorElement);
});
}
- public async addMessagesErrorIndicatorAfter(guild: CombinedGuild, channel: Channel, errorIndicatorElement: HTMLElement): Promise {
+ public async addMessagesErrorIndicatorAfter(guild: CombinedGuild, channel: Channel, errorIndicatorElement: Element): Promise {
await this.lockMessages(guild, channel, () => {
this.q.$('#channel-feed').appendChild(errorIndicatorElement);
});
}
- public async setMessagesErrorIndicator(guild: CombinedGuild, channel: Channel | { id: string }, errorIndicatorElement: HTMLElement): Promise {
+ public async setMessagesErrorIndicator(guild: CombinedGuild, channel: Channel | { id: string }, errorIndicatorElement: Element): Promise {
await this.lockMessages(guild, channel, () => {
Q.clearChildren(this.q.$('#channel-feed'));
this.q.$('#channel-feed').appendChild(errorIndicatorElement);
diff --git a/src/client/webapp/util.ts b/src/client/webapp/util.ts
index 76d81e4..0cb80f7 100644
--- a/src/client/webapp/util.ts
+++ b/src/client/webapp/util.ts
@@ -14,7 +14,7 @@ import createErrorIndicator from './elements/error-indicator';
interface WithPotentialErrorParams {
taskFunc: (() => Promise),
- errorIndicatorAddFunc: ((element: HTMLElement) => Promise),
+ errorIndicatorAddFunc: ((element: Element) => Promise),
errorContainer: HTMLElement,
errorClasses?: string[],
errorMessage: string,