diff --git a/makefile b/makefile index 2585cc8..0058b20 100644 --- a/makefile +++ b/makefile @@ -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 diff --git a/src/client/webapp/elements/components/overlay.tsx b/src/client/webapp/elements/components/overlay.tsx index fdacaac..e591a8f 100644 --- a/src/client/webapp/elements/components/overlay.tsx +++ b/src/client/webapp/elements/components/overlay.tsx @@ -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 = (props: OverlayProps) => { - const { children } = props; + const { document, children } = props; const node = useRef(null); @@ -23,12 +25,14 @@ const Overlay: FC = (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 }; diff --git a/src/client/webapp/elements/components/submit-overlay-lower.tsx b/src/client/webapp/elements/components/submit-overlay-lower.tsx index 0ea834a..da2cd6d 100644 --- a/src/client/webapp/elements/components/submit-overlay-lower.tsx +++ b/src/client/webapp/elements/components/submit-overlay-lower.tsx @@ -36,8 +36,6 @@ const SubmitOverlayLower: FC = (props: SubmitOverlayLow if (!succeeded) { await ElementsUtil.delayToggleState(setShaking, 400); } - - } const isEmpty = useMemo(() => errorMessage === null, [ errorMessage ]); diff --git a/src/client/webapp/elements/events-add-guild.tsx b/src/client/webapp/elements/events-add-guild.tsx index e851c49..7f5e6aa 100644 --- a/src/client/webapp/elements/events-add-guild.tsx +++ b/src/client/webapp/elements/events-add-guild.tsx @@ -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, ) + ElementsUtil.presentReactOverlay(document, ) // const overlayElement = createAddGuildOverlay(document, q, ui, guildsManager, addGuildData as IAddGuildData); // document.body.appendChild(overlayElement); } catch (e: unknown) { diff --git a/src/client/webapp/elements/overlay-add-guild.tsx b/src/client/webapp/elements/overlay-add-guild.tsx deleted file mode 100644 index ea37fa0..0000000 --- a/src/client/webapp/elements/overlay-add-guild.tsx +++ /dev/null @@ -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, ( -
-
- icon -
-
{addGuildData.name}
-
{addGuildData.url}
-
{(expired ? 'Invite Expired ' : 'Invite Expires ') + moment(addGuildData.expires).fromNow()}
-
-
-
-
-
avatar
-
-
-
{displayName}
-
{moment().calendar(ElementsUtil.calendarFormats)}
-
-
What's up, gamers?
-
-
-
-
- -
-
-
-
-
Add Guild
-
-
-
- )); - - 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; -} diff --git a/src/client/webapp/elements/overlays/overlay-add-guild.tsx b/src/client/webapp/elements/overlays/overlay-add-guild.tsx index 9dcd0aa..67d8b3b 100644 --- a/src/client/webapp/elements/overlays/overlay-add-guild.tsx +++ b/src/client/webapp/elements/overlays/overlay-add-guild.tsx @@ -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 = (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 = (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 = (props: AddGuildOverlayProps) } return false; } + + const guildElement = await ui.addGuild(guildsManager, newGuild); + ElementsUtil.closeReactOverlay(document); + guildElement.click(); + + return true; }, [ displayName, avatarBuff, displayNameInputValid, avatarInputValid ]); diff --git a/src/client/webapp/elements/require/elements-util.tsx b/src/client/webapp/elements/require/elements-util.tsx index dfb5e29..ed9fd16 100644 --- a/src/client/webapp/elements/require/elements-util.tsx +++ b/src/client/webapp/elements/require/elements-util.tsx @@ -339,10 +339,14 @@ export default class ElementsUtil { } static presentReactOverlay(document: Document, content: JSX.Element) { - const overlay = {content}; + const overlay = {content}; 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, diff --git a/src/client/webapp/guilds-manager.ts b/src/client/webapp/guilds-manager.ts index 5038160..53b9ed7 100644 --- a/src/client/webapp/guilds-manager.ts +++ b/src/client/webapp/guilds-manager.ts @@ -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';