finish reactify add guild overlay
This commit is contained in:
parent
f57ef4848f
commit
f87fdeff3c
4
makefile
4
makefile
@ -28,8 +28,8 @@ move:
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
mkdir -p ./dist
|
mkdir -p ./dist
|
||||||
rm -r ./dist
|
rm -r ./dist || true
|
||||||
rm -r ./db
|
rm -r ./db || true
|
||||||
|
|
||||||
reset-server:
|
reset-server:
|
||||||
psql postgres postgres < ./src/server/sql/init.sql
|
psql postgres postgres < ./src/server/sql/init.sql
|
||||||
|
@ -5,12 +5,14 @@ const LOG = Logger.create(__filename, electronConsole);
|
|||||||
|
|
||||||
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
import ElementsUtil from '../require/elements-util';
|
||||||
|
|
||||||
interface OverlayProps {
|
interface OverlayProps {
|
||||||
|
document: Document;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
const Overlay: FC<OverlayProps> = (props: OverlayProps) => {
|
const Overlay: FC<OverlayProps> = (props: OverlayProps) => {
|
||||||
const { children } = props;
|
const { document, children } = props;
|
||||||
|
|
||||||
const node = useRef<HTMLDivElement | null>(null);
|
const node = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
@ -23,12 +25,14 @@ const Overlay: FC<OverlayProps> = (props: OverlayProps) => {
|
|||||||
setMouseUpInChild(false);
|
setMouseUpInChild(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (node.current) {
|
// TODO: This is pretty messy and should be re-thought when we full-convert to react.
|
||||||
ReactDOM.unmountComponentAtNode(node.current.parentElement as Element);
|
// The window event listener could be re-called if we call closeReactOverlay elsewhere...
|
||||||
}
|
|
||||||
if (keyDownHandler) {
|
if (keyDownHandler) {
|
||||||
window.removeEventListener('keydown', keyDownHandler);
|
window.removeEventListener('keydown', keyDownHandler);
|
||||||
}
|
}
|
||||||
|
if (node.current) {
|
||||||
|
ElementsUtil.closeReactOverlay(document);
|
||||||
|
}
|
||||||
// otherwise, this isn't in the DOM anyway
|
// otherwise, this isn't in the DOM anyway
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,8 +36,6 @@ const SubmitOverlayLower: FC<SubmitOverlayLowerProps> = (props: SubmitOverlayLow
|
|||||||
if (!succeeded) {
|
if (!succeeded) {
|
||||||
await ElementsUtil.delayToggleState(setShaking, 400);
|
await ElementsUtil.delayToggleState(setShaking, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEmpty = useMemo(() => errorMessage === null, [ errorMessage ]);
|
const isEmpty = useMemo(() => errorMessage === null, [ errorMessage ]);
|
||||||
|
@ -7,7 +7,6 @@ import * as fs from 'fs/promises';
|
|||||||
|
|
||||||
import ElementsUtil from './require/elements-util';
|
import ElementsUtil from './require/elements-util';
|
||||||
|
|
||||||
import createAddGuildOverlay, { IAddGuildData } from './overlay-add-guild';
|
|
||||||
import Q from '../q-module';
|
import Q from '../q-module';
|
||||||
import UI from '../ui';
|
import UI from '../ui';
|
||||||
import GuildsManager from '../guilds-manager';
|
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 })
|
LOG.debug('bad guild data:', { addGuildData, fileText })
|
||||||
throw new Error('bad guild data');
|
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);
|
// const overlayElement = createAddGuildOverlay(document, q, ui, guildsManager, addGuildData as IAddGuildData);
|
||||||
// document.body.appendChild(overlayElement);
|
// document.body.appendChild(overlayElement);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -5,7 +5,6 @@ const LOG = Logger.create(__filename, electronConsole);
|
|||||||
|
|
||||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import GuildsManager from '../../guilds-manager';
|
import GuildsManager from '../../guilds-manager';
|
||||||
import { IAddGuildData } from '../overlay-add-guild';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import TextInput from '../components/input-text';
|
import TextInput from '../components/input-text';
|
||||||
import ImageEditInput from '../components/input-image-edit';
|
import ImageEditInput from '../components/input-image-edit';
|
||||||
@ -14,6 +13,18 @@ import SubmitOverlayLower from '../components/submit-overlay-lower';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import Util from '../../util';
|
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 {
|
function getExampleDisplayName(): string {
|
||||||
const names = [
|
const names = [
|
||||||
@ -38,12 +49,14 @@ function getExampleAvatarPath(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AddGuildOverlayProps {
|
export interface AddGuildOverlayProps {
|
||||||
|
document: Document;
|
||||||
|
ui: UI;
|
||||||
guildsManager: GuildsManager;
|
guildsManager: GuildsManager;
|
||||||
addGuildData: IAddGuildData;
|
addGuildData: IAddGuildData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps) => {
|
const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps) => {
|
||||||
const { guildsManager, addGuildData } = props;
|
const { document, ui, guildsManager, addGuildData } = props;
|
||||||
|
|
||||||
const expired = addGuildData.expires < new Date().getTime();
|
const expired = addGuildData.expires < new Date().getTime();
|
||||||
const exampleDisplayName = useMemo(() => getExampleDisplayName(), []);
|
const exampleDisplayName = useMemo(() => getExampleDisplayName(), []);
|
||||||
@ -97,10 +110,11 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
|
|||||||
setAddGuildFailedMessage('token expired');
|
setAddGuildFailedMessage('token expired');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let newGuild: CombinedGuild;
|
||||||
try {
|
try {
|
||||||
await guildsManager.addNewGuild(addGuildData, displayName, avatarBuff);
|
newGuild = await guildsManager.addNewGuild(addGuildData, displayName, avatarBuff);
|
||||||
setAddGuildFailedMessage(null);
|
setAddGuildFailedMessage(null);
|
||||||
return true;
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
LOG.error('error adding new guild', e);
|
LOG.error('error adding new guild', e);
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
@ -111,6 +125,12 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const guildElement = await ui.addGuild(guildsManager, newGuild);
|
||||||
|
ElementsUtil.closeReactOverlay(document);
|
||||||
|
guildElement.click();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
}, [ displayName, avatarBuff, displayNameInputValid, avatarInputValid ]);
|
}, [ displayName, avatarBuff, displayNameInputValid, avatarInputValid ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -339,10 +339,14 @@ export default class ElementsUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static presentReactOverlay(document: Document, content: JSX.Element) {
|
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'));
|
ReactDOM.render(overlay, document.querySelector('#react-overlays'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static closeReactOverlay(document: Document) {
|
||||||
|
ReactDOM.unmountComponentAtNode(document.querySelector('#react-overlays') as HTMLElement);
|
||||||
|
}
|
||||||
|
|
||||||
static bindHoverableContextElement(
|
static bindHoverableContextElement(
|
||||||
hoverElement: HTMLElement,
|
hoverElement: HTMLElement,
|
||||||
contextElement: IHTMLElementWithRemovalType,
|
contextElement: IHTMLElementWithRemovalType,
|
||||||
|
@ -11,7 +11,7 @@ import * as socketio from 'socket.io-client';
|
|||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
import { Changes, Channel, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, SocketConfig, Token } from './data-types';
|
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 { EventEmitter } from 'tsee';
|
||||||
import CombinedGuild from './guild-combined';
|
import CombinedGuild from './guild-combined';
|
||||||
import PersonalDB from './personal-db';
|
import PersonalDB from './personal-db';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user