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 * as 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 { $, $$, $$$, $$$$ } from './require/q-module'; import IState from './require/elements-state'; import ClientController from '../client-controller'; export interface IAddServerData { name: string, url: string, cert: string, token: string, expires: number, iconSrc: string } function getExampleDisplayName(): string { let names = [ 'gamer69', 'exdenoccp', 'TiggerEliminator', 'FreshDingus', 'Wonky Experiment', 'TheLegend27' ]; return names[Math.floor(Math.random() * names.length)]; } function getExampleAvatarPath(): string { let 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)]; } export default function createAddServerOverlay(state: IState, addServerData: IAddServerData): HTMLElement { const { document, controller, ui } = state; $.setDocument(document); let expired = addServerData.expires < new Date().getTime(); let displayName = getExampleDisplayName(); let avatarPath = getExampleAvatarPath(); //LOG.debug('addserverdata:', { addServerData }); let element = BaseElements.createOverlay(document, { class: 'content add-server', content: [ { class: 'preview', content: [ { tag: 'img', class: 'icon', src: addServerData.iconSrc, alt: 'icon' }, { content: [ { class: 'name', content: addServerData.name }, { class: 'url', content: addServerData.url }, { class: 'expires', content: (expired ? 'Invite Expired ' : 'Invite Expires ') + moment(addServerData.expires).fromNow() } ] } ] }, { class: 'divider' }, { class: 'message-preview message', content: [ { class: 'member-avatar', content: { tag: 'img', src: './img/loading.svg', alt: 'avatar' } }, { class: 'right', content: [ { class: 'header', content: [ { class: 'member-name', content: displayName }, { class: 'timestamp', content: moment().calendar(ElementsUtil.calendarFormats) } ] }, { class: 'content text', content: 'Example Message' } ] } ] }, { class: 'display-name-input', placeholder: 'Display Name', spellcheck: 'false', contenteditable: 'plaintext-only', content: displayName }, { class: 'avatar-input', content: [ { tag: 'label', class: 'avatar-upload-label button', content: [ 'Select Avatar', { class: 'avatar-upload', tag: 'input', type: 'file', accept: '.png,.jpg,.jpeg', style: 'display: none;' }, ] } ] }, { class: 'lower', content: [ { class: 'error' }, { class: 'buttons', content: [ { class: 'button submit', content: 'Add Server' } ] } ] } ] }); let avatarBuff: Buffer | null; let defaultAvatarBuff: Buffer | null; (async () => { try { defaultAvatarBuff = await fs.readFile(avatarPath); // TODO: on error let src = await ElementsUtil.getImageBufferSrc(defaultAvatarBuff); ($$$(element, '.member-avatar img') as HTMLImageElement).src = src; avatarBuff = defaultAvatarBuff; } catch (e) { LOG.error('error setting default avatar', e); ($$$(element, '.member-avatar img') as HTMLImageElement).src = './img/error.png'; defaultAvatarBuff = null; avatarBuff = null; } })(); $$$(element, '.avatar-upload').addEventListener('change', async () => { $$$(element, '.error').innerText = ''; let files = ($$$(element, '.avatar-upload') as HTMLInputElement).files; if (!files || files.length == 0) { avatarBuff = null; return; } let file = files[0]; if (file.size > Globals.MAX_AVATAR_SIZE) { $$$(element, '.error').innerText = 'Image too large. Max size: ' + ElementsUtil.humanSize(Globals.MAX_AVATAR_SIZE); await ElementsUtil.shakeElement($$$(element, '.avatar-upload-label'), 400); ($$$(element, '.avatar-upload') as HTMLInputElement).value = ''; avatarBuff = null; return; } let buff = Buffer.from(await file.arrayBuffer()); let typeResult = await FileType.fromBuffer(buff); if (!typeResult || !['image/png', 'image/jpeg', 'image/jpg'].includes(typeResult.mime)) { $$$(element, '.error').innerText = 'Invalid avatar image (png/jpg only)'; await ElementsUtil.shakeElement($$$(element, '.avatar-upload-label'), 400); avatarBuff = null; return; } try { let src = await ElementsUtil.getImageBufferSrc(buff); ($$$(element, '.member-avatar img') as HTMLImageElement).src = src; avatarBuff = buff; } catch (e) { LOG.warn('unable to create image src from buffer', e); $$$(element, '.error').innerText = 'Invalid avatar image'; await ElementsUtil.shakeElement($$$(element, '.avatar-upload-label'), 400); ($$$(element, '.avatar-upload') as HTMLInputElement).value = ''; avatarBuff = null; return; } }); $$$(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 $$$(element, '.button.submit').click(); } }); $$$(element, '.display-name-input').addEventListener('input', () => { $$$(element, '.member-name').innerText = $$$(element, '.display-name-input').innerText; }); let submitting = false; $$$(element, '.button.submit').addEventListener('click', async () => { if (submitting) return; submitting = true; displayName = $$$(element, '.display-name-input').innerText; $$$(element, '.display-name-input').removeAttribute('contenteditable'); let newServer: ClientController | null = null; if (addServerData == null) { $$$(element, '.error').innerText = 'Very bad server file'; $$$(element, '.submit').innerText = 'Try Again'; await ElementsUtil.shakeElement($$$(element, '.submit'), 400); } else if (addServerData.expires < new Date().getTime()) { $$$(element, '.error').innerText = 'Token expired'; $$$(element, '.submit').innerText = 'Try Again'; await ElementsUtil.shakeElement($$$(element, '.submit'), 400); } else if (displayName.length > Globals.MAX_DISPLAY_NAME_LENGTH) { $$$(element, '.error').innerText = 'Display name too long: ' + displayName.length + ' > ' + Globals.MAX_DISPLAY_NAME_LENGTH; $$$(element, '.submit').innerText = 'Try Again'; await ElementsUtil.shakeElement($$$(element, '.submit'), 400); } else if (displayName.length == 0) { $$$(element, '.error').innerText = 'Display name is empty'; $$$(element, '.submit').innerText = 'Try Again'; await ElementsUtil.shakeElement($$$(element, '.submit'), 400); } else if (avatarBuff === null) { $$$(element, '.error').innerText = 'Unable to parse avatar'; $$$(element, '.submit').innerText = 'Try Again'; await ElementsUtil.shakeElement($$$(element, '.submit'), 400); } else { // NOTE: Avatar size is checked above $$$(element, '.submit').innerText = 'Registering...'; try { newServer = await controller.addNewServer(addServerData, displayName, avatarBuff); } catch (e) { LOG.warn('error adding new server: ' + e.message, e); // explicitly not printing stack trace here $$$(element, '.error').innerText = e.message; $$$(element, '.submit').innerText = 'Try Again'; await ElementsUtil.shakeElement($$$(element, '.submit'), 400); newServer = null; } } if (newServer !== null) { let serverElement = await ui.addServer(newServer); element.removeSelf(); // close the overlay since we got a new server serverElement.click(); // click on the new server } $$$(element, '.display-name-input').setAttribute('contenteditable', 'plaintext-only'); $$$(element, '.display-name-input').focus(); submitting = false; }); return element; }