From 87a4d8584fb5068946bee7de8224626193c69356 Mon Sep 17 00:00:00 2001 From: Michael Peters Date: Sun, 21 Nov 2021 20:47:29 -0600 Subject: [PATCH] combined guild mostly implemented --- client/main.ts | 2 +- client/webapp/elements/channel.ts | 10 +- client/webapp/elements/context-menu-conn.ts | 8 +- ...v-title.ts => context-menu-guild-title.ts} | 36 +-- ...text-menu-srv.ts => context-menu-guild.ts} | 12 +- client/webapp/elements/context-menu-img.ts | 6 +- ...ents-add-server.ts => events-add-guild.ts} | 49 ++- client/webapp/elements/events-connection.ts | 2 +- ...-server-title.ts => events-guild-title.ts} | 4 +- client/webapp/elements/events-text-input.ts | 8 +- client/webapp/elements/guild-list-guild.ts | 14 +- client/webapp/elements/member.ts | 4 +- client/webapp/elements/message.ts | 14 +- client/webapp/elements/msg-img-res-cont.ts | 14 +- client/webapp/elements/msg-img-res.ts | 16 +- client/webapp/elements/msg-res-cont.ts | 6 +- client/webapp/elements/msg-res.ts | 8 +- client/webapp/elements/msg-txt-cont.ts | 4 +- client/webapp/elements/msg-txt.ts | 6 +- ...lay-add-server.ts => overlay-add-guild.ts} | 47 ++- .../webapp/elements/overlay-create-channel.ts | 4 +- .../elements/overlay-create-invite-token.ts | 3 +- ...-settings.ts => overlay-guild-settings.ts} | 53 ++-- client/webapp/elements/overlay-image.ts | 14 +- .../webapp/elements/overlay-modify-channel.ts | 4 +- client/webapp/elements/overlay-personalize.ts | 8 +- client/webapp/elements/overlay-token-log.ts | 6 +- .../elements/overlay-upload-datatransfer.ts | 4 +- .../elements/overlay-upload-drop-target.ts | 4 +- client/webapp/elements/overlay-upload-path.ts | 4 +- .../webapp/elements/require/base-elements.ts | 8 +- .../webapp/elements/require/elements-util.ts | 18 +- client/webapp/entrypoint.ts | 257 +++++++++++++++ client/webapp/globals.ts | 11 +- client/webapp/guild-combined.ts | 66 +++- client/webapp/guild-socket.ts | 6 +- client/webapp/guild-types.ts | 6 +- .../{controller.ts => guilds-manager.ts} | 10 +- client/webapp/personal-db.ts | 2 +- client/webapp/script.ts | 292 ------------------ client/webapp/socket-verifier.ts | 6 +- client/webapp/ui.ts | 133 +++++--- 42 files changed, 626 insertions(+), 563 deletions(-) rename client/webapp/elements/{context-menu-srv-title.ts => context-menu-guild-title.ts} (70%) rename client/webapp/elements/{context-menu-srv.ts => context-menu-guild.ts} (74%) rename client/webapp/elements/{events-add-server.ts => events-add-guild.ts} (53%) rename client/webapp/elements/{events-server-title.ts => events-guild-title.ts} (84%) rename client/webapp/elements/{overlay-add-server.ts => overlay-add-guild.ts} (82%) rename client/webapp/elements/{overlay-server-settings.ts => overlay-guild-settings.ts} (65%) create mode 100644 client/webapp/entrypoint.ts rename client/webapp/{controller.ts => guilds-manager.ts} (90%) delete mode 100644 client/webapp/script.ts diff --git a/client/main.ts b/client/main.ts index 525400b..bcf4f3c 100644 --- a/client/main.ts +++ b/client/main.ts @@ -35,7 +35,7 @@ electronMain.initialize(); webPreferences: { //@ts-ignore enableRemoteModule is enabled with @electron/remote and not included in electron's typing enableRemoteModule: true, // so we can get console logs properly - preload: path.join(__dirname, 'webapp', 'script.js') + preload: path.join(__dirname, 'webapp', 'entrypoint.js') } }); diff --git a/client/webapp/elements/channel.ts b/client/webapp/elements/channel.ts index 89bc030..d0deaff 100644 --- a/client/webapp/elements/channel.ts +++ b/client/webapp/elements/channel.ts @@ -1,14 +1,14 @@ import ElementsUtil from './require/elements-util'; import BaseElements from './require/base-elements'; -import ClientController from '../client-controller'; import { Channel } from '../data-types'; import createModifyChannelOverlay from './overlay-modify-channel'; import UI from '../ui'; import Actions from '../actions'; import Q from '../q-module'; +import CombinedGuild from '../guild-combined'; export default function createChannel(document: Document, q: Q, ui: UI, guild: CombinedGuild, channel: Channel) { - let element = q.create({ class: 'channel text', 'meta-id': channel.id, 'meta-server-id': server.id, content: [ + let element = q.create({ class: 'channel text', 'meta-id': channel.id, 'meta-server-id': guild.id, content: [ // Scraped directly from discord (#) { class: 'icon', content: BaseElements.TEXT_CHANNEL_ICON }, { class: 'name', content: channel.name }, @@ -17,8 +17,8 @@ export default function createChannel(document: Document, q: Q, ui: UI, guild: C element.addEventListener('click', async () => { if (element.classList.contains('active')) return; - await ui.setActiveChannel(server, channel); - await Actions.fetchAndUpdateMessagesRecent(q, ui, server, channel); + await ui.setActiveChannel(guild, channel); + await Actions.fetchAndUpdateMessagesRecent(q, ui, guild, channel); q.$('#text-input').focus(); }); @@ -34,7 +34,7 @@ export default function createChannel(document: Document, q: Q, ui: UI, guild: C if (modifyContextElement.parentElement) { modifyContextElement.parentElement.removeChild(modifyContextElement); } - let modifyOverlay = createModifyChannelOverlay(document, q, server, channel); + let modifyOverlay = createModifyChannelOverlay(document, q, guild, channel); document.body.appendChild(modifyOverlay); q.$$$(modifyOverlay, '.text-input.channel-name').focus(); ElementsUtil.setCursorToEnd(q.$$$(modifyOverlay, '.text-input.channel-name')); diff --git a/client/webapp/elements/context-menu-conn.ts b/client/webapp/elements/context-menu-conn.ts index 52eb2ce..8bab4ca 100644 --- a/client/webapp/elements/context-menu-conn.ts +++ b/client/webapp/elements/context-menu-conn.ts @@ -1,10 +1,10 @@ import ElementsUtil from './require/elements-util.js'; import BaseElements from './require/base-elements.js'; -import ClientController from '../client-controller'; import createPersonalizeOverlay from './overlay-personalize.js'; import Q from '../q-module.js'; import UI from '../ui.js'; +import CombinedGuild from '../guild-combined.js'; export default function createConnectionContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild) { let statuses = [ 'online', 'away', 'busy', 'invisible' ]; @@ -26,7 +26,7 @@ export default function createConnectionContextMenu(document: Document, q: Q, ui q.$$$(element, '.personalize').addEventListener('click', async () => { element.removeSelf(); if (ui.activeConnection === null) return; - let overlayElement = createPersonalizeOverlay(document, q, server, ui.activeConnection); + let overlayElement = createPersonalizeOverlay(document, q, guild, ui.activeConnection); document.body.appendChild(overlayElement); q.$$$(overlayElement, '.text-input').focus(); ElementsUtil.setCursorToEnd(q.$$$(overlayElement, '.text-input')); @@ -35,9 +35,9 @@ export default function createConnectionContextMenu(document: Document, q: Q, ui for (let status of statuses) { q.$$$(element, '.' + status).addEventListener('click', async () => { element.removeSelf(); - let currentConnection = await server.fetchConnectionInfo(); + let currentConnection = await guild.fetchConnectionInfo(); if (status != currentConnection.status) { - await server.setStatus(status); + await guild.requestSetStatus(status); } }); } diff --git a/client/webapp/elements/context-menu-srv-title.ts b/client/webapp/elements/context-menu-guild-title.ts similarity index 70% rename from client/webapp/elements/context-menu-srv-title.ts rename to client/webapp/elements/context-menu-guild-title.ts index 53e9c7c..2c4a99e 100644 --- a/client/webapp/elements/context-menu-srv-title.ts +++ b/client/webapp/elements/context-menu-guild-title.ts @@ -5,29 +5,29 @@ const LOG = Logger.create(__filename, electronConsole); import ElementsUtil from './require/elements-util.js'; import BaseElements from './require/base-elements.js'; -import ClientController from '../client-controller'; -import { CacheServerData, ServerMetaData } from '../data-types'; +import { GuildMetadata } from '../data-types'; import Q from '../q-module'; import UI from '../ui'; import createErrorMessageOverlay from './overlay-error-message'; -import createServerSettingsOverlay from './overlay-server-settings'; +import createGuildSettingsOverlay from './overlay-guild-settings'; import createCreateInviteTokenOverlay from './overlay-create-invite-token'; import createCreateChannelOverlay from './overlay-create-channel'; import createTokenLogOverlay from './overlay-token-log'; +import CombinedGuild from '../guild-combined'; -export default function createServerTitleContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild): HTMLElement { +export default function createGuildTitleContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild): HTMLElement { if (ui.activeConnection === null) { - LOG.warn('no active connection when creating server title context menu'); + LOG.warn('no active connection when creating guild title context menu'); return q.create({}) as HTMLElement; } let menuItems: any[] = []; if (ui.activeConnection.privileges.includes('modify_profile')) { - menuItems.push({ class: 'item server-settings', content: [ + menuItems.push({ class: 'item guild-settings', content: [ { class: 'icon', content: BaseElements.COG }, - 'Server Settings' + 'Guild Settings' ] }); } @@ -59,23 +59,23 @@ export default function createServerTitleContextMenu(document: Document, q: Q, u } let element = BaseElements.createContextMenu(document, { - class: 'server-title-context', content: menuItems + class: 'guild-title-context', content: menuItems }); if (ui.activeConnection.privileges.includes('modify_profile')) { - q.$$$(element, '.item.server-settings').addEventListener('click', async () => { + q.$$$(element, '.item.guild-settings').addEventListener('click', async () => { element.removeSelf(); - let serverMeta: ServerMetaData | CacheServerData | null = null; + let guildMeta: GuildMetadata | null = null; try { - serverMeta = await server.grabMetadata(); + guildMeta = await guild.fetchMetadata(); } catch (e) { - LOG.error('error fetching server info', e); + LOG.error('error fetching guild info', e); } - if (serverMeta === null) { - let overlay = createErrorMessageOverlay(document, 'Error Opening Settings', 'Could not load server information'); + if (guildMeta === null) { + let overlay = createErrorMessageOverlay(document, 'Error Opening Settings', 'Could not load guild information'); document.body.appendChild(overlay); } else { - let overlay = createServerSettingsOverlay(document, q, server, serverMeta); + let overlay = createGuildSettingsOverlay(document, q, guild, guildMeta); document.body.appendChild(overlay); q.$$$(overlay, '.text-input').focus(); ElementsUtil.setCursorToEnd(q.$$$(overlay, '.text-input')); @@ -86,7 +86,7 @@ export default function createServerTitleContextMenu(document: Document, q: Q, u if (ui.activeConnection.privileges.includes('modify_channels')) { q.$$$(element, '.item.create-channel').addEventListener('click', () => { element.removeSelf(); - let overlay = createCreateChannelOverlay(document, q, server); + let overlay = createCreateChannelOverlay(document, q, guild); document.body.appendChild(overlay); q.$$$(overlay, '.text-input.channel-name').focus(); ElementsUtil.setCursorToEnd(q.$$$(overlay, '.text-input.channel-name')); @@ -96,13 +96,13 @@ export default function createServerTitleContextMenu(document: Document, q: Q, u if (ui.activeConnection.privileges.includes('modify_members')) { q.$$$(element, '.item.create-invite-token').addEventListener('click', () => { element.removeSelf(); - let overlay = createCreateInviteTokenOverlay(document, server); + let overlay = createCreateInviteTokenOverlay(document, guild); document.body.appendChild(overlay); //LOG.info('create invite token clicked'); }); q.$$$(element, '.item.token-log').addEventListener('click', () => { element.removeSelf(); - let overlay = createTokenLogOverlay(document, q, server); + let overlay = createTokenLogOverlay(document, q, guild); document.body.appendChild(overlay); }); } diff --git a/client/webapp/elements/context-menu-srv.ts b/client/webapp/elements/context-menu-guild.ts similarity index 74% rename from client/webapp/elements/context-menu-srv.ts rename to client/webapp/elements/context-menu-guild.ts index 29d290b..767d5f9 100644 --- a/client/webapp/elements/context-menu-srv.ts +++ b/client/webapp/elements/context-menu-guild.ts @@ -5,12 +5,12 @@ const LOG = Logger.create(__filename, electronConsole); import BaseElements from './require/base-elements.js'; -import ClientController from '../client-controller.js'; import Q from '../q-module'; import UI from '../ui'; -import Controller from '../controller'; +import GuildsManager from '../guilds-manager'; +import CombinedGuild from '../guild-combined'; -export default function createServerContextMenu(document: Document, q: Q, ui: UI, controller: Controller, guild: CombinedGuild) { +export default function createServerContextMenu(document: Document, q: Q, ui: UI, guildsManager: GuildsManager, guild: CombinedGuild) { let element = BaseElements.createContextMenu(document, { class: 'server-context', content: [ { class: 'item red leave-server', content: 'Leave Server' } @@ -19,9 +19,9 @@ export default function createServerContextMenu(document: Document, q: Q, ui: UI q.$$$(element, '.leave-server').addEventListener('click', async () => { element.removeSelf(); - await server.disconnect(); - await controller.removeServer(server); - await ui.removeServer(server); + guild.disconnect(); + await guildsManager.removeServer(guild); + await ui.removeGuild(guild); let firstServerElement = q.$_('#server-list .server'); if (firstServerElement) { firstServerElement.click(); diff --git a/client/webapp/elements/context-menu-img.ts b/client/webapp/elements/context-menu-img.ts index 9d2fcf5..b3c3f74 100644 --- a/client/webapp/elements/context-menu-img.ts +++ b/client/webapp/elements/context-menu-img.ts @@ -6,8 +6,8 @@ import * as electron from 'electron'; import BaseElements from './require/base-elements'; import ElementsUtil from './require/elements-util'; -import ClientController from '../client-controller'; import Q from '../q-module'; +import CombinedGuild from '../guild-combined'; export default function createImageContextMenu( document: Document, @@ -28,7 +28,7 @@ export default function createImageContextMenu( q.$$$(contextMenu, '.copy-image').innerText = 'Copying...'; let nativeImage: electron.NativeImage; if (mime != 'image/png' && mime != 'image/jpeg' && mime != 'image/jpg') { - // use sharp to convert to png since nativeImage only supports jpeg/png + // use sharp to convserver: serverrt to png since nativeImage only supports jpeg/png nativeImage = electron.nativeImage.createFromBuffer(await sharp(buffer).png().toBuffer()); } else { nativeImage = electron.nativeImage.createFromBuffer(buffer); @@ -37,7 +37,7 @@ export default function createImageContextMenu( q.$$$(contextMenu, '.copy-image').innerText = 'Copied to Clipboard'; }); q.$$$(contextMenu, '.save-image').addEventListener('click', ElementsUtil.createDownloadListener({ - downloadBuff: buffer, server: server, + downloadBuff: buffer, guild: guild, resourceName: path.basename(resourceName, '.' + ext) + (isPreview ? '-preview.' : '.') + ext, downloadStartFunc: () => {}, writeStartFunc: () => { q.$$$(contextMenu, '.save-image').innerText = 'Writing...'; }, diff --git a/client/webapp/elements/events-add-server.ts b/client/webapp/elements/events-add-guild.ts similarity index 53% rename from client/webapp/elements/events-add-server.ts rename to client/webapp/elements/events-add-guild.ts index 90f7eb0..bb03845 100644 --- a/client/webapp/elements/events-add-server.ts +++ b/client/webapp/elements/events-add-guild.ts @@ -7,25 +7,24 @@ import * as fs from 'fs/promises'; import ElementsUtil from './require/elements-util'; -import createAddServerOverlay, { IAddServerData } from './overlay-add-server'; +import createAddGuildOverlay, { IAddGuildData } from './overlay-add-guild'; import Q from '../q-module'; import UI from '../ui'; -import Controller from '../controller'; +import GuildsManager from '../guilds-manager'; import createErrorMessageOverlay from './overlay-error-message'; -import Actions from '../actions'; -export default function bindAddServerEvents(document: Document, q: Q, ui: UI, controller: Controller): void { +export default function bindAddGuildEvents(document: Document, q: Q, ui: UI, guildsManager: GuildsManager): void { let choosingFile = false; - q.$('#add-server').addEventListener('click', async () => { + q.$('#add-guild').addEventListener('click', async () => { if (choosingFile) return; choosingFile = true; let result = await electronRemote.dialog.showOpenDialog({ - title: 'Select Server File', + title: 'Select Guild File', defaultPath: '.', // TODO: better path name properties: [ 'openFile' ], filters: [ - { name: 'Cordis Server Files', extensions: [ 'cordis' ] } + { name: 'Cordis Guild Files', extensions: [ 'cordis' ] } ] }); @@ -37,26 +36,26 @@ export default function bindAddServerEvents(document: Document, q: Q, ui: UI, co let filePath = result.filePaths[0]; let fileText = (await fs.readFile(filePath)).toString('utf-8'); // TODO: try/catch? - let addServerData: any | null = null; + let addGuildData: any | null = null; try { - addServerData = JSON.parse(fileText); + addGuildData = JSON.parse(fileText); if ( - typeof addServerData !== 'object' || - typeof addServerData?.name !== 'string' || - typeof addServerData?.url !== 'string' || - typeof addServerData?.cert !== 'string' || - typeof addServerData?.token !== 'string' || - typeof addServerData?.expires !== 'number' || - typeof addServerData?.iconSrc !== 'string' + typeof addGuildData !== 'object' || + typeof addGuildData?.name !== 'string' || + typeof addGuildData?.url !== 'string' || + typeof addGuildData?.cert !== 'string' || + typeof addGuildData?.token !== 'string' || + typeof addGuildData?.expires !== 'number' || + typeof addGuildData?.iconSrc !== 'string' ) { - LOG.debug('bad server data:', { addServerData, fileText }) - throw new Error('bad server data'); + LOG.debug('bad guild data:', { addGuildData, fileText }) + throw new Error('bad guild data'); } - let overlayElement = createAddServerOverlay(document, q, ui, controller, addServerData as IAddServerData); + let overlayElement = createAddGuildOverlay(document, q, ui, guildsManager, addGuildData as IAddGuildData); document.body.appendChild(overlayElement); } catch (e) { - LOG.error('Unable to parse server data', e); - let errorOverlayElement = createErrorMessageOverlay(document, 'Unable to parse server file', e.message); + LOG.error('Unable to parse guild data', e); + let errorOverlayElement = createErrorMessageOverlay(document, 'Unable to parse guild file', e.message); document.body.appendChild(errorOverlayElement); } @@ -72,14 +71,14 @@ export default function bindAddServerEvents(document: Document, q: Q, ui: UI, co 'L 8,0 ' + 'Z' } ] }, - { class: 'content', content: 'Add a Server' } + { class: 'content', content: 'Add a Guild' } ] } }) as HTMLElement; - q.$('#add-server').addEventListener('mouseenter', () => { + q.$('#add-guild').addEventListener('mouseenter', () => { document.body.appendChild(contextElement); - ElementsUtil.alignContextElement(contextElement, q.$('#add-server'), { left: 'right', centerY: 'centerY' }) + ElementsUtil.alignContextElement(contextElement, q.$('#add-guild'), { left: 'right', centerY: 'centerY' }) }); - q.$('#add-server').addEventListener('mouseleave', () => { + q.$('#add-guild').addEventListener('mouseleave', () => { if (contextElement.parentElement) { contextElement.parentElement.removeChild(contextElement); } diff --git a/client/webapp/elements/events-connection.ts b/client/webapp/elements/events-connection.ts index c765db2..cc46663 100644 --- a/client/webapp/elements/events-connection.ts +++ b/client/webapp/elements/events-connection.ts @@ -7,7 +7,7 @@ import createConnectionContextMenu from './context-menu-conn'; export default function bindConnectionEvents(document: Document, q: Q, ui: UI): void { q.$('#connection').addEventListener('click', () => { if (ui.activeGuild === null) return; - if (!ui.activeGuild.isVerified) return; + if (!ui.activeGuild.isSocketVerified()) return; let contextMenu = createConnectionContextMenu(document, q, ui, ui.activeGuild); document.body.appendChild(contextMenu); diff --git a/client/webapp/elements/events-server-title.ts b/client/webapp/elements/events-guild-title.ts similarity index 84% rename from client/webapp/elements/events-server-title.ts rename to client/webapp/elements/events-guild-title.ts index 4343a82..18325e6 100644 --- a/client/webapp/elements/events-server-title.ts +++ b/client/webapp/elements/events-guild-title.ts @@ -1,13 +1,13 @@ import Q from '../q-module'; import UI from '../ui'; -import createServerTitleContextMenu from './context-menu-srv-title'; +import createServerTitleContextMenu from './context-menu-guild-title'; import ElementsUtil from './require/elements-util'; export default function bindAddServerTitleEvents(document: Document, q: Q, ui: UI) { q.$('#server-name-container').addEventListener('click', () => { if (ui.activeConnection === null) return; if (ui.activeGuild === null) return; - if (!ui.activeGuild.isVerified) return; + if (!ui.activeGuild.isSocketVerified()) return; if ( !ui.activeConnection.privileges.includes('modify_profile') && !ui.activeConnection.privileges.includes('modify_members') diff --git a/client/webapp/elements/events-text-input.ts b/client/webapp/elements/events-text-input.ts index 39b9252..912c2e5 100644 --- a/client/webapp/elements/events-text-input.ts +++ b/client/webapp/elements/events-text-input.ts @@ -7,12 +7,12 @@ import ElementsUtil from './require/elements-util.js'; import Globals from '../globals'; import { Channel } from '../data-types'; -import ClientController from '../client-controller'; import Q from '../q-module'; import UI from '../ui'; import createUploadOverlayFromPath from './overlay-upload-path'; import createUploadOverlayFromDataTransferItem from './overlay-upload-datatransfer'; import createUploadDropTarget from './overlay-upload-drop-target'; +import CombinedGuild from '../guild-combined'; export default function bindTextInputEvents(document: Document, q: Q, ui: UI): void { // Send Current Channel Messages @@ -27,10 +27,10 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v sendingMessage = true; - let server = ui.activeGuild as ClientController; + let server = ui.activeGuild as CombinedGuild; let channel = ui.activeChannel as Channel; - if (!server.isVerified) { + if (!server.isSocketVerified()) { LOG.warn('client attempted to send message while not verified'); q.$('#send-error').innerText = 'Not Connected to Server'; await ElementsUtil.shakeElement(q.$('#send-error'), 400); @@ -50,7 +50,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v q.$('#text-input').removeAttribute('contenteditable'); q.$('#text-input').classList.add('sending'); try { - await server.sendMessage(channel.id, text); + await server.requestSendMessage(channel.id, text); q.$('#send-error').innerText = ''; q.$('#text-input').innerText = ''; diff --git a/client/webapp/elements/guild-list-guild.ts b/client/webapp/elements/guild-list-guild.ts index 7f4e3ed..3f240d4 100644 --- a/client/webapp/elements/guild-list-guild.ts +++ b/client/webapp/elements/guild-list-guild.ts @@ -10,11 +10,11 @@ import { GuildMetadata } from '../data-types'; import Q from '../q-module'; import UI from '../ui'; import Actions from '../actions'; -import createGuildContextMenu from './context-menu-srv'; -import Controller from '../controller'; +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, controller: Controller, guild: CombinedGuild) { +export default function createGuildListGuild(document: Document, q: Q, ui: UI, guildsManager: GuildsManager, guild: CombinedGuild) { let element = q.create({ class: 'guild', 'meta-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 @@ -24,10 +24,10 @@ export default function createGuildListGuild(document: Document, q: Q, ui: UI, c (async () => { let guildData: GuildMetadata; try { - guildData = await guild.grabMetadata(); + guildData = await guild.fetchMetadata(); if (!guildData.iconResourceId) throw new Error('guild icon not identified yet'); let guildIcon = await guild.fetchResource(guildData.iconResourceId); - let guildIconSrc = await ElementsUtil.getImageBufferSrc(guildIcon); + let guildIconSrc = await ElementsUtil.getImageBufferSrc(guildIcon.data); (q.$$$(element, 'img') as HTMLImageElement).src = guildIconSrc; } catch (e) { LOG.error('Error fetching guild icon', e); @@ -83,7 +83,7 @@ export default function createGuildListGuild(document: Document, q: Q, ui: UI, c (async () => { // Explicitly not using a withPotentialError to make this simpler try { - let guildData = await guild.grabMetadata(); + let guildData = await guild.fetchMetadata(); ui.updateGuildName(guild, guildData.name); } catch (e) { LOG.error('Error fetching guild name', e); @@ -103,7 +103,7 @@ export default function createGuildListGuild(document: Document, q: Q, ui: UI, c }); element.addEventListener('contextmenu', (e) => { - let contextMenu = createGuildContextMenu(document, q, ui, controller, guild); + let contextMenu = createGuildContextMenu(document, q, ui, guildsManager, guild); document.body.appendChild(contextMenu); let relativeTo = { x: e.pageX, y: e.pageY }; ElementsUtil.alignContextElement(contextMenu, relativeTo, { top: 'centerY', left: 'centerX' }); diff --git a/client/webapp/elements/member.ts b/client/webapp/elements/member.ts index 21e23ba..6ea78c2 100644 --- a/client/webapp/elements/member.ts +++ b/client/webapp/elements/member.ts @@ -1,5 +1,5 @@ -import ClientController from "../client-controller"; import { Member } from "../data-types"; +import CombinedGuild from "../guild-combined"; import Q from "../q-module"; import ElementsUtil from "./require/elements-util"; @@ -18,7 +18,7 @@ export default function createMember(q: Q, guild: CombinedGuild, member: Member) ] }) as HTMLElement; (async () => { (q.$$$(element, 'img.avatar') as HTMLImageElement).src = - await ElementsUtil.getImageBufferFromResourceFailSoftly(server, member.avatarResourceId); + await ElementsUtil.getImageBufferFromResourceFailSoftly(guild, member.avatarResourceId); })(); return element; } diff --git a/client/webapp/elements/message.ts b/client/webapp/elements/message.ts index 8a7308b..e32f0f1 100644 --- a/client/webapp/elements/message.ts +++ b/client/webapp/elements/message.ts @@ -1,5 +1,5 @@ -import ClientController from '../client-controller'; import { Message } from '../data-types'; +import CombinedGuild from '../guild-combined'; import Q from '../q-module'; import createImageResourceMessage from './msg-img-res'; import createImageResourceMessageContinued from './msg-img-res-cont'; @@ -13,22 +13,22 @@ export default function createMessage(document: Document, q: Q, guild: CombinedG if (message.hasResource()) { if (message.isImageResource()) { if (message.isContinued(lastMessage)) { - element = createImageResourceMessageContinued(document, q, server, message); + element = createImageResourceMessageContinued(document, q, guild, message); } else { - element = createImageResourceMessage(document, q, server, message); + element = createImageResourceMessage(document, q, guild, message); } } else { if (message.isContinued(lastMessage)) { - element = createResourceMessageContinued(q, server, message); + element = createResourceMessageContinued(q, guild, message); } else { - element = createResourceMessage(q, server, message); + element = createResourceMessage(q, guild, message); } } } else { if (message.isContinued(lastMessage)) { - element = createTextMessageContinued(q, server, message); + element = createTextMessageContinued(q, guild, message); } else { - element = createTextMessage(q, server, message); + element = createTextMessage(q, guild, message); } } return element; diff --git a/client/webapp/elements/msg-img-res-cont.ts b/client/webapp/elements/msg-img-res-cont.ts index f7e8695..ceba7eb 100644 --- a/client/webapp/elements/msg-img-res-cont.ts +++ b/client/webapp/elements/msg-img-res-cont.ts @@ -9,17 +9,17 @@ import * as FileType from 'file-type'; import ElementsUtil from './require/elements-util.js'; import { Message, ShouldNeverHappenError } from '../data-types'; -import ClientController from '../client-controller'; import Q from '../q-module'; 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 { if (!message.resourceId || !message.resourcePreviewId || !message.resourceName) { throw new ShouldNeverHappenError('Message is not a resource message'); } - let element = q.create({ class: 'message continued', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-server-id': server.id, content: [ + let element = q.create({ class: 'message continued', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-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: @@ -28,20 +28,20 @@ export default function createImageResourceMessageContinued(document: Document, ] } ] }) as HTMLElement; q.$$$(element, '.content.image').addEventListener('click', () => { - document.body.appendChild(createImageOverlay(document, q, server, message.resourceId as string, message.resourceName as string)); + document.body.appendChild(createImageOverlay(document, q, guild, message.resourceId as string, message.resourceName as string)); }); (async () => { try { - let buffer = await server.fetchResource(message.resourcePreviewId as string); - let src = await ElementsUtil.getImageBufferSrc(buffer); + let resource = await guild.fetchResource(message.resourcePreviewId as string); + let src = await ElementsUtil.getImageBufferSrc(resource.data); (q.$$$(element, '.content.image img') as HTMLImageElement).src = src; - let { mime, ext } = (await FileType.fromBuffer(buffer)) ?? { mime: null, ext: null }; + let { mime, ext } = (await FileType.fromBuffer(resource.data)) ?? { mime: null, ext: null }; if (mime === null || ext === null) throw new Error('unable to get mime/ext'); q.$$$(element, '.content.image').addEventListener('contextmenu', (e) => { - let contextMenu = createImageContextMenu(document, q, server, message.resourceName as string, buffer, mime as string, ext as string, true); + let contextMenu = createImageContextMenu(document, q, guild, message.resourceName as string, resource.data, mime as string, ext as string, true); document.body.appendChild(contextMenu); let relativeTo = { x: e.pageX, y: e.pageY }; ElementsUtil.alignContextElement(contextMenu, relativeTo, { top: 'centerY', left: 'right' }); diff --git a/client/webapp/elements/msg-img-res.ts b/client/webapp/elements/msg-img-res.ts index 6350424..b785be7 100644 --- a/client/webapp/elements/msg-img-res.ts +++ b/client/webapp/elements/msg-img-res.ts @@ -9,10 +9,10 @@ import * as FileType from 'file-type'; import ElementsUtil from './require/elements-util.js'; import { Message, Member, ShouldNeverHappenError } from '../data-types'; -import ClientController from '../client-controller'; import Q from '../q-module'; 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) { if (!message.resourceId || !message.resourcePreviewId || !message.resourceName) { @@ -39,7 +39,7 @@ export default function createImageResourceMessage(document: Document, q: Q, gui } let nameStyle = memberInfo.roleColor != null ? 'color: ' + memberInfo.roleColor : ''; - let element = q.create({ class: 'message', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-server-id': server.id, content: [ + let element = q.create({ class: 'message', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-guild-id': guild.id, content: [ { class: 'member-avatar', content: { tag: 'img', src: './img/loading.svg', alt: memberInfo.displayName } }, { class: 'right', content: [ { class: 'header', content: [ @@ -52,23 +52,23 @@ export default function createImageResourceMessage(document: Document, q: Q, gui ] } ] }) as HTMLElement; q.$$$(element, '.content.image').addEventListener('click', (e) => { - document.body.appendChild(createImageOverlay(document, q, server, message.resourceId as string, message.resourceName as string)); + document.body.appendChild(createImageOverlay(document, q, guild, message.resourceId as string, message.resourceName as string)); }); (async () => { (q.$$$(element, '.member-avatar img') as HTMLImageElement).src = - await ElementsUtil.getImageBufferFromResourceFailSoftly(server, memberInfo.avatarResourceId); + await ElementsUtil.getImageBufferFromResourceFailSoftly(guild, memberInfo.avatarResourceId); })(); (async () => { try { - let buffer = await server.fetchResource(message.resourcePreviewId as string); - let src = await ElementsUtil.getImageBufferSrc(buffer); + let resource = await guild.fetchResource(message.resourcePreviewId as string); + let src = await ElementsUtil.getImageBufferSrc(resource.data); (q.$$$(element, '.content.image img') as HTMLImageElement).src = src; - let { mime, ext } = (await FileType.fromBuffer(buffer)) ?? { mime: null, ext: null }; + let { mime, ext } = (await FileType.fromBuffer(resource.data)) ?? { mime: null, ext: null }; if (mime === null || ext === null) throw new Error('unable to get mime/ext'); q.$$$(element, '.content.image').addEventListener('contextmenu', (e) => { - let contextMenu = createImageContextMenu(document, q, server, message.resourceName as string, buffer, mime as string, ext as string, true); + let contextMenu = createImageContextMenu(document, q, guild, message.resourceName as string, resource.data, mime as string, ext as string, true); document.body.appendChild(contextMenu); let relativeTo = { x: e.pageX, y: e.pageY }; ElementsUtil.alignContextElement(contextMenu, relativeTo, { top: 'centerY', left: 'right' }); diff --git a/client/webapp/elements/msg-res-cont.ts b/client/webapp/elements/msg-res-cont.ts index 6efd4f7..ad0aee2 100644 --- a/client/webapp/elements/msg-res-cont.ts +++ b/client/webapp/elements/msg-res-cont.ts @@ -1,6 +1,6 @@ import * as moment from "moment"; -import ClientController from "../client-controller"; import { Message } from '../data-types'; +import CombinedGuild from "../guild-combined"; import Q from "../q-module"; import ElementsUtil from "./require/elements-util"; @@ -17,7 +17,7 @@ export default function createResourceMessageContinued(q: Q, guild: CombinedGuil throw new ShouldNeverHappenError('Message is not a resource message'); } - let element = q.create({ class: 'message continued', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-server-id': server.id, content: [ + let element = q.create({ class: 'message continued', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-guild-id': guild.id, content: [ { class: 'timestamp', content: moment(message.sent).format('HH:mm') }, { class: 'right', content: [ { class: 'content resource', content: [ @@ -31,7 +31,7 @@ export default function createResourceMessageContinued(q: Q, guild: CombinedGuil ] } ] }) as HTMLElement; q.$$$(element, '.resource').addEventListener('click', ElementsUtil.createDownloadListener({ - server: server, resourceId: message.resourceId, resourceName: message.resourceName, + guild: guild, resourceId: message.resourceId, resourceName: message.resourceName, downloadStartFunc: () => { q.$$$(element, '.resource .download-status').innerText = 'Downloading...'; }, diff --git a/client/webapp/elements/msg-res.ts b/client/webapp/elements/msg-res.ts index 7d580d6..3d5a1cd 100644 --- a/client/webapp/elements/msg-res.ts +++ b/client/webapp/elements/msg-res.ts @@ -1,6 +1,6 @@ import * as moment from 'moment'; -import ClientController from "../client-controller"; import { Message, Member } from '../data-types'; +import CombinedGuild from '../guild-combined'; import Q from '../q-module'; import ElementsUtil from './require/elements-util'; @@ -37,7 +37,7 @@ export default function createResourceMessage(q: Q, guild: CombinedGuild, messag } let nameStyle = memberInfo.roleColor ? 'color: ' + memberInfo.roleColor : ''; - let element = q.create({ class: 'message', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-server-id': server.id, content: [ + let element = q.create({ class: 'message', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-guild-id': guild.id, content: [ { class: 'member-avatar', content: { tag: 'img', src: './img/loading.svg', alt: memberInfo.displayName } }, { class: 'right', content: [ { class: 'header', content: [ @@ -55,7 +55,7 @@ export default function createResourceMessage(q: Q, guild: CombinedGuild, messag ] } ] }) as HTMLElement; q.$$$(element, '.resource').addEventListener('click', ElementsUtil.createDownloadListener({ - server: server, resourceId: message.resourceId, resourceName: message.resourceName, + guild: guild, resourceId: message.resourceId, resourceName: message.resourceName, downloadStartFunc: () => { q.$$$(element, '.resource .download-status').innerText = 'Downloading...'; }, @@ -76,7 +76,7 @@ export default function createResourceMessage(q: Q, guild: CombinedGuild, messag })); (async () => { (q.$$$(element, '.member-avatar img') as HTMLImageElement).src = - await ElementsUtil.getImageBufferFromResourceFailSoftly(server, memberInfo.avatarResourceId); + await ElementsUtil.getImageBufferFromResourceFailSoftly(guild, memberInfo.avatarResourceId); })(); return element; } diff --git a/client/webapp/elements/msg-txt-cont.ts b/client/webapp/elements/msg-txt-cont.ts index 53759ac..f7d48f4 100644 --- a/client/webapp/elements/msg-txt-cont.ts +++ b/client/webapp/elements/msg-txt-cont.ts @@ -1,12 +1,12 @@ import * as moment from 'moment'; -import ClientController from '../client-controller.js'; 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', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-server-id': server.id, content: [ + return q.create({ class: 'message continued', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-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 ?? '') } diff --git a/client/webapp/elements/msg-txt.ts b/client/webapp/elements/msg-txt.ts index 24aad5e..fb9f453 100644 --- a/client/webapp/elements/msg-txt.ts +++ b/client/webapp/elements/msg-txt.ts @@ -3,8 +3,8 @@ import * as moment from 'moment'; import ElementsUtil from './require/elements-util'; import { Message, Member, IDummyTextMessage } from '../data-types'; -import ClientController from '../client-controller'; import Q from '../q-module'; +import CombinedGuild from '../guild-combined'; export default function createTextMessage(q: Q, guild: CombinedGuild, message: Message | IDummyTextMessage): HTMLElement { let memberInfo: { @@ -35,7 +35,7 @@ export default function createTextMessage(q: Q, guild: CombinedGuild, message: M } let nameStyle = memberInfo.roleColor ? 'color: ' + memberInfo.roleColor : ''; - let element = q.create({ class: 'message', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-server-id': server.id, content: [ + let element = q.create({ class: 'message', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-guild-id': guild.id, content: [ { class: 'member-avatar', content: { tag: 'img', src: './img/loading.svg', alt: memberInfo.displayName } }, { class: 'right', content: [ { class: 'header', content: [ @@ -47,7 +47,7 @@ export default function createTextMessage(q: Q, guild: CombinedGuild, message: M ] }) as HTMLElement; (async () => { (q.$$$(element, '.member-avatar img') as HTMLImageElement).src = - await ElementsUtil.getImageBufferFromResourceFailSoftly(server, memberInfo.avatarResourceId); + await ElementsUtil.getImageBufferFromResourceFailSoftly(guild, memberInfo.avatarResourceId); })(); return element; } diff --git a/client/webapp/elements/overlay-add-server.ts b/client/webapp/elements/overlay-add-guild.ts similarity index 82% rename from client/webapp/elements/overlay-add-server.ts rename to client/webapp/elements/overlay-add-guild.ts index 8ceea46..c82aa17 100644 --- a/client/webapp/elements/overlay-add-server.ts +++ b/client/webapp/elements/overlay-add-guild.ts @@ -14,13 +14,12 @@ import Globals from '../globals'; import BaseElements from './require/base-elements'; import ElementsUtil from './require/elements-util'; -import ClientController from '../client-controller'; import Q from '../q-module'; import UI from '../ui'; -import Controller from '../controller'; -import Actions from '../actions'; +import GuildsManager from '../guilds-manager'; +import CombinedGuild from '../guild-combined'; -export interface IAddServerData { +export interface IAddGuildData { name: string, url: string, cert: string, @@ -51,23 +50,23 @@ function getExampleAvatarPath(): string { return paths[Math.floor(Math.random() * paths.length)]; } -export default function createAddServerOverlay(document: Document, q: Q, ui: UI, controller: Controller, addServerData: IAddServerData): HTMLElement { - let expired = addServerData.expires < new Date().getTime(); +export default function createAddGuildOverlay(document: Document, q: Q, ui: UI, guildsManager: GuildsManager, addGuildData: IAddGuildData): HTMLElement { + let expired = addGuildData.expires < new Date().getTime(); let displayName = getExampleDisplayName(); let avatarPath = getExampleAvatarPath(); - //LOG.debug('addserverdata:', { addServerData }); + //LOG.debug('addguilddata:', { addGuildData }); let element = BaseElements.createOverlay(document, { - class: 'content add-server', content: [ + class: 'content add-guild', content: [ { class: 'preview', content: [ - { tag: 'img', class: 'icon', src: addServerData.iconSrc, alt: 'icon' }, + { tag: 'img', class: 'icon', src: addGuildData.iconSrc, alt: 'icon' }, { content: [ - { class: 'name', content: addServerData.name }, - { class: 'url', content: addServerData.url }, + { class: 'name', content: addGuildData.name }, + { class: 'url', content: addGuildData.url }, { class: 'expires', - content: (expired ? 'Invite Expired ' : 'Invite Expires ') + moment(addServerData.expires).fromNow() } + content: (expired ? 'Invite Expired ' : 'Invite Expires ') + moment(addGuildData.expires).fromNow() } ] } ] }, { class: 'divider' }, @@ -93,7 +92,7 @@ export default function createAddServerOverlay(document: Document, q: Q, ui: UI, { class: 'lower', content: [ { class: 'error' }, { class: 'buttons', content: [ - { class: 'button submit', content: 'Add Server' } + { class: 'button submit', content: 'Add Guild' } ] } ] } ] @@ -177,12 +176,12 @@ export default function createAddServerOverlay(document: Document, q: Q, ui: UI, q.$$$(element, '.display-name-input').removeAttribute('contenteditable'); - let newguild: CombinedGuild | null = null; - if (addServerData == null) { - q.$$$(element, '.error').innerText = 'Very bad server file'; + 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 (addServerData.expires < new Date().getTime()) { + } 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); @@ -201,20 +200,20 @@ export default function createAddServerOverlay(document: Document, q: Q, ui: UI, } else { // NOTE: Avatar size is checked above q.$$$(element, '.submit').innerText = 'Registering...'; try { - newServer = await controller.addNewGuild(addServerData, displayName, avatarBuff); + newGuild = await guildsManager.addNewGuild(addGuildData, displayName, avatarBuff); } catch (e) { - LOG.warn('error adding new server: ' + e.message, e); // explicitly not printing stack trace here + LOG.warn('error adding new guild: ' + e.message, e); // explicitly not printing stack trace here q.$$$(element, '.error').innerText = e.message; q.$$$(element, '.submit').innerText = 'Try Again'; await ElementsUtil.shakeElement(q.$$$(element, '.submit'), 400); - newServer = null; + newGuild = null; } } - if (newServer !== null) { - let serverElement = await ui.addServer(controller, newServer); - element.removeSelf(); // close the overlay since we got a new server - serverElement.click(); // click on the new server + if (newGuild !== null) { + let 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'); diff --git a/client/webapp/elements/overlay-create-channel.ts b/client/webapp/elements/overlay-create-channel.ts index f519fa6..b159591 100644 --- a/client/webapp/elements/overlay-create-channel.ts +++ b/client/webapp/elements/overlay-create-channel.ts @@ -3,12 +3,12 @@ const electronConsole = electronRemote.getGlobal('console') as Console; import Logger from '../../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); -import ClientController from "../client-controller"; import Globals from "../globals"; import ElementsUtil from "./require/elements-util"; import BaseElements from "./require/base-elements"; import Q from '../q-module'; +import CombinedGuild from '../guild-combined'; export default function createCreateChannelOverlay(document: Document, q: Q, guild: CombinedGuild): HTMLElement { @@ -83,7 +83,7 @@ export default function createCreateChannelOverlay(document: Document, q: Q, gui newFlavorText = null; } try { - await server.createChannel(newName, newFlavorText); + await guild.requestDoCreateChannel(newName, newFlavorText); success = true; } catch (e) { LOG.error('error updating channel', e); diff --git a/client/webapp/elements/overlay-create-invite-token.ts b/client/webapp/elements/overlay-create-invite-token.ts index 956c65d..d231e9e 100644 --- a/client/webapp/elements/overlay-create-invite-token.ts +++ b/client/webapp/elements/overlay-create-invite-token.ts @@ -1,5 +1,4 @@ -import ClientController from "../client-controller"; - +import CombinedGuild from "../guild-combined"; import BaseElements from "./require/base-elements"; export default function createCreateInviteTokenOverlay(document: Document, guild: CombinedGuild): HTMLElement { diff --git a/client/webapp/elements/overlay-server-settings.ts b/client/webapp/elements/overlay-guild-settings.ts similarity index 65% rename from client/webapp/elements/overlay-server-settings.ts rename to client/webapp/elements/overlay-guild-settings.ts index ae3e440..988da8d 100644 --- a/client/webapp/elements/overlay-server-settings.ts +++ b/client/webapp/elements/overlay-guild-settings.ts @@ -8,20 +8,20 @@ import Globals from '../globals'; import BaseElements from './require/base-elements'; import ElementsUtil from './require/elements-util'; -import ClientController from '../client-controller'; -import { CacheServerData, ServerMetaData } from '../data-types'; +import { GuildMetadata } from '../data-types'; import Q from '../q-module'; +import CombinedGuild from '../guild-combined'; -export default function createServerSettingsOverlay(document: Document, q: Q, guild: CombinedGuild, serverMeta: ServerMetaData | CacheServerData): HTMLElement { +export default function createGuildSettingsOverlay(document: Document, q: Q, guild: CombinedGuild, guildMeta: GuildMetadata): HTMLElement { let element = BaseElements.createOverlay(document, { - class: 'content submit-dialog server-settings', content: [ - { class: 'server preview', content: [ + class: 'content submit-dialog guild-settings', content: [ + { class: 'guild preview', content: [ { class: 'icon', content: { tag: 'img', src: './img/loading.svg', alt: 'icon' } }, - { class: 'name', content: serverMeta.name } + { class: 'name', content: guildMeta.name } ] }, - { class: 'text-input server-name', placeholder: 'New Server Name', - contenteditable: 'plaintext-only', content: serverMeta.name }, - { class: 'image-input server-icon', content: [ + { class: 'text-input guild-name', placeholder: 'New Guild Name', + contenteditable: 'plaintext-only', content: guildMeta.name }, + { class: 'image-input guild-icon', content: [ { tag: 'label', class: 'image-input-label button', content: [ 'Select New Icon', { class: 'image-input-upload', tag: 'input', type: 'file', accept: '.png,.jpg,.jpeg', style: 'display: none;' } @@ -37,7 +37,7 @@ export default function createServerSettingsOverlay(document: Document, q: Q, gu }); (async () => { - (q.$$$(element, '.icon img') as HTMLImageElement).src = await ElementsUtil.getImageBufferFromResourceFailSoftly(server, serverMeta.iconResourceId); + (q.$$$(element, '.icon img') as HTMLImageElement).src = await ElementsUtil.getImageBufferFromResourceFailSoftly(guild, guildMeta.iconResourceId); })(); let newIconBuff: Buffer | null = null; @@ -48,11 +48,11 @@ export default function createServerSettingsOverlay(document: Document, q: Q, gu onCleared: () => {}, onError: async (errMsg) => { q.$$$(element, '.error').innerText = errMsg; - await ElementsUtil.shakeElement(q.$$$(element, '.image-input-upload.server-icon'), 400); + await ElementsUtil.shakeElement(q.$$$(element, '.image-input-upload.guild-icon'), 400); }, onLoaded: (buff, src) => { newIconBuff = buff; - (q.$$$(element, '.server .icon img') as HTMLImageElement).src = src; + (q.$$$(element, '.guild .icon img') as HTMLImageElement).src = src; } }); @@ -64,7 +64,7 @@ export default function createServerSettingsOverlay(document: Document, q: Q, gu }); q.$$$(element, '.text-input').addEventListener('input', () => { - q.$$$(element, '.server.preview .name').innerText = q.$$$(element, '.text-input').innerText; + q.$$$(element, '.guild.preview .name').innerText = q.$$$(element, '.text-input').innerText; }); let submitting = false; @@ -76,33 +76,34 @@ export default function createServerSettingsOverlay(document: Document, q: Q, gu let newName = q.$$$(element, '.text-input').innerText; - if (newName == serverMeta.name && newIconBuff == null) { + if (newName == guildMeta.name && newIconBuff == null) { // nothing changed, close the dialog element.removeSelf(); return; } let success = false; - if (newName != serverMeta.name && newName.length == 0) { - LOG.warn('attempted to set empty server name'); + if (newName != guildMeta.name && newName.length == 0) { + LOG.warn('attempted to set empty guild name'); q.$$$(element, '.button.submit').innerText = 'Try Again'; q.$$$(element, '.error').innerText = 'New name is empty'; await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400); - } else if (newName != serverMeta.name && newName.length > Globals.MAX_SERVER_NAME_LENGTH) { - LOG.warn('attempted to oversized server name'); + } else if (newName != guildMeta.name && newName.length > Globals.MAX_GUILD_NAME_LENGTH) { + LOG.warn('attempted to oversized guild name'); q.$$$(element, '.button.submit').innerText = 'Try Again'; - q.$$$(element, '.error').innerText = 'New name is too long. ' + newName.length + ' > ' + Globals.MAX_SERVER_NAME_LENGTH; + q.$$$(element, '.error').innerText = 'New name is too long. ' + newName.length + ' > ' + Globals.MAX_GUILD_NAME_LENGTH; await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400); } else { // client-size icon size checks are handled above let failed = false; // Set Name - if (newName != serverMeta.name) { + if (newName != guildMeta.name) { try { - serverMeta = await server.setName(newName); + await guild.requestSetServerName(newName); + guildMeta = await guild.fetchMetadata(); } catch (e) { - LOG.error('error setting new server name', e); + LOG.error('error setting new guild name', e); q.$$$(element, '.button.submit').innerText = 'Try Again'; - q.$$$(element, '.error').innerText = 'Error setting new server name'; + q.$$$(element, '.error').innerText = 'Error setting new guild name'; await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400); failed = true; } @@ -111,12 +112,12 @@ export default function createServerSettingsOverlay(document: Document, q: Q, gu // Set Icon if (!failed && newIconBuff != null) { try { - await server.setIcon(newIconBuff); + await guild.requestSetServerIcon(newIconBuff); newIconBuff = null; // prevent resubmit } catch (e) { - LOG.error('error setting new server icon', e); + LOG.error('error setting new guild icon', e); q.$$$(element, '.button.submit').innerText = 'Try Again'; - q.$$$(element, '.error').innerText = 'Error setting new server icon'; + q.$$$(element, '.error').innerText = 'Error setting new guild icon'; await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400); failed = true; } diff --git a/client/webapp/elements/overlay-image.ts b/client/webapp/elements/overlay-image.ts index 81338d5..a57f962 100644 --- a/client/webapp/elements/overlay-image.ts +++ b/client/webapp/elements/overlay-image.ts @@ -8,9 +8,9 @@ import * as FileType from 'file-type' import BaseElements from './require/base-elements'; import ElementsUtil from './require/elements-util'; -import ClientController from '../client-controller'; import Q from '../q-module'; import createImageContextMenu from './context-menu-img'; +import CombinedGuild from '../guild-combined'; export default function createImageOverlay(document: Document, q: Q, guild: CombinedGuild, resourceId: string, resourceName: string): HTMLElement { let element = BaseElements.createOverlay(document, { class: 'content popup-image', content: [ @@ -26,16 +26,16 @@ export default function createImageOverlay(document: Document, q: Q, guild: Comb (async () => { try { - let resourceBuff = await server.fetchResource(resourceId); - let src = await ElementsUtil.getImageBufferSrc(resourceBuff); + let resource = await guild.fetchResource(resourceId); + let src = await ElementsUtil.getImageBufferSrc(resource.data); (q.$$$(element, '.content img') as HTMLImageElement).src = src; - q.$$$(element, '.download .size').innerText = ElementsUtil.humanSize(resourceBuff.length); + q.$$$(element, '.download .size').innerText = ElementsUtil.humanSize(resource.data.length); - let { mime, ext } = (await FileType.fromBuffer(resourceBuff)) ?? { mime: null, ext: null }; + let { mime, ext } = (await FileType.fromBuffer(resource.data)) ?? { mime: null, ext: null }; if (mime === null || ext === null) throw new Error('unable to get mime/ext'); q.$$$(element, '.content img').addEventListener('contextmenu', (e) => { - let contextMenu = createImageContextMenu(document, q, server, resourceName, resourceBuff, mime as string, ext as string, false); + let contextMenu = createImageContextMenu(document, q, guild, resourceName, resource.data, mime as string, ext as string, false); document.body.appendChild(contextMenu); let relativeTo = { x: e.pageX, y: e.pageY }; ElementsUtil.alignContextElement(contextMenu, relativeTo, { top: 'centerY', left: 'right' }); @@ -43,7 +43,7 @@ export default function createImageOverlay(document: Document, q: Q, guild: Comb q.$$$(element, '.button').innerText = 'Save'; q.$$$(element, '.button').addEventListener('click', ElementsUtil.createDownloadListener({ - downloadBuff: resourceBuff, + downloadBuff: resource.data, resourceName: resourceName, downloadStartFunc: () => { q.$$$(element, '.button').innerText = 'Downloading...'; diff --git a/client/webapp/elements/overlay-modify-channel.ts b/client/webapp/elements/overlay-modify-channel.ts index 91b6ce4..3de58e4 100644 --- a/client/webapp/elements/overlay-modify-channel.ts +++ b/client/webapp/elements/overlay-modify-channel.ts @@ -4,12 +4,12 @@ import Logger from '../../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); import { Channel } from '../data-types'; -import ClientController from '../client-controller.js'; import Globals from '../globals.js'; import BaseElements from './require/base-elements.js'; import ElementsUtil from './require/elements-util.js'; import Q from '../q-module'; +import CombinedGuild from '../guild-combined'; export default function createModifyChannelOverlay(document: Document, q: Q, guild: CombinedGuild, channel: Channel): HTMLElement { // See also overlay-create-channel @@ -85,7 +85,7 @@ export default function createModifyChannelOverlay(document: Document, q: Q, gui newFlavorText = null; } try { - await server.updateChannel(channel.id, newName, newFlavorText); + await guild.requestDoUpdateChannel(channel.id, newName, newFlavorText); success = true; } catch (e) { LOG.error('error updating channel', e); diff --git a/client/webapp/elements/overlay-personalize.ts b/client/webapp/elements/overlay-personalize.ts index 0c61e92..72824c6 100644 --- a/client/webapp/elements/overlay-personalize.ts +++ b/client/webapp/elements/overlay-personalize.ts @@ -8,14 +8,14 @@ import ElementsUtil from './require/elements-util'; import Globals from '../globals'; -import ClientController from '../client-controller'; import Q from '../q-module'; import createTextMessage from './msg-txt'; +import CombinedGuild from '../guild-combined'; export default function createPersonalizeOverlay(document: Document, q: Q, guild: CombinedGuild, connection: any): HTMLElement { let element = BaseElements.createOverlay(document, { class: 'content submit-dialog personalize', content: [ - createTextMessage(q, server, { id: 'test-message', member: connection, sent: new Date(), text: 'Example Message' }), + createTextMessage(q, guild, { id: 'test-message', member: connection, sent: new Date(), text: 'Example Message' }), { class: 'text-input', placeholder: 'New Display Name', spellcheck: 'false', contenteditable: 'plaintext-only', content: connection.display_name }, { class: 'image-input avatar-input', content: [ @@ -95,7 +95,7 @@ export default function createPersonalizeOverlay(document: Document, q: Q, guild let failed = false; if (newDisplayName != connection.display_name) { try { - await server.setDisplayName(newDisplayName); + await guild.requestSetDisplayName(newDisplayName); connection.display_name = newDisplayName; // prevent resubmit } catch (e) { LOG.error('error setting display name', e); @@ -109,7 +109,7 @@ export default function createPersonalizeOverlay(document: Document, q: Q, guild // Set New Avatar if (!failed && newAvatarBuffer != null) { try { - await server.setAvatar(newAvatarBuffer); + await guild.requestSetAvatar(newAvatarBuffer); newAvatarBuffer = null; // prevent resubmit } catch (e) { LOG.error('error setting avatar buffer', e); diff --git a/client/webapp/elements/overlay-token-log.ts b/client/webapp/elements/overlay-token-log.ts index 8c94685..a69ecb7 100644 --- a/client/webapp/elements/overlay-token-log.ts +++ b/client/webapp/elements/overlay-token-log.ts @@ -9,9 +9,9 @@ import BaseElements from './require/base-elements'; import ElementsUtil from './require/elements-util'; import Util from '../util'; -import ClientController from '../client-controller'; import { Member } from '../data-types'; import Q from '../q-module'; +import CombinedGuild from '../guild-combined'; export default function createTokenLogOverlay(document: Document, q: Q, guild: CombinedGuild): HTMLElement { let element = BaseElements.createOverlay(document, { @@ -23,7 +23,7 @@ export default function createTokenLogOverlay(document: Document, q: Q, guild: C (async () => { Util.withPotentialErrorWarnOnCancel(q, { taskFunc: async () => { - let tokens = await server.queryTokens(); + let tokens = await guild.fetchTokens(); Q.clearChildren(q.$$$(element, '.tokens')); let displayed = 0; for (let token of tokens) { @@ -81,7 +81,7 @@ export default function createTokenLogOverlay(document: Document, q: Q, guild: C q.$$$(contextElement, '.content').innerText = 'Revoking...'; ElementsUtil.alignContextElement(contextElement, revokeElement, alignment); try { - await server.revokeToken(token.token); + await guild.requestDoRevokeToken(token.token); } catch (e) { LOG.error('unable to revoke token', e); q.$$$(contextElement, '.content').innerText = 'Unable to Revoke'; diff --git a/client/webapp/elements/overlay-upload-datatransfer.ts b/client/webapp/elements/overlay-upload-datatransfer.ts index 12649f7..781cd6b 100644 --- a/client/webapp/elements/overlay-upload-datatransfer.ts +++ b/client/webapp/elements/overlay-upload-datatransfer.ts @@ -1,13 +1,13 @@ import BaseElements from './require/base-elements.js'; import { Channel, ShouldNeverHappenError } from '../data-types'; -import ClientController from '../client-controller.js'; +import CombinedGuild from '../guild-combined.js'; export default function createUploadOverlayFromDataTransferItem(document: Document, guild: CombinedGuild, channel: Channel, dataTransferItem: DataTransferItem): HTMLElement { let file = dataTransferItem.getAsFile(); if (file === null) throw new ShouldNeverHappenError('no file in the data transfer item'); let element = BaseElements.createUploadOverlay(document, { - server: server, channel: channel, resourceName: file.name, + guild: guild, channel: channel, resourceName: file.name, resourceBuffFunc: async () => { if (file === null) throw new ShouldNeverHappenError('no file in the data transfer item'); return Buffer.from(await file.arrayBuffer()); diff --git a/client/webapp/elements/overlay-upload-drop-target.ts b/client/webapp/elements/overlay-upload-drop-target.ts index f87f028..9890617 100644 --- a/client/webapp/elements/overlay-upload-drop-target.ts +++ b/client/webapp/elements/overlay-upload-drop-target.ts @@ -5,9 +5,9 @@ const LOG = Logger.create(__filename, electronConsole); import { Channel } from '../data-types'; import BaseElements from './require/base-elements'; -import ClientController from '../client-controller'; import Q from '../q-module'; import createUploadOverlayFromDataTransferItem from './overlay-upload-datatransfer'; +import CombinedGuild from '../guild-combined'; export default function createUploadDropTarget(document: Document, q: Q, guild: CombinedGuild, channel: Channel): HTMLElement { let element = BaseElements.createOverlay(document, { class: 'content drop-target', content: [ @@ -37,7 +37,7 @@ export default function createUploadDropTarget(document: Document, q: Q, guild: } } if (fileTransferItem) { - let element = createUploadOverlayFromDataTransferItem(document, server, channel, fileTransferItem); + let element = createUploadOverlayFromDataTransferItem(document, guild, channel, fileTransferItem); document.body.appendChild(element); q.$$$(element, '.text-input').focus(); } else { diff --git a/client/webapp/elements/overlay-upload-path.ts b/client/webapp/elements/overlay-upload-path.ts index a52d04a..a59d3d9 100644 --- a/client/webapp/elements/overlay-upload-path.ts +++ b/client/webapp/elements/overlay-upload-path.ts @@ -4,12 +4,12 @@ import * as path from 'path'; import BaseElements from './require/base-elements'; import { Channel } from '../data-types'; -import ClientController from '../client-controller'; +import CombinedGuild from '../guild-combined'; export default function createUploadOverlayFromPath(document: Document, guild: CombinedGuild, channel: Channel, resourcePath: string): HTMLElement { let resourceName = path.basename(resourcePath); let element = BaseElements.createUploadOverlay(document, { - server: server, channel: channel, resourceName: resourceName, + guild: guild, channel: channel, resourceName: resourceName, resourceBuffFunc: async () => { return await fs.readFile(resourcePath); }, diff --git a/client/webapp/elements/require/base-elements.ts b/client/webapp/elements/require/base-elements.ts index 52df774..6f5a111 100644 --- a/client/webapp/elements/require/base-elements.ts +++ b/client/webapp/elements/require/base-elements.ts @@ -10,7 +10,7 @@ import Globals from '../../globals'; import ElementsUtil from './elements-util'; import { Channel } from '../../data-types'; -import ClientController from '../../client-controller'; +import CombinedGuild from '../../guild-combined'; import Q from '../../q-module'; interface HTMLElementWithRemoveSelf extends HTMLElement { @@ -209,7 +209,7 @@ export default class BaseElements { static createUploadOverlay(document: Document, props: CreateUploadOverlayProps): HTMLElementWithRemoveSelf { const q = new Q(document); - const { server, channel, resourceName, resourceBuffFunc, resourceSizeFunc } = props; + const { guild, channel, resourceName, resourceBuffFunc, resourceSizeFunc } = props; let element = BaseElements.createOverlay(document, { class: 'content upload', content: [ { class: 'title', content: [ @@ -248,7 +248,7 @@ export default class BaseElements { return; } sending = true; - if (!server.isVerified) { + if (!guild.isSocketVerified()) { LOG.warn('client attempted to send message with resource while not verified'); q.$$$(element, '.error').innerText = 'Not Connected to Server'; q.$$$(element, '.button.upload').innerText = 'Try Again'; @@ -290,7 +290,7 @@ export default class BaseElements { return; } try { - await server.sendMessageWithResource(channel.id, text, resourceBuff, resourceName); + await guild.requestSendMessageWithResource(channel.id, text, resourceBuff, resourceName); } catch (e) { q.$$$(element, '.error').innerText = 'Error uploading resource.'; q.$$$(element, '.button.upload').innerText = 'Try Again'; diff --git a/client/webapp/elements/require/elements-util.ts b/client/webapp/elements/require/elements-util.ts index 9e44d75..da6e033 100644 --- a/client/webapp/elements/require/elements-util.ts +++ b/client/webapp/elements/require/elements-util.ts @@ -13,7 +13,7 @@ import * as FileType from 'file-type'; import Util from '../../util'; import Globals from '../../globals'; -import ClientController from '../../client-controller'; +import CombinedGuild from '../../guild-combined'; import { ShouldNeverHappenError } from '../../data-types'; // TODO: pass-through Globals in init function @@ -36,7 +36,7 @@ interface IHTMLElementWithRemovalType extends HTMLElement { interface CreateDownloadListenerProps { downloadBuff?: Buffer; - server?: ClientController; + guild?: CombinedGuild; resourceId?: string; resourceName: string; downloadStartFunc: (() => Promise | void); @@ -101,15 +101,15 @@ export default class ElementsUtil { static async getImageBufferFromResourceFailSoftly(guild: CombinedGuild, resourceId: string | null): Promise { if (!resourceId) { - LOG.warn('no server resource specified, showing error instead', new Error()); + LOG.warn('no guild resource specified, showing error instead', new Error()); return './img/error.png'; } try { - let resourceBuff = await server.fetchResource(resourceId); - let src = await ElementsUtil.getImageBufferSrc(resourceBuff); + let resource = await guild.fetchResource(resourceId); + let src = await ElementsUtil.getImageBufferSrc(resource.data); return src; } catch (e) { - LOG.warn('unable to fetch server resource, showing error instead', e); + LOG.warn('unable to fetch guild resource, showing error instead', e); return './img/error.png'; } } @@ -340,7 +340,7 @@ export default class ElementsUtil { static createDownloadListener(props: CreateDownloadListenerProps): (() => Promise) { const { downloadBuff, // pre-downloaded buffer to save rather than submit a download request (downloadStartFunc still required) - server, resourceId, resourceName, + guild, resourceId, resourceName, downloadStartFunc, downloadFailFunc, writeStartFunc, writeFailFunc, successFunc @@ -362,10 +362,10 @@ export default class ElementsUtil { if (downloadBuff) { resourceBuff = downloadBuff; } else { - if (!server) throw new ShouldNeverHappenError('server is null and we are not using a pre-download'); + if (!guild) throw new ShouldNeverHappenError('guild is null and we are not using a pre-download'); if (!resourceId) throw new ShouldNeverHappenError('resourceId is null and we are not using a pre-download'); try { - resourceBuff = await server.fetchResource(resourceId); + resourceBuff = (await guild.fetchResource(resourceId)).data; } catch (e) { LOG.error('Error downloading resource', e); if (downloadFailFunc) await downloadFailFunc(e); diff --git a/client/webapp/entrypoint.ts b/client/webapp/entrypoint.ts new file mode 100644 index 0000000..95c06d7 --- /dev/null +++ b/client/webapp/entrypoint.ts @@ -0,0 +1,257 @@ +import * as electronRemote from '@electron/remote'; +const electronConsole = electronRemote.getGlobal('console') as Console; +import Logger from '../../logger/logger'; +const LOG = Logger.create(__filename, electronConsole); + +LOG.silly('script begins'); + +import * as path from 'path'; +import * as fs from 'fs/promises'; + +import GuildsManager from './guilds-manager'; + +import Globals from './globals'; + +import UI from './ui'; +import Actions from './actions'; +import { Changes, Channel, ConnectionInfo, GuildMetadata, Member, Message, Resource, Token } from './data-types'; +import Q from './q-module'; +import bindWindowButtonEvents from './elements/events-window-buttons'; +import bindTextInputEvents from './elements/events-text-input'; +import bindInfiniteScrollEvents from './elements/events-infinite-scroll'; +import bindConnectionEvents from './elements/events-connection'; +import bindAddGuildTitleEvents from './elements/events-guild-title'; +import bindAddGuildEvents from './elements/events-add-guild'; +import PersonalDB from './personal-db'; +import MessageRAMCache from './message-ram-cache'; +import ResourceRAMCache from './resource-ram-cache'; +import CombinedGuild from './guild-combined'; + +LOG.silly('modules loaded'); + +if (Globals.MESSAGES_PER_REQUEST >= Globals.MAX_CURRENT_MESSAGES) throw new Error('messages per request must be less than max current messages'); + +window.addEventListener('unhandledrejection', (e) => { + LOG.error('Unhandled Promise Rejection', e.reason); +}); + +window.addEventListener('error', (e) => { + LOG.error('Uncaught Error', e.error); +}); + +window.addEventListener('DOMContentLoaded', () => { + document.body.classList.remove('preload'); + + (async () => { + // Wait for the log to load the typescript source maps so that + // logs will include typescript files+line numbers instead of + // compiled javascript ones. + await LOG.ensureSourceMaps(); + LOG.silly('web client log source maps loaded'); + + // make sure the personaldb directory exists + await fs.mkdir(path.dirname(Globals.PERSONALDB_FILE), { recursive: true }); + const personalDB = await PersonalDB.create(Globals.PERSONALDB_FILE); + await personalDB.init(); + + LOG.silly('personal db initialized'); + + let messageRAMCache = new MessageRAMCache(); + let resourceRAMCache = new ResourceRAMCache(); + + LOG.silly('ram caches initialized'); + + const guildsManager = new GuildsManager(messageRAMCache, resourceRAMCache, personalDB); + await guildsManager.init(); + + LOG.silly('controller initialized'); + + const q = new Q(document); + const ui = new UI(document, q); + + LOG.silly('action classes initialized'); + + bindWindowButtonEvents(q); + bindTextInputEvents(document, q, ui); + bindInfiniteScrollEvents(q, ui); + bindConnectionEvents(document, q, ui); + bindAddGuildTitleEvents(document, q, ui); + bindAddGuildEvents(document, q, ui, guildsManager); + + LOG.silly('events bound'); + + // Add guild icons + await ui.setGuilds(guildsManager, guildsManager.guilds); + + if (guildsManager.guilds.length > 0) { + // Click on the first guild in the list + q.$('#guild-list .guild').click(); + } + + // Connection Events + + guildsManager.on('verified', async (guild: CombinedGuild) => { + (async () => { // update connection info + await Actions.fetchAndUpdateConnection(ui, guild); + })(); + (async () => { // refresh members list + await Actions.fetchAndUpdateMembers(q, ui, guild); + })(); + (async () => { // refresh channels list + await Actions.fetchAndUpdateChannels(q, ui, guild); + })(); + (async () => { // refresh current channel messages + if (ui.activeChannel === null) return; + if (ui.messagePairs.size == 0) { + // fetch messages again since there are no messages yet + await Actions.fetchAndUpdateMessagesRecent(q, ui, guild, ui.activeChannel); + } else { + // If we already have messages, just update the infinite scroll. + // NOTE: this will not add/remove new/deleted messages + ui.messagesAtTop = false; + ui.messagesAtBottom = false; + (q.$('#channel-feed-content-wrapper') as any).updateInfiniteScroll(); + } + })(); + }); + + guildsManager.on('disconnect', (guild: CombinedGuild) => { + // Update everyone with the 'unknown' status + (async () => { + await Actions.fetchAndUpdateConnection(ui, guild); + })(); + (async () => { + await Actions.fetchAndUpdateMembers(q, ui, guild); + })(); + }); + + // Change Events + + guildsManager.on('new-messages', async (guild: CombinedGuild, messages: Message[]) => { + if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return; + for (let message of messages) { + if (ui.activeChannel === null || ui.activeChannel.id !== message.channel.id) return; + if (ui.messagesAtBottom) { + // add the message to the bottom of the message feed + await ui.addMessages(guild, [ message ]); + ui.jumpMessagesToBottom(); + } else if (message.member.id == guild.memberId) { + // this set of messages will include the new messageguildId + LOG.debug('not at bottom, jumping down since message was sent by the current user'); + await Actions.fetchAndUpdateMessagesRecent(q, ui, guild, message.channel); + } + } + }); + + guildsManager.on('update-metadata', async (guild: CombinedGuild, guildMeta: GuildMetadata) => { + LOG.debug(`g#${guild.id} metadata updated`) + await ui.updateGuildName(guild, guildMeta.name); + + // Not using withPotentialError since keeping the old icon is a fine fallback + if (guildMeta.iconResourceId) { + try { + let icon = await guild.fetchResource(guildMeta.iconResourceId); + await ui.updateGuildIcon(guild, icon.data); + } catch (e) { + LOG.error('Error fetching new guild icon', e); + // Keep the old guild icon, just log an error. + // Should go through another try after a restart + } + } + }); + + guildsManager.on('remove-members', async (guild: CombinedGuild, members: Member[]) => { + LOG.debug(members.length + ' removed members'); + await ui.deleteMembers(guild, members); + }); + + guildsManager.on('update-members', async (guild: CombinedGuild, updatedMembers: Member[]) => { + LOG.debug(updatedMembers.length + ' updated members g#' + guild.id); + await ui.updateMembers(guild, updatedMembers); + if ( + ui.activeConnection !== null && + updatedMembers.find(member => member.id === (ui.activeConnection as ConnectionInfo).id) + ) { + await Actions.fetchAndUpdateConnection(ui, guild); + } + }); + + guildsManager.on('new-members', async (guild: CombinedGuild, members: Member[]) => { + LOG.debug(members.length + ' new members'); + await ui.addMembers(guild, members); + }); + + guildsManager.on('remove-channels', async (guild: CombinedGuild, channels: Channel[]) => { + LOG.debug(channels.length + ' removed channels'); + await ui.deleteChannels(guild, channels); + }); + + guildsManager.on('update-channels', async (guild: CombinedGuild, updatedChannels: Channel[]) => { + LOG.debug(updatedChannels.length + ' updated channels'); + await ui.updateChannels(guild, updatedChannels); + }); + + guildsManager.on('new-channels', async (guild: CombinedGuild, channels: Channel[]) => { + LOG.debug(channels.length + ' added channels'); + await ui.addChannels(guild, channels); + }); + + guildsManager.on('remove-messages', async (guild: CombinedGuild, messages: Message[]) => { + LOG.debug(messages.length + ' deleted messages'); + await ui.deleteMessages(guild, messages); + }); + + guildsManager.on('update-messages', async (guild: CombinedGuild, updatedMessages: Message[]) => { + LOG.debug(updatedMessages.length + ' updated messages'); + await ui.updateMessages(guild, updatedMessages); + }); + + guildsManager.on('new-messages', async (guild: CombinedGuild, messages: Message[]) => { + LOG.debug(messages.length + ' new messages'); + await ui.addMessages(guild, messages); + }); + + // Conflict Events + + guildsManager.on('conflict-metadata', async (guild: CombinedGuild, guildMeta: GuildMetadata) => { + LOG.debug('metadata conflict', { newMetadata: guildMeta }); + (async () => { await ui.updateGuildName(guild, guildMeta.name); })(); + (async () => { + let icon = await guild.fetchResource(guildMeta.iconResourceId); + await ui.updateGuildIcon(guild, icon.data); + })(); + }); + + guildsManager.on('conflict-channels', async (guild: CombinedGuild, changes: Changes) => { + LOG.debug('channels conflict', { changes }); + if (changes.deleted.length > 0) await ui.deleteChannels(guild, changes.deleted); + if (changes.added.length > 0) await ui.addChannels(guild, changes.added); + if (changes.updated.length > 0) await ui.updateChannels(guild, changes.updated.map(pair => pair.newDataPoint)); + }); + + guildsManager.on('conflict-members', async (guild: CombinedGuild, changes: Changes) => { + LOG.debug('members conflict', { changes }); + if (changes.deleted.length > 0) await ui.deleteMembers(guild, changes.deleted); + if (changes.added.length > 0) await ui.addMembers(guild, changes.added); + if (changes.updated.length > 0) await ui.updateMembers(guild, changes.updated.map(pair => pair.newDataPoint)); + }); + + guildsManager.on('conflict-messages', async (guild: CombinedGuild, changes: Changes) => { + LOG.debug('messages conflict', { changes }); + if (changes.deleted.length > 0) await ui.deleteMessages(guild, changes.deleted); + if (changes.added.length > 0) await ui.addMessages(guild, changes.added); + if (changes.updated.length > 0) await ui.updateMessages(guild, changes.updated.map(pair => pair.newDataPoint)); + }); + + guildsManager.on('conflict-tokens', async (guild: CombinedGuild, changes: Changes) => { + LOG.debug('tokens conflict', { changes }); + // TODO + }); + + guildsManager.on('conflict-resource', async (guild: CombinedGuild, oldResource: Resource, newResource: Resource) => { + LOG.debug('resource conflict', { oldResource, newResource }); + // TODO (these changes should not happen often if at all) + }); + })(); +}); + diff --git a/client/webapp/globals.ts b/client/webapp/globals.ts index d5f8030..e1e5dc5 100644 --- a/client/webapp/globals.ts +++ b/client/webapp/globals.ts @@ -1,26 +1,27 @@ export default class Globals { static DOWNLOAD_DIR = '/home/michael/Downloads'; // TODO: not hard coded + static PERSONALDB_FILE = './db/personal.db'; - static DEFAULT_SOCKET_TIMEOUT = 5000; // Wait up to 5000ms for the server to respond before rejecting/throwing an error + static DEFAULT_SOCKET_TIMEOUT = 5000; // Wait up to 5000ms for the guild to respond before rejecting/throwing an error static MAX_CURRENT_MESSAGES = 300; static MESSAGES_PER_REQUEST = 100; static MAX_TEXT_MESSAGE_LENGTH = 1024 * 2; // 2 KB character message max (for readability) - static MAX_RESOURCE_SIZE = 1024 * 1024 * 10; // 10 MB max resource size (server is 50, client-side is limited until socket.io-stream) + static MAX_RESOURCE_SIZE = 1024 * 1024 * 10; // 10 MB max resource size (guild is 50, client-side is limited until socket.io-stream) static MAX_AVATAR_SIZE = 1024 * 128; // 128 KB max avatar size static MAX_DISPLAY_NAME_LENGTH = 32; // 32 char max display name length - static MAX_ICON_SIZE = 1024 * 128; // 128 KB max server icon size - static MAX_SERVER_NAME_LENGTH = 64; // 64 char max server name length + static MAX_ICON_SIZE = 1024 * 128; // 128 KB max guild icon size + static MAX_GUILD_NAME_LENGTH = 64; // 64 char max guild name length static MAX_CHANNEL_NAME_LENGTH = 32; // 32 char max channel name length static MAX_CHANNEL_FLAVOR_TEXT_LENGTH = 256; // 256 char max channel flavor text length static MAX_CACHED_CHANNEL_MESSAGES = 1000; // the 1000 most recent messages in each text channel are cached (in the sqlite db) - static MAX_SERVER_RESOURCE_CACHE_SIZE = 1024 * 1024 * 1024; // 1 GB max resource cache per server + static MAX_SERVER_RESOURCE_CACHE_SIZE = 1024 * 1024 * 1024; // 1 GB max resource cache per guild static MAX_CACHED_RESOURCE_SIZE = 1024 * 1024 * 4; // 4 MB is the biggest resource that will be cached static MAX_RAM_CACHED_MESSAGES_CHANNEL_CHARACTERS = 1024 * 1024 * 64; // at most, 64 MB of channel diff --git a/client/webapp/guild-combined.ts b/client/webapp/guild-combined.ts index 4be5a68..0281090 100644 --- a/client/webapp/guild-combined.ts +++ b/client/webapp/guild-combined.ts @@ -7,7 +7,7 @@ import * as socketio from 'socket.io-client'; import PersonalDBGuild from './guild-personal-db'; import RAMGuild from './guild-ram'; import SocketGuild from './guild-socket'; -import { Changes, Channel, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, ServerMetaData, SocketConfig, Token } from './data-types'; +import { Changes, Channel, ConnectionInfo, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, ServerMetaData, SocketConfig, Token } from './data-types'; import MessageRAMCache from "./message-ram-cache"; import PersonalDB from "./personal-db"; @@ -28,7 +28,7 @@ export default class CombinedGuild extends EventEmittersocket connection on d/c? - // Connect/Disconnect this.socketGuild.on('connect', () => { LOG.info(`g#${this.id} connected`); this.emit('connect'); }); - this.socketGuild.on('disconnect', () => { + this.socketGuild.on('disconnect', async () => { LOG.info(`g#${this.id} disconnected`); this.unverify(); + await personalDB.clearAllMembersStatus(this.id); this.emit('disconnect'); }); @@ -175,12 +174,20 @@ export default class CombinedGuild extends EventEmitter { if (this.ramGuild.getMembers().size === 0) { await this.fetchMembers(); @@ -195,15 +202,59 @@ export default class CombinedGuild extends EventEmitter> { + async grabRAMMembersMap(): Promise> { await this.ensureRAMMembers(); return this.ramGuild.getMembers(); } - public async grabRAMChannelsMap(): Promise> { + async grabRAMChannelsMap(): Promise> { await this.ensureRAMChannels(); return this.ramGuild.getChannels(); } + + async fetchConnectionInfo(): Promise { + let connection: ConnectionInfo = { + id: null, + avatarResourceId: null, + displayName: 'Connecting...', + status: '', + privileges: [], + roleName: null, + roleColor: null, + rolePriority: null + }; + if (this.socketGuild.verifier.isVerified) { + let members = await this.grabRAMMembersMap(); + let member = members.get(this.memberId); + if (member) { + connection.id = member.id; + connection.avatarResourceId = member.avatarResourceId; + connection.displayName = member.displayName; + connection.status = member.status; + connection.roleName = member.roleName; + connection.roleColor = member.roleColor; + connection.rolePriority = member.rolePriority; + connection.privileges = member.privileges; + } else { + LOG.warn('unable to find self in members'); + } + } else { + let members = await this.personalDBGuild.fetchMembers(); + if (members) { + let member = members.find(m => m.id === this.memberId); + if (member) { + connection.id = member.id; + connection.avatarResourceId = member.avatarResourceId; + connection.displayName = member.displayName; + connection.status = 'connecting'; + connection.privileges = []; + } else { + LOG.warn('unable to find self in cached members'); + } + } + } + return connection; + } // Fetched through the triple-cache system (RAM -> Disk -> Server) async fetchMetadata(): Promise { @@ -247,6 +298,7 @@ export default class CombinedGuild extends EventEmitter { await this.socketGuild.requestSetAvatar(avatar); } + // TODO: Rename Server -> Guild async requestSetServerName(serverName: string): Promise { await this.socketGuild.requestSetServerName(serverName); } diff --git a/client/webapp/guild-socket.ts b/client/webapp/guild-socket.ts index 1a64482..36e81d3 100644 --- a/client/webapp/guild-socket.ts +++ b/client/webapp/guild-socket.ts @@ -18,7 +18,7 @@ export default class SocketGuild extends EventEmitter implements As constructor( private socket: socketio.Socket, - private verifier: SocketVerifier + public verifier: SocketVerifier ) { super(); this.socket.on('connect', async () => { @@ -63,6 +63,10 @@ export default class SocketGuild extends EventEmitter implements As }); } + public disconnect() { + this.socket.disconnect(); + } + // server helper functions private async query(timeout: number, endpoint: string, ...args: any[]): Promise { diff --git a/client/webapp/guild-types.ts b/client/webapp/guild-types.ts index a987bc3..261185b 100644 --- a/client/webapp/guild-types.ts +++ b/client/webapp/guild-types.ts @@ -107,23 +107,25 @@ export interface SyncLackable { export type Lackable = AsyncLackable | SyncLackable; - - // A Connectable can emit server-like events export type Connectable = { 'connect': () => void; 'disconnect': () => void; + 'verified': () => void; 'update-metadata': (guildMeta: GuildMetadata) => void; 'new-channels': (channels: Channel[]) => void; 'update-channels': (updatedChannels: Channel[]) => void; + 'remove-channels': (removedChannels: Channel[]) => void; 'new-members': (members: Member[]) => void; 'update-members': (updatedMembers: Member[]) => void; + 'remove-members': (removedMembers: Member[]) => void; 'new-messages': (messages: Message[]) => void; 'update-messages': (updatedMessages: Message[]) => void; + 'remove-messages': (removedMessages: Message[]) => void; } // A Conflictable could emit conflict-based events if data changed based on verification diff --git a/client/webapp/controller.ts b/client/webapp/guilds-manager.ts similarity index 90% rename from client/webapp/controller.ts rename to client/webapp/guilds-manager.ts index c97a571..dab42c1 100644 --- a/client/webapp/controller.ts +++ b/client/webapp/guilds-manager.ts @@ -11,26 +11,30 @@ import * as crypto from 'crypto'; import { Changes, Channel, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, ServerConfig, SocketConfig, Token } from './data-types'; import { IAddServerData } from './elements/overlay-add-server'; -import { DefaultEventMap, EventEmitter } from 'tsee'; +import { EventEmitter } from 'tsee'; import CombinedGuild from './guild-combined'; import PersonalDB from './personal-db'; import MessageRAMCache from './message-ram-cache'; import ResourceRAMCache from './resource-ram-cache'; -export default class Controller extends EventEmitter<{ +export default class GuildsManager extends EventEmitter<{ 'connect': (guild: CombinedGuild) => void; 'disconnect': (guild: CombinedGuild) => void; + 'verified': (guild: CombinedGuild) => void; 'update-metadata': (guild: CombinedGuild, guildMeta: GuildMetadata) => void; 'new-channels': (guild: CombinedGuild, channels: Channel[]) => void; 'update-channels': (guild: CombinedGuild, updatedChannels: Channel[]) => void; + 'remove-channels': (guild: CombinedGuild, removedChannels: Channel[]) => void; 'new-members': (guild: CombinedGuild, members: Member[]) => void; 'update-members': (guild: CombinedGuild, updatedMembers: Member[]) => void; + 'remove-members': (guild: CombinedGuild, removedMembers: Member[]) => void; 'new-messages': (guild: CombinedGuild, messages: Message[]) => void; 'update-messages': (guild: CombinedGuild, updatedMessages: Message[]) => void; + 'remove-messages': (guild: CombinedGuild, removedMessages: Message[]) => void; 'conflict-metadata': (guild: CombinedGuild, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => void; 'conflict-channels': (guild: CombinedGuild, changes: Changes) => void; @@ -141,7 +145,7 @@ export default class Controller extends EventEmitter<{ return await new Promise((resolve, reject) => { let clientPublicKeyDerBuff = publicKey.export({ type: 'spki', format: 'der' }); - Controller._socketEmitTimeout(socket, 5000, 'register-with-token', + GuildsManager._socketEmitTimeout(socket, 5000, 'register-with-token', token, clientPublicKeyDerBuff, displayName, avatarBuff, async (errStr: string, dataMember: any, dataMetadata: any) => { if (errStr) { reject(new Error(errStr)); diff --git a/client/webapp/personal-db.ts b/client/webapp/personal-db.ts index 779ce74..2e46b3c 100644 --- a/client/webapp/personal-db.ts +++ b/client/webapp/personal-db.ts @@ -14,7 +14,7 @@ export default class PersonalDB { private readonly db: sqlite.Database ) {} - public async create(filePath: string): Promise { + public static async create(filePath: string): Promise { return new PersonalDB( await sqlite.open({ driver: sqlite3.Database, filename: filePath }) ); diff --git a/client/webapp/script.ts b/client/webapp/script.ts deleted file mode 100644 index dc6a309..0000000 --- a/client/webapp/script.ts +++ /dev/null @@ -1,292 +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); - -LOG.silly('script begins'); - -import Controller from './controller'; - -import DBCache from './db-cache'; -import Globals from './globals'; - -import UI from './ui'; -import Actions from './actions'; -import { CacheServerData, Channel, ConnectionInfo, Member, Message, ServerMetaData } from './data-types'; -import ClientController from './client-controller'; -import Q from './q-module'; -import bindWindowButtonEvents from './elements/events-window-buttons'; -import bindTextInputEvents from './elements/events-text-input'; -import bindInfiniteScrollEvents from './elements/events-infinite-scroll'; -import bindConnectionEvents from './elements/events-connection'; -import bindAddServerTitleEvents from './elements/events-server-title'; -import bindAddServerEvents from './elements/events-add-server'; - -LOG.silly('modules loaded'); - -if (Globals.MESSAGES_PER_REQUEST >= Globals.MAX_CURRENT_MESSAGES) throw new Error('messages per request must be less than max current messages'); - -window.addEventListener('unhandledrejection', (e) => { - LOG.error('Unhandled Promise Rejection', e.reason); -}); - -window.addEventListener('error', (e) => { - LOG.error('Uncaught Error', e.error); -}); - -window.addEventListener('DOMContentLoaded', () => { - document.body.classList.remove('preload'); - - (async () => { - await LOG.ensureSourceMaps(); - LOG.silly('web client log source maps loaded'); - - const dbCache = await DBCache.connect(); - await dbCache.init(); - - LOG.silly('cache initialized'); - - const controller = new Controller(dbCache); - await controller.init(); - - LOG.silly('controller initialized'); - - const q = new Q(document); - const ui = new UI(document, q); - - LOG.silly('action classes initialized'); - - bindWindowButtonEvents(q); - bindTextInputEvents(document, q, ui); - bindInfiniteScrollEvents(q, ui); - bindConnectionEvents(document, q, ui); - bindAddServerTitleEvents(document, q, ui); - bindAddServerEvents(document, q, ui, controller); - - LOG.silly('events bound'); - - // Add server icons - await ui.setServers(controller, controller.servers); - - if (controller.servers.length > 0) { - // Click on the first server in the list - q.$('#server-list .server').click(); - } - - // Receive Current Channel Messages - controller.on('new-message', async (guild: CombinedGuild, message: Message) => { - if (ui.activeGuild === null || ui.activeGuild.id !== server.id) return; - if (ui.activeChannel === null || ui.activeChannel.id !== message.channel.id) return; - if (ui.messagesAtBottom) { - // add the message to the bottom of the message feed - await ui.addMessagesAfter(server, message.channel, [ message ], null); - ui.jumpMessagesToBottom(); - } else if (message.member.id == server.memberId) { - // this set of messages will include the new messageguildId - LOG.debug('not at bottom, jumping down since message was sent by the current user'); - await Actions.fetchAndUpdateMessagesRecent(q, ui, server, message.channel); - } - }); - - controller.on('verified', async (guild: CombinedGuild) => { - (async () => { // update connection info - await Actions.fetchAndUpdateConnection(ui, server); - })(); - (async () => { // refresh members cache - if (server.members) { - await server.fetchMembers(); - } else { - await Actions.fetchAndUpdateMembers(q, ui, server); - } - })(); - (async () => { // refresh channels cache - if (server.channels) { - await server.fetchChannels(); - } else { - await Actions.fetchAndUpdateChannels(q, ui, server); - } - })(); - (async () => { // refresh current channel messages - if (ui.activeChannel === null) return; - if (ui.messagePairs.size == 0) { - // fetch messages again since there are no messages yet - await Actions.fetchAndUpdateMessagesRecent(q, ui, server, ui.activeChannel); - } else { - // Just update the infinite scroll. NOTE: this will not remove deleted messages - ui.messagesAtTop = false; - ui.messagesAtBottom = false; - (q.$('#channel-feed-content-wrapper') as any).updateInfiniteScroll(); - } - })(); - }); - - controller.on('disconnected', (guild: CombinedGuild) => { - (async () => { - await Actions.fetchAndUpdateConnection(ui, server); - })(); - (async () => { - await Actions.fetchAndUpdateMembers(q, ui, server); - })(); - }); - - controller.on('update-server', async (guild: CombinedGuild, serverData: ServerMetaData | CacheServerData) => { - LOG.debug(`s#${server.id} metadata updated`) - await ui.updateServerName(server, serverData.name); - - // Not using withPotentialError since keeping the old icon is a fine fallback - if (serverData.iconResourceId) { - try { - let iconBuff = await server.fetchResource(serverData.iconResourceId); - await ui.updateServerIcon(server, iconBuff); - } catch (e) { - LOG.error('Error fetching new server icon', e); - // Keep the old server icon, just log an error. - // Should go through another try after a restart - } - } - }); - - controller.on('deleted-members', async (guild: CombinedGuild, members: Member[]) => { - LOG.debug(members.length + ' deleted members'); - await ui.deleteMembers(server, members); - }); - - controller.on('updated-members', async (guild: CombinedGuild, data: { oldMember: Member, newMember: Member }[]) => { - LOG.debug(data.length + ' updated members s#' + server.id); - await ui.updateMembers(server, data); - if ( - ui.activeConnection !== null && - data.find(change => change.newMember.id === (ui.activeConnection as ConnectionInfo).id) - ) { - await Actions.fetchAndUpdateConnection(ui, server); - } - }); - - controller.on('added-members', async (guild: CombinedGuild, members: Member[]) => { - LOG.debug(members.length + ' added members'); - await ui.addMembers(server, members); - }); - - controller.on('deleted-channels', async (guild: CombinedGuild, channels: Channel[]) => { - LOG.debug(channels.length + ' deleted channels'); - await ui.deleteChannels(server, channels); - }); - - controller.on('updated-channels', async (guild: CombinedGuild, data: { oldChannel: Channel, newChannel: Channel }[]) => { - LOG.debug(data.length + ' updated channels'); - await ui.updateChannels(server, data); - }); - - controller.on('added-channels', async (guild: CombinedGuild, channels: Channel[]) => { - LOG.debug(channels.length + ' added channels'); - await ui.addChannels(server, channels); - }); - - controller.on('deleted-messages', async (guild: CombinedGuild, channel: Channel, messages: Message[]) => { - LOG.debug(messages.length + ' deleted messages'); - //LOG.debug('deleted messages:', { messages: deletedMessages.map(message => message.text) }); - // messages were deleted but the cache still had them - await ui.deleteMessages(server, channel, messages); - }); - - controller.on('updated-messages', async (guild: CombinedGuild, channel: Channel, data: { oldMessage: Message, newMessage: Message }[]) => { - LOG.debug(data.length + ' updated messages'); - // messages were updated on the server-side - await ui.updateMessages(server, channel, data); - }); - - controller.on('added-messages', async (guild: CombinedGuild, channel: Channel, addedAfter: Map, addedBefore: Map) => { - LOG.debug(addedAfter.size + ' added messages'); // addedBefore.size should equal addedAfter.size - //LOG.debug('added messages', { messages: Array.from(addedAfter.values()).map(message => message.text) }); - // messages were added in a place that the cache did not have them - - if (!ui.isMessagePairsServer(server)) return; // these messages are not from the ones in the feed - if (!ui.isMessagePairsChannel(channel)) return; // these messages are not from the ones in the feed - - let currentMessagesSorted = Array.from(ui.messagePairs.values()).sort((a, b) => { - return a.message.sent.getTime() - b.message.sent.getTime(); - }); - - // length could be 0 if all previous messages were 'deleted' - if (currentMessagesSorted.length == 0) { - // Simply set the text channel messages rather than calculating where to add them - ui.setMessages(server, channel, Array.from(addedAfter.values()), { atTop: false, atBottom: true }); - return; - } - - let firstMessage = currentMessagesSorted[0].message; - let lastMessage = currentMessagesSorted[currentMessagesSorted.length - 1].message; - - // messages that were updated (currently extraneous) - // Note: this should never happen since these should be handled by updated-messages - // Note: this may be used to replace dummy pre-sent messages - let toUpdate: { newMessage: Message, oldMessage: Message }[] = []; - for (let addedMessage of addedBefore.values()) { - if (ui.messagePairs.has(addedMessage.id)) { - toUpdate.push({ - newMessage: addedMessage, - oldMessage: (ui.messagePairs.get(addedMessage.id) as { message: Message, element: HTMLElement }).message - }); - } - } - if (toUpdate.length > 0) { - LOG.warn('updating messages in added-messages... this was intended to be extraneous...', { toUpdate: toUpdate, toUpdateLength: toUpdate.length }); - await ui.updateMessages(server, channel, toUpdate); - } - - // messages before the first message - let toAddBefore: Message[] = []; - let nextFirstMessage = firstMessage; - while (addedBefore.has(nextFirstMessage.id)) { - nextFirstMessage = addedBefore.get(nextFirstMessage.id) as Message; - toAddBefore.unshift(nextFirstMessage); - } - if (toAddBefore.length > 0) { - LOG.debug('adding ' + toAddBefore.length + ' before'); - await ui.addMessagesBefore(server, channel, toAddBefore, firstMessage); - } - - // messages after the last message - let toAddAfter: Message[] = []; - let nextLastMessage = lastMessage; - while (addedAfter.has(nextLastMessage.id)) { - nextLastMessage = addedAfter.get(nextLastMessage.id) as Message; - toAddAfter.push(nextLastMessage); - } - if (toAddAfter.length > 0) { - LOG.debug('adding ' + toAddAfter.length + ' after'); - await ui.addMessagesAfter(server, channel, toAddAfter, lastMessage); - } - - // messages added between messages already in the feed - let toAddBetween: { messageTop: HTMLElement, messageBottom: HTMLElement, betweenMessages: Message[] }[] = []; - for (let i = 1; i < currentMessagesSorted.length; ++i) { - let messageTop = currentMessagesSorted[i - 1].message; - let messageBottom = currentMessagesSorted[i].message; - let betweenMessages: Message[] = []; - let followMessage = messageTop; - // this should never be null as long as there is an addedAfter (will throw error if this is not the case) - let lastFollowMessage = addedBefore.get(messageBottom.id) as Message; - while (addedAfter.has(followMessage.id) && followMessage.id != lastFollowMessage.id) { - followMessage = addedAfter.get(followMessage.id) as Message; - betweenMessages.push(followMessage); - } - if (betweenMessages.length > 0) { - toAddBetween.push({ - messageTop: (ui.messagePairs.get(messageTop.id) as { message: Message, element: HTMLElement }).element, - messageBottom: (ui.messagePairs.get(messageBottom.id) as { message: Message, element: HTMLElement }).element, - betweenMessages: betweenMessages - }); - } - } - // add messages in between - if (toAddBetween.length > 0) { - LOG.debug('adding ' + toAddBetween.length + ' between sets'); - } - for (let messageSet of toAddBetween) { - await ui.addMessagesBetween(server, channel, messageSet.betweenMessages, messageSet.messageTop, messageSet.messageBottom); - } - }); - })(); -}); - diff --git a/client/webapp/socket-verifier.ts b/client/webapp/socket-verifier.ts index f7d4812..c09b615 100644 --- a/client/webapp/socket-verifier.ts +++ b/client/webapp/socket-verifier.ts @@ -1,11 +1,12 @@ import * as crypto from 'crypto'; import * as socketio from 'socket.io-client'; +import { EventEmitter } from 'tsee'; import DedupAwaiter from "./dedup-awaiter"; import Util from './util'; // Automatically re-verifies the socket when connected -export default class SocketVerifier { +export default class SocketVerifier extends EventEmitter<{ 'verified': () => void }> { public isVerified = false; private memberId: string | null = null; private verifyDedup = new DedupAwaiter(async () => { return await this.doVerify(); }); @@ -15,6 +16,7 @@ export default class SocketVerifier { private publicKey: crypto.KeyObject, private privateKey: crypto.KeyObject ) { + super(); socket.on('connect', async () => { await this.verify(); }); @@ -23,7 +25,6 @@ export default class SocketVerifier { }); } - // TODO: Move this to a "query/queryDedup" request /** Verifies this client with the server. This function must be called before the server will send messages or give results */ private async doVerify(): Promise { if (this.socket.disconnected) throw new Error('socket is disconnected'); @@ -48,6 +49,7 @@ export default class SocketVerifier { } this.memberId = null; resolve(memberId); + this.emit('verified'); }); }); }); diff --git a/client/webapp/ui.ts b/client/webapp/ui.ts index 4deea0d..6ad05ca 100644 --- a/client/webapp/ui.ts +++ b/client/webapp/ui.ts @@ -15,7 +15,7 @@ import Q from './q-module'; import createGuildListGuild from './elements/guild-list-guild'; import createChannel from './elements/channel'; import createMember from './elements/member'; -import Controller from './controller'; +import GuildsManager from './guilds-manager'; import createMessage from './elements/message'; interface SetMessageProps { @@ -161,20 +161,20 @@ export default class UI { }); } - public async setGuilds(controller: Controller, guilds: CombinedGuild[]): Promise { + public async setGuilds(guildsManager: GuildsManager, guilds: CombinedGuild[]): Promise { await this._guildsLock.push(() => { Q.clearChildren(this.q.$('#guild-list')); for (let guild of guilds) { - let element = createGuildListGuild(this.document, this.q, this, controller, guild); + let element = createGuildListGuild(this.document, this.q, this, guildsManager, guild); this.q.$('#guild-list').appendChild(element); } }); } - public async addGuild(controller: Controller, guild: CombinedGuild): Promise { + public async addGuild(guildsManager: GuildsManager, guild: CombinedGuild): Promise { let element: HTMLElement | null = null; await this._guildsLock.push(() => { - element = createGuildListGuild(this.document, this.q, this, controller, guild) as HTMLElement; + element = createGuildListGuild(this.document, this.q, this, guildsManager, guild) as HTMLElement; this.q.$('#guild-list').appendChild(element); }); if (element == null) throw new ShouldNeverHappenError('element was not set'); @@ -262,22 +262,22 @@ export default class UI { }); } - public async updateChannels(guild: CombinedGuild, data: { oldChannel: Channel, newChannel: Channel }[]): Promise { + public async updateChannels(guild: CombinedGuild, updatedChannels: Channel[]): Promise { await this.lockChannels(guild, async () => { - for (const { oldChannel, newChannel } of data) { - let oldElement = this.q.$('#channel-list .channel[meta-id="' + newChannel.id + '"]'); - let newElement = createChannel(this.document, this.q, this, guild, newChannel); + for (const channel of updatedChannels) { + let oldElement = this.q.$('#channel-list .channel[meta-id="' + channel.id + '"]'); + let newElement = createChannel(this.document, this.q, this, guild, channel); oldElement.parentElement?.replaceChild(newElement, oldElement); await this.updateChannelPosition(guild, newElement); - if (this.activeChannel !== null && this.activeChannel.id === newChannel.id) { + if (this.activeChannel !== null && this.activeChannel.id === channel.id) { newElement.classList.add('active'); // See also setActiveChannel - this.q.$('#channel-name').innerText = newChannel.name; - this.q.$('#channel-flavor-text').innerText = newChannel.flavorText ?? ''; - this.q.$('#channel-flavor-divider').style.visibility = newChannel.flavorText ? 'visible' : 'hidden'; - this.q.$('#text-input').setAttribute('placeholder', 'Message #' + newChannel.name); + this.q.$('#channel-name').innerText = channel.name; + this.q.$('#channel-flavor-text').innerText = channel.flavorText ?? ''; + this.q.$('#channel-flavor-divider').style.visibility = channel.flavorText ? 'visible' : 'hidden'; + this.q.$('#text-input').setAttribute('placeholder', 'Message #' + channel.name); } } }); @@ -358,24 +358,26 @@ export default class UI { }); } - public async updateMembers(guild: CombinedGuild, data: { oldMember: Member, newMember: Member }[]): Promise { + public async updateMembers(guild: CombinedGuild, updatedMembers: Member[]): Promise { await this.lockMembers(guild, async () => { - for (const { oldMember, newMember } of data) { - let oldElement = this.q.$_('#guild-members .member[meta-id="' + newMember.id + '"]'); + for (const member of updatedMembers) { + let oldElement = this.q.$_('#guild-members .member[meta-id="' + member.id + '"]'); if (oldElement) { - let newElement = createMember(this.q, guild, newMember); + let newElement = createMember(this.q, guild, member); oldElement.parentElement?.replaceChild(newElement, oldElement); await this.updateMemberPosition(guild, newElement); } } }); + + // Update the messages too if (this.activeChannel === null) return; await this.lockMessages(guild, this.activeChannel, () => { - for (const { oldMember, newMember } of data) { - let newStyle = newMember.roleColor ? 'color: ' + newMember.roleColor : null; - let newName = newMember.displayName; + for (const member of updatedMembers) { + let newStyle = member.roleColor ? 'color: ' + member.roleColor : null; + let newName = member.displayName; // the extra query selectors may be overkill - for (let messageElement of this.q.$$(`.message[meta-member-id="${newMember.id}"]`)) { + for (let messageElement of this.q.$$(`.message[meta-member-id="${member.id}"]`)) { let 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); @@ -408,7 +410,32 @@ export default class UI { return element && this.messagePairs.get(element.getAttribute('meta-id')) || null; } - public async addMessagesBefore(guild: CombinedGuild, channel: Channel, messages: Message[], prevTopMessage: Message | null): Promise { + public async addMessages(guild: CombinedGuild, messages: Message[]) { + let channelIds = new Set(messages.map(message => message.channel.id)); + for (let channelId of channelIds) { + let channelMessages = messages.filter(message => message.channel.id === channelId); + channelMessages = channelMessages.sort((a, b) => a.sent.getTime() - b.sent.getTime()); + + // No Previous Messages is an easy case + if (this.messagePairs.size === 0) { + await this.addMessagesBefore(guild, { id: channelId }, channelMessages, null); + continue; + } + + let topMessagePair = this.getTopMessagePair() as { message: Message, element: HTMLElement }; + let bottomMessagePair = this.getBottomMessagePair() as { message: Message, element: HTMLElement }; + + let aboveMessages = messages.filter(message => message.sent < topMessagePair.message.sent); + let belowMessages = messages.filter(message => message.sent > bottomMessagePair.message.sent); + let betweenMessages = messages.filter(message => message.sent >= topMessagePair.message.sent && message.sent <= bottomMessagePair.message.sent); + + if (aboveMessages.length > 0) await this.addMessagesBefore(guild, { id: channelId }, aboveMessages, topMessagePair.message); + if (belowMessages.length > 0) await this.addMessagesAfter(guild, { id: channelId }, belowMessages, bottomMessagePair.message); + if (betweenMessages.length > 0) await this.addMessagesBetween(guild, { id: channelId }, betweenMessages, topMessagePair.element, bottomMessagePair.element); + } + } + + public async addMessagesBefore(guild: CombinedGuild, channel: Channel | { id: string }, messages: Message[], prevTopMessage: Message | null): Promise { this.lockMessages(guild, channel, () => { if (prevTopMessage && this.getTopMessagePair()?.message.id !== prevTopMessage.id) return; @@ -495,7 +522,7 @@ export default class UI { } // TODO: use topMessage, bottomMessage / topMessageId, bottomMessageId instead? - public async addMessagesBetween(guild: CombinedGuild, channel: Channel, messages: Message[], topElement: HTMLElement, bottomElement: HTMLElement): Promise { + private async addMessagesBetween(guild: CombinedGuild, channel: Channel | { id: string }, messages: Message[], topElement: HTMLElement, bottomElement: HTMLElement): Promise { await this.lockMessages(guild, channel, () => { if (!(messages.length > 0 && topElement != null && bottomElement != null && bottomElement == Q.nextElement(topElement))) { LOG.error('invalid messages between', { messages, top: topElement.innerText, bottom: bottomElement.innerText, afterTop: Q.nextElement(topElement)?.innerText }); @@ -618,35 +645,43 @@ export default class UI { this.messagesAtBottom = true; } - public async deleteMessages(guild: CombinedGuild, channel: Channel, messages: Message[]) { - await this.lockMessages(guild, channel, () => { - for (let message of messages) { - if (this.messagePairs.has(message.id)) { - let messagePair = this.messagePairs.get(message.id) as { message: Message, element: HTMLElement }; - messagePair.element.parentElement?.removeChild(messagePair.element); - // TODO: we should be updating messages sent below this message - // however, these events should be relatively rare so that's for the future - this.messagePairs.delete(message.id); + public async deleteMessages(guild: CombinedGuild, messages: Message[]) { + let channelIds = new Set(messages.map(message => message.channel.id)); + for (let channelId of channelIds) { + let channelMessages = messages.filter(message => message.channel.id === channelId); + await this.lockMessages(guild, { id: channelId }, () => { + for (let message of channelMessages) { + if (this.messagePairs.has(message.id)) { + let messagePair = this.messagePairs.get(message.id) as { message: Message, element: HTMLElement }; + messagePair.element.parentElement?.removeChild(messagePair.element); + // TODO: we should be updating messages sent below this message + // however, these events should be relatively rare so that's for the future + this.messagePairs.delete(message.id); + } } - } - }); + }); + } } - public async updateMessages(guild: CombinedGuild, channel: Channel, data: { oldMessage: Message, newMessage: Message }[]): Promise { - await this.lockMessages(guild, channel, () => { - for (const { oldMessage, newMessage } of data) { - if (this.messagePairs.has(oldMessage.id)) { - let oldElement = (this.messagePairs.get(oldMessage.id) as { message: Message, element: HTMLElement }).element; - let prevElement = Q.previousElement(oldElement); - let prevMessage = prevElement && (this.messagePairs.get(prevElement.getAttribute('meta-id')) as { message: Message, element: HTMLElement }).message; - let newElement = createMessage(this.document, this.q, guild, newMessage, prevMessage); - oldElement.parentElement?.replaceChild(newElement, oldElement); - // TODO: we should be updating messages sent below this message - // however, these events should be relatively rare so that's for the future - this.messagePairs.set(oldMessage.id, { message: newMessage, element: newElement }); + public async updateMessages(guild: CombinedGuild, updatedMessages: Message[]): Promise { + let channelIds = new Set(updatedMessages.map(message => message.channel.id)); + for (let channelId of channelIds) { + let channelMessages = updatedMessages.filter(message => message.channel.id === channelId); + await this.lockMessages(guild, { id: channelId }, () => { + for (const message of channelMessages) { + if (this.messagePairs.has(message.id)) { + let oldElement = (this.messagePairs.get(message.id) as { message: Message, element: HTMLElement }).element; + let prevElement = Q.previousElement(oldElement); + let prevMessage = prevElement && (this.messagePairs.get(prevElement.getAttribute('meta-id')) as { message: Message, element: HTMLElement }).message; + let newElement = createMessage(this.document, this.q, guild, message, prevMessage); + oldElement.parentElement?.replaceChild(newElement, oldElement); + // TODO: we should be updating messages sent below this message + // however, these events should be relatively rare so that's for the future + this.messagePairs.set(message.id, { message: message, element: newElement }); + } } - } - }); + }); + } } public async addMessagesErrorIndicatorBefore(guild: CombinedGuild, channel: Channel, errorIndicatorElement: HTMLElement): Promise {