finish reactify add guild overlay

This commit is contained in:
Michael Peters 2021-12-08 23:55:05 -06:00
parent f57ef4848f
commit f87fdeff3c
8 changed files with 41 additions and 244 deletions

View File

@ -28,8 +28,8 @@ move:
clean:
mkdir -p ./dist
rm -r ./dist
rm -r ./db
rm -r ./dist || true
rm -r ./db || true
reset-server:
psql postgres postgres < ./src/server/sql/init.sql

View File

@ -5,12 +5,14 @@ const LOG = Logger.create(__filename, electronConsole);
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
import ReactDOM from "react-dom";
import ElementsUtil from '../require/elements-util';
interface OverlayProps {
document: Document;
children: React.ReactNode;
}
const Overlay: FC<OverlayProps> = (props: OverlayProps) => {
const { children } = props;
const { document, children } = props;
const node = useRef<HTMLDivElement | null>(null);
@ -23,12 +25,14 @@ const Overlay: FC<OverlayProps> = (props: OverlayProps) => {
setMouseUpInChild(false);
return;
}
if (node.current) {
ReactDOM.unmountComponentAtNode(node.current.parentElement as Element);
}
// TODO: This is pretty messy and should be re-thought when we full-convert to react.
// The window event listener could be re-called if we call closeReactOverlay elsewhere...
if (keyDownHandler) {
window.removeEventListener('keydown', keyDownHandler);
}
if (node.current) {
ElementsUtil.closeReactOverlay(document);
}
// otherwise, this isn't in the DOM anyway
};

View File

@ -36,8 +36,6 @@ const SubmitOverlayLower: FC<SubmitOverlayLowerProps> = (props: SubmitOverlayLow
if (!succeeded) {
await ElementsUtil.delayToggleState(setShaking, 400);
}
}
const isEmpty = useMemo(() => errorMessage === null, [ errorMessage ]);

View File

@ -7,7 +7,6 @@ import * as fs from 'fs/promises';
import ElementsUtil from './require/elements-util';
import createAddGuildOverlay, { IAddGuildData } from './overlay-add-guild';
import Q from '../q-module';
import UI from '../ui';
import GuildsManager from '../guilds-manager';
@ -54,7 +53,7 @@ export default function bindAddGuildEvents(document: Document, q: Q, ui: UI, gui
LOG.debug('bad guild data:', { addGuildData, fileText })
throw new Error('bad guild data');
}
ElementsUtil.presentReactOverlay(document, <AddGuildOverlay guildsManager={guildsManager} addGuildData={addGuildData} />)
ElementsUtil.presentReactOverlay(document, <AddGuildOverlay document={document} ui={ui} guildsManager={guildsManager} addGuildData={addGuildData} />)
// const overlayElement = createAddGuildOverlay(document, q, ui, guildsManager, addGuildData as IAddGuildData);
// document.body.appendChild(overlayElement);
} catch (e: unknown) {

View File

@ -1,228 +0,0 @@
import * as electronRemote from '@electron/remote';
const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import * as fs from 'fs/promises';
import * as path from 'path';
import moment from 'moment';
import * as FileType from 'file-type';
import Globals from '../globals';
import BaseElements from './require/base-elements';
import ElementsUtil from './require/elements-util';
import Q from '../q-module';
import UI from '../ui';
import GuildsManager from '../guilds-manager';
import CombinedGuild from '../guild-combined';
import React from 'react';
import ReactHelper from './require/react-helper';
export interface IAddGuildData {
name: string,
url: string,
cert: string,
token: string,
expires: number,
iconSrc: string
}
function getExampleDisplayName(): string {
const names = [
'gamer69',
'exdenoccp',
'TiggerEliminator',
'FreshDingus',
'Wonky Experiment',
'TheLegend27'
];
return names[Math.floor(Math.random() * names.length)] as string;
}
function getExampleAvatarPath(): string {
const paths = [ // these are relative to the working directory
path.join(__dirname, '../img/default-avatars/avatar-airstrip.png'),
path.join(__dirname, '../img/default-avatars/avatar-building.png'),
path.join(__dirname, '../img/default-avatars/avatar-frog.png'),
path.join(__dirname, '../img/default-avatars/avatar-sun.png')
];
return paths[Math.floor(Math.random() * paths.length)] as string;
}
export default function createAddGuildOverlay(document: Document, q: Q, ui: UI, guildsManager: GuildsManager, addGuildData: IAddGuildData): HTMLElement {
const expired = addGuildData.expires < new Date().getTime();
let displayName = getExampleDisplayName();
const avatarPath = getExampleAvatarPath();
//LOG.debug('addguilddata:', { addGuildData });
const element = BaseElements.createOverlay(document, (
<div className="content add-guild">
<div className="preview">
<img className="icon" src={addGuildData.iconSrc} alt="icon"></img>
<div>
<div className="name">{addGuildData.name}</div>
<div className="url">{addGuildData.url}</div>
<div className="expires">{(expired ? 'Invite Expired ' : 'Invite Expires ') + moment(addGuildData.expires).fromNow()}</div>
</div>
</div>
<div className="divider"></div>
<div className="message-preview message">
<div className="member-avatar"><img src="./img/loading.svg" alt="avatar"></img></div>
<div className="right">
<div className="header">
<div className="member-name">{displayName}</div>
<div className="timestamp">{moment().calendar(ElementsUtil.calendarFormats)}</div>
</div>
<div className="content text">What's up, gamers?</div>
</div>
</div>
<div className="display-name-input" placeholder="Display Name" spellCheck="false"
contentEditable={'plaintext-only' as unknown as boolean /* React doesn't have plaintext-only in its types (https://github.com/DefinitelyTyped/DefinitelyTyped/pull/54779) */}></div>
<div className="avatar-input">
<label className="avatar-upload-label button">
Select Avatar
<input className="avatar-upload" type="file" accept=".png,.jpg.,.jpeg" style={{ display: 'none' }}></input>
</label>
</div>
<div className="lower">
<div className="error"></div>
<div className="buttons">
<div className="button submit">Add Guild</div>
</div>
</div>
</div>
));
let avatarBuff: Buffer | null;
let defaultAvatarBuff: Buffer | null;
(async () => {
try {
defaultAvatarBuff = await fs.readFile(avatarPath); // TODO: on error
const src = await ElementsUtil.getImageBufferSrc(defaultAvatarBuff);
(q.$$$(element, '.member-avatar img') as HTMLImageElement).src = src;
avatarBuff = defaultAvatarBuff;
} catch (e) {
LOG.error('error setting default avatar', e);
(q.$$$(element, '.member-avatar img') as HTMLImageElement).src = './img/error.png';
defaultAvatarBuff = null;
avatarBuff = null;
}
})();
q.$$$(element, '.avatar-upload').addEventListener('change', async () => {
q.$$$(element, '.error').innerText = '';
const files = (q.$$$(element, '.avatar-upload') as HTMLInputElement).files;
if (!files || files.length == 0) {
avatarBuff = null;
return;
}
const file = files[0] as File;
if (file.size > Globals.MAX_AVATAR_SIZE) {
q.$$$(element, '.error').innerText = 'Image too large. Max size: ' + ElementsUtil.humanSize(Globals.MAX_AVATAR_SIZE);
await ElementsUtil.shakeElement(q.$$$(element, '.avatar-upload-label'), 400);
(q.$$$(element, '.avatar-upload') as HTMLInputElement).value = '';
avatarBuff = null;
return;
}
const buff = Buffer.from(await file.arrayBuffer());
const typeResult = await FileType.fromBuffer(buff);
if (!typeResult || !['image/png', 'image/jpeg', 'image/jpg'].includes(typeResult.mime)) {
q.$$$(element, '.error').innerText = 'Invalid avatar image (png/jpg only)';
await ElementsUtil.shakeElement(q.$$$(element, '.avatar-upload-label'), 400);
avatarBuff = null;
return;
}
try {
const src = await ElementsUtil.getImageBufferSrc(buff);
(q.$$$(element, '.member-avatar img') as HTMLImageElement).src = src;
avatarBuff = buff;
} catch (e) {
LOG.warn('unable to create image src from buffer', e);
q.$$$(element, '.error').innerText = 'Invalid avatar image';
await ElementsUtil.shakeElement(q.$$$(element, '.avatar-upload-label'), 400);
(q.$$$(element, '.avatar-upload') as HTMLInputElement).value = '';
avatarBuff = null;
return;
}
});
q.$$$(element, '.display-name-input').addEventListener('keydown', (e) => {
if (e.key == 'Enter') {
e.preventDefault();
if (e.shiftKey) return; // since the shift key makes newlines other places
q.$$$(element, '.button.submit').click();
}
});
q.$$$(element, '.display-name-input').addEventListener('input', () => {
q.$$$(element, '.member-name').innerText = q.$$$(element, '.display-name-input').innerText;
});
let submitting = false;
q.$$$(element, '.button.submit').addEventListener('click', async () => {
if (submitting) return;
submitting = true;
displayName = q.$$$(element, '.display-name-input').innerText;
q.$$$(element, '.display-name-input').removeAttribute('contenteditable');
let newGuild: CombinedGuild | null = null;
if (addGuildData == null) {
q.$$$(element, '.error').innerText = 'Very bad guild file';
q.$$$(element, '.submit').innerText = 'Try Again';
await ElementsUtil.shakeElement(q.$$$(element, '.submit'), 400);
} else if (addGuildData.expires < new Date().getTime()) {
q.$$$(element, '.error').innerText = 'Token expired';
q.$$$(element, '.submit').innerText = 'Try Again';
await ElementsUtil.shakeElement(q.$$$(element, '.submit'), 400);
} else if (displayName.length > Globals.MAX_DISPLAY_NAME_LENGTH) {
q.$$$(element, '.error').innerText = 'Display name too long: ' + displayName.length + ' > ' + Globals.MAX_DISPLAY_NAME_LENGTH;
q.$$$(element, '.submit').innerText = 'Try Again';
await ElementsUtil.shakeElement(q.$$$(element, '.submit'), 400);
} else if (displayName.length == 0) {
q.$$$(element, '.error').innerText = 'Display name is empty';
q.$$$(element, '.submit').innerText = 'Try Again';
await ElementsUtil.shakeElement(q.$$$(element, '.submit'), 400);
} else if (avatarBuff === null) {
q.$$$(element, '.error').innerText = 'Unable to parse avatar';
q.$$$(element, '.submit').innerText = 'Try Again';
await ElementsUtil.shakeElement(q.$$$(element, '.submit'), 400);
} else { // NOTE: Avatar size is checked above
q.$$$(element, '.submit').innerText = 'Registering...';
try {
newGuild = await guildsManager.addNewGuild(addGuildData, displayName, avatarBuff);
} catch (e: unknown) {
LOG.warn('error adding new guild: ' + (e as Error).message, e); // explicitly not printing stack trace here
q.$$$(element, '.error').innerText = (e as Error).message;
q.$$$(element, '.submit').innerText = 'Try Again';
await ElementsUtil.shakeElement(q.$$$(element, '.submit'), 400);
newGuild = null;
}
}
if (newGuild !== null) {
const guildElement = await ui.addGuild(guildsManager, newGuild);
element.removeSelf(); // close the overlay since we got a new guild
guildElement.click(); // click on the new guild
}
q.$$$(element, '.display-name-input').setAttribute('contenteditable', 'plaintext-only');
q.$$$(element, '.display-name-input').focus();
submitting = false;
});
return element;
}

View File

@ -5,7 +5,6 @@ const LOG = Logger.create(__filename, electronConsole);
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import GuildsManager from '../../guilds-manager';
import { IAddGuildData } from '../overlay-add-guild';
import moment from 'moment';
import TextInput from '../components/input-text';
import ImageEditInput from '../components/input-image-edit';
@ -14,6 +13,18 @@ import SubmitOverlayLower from '../components/submit-overlay-lower';
import path from 'path';
import fs from 'fs/promises';
import Util from '../../util';
import UI from '../../ui';
import CombinedGuild from '../../guild-combined';
import ElementsUtil from '../require/elements-util';
export interface IAddGuildData {
name: string,
url: string,
cert: string,
token: string,
expires: number,
iconSrc: string
}
function getExampleDisplayName(): string {
const names = [
@ -38,12 +49,14 @@ function getExampleAvatarPath(): string {
}
export interface AddGuildOverlayProps {
document: Document;
ui: UI;
guildsManager: GuildsManager;
addGuildData: IAddGuildData;
}
const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps) => {
const { guildsManager, addGuildData } = props;
const { document, ui, guildsManager, addGuildData } = props;
const expired = addGuildData.expires < new Date().getTime();
const exampleDisplayName = useMemo(() => getExampleDisplayName(), []);
@ -97,10 +110,11 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
setAddGuildFailedMessage('token expired');
return false;
}
let newGuild: CombinedGuild;
try {
await guildsManager.addNewGuild(addGuildData, displayName, avatarBuff);
newGuild = await guildsManager.addNewGuild(addGuildData, displayName, avatarBuff);
setAddGuildFailedMessage(null);
return true;
} catch (e: unknown) {
LOG.error('error adding new guild', e);
if (e instanceof Error) {
@ -110,6 +124,12 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
}
return false;
}
const guildElement = await ui.addGuild(guildsManager, newGuild);
ElementsUtil.closeReactOverlay(document);
guildElement.click();
return true;
}, [ displayName, avatarBuff, displayNameInputValid, avatarInputValid ]);

View File

@ -339,10 +339,14 @@ export default class ElementsUtil {
}
static presentReactOverlay(document: Document, content: JSX.Element) {
const overlay = <Overlay>{content}</Overlay>;
const overlay = <Overlay document={document}>{content}</Overlay>;
ReactDOM.render(overlay, document.querySelector('#react-overlays'));
}
static closeReactOverlay(document: Document) {
ReactDOM.unmountComponentAtNode(document.querySelector('#react-overlays') as HTMLElement);
}
static bindHoverableContextElement(
hoverElement: HTMLElement,
contextElement: IHTMLElementWithRemovalType,

View File

@ -11,7 +11,7 @@ import * as socketio from 'socket.io-client';
import * as crypto from 'crypto';
import { Changes, Channel, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, SocketConfig, Token } from './data-types';
import { IAddGuildData } from './elements/overlay-add-guild';
import { IAddGuildData } from './elements/overlays/overlay-add-guild';
import { EventEmitter } from 'tsee';
import CombinedGuild from './guild-combined';
import PersonalDB from './personal-db';