further through .tsx conversion

This commit is contained in:
Michael Peters 2021-12-05 22:24:16 -06:00
parent 01da06b57b
commit 6979c450e6
20 changed files with 283 additions and 174 deletions

View File

@ -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[] = [
<div key="personalize" className="item personalize">
<div className="icon"><img src="./img/pencil-icon.png"></img></div>
<div>Personalize</div>
</div>,
<div key="spacer-1" className="item-spacer"></div>
];
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 => (
<div key={status} className={'item ' + status}>
<div className="status-circle"></div>
<div className="status-text">{status}</div>
</div>
)));
const element = BaseElements.createContextMenu(document, <div className="member-context">{content}</div>);
q.$$$(element, '.personalize').addEventListener('click', async () => {
element.removeSelf();

View File

@ -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(<div></div>);
}
// 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(
<div key="guild-settings" className="item guild-settings">
<div className="icon">{BaseElements.COG}</div>
<div>Guild Settings</div>
</div>
);
}
if (ui.activeConnection.privileges.includes('modify_channels')) {
if (ui.activeConnection.privileges.includes('modify_profile')) {
menuItems.push({ class: 'item-spacer' });
menuItems.push(<div key="spacer-1" className="item-spacer"></div>);
}
menuItems.push({ class: 'item create-channel', content: [
{ class: 'icon', content: BaseElements.Q_CREATE },
'Create Channel'
] });
menuItems.push(
<div key="create-channel" className="item create-channel">
<div className="icon">{BaseElements.CREATE}</div>
<div>Create Channel</div>
</div>
);
}
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(<div key="spacer-2" className="item-spacer"></div>);
}
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(
<div key="create-invite-token" className="item create-invite-token">
<div className="icon">{BaseElements.TOKEN}</div>
<div>Create Invite Token</div>
</div>
);
menuItems.push(
<div key="token-log" className="item token-log">
<div className="icon">{BaseElements.TOKEN}</div>
<div>Token Log</div>
</div>
);
}
const element = BaseElements.createContextMenu(document, {
class: 'guild-title-context', content: menuItems
});
const element = BaseElements.createContextMenu(document, (
<div className="guild-title-context">{menuItems}</div>
));
if (ui.activeConnection.privileges.includes('modify_profile')) {
q.$$$(element, '.item.guild-settings').addEventListener('click', async () => {

View File

@ -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, (
<div className="guild-context">
<div className="item red leave-guild">Leave Guild</div>
</div>
));
q.$$$(element, '.leave-guild').addEventListener('click', async () => {
element.removeSelf();

View File

@ -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, (
<div className="image">
<div className="item copy-image">Copy Image{isPreview ? ' Preview' : ''}</div>
<div className="item save-image">Save Image{isPreview ? ' Preview' : ''}</div>
</div>
));
q.$$$(contextMenu, '.copy-image').addEventListener('click', async () => {
q.$$$(contextMenu, '.copy-image').innerText = 'Copying...';
let nativeImage: electron.NativeImage;

View File

@ -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(
<div className={['error-indicator', ...classes].join(' ')}>
<img src="./img/error.png" alt="error"></img>
<div>
<div>{message}</div>
<div className="retry-button">Try Again</div>
</div>
</div>
);
const observer = new MutationObserver(() => {
if (element.parentElement == null) {

View File

@ -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' });
});

View File

@ -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(
<div className="guild" data-id={guild.id}>
<div className="pill"></div>
<img src="./img/loading.svg" alt="guild"></img>
</div>
) as HTMLElement;
// Hover over for name + connection info
(async () => {

View File

@ -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)) {

View File

@ -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(
<div className="message continued" data-id={message.id} data-member-id={message.member.id} data-guild-id={guild.id}>
<div className="timestamp">{moment(message.sent).format('HH:mm')}</div>
<div className="right">
<div className="content image" style={{width: message.previewWidth + 'px', height: message.previewHeight + 'px'}}>
<img src="./img/loading.svg" alt={message.resourceName}></img>
</div>
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
</div>
</div>
);
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;
}

View File

@ -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(
<div className="message" data-id={message.id} data-member-id={message.member.id} data-guild-id={guild.id}>
<div className="member-avatar">
<img src="./img/loading.svg" alt={memberInfo.displayName}></img>
</div>
<div className="right">
<div className="header">
<div className="member-name" style={nameStyle}>{memberInfo.displayName}</div>
<div className="timestamp">{moment(message.sent).calendar(ElementsUtil.calendarFormats)}</div>
</div>
<div className="content image" style={{width: message.previewWidth + 'px', height: message.previewHeight + 'px'}}>
<img src="./img/loading.svg" alt={message.resourceName}></img>
</div>
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
</div>
</div>
);
q.$$$(element, '.content.image').addEventListener('click', (e) => {
document.body.appendChild(createImageOverlay(document, q, guild, message.resourceId as string, message.resourceName as string));
});

View File

@ -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(
<div className="message continued" data-id={message.id} data-member-id={message.member.id} data-guild-id={guild.id}>
<div className="timestamp">{moment(message.sent).format('HH:mm')}</div>
<div className="right">
<div className="content resource">
<img className="icon" src="./img/file-icon.png" alt="file"></img>
<div className="text">
<div className="filename">{message.resourceName}</div>
<div className="download-status">Click to Download</div>
</div>
</div>
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
</div>
</div>
);
q.$$$(element, '.resource').addEventListener('click', ElementsUtil.createDownloadListener({
guild: guild, resourceId: message.resourceId, resourceName: message.resourceName,
downloadStartFunc: () => {

View File

@ -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(
<div className="message" data-id={message.id} data-member-id={message.member.id} data-guild-id={guild.id}>
<div className="member-avatar">
<img src="./img/loading.svg" alt={memberInfo.displayName}></img>
</div>
<div className="right">
<div className="header">
<div className="member-name" style={nameStyle}>{memberInfo.displayName}</div>
<div className="timestamp">{moment(message.sent).calendar(ElementsUtil.calendarFormats)}</div>
</div>
<div className="content resource">
<img className="icon" src="./img/file-icon.png" alt="file"></img>
<div className="text">
<div className="filename">{message.resourceName}</div>
<div className="download-status">Click to Download</div>
</div>
</div>
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
</div>
</div>
);
q.$$$(element, '.resource').addEventListener('click', ElementsUtil.createDownloadListener({
guild: guild, resourceId: message.resourceId, resourceName: message.resourceName,
downloadStartFunc: () => {

View File

@ -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;
}

View File

@ -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(
<div className="message continued" data-id={message.id} data-member-id={message.member.id} data-guild-id={guild.id}>
<div className="timestamp">{moment(message.sent).format('HH:mm')}</div>
<div className="right">
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
</div>
</div>
);
}

View File

@ -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(
<div className="message" data-id={message.id} data-member-id={message.member.id} data-guild-id={guild.id}>
<div className="member-avatar">
<img src="./img/loading.svg" alt={memberInfo.displayName}></img>
</div>
<div className="right">
<div className="header">
<div className="member-name" style={nameStyle}>{memberInfo.displayName}</div>
<div className="timestamp">{moment(message.sent).calendar(ElementsUtil.calendarFormats)}</div>
</div>
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
</div>
</div>
);
(async () => {
(q.$$$(element, '.member-avatar img') as HTMLImageElement).src =
await ElementsUtil.getImageBufferFromResourceFailSoftly(guild, memberInfo.avatarResourceId);

View File

@ -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 = (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="currentColor" fillRule="evenodd" clipRule="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"
></path>
</svg>
);
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 = (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="currentColor" fillRule="evenodd" clipRule="evenodd"
d="M 8 0 A 8 8 0 0 0 0 8 A 8 8 0 0 0 8 16 A 8 8 0 0 0 16 8 A 8 8 0 0 0 8 0 z M 7 4 L 9 4 L 9 7 L 12 7 L 12 9 L 9 9 L 9 12 L 7 12 L 7 9 L 4 9 L 4 7 L 7 7 L 7 4 z"
></path>
</svg>
);
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(
<div className="context">
<div className="menu">{content}</div>
</div>
) 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;
}

View File

@ -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 <span class="bold"/"italic"/"bold italic"/"underline"> spans to format the text (all in q.js element markup)
// creates <span class="bold"/"italic"/"bold italic"/"underline"> 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

View File

@ -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<any, string | React.JSXElementConstructor<any>>): 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();

View File

@ -34,7 +34,7 @@ export default class UI {
public messagePairsGuild: CombinedGuild | null = null;
public messagePairsChannel: Channel | { id: string } | null = null;
public messagePairs = new Map<string | null, { message: Message, element: HTMLElement }>(); // messageId -> { message: Message, element: HTMLElement }
public messagePairs = new Map<string | null, { message: Message, element: Element }>(); // 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<void> {
public async setChannelsErrorIndicator(guild: CombinedGuild, errorIndicatorElement: Element): Promise<void> {
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<void> {
public async setMembersErrorIndicator(guild: CombinedGuild, errorIndicatorElement: Element): Promise<void> {
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<void> {
public async addMessagesErrorIndicatorBefore(guild: CombinedGuild, channel: Channel, errorIndicatorElement: Element): Promise<void> {
await this.lockMessages(guild, channel, () => {
this.q.$('#channel-feed').prepend(errorIndicatorElement);
});
}
public async addMessagesErrorIndicatorAfter(guild: CombinedGuild, channel: Channel, errorIndicatorElement: HTMLElement): Promise<void> {
public async addMessagesErrorIndicatorAfter(guild: CombinedGuild, channel: Channel, errorIndicatorElement: Element): Promise<void> {
await this.lockMessages(guild, channel, () => {
this.q.$('#channel-feed').appendChild(errorIndicatorElement);
});
}
public async setMessagesErrorIndicator(guild: CombinedGuild, channel: Channel | { id: string }, errorIndicatorElement: HTMLElement): Promise<void> {
public async setMessagesErrorIndicator(guild: CombinedGuild, channel: Channel | { id: string }, errorIndicatorElement: Element): Promise<void> {
await this.lockMessages(guild, channel, () => {
Q.clearChildren(this.q.$('#channel-feed'));
this.q.$('#channel-feed').appendChild(errorIndicatorElement);

View File

@ -14,7 +14,7 @@ import createErrorIndicator from './elements/error-indicator';
interface WithPotentialErrorParams {
taskFunc: (() => Promise<void>),
errorIndicatorAddFunc: ((element: HTMLElement) => Promise<void>),
errorIndicatorAddFunc: ((element: Element) => Promise<void>),
errorContainer: HTMLElement,
errorClasses?: string[],
errorMessage: string,