From 64490d027fc183d6a70682d037f5e1f6921df162 Mon Sep 17 00:00:00 2001 From: Michael Peters Date: Sun, 21 Nov 2021 12:29:42 -0600 Subject: [PATCH] controller is guildized --- client/webapp/actions.ts | 62 ++-- client/webapp/auto-verifier.ts | 6 + client/webapp/controller.ts | 136 +++++---- client/webapp/data-types.ts | 4 +- client/webapp/elements/channel.ts | 2 +- client/webapp/elements/context-menu-conn.ts | 2 +- client/webapp/elements/context-menu-img.ts | 2 +- .../webapp/elements/context-menu-srv-title.ts | 2 +- client/webapp/elements/context-menu-srv.ts | 2 +- client/webapp/elements/events-connection.ts | 6 +- .../webapp/elements/events-infinite-scroll.ts | 6 +- client/webapp/elements/events-server-title.ts | 6 +- client/webapp/elements/events-text-input.ts | 16 +- ...ver-list-server.ts => guild-list-guild.ts} | 56 ++-- client/webapp/elements/member.ts | 2 +- client/webapp/elements/message.ts | 2 +- client/webapp/elements/msg-img-res-cont.ts | 2 +- client/webapp/elements/msg-img-res.ts | 2 +- client/webapp/elements/msg-res-cont.ts | 2 +- client/webapp/elements/msg-res.ts | 2 +- client/webapp/elements/msg-txt-cont.ts | 2 +- client/webapp/elements/msg-txt.ts | 2 +- client/webapp/elements/overlay-add-server.ts | 4 +- .../webapp/elements/overlay-create-channel.ts | 2 +- .../elements/overlay-create-invite-token.ts | 2 +- client/webapp/elements/overlay-image.ts | 2 +- .../webapp/elements/overlay-modify-channel.ts | 2 +- client/webapp/elements/overlay-personalize.ts | 2 +- .../elements/overlay-server-settings.ts | 2 +- client/webapp/elements/overlay-token-log.ts | 2 +- .../elements/overlay-upload-datatransfer.ts | 2 +- .../elements/overlay-upload-drop-target.ts | 2 +- client/webapp/elements/overlay-upload-path.ts | 2 +- .../webapp/elements/require/base-elements.ts | 2 +- .../webapp/elements/require/elements-util.ts | 2 +- client/webapp/fetchable-pair-verifier.ts | 2 +- client/webapp/guild-combined.ts | 27 +- client/webapp/message-ram-cache.ts | 2 +- client/webapp/personal-db.ts | 44 ++- client/webapp/script.ts | 28 +- client/webapp/socket-verifier.ts | 1 + client/webapp/ui.ts | 272 +++++++++--------- server/server-controller.ts | 3 +- 43 files changed, 398 insertions(+), 333 deletions(-) rename client/webapp/elements/{server-list-server.ts => guild-list-guild.ts} (59%) diff --git a/client/webapp/actions.ts b/client/webapp/actions.ts index 6e7c236..6053a2d 100644 --- a/client/webapp/actions.ts +++ b/client/webapp/actions.ts @@ -7,44 +7,44 @@ import Util from './util'; import Globals from './globals'; import UI from './ui'; -import ClientController from './client-controller'; +import CombinedGuild from './guild-combined'; import { Channel } from './data-types'; import Q from './q-module'; export default class Actions { - static async fetchAndUpdateConnection(ui: UI, server: ClientController) { + static async fetchAndUpdateConnection(ui: UI, guild: CombinedGuild) { // Explicitly not using withPotentialError to make this simpler try { - let connection = await server.fetchConnectionInfo(); - ui.setActiveConnection(server, connection); + let connection = await guild.fetchConnectionInfo(); + ui.setActiveConnection(guild, connection); } catch (e) { LOG.error('Error updating current connection', e); - ui.setActiveConnection(server, { id: null, avatarResourceId: null, displayName: 'Error', status: 'Error', privileges: [], roleName: null, roleColor: null, rolePriority: null }); + ui.setActiveConnection(guild, { id: null, avatarResourceId: null, displayName: 'Error', status: 'Error', privileges: [], roleName: null, roleColor: null, rolePriority: null }); } } - static async fetchAndUpdateMembers(q: Q, ui: UI, server: ClientController) { + static async fetchAndUpdateMembers(q: Q, ui: UI, guild: CombinedGuild) { await Util.withPotentialErrorWarnOnCancel(q, { taskFunc: async () => { - if (ui.activeServer === null || ui.activeServer.id !== server.id) return; - let members = await server.grabMembers(); - await ui.setMembers(server, members); + if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return; + let members = await guild.fetchMembers(); + await ui.setMembers(guild, members); }, errorIndicatorAddFunc: async (errorIndicatorElement) => { - await ui.setMembersErrorIndicator(server, errorIndicatorElement); + await ui.setMembersErrorIndicator(guild, errorIndicatorElement); }, errorContainer: q.$('#server-members'), errorMessage: 'Error loading members' }); } - static async fetchAndUpdateChannels(q: Q, ui: UI, server: ClientController) { + static async fetchAndUpdateChannels(q: Q, ui: UI, guild: CombinedGuild) { await Util.withPotentialErrorWarnOnCancel(q, { taskFunc: async () => { - if (ui.activeServer === null || ui.activeServer.id !== server.id) return; - let channels = await server.grabChannels(); - await ui.setChannels(server, channels); - if (ui.activeServer === null || ui.activeServer.id !== server.id) return; + if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return; + let channels = await guild.fetchChannels(); + await ui.setChannels(guild, channels); + if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return; if (ui.activeChannel === null) { // click on the first channel in the list if no channel is active yet let element = q.$_('#channel-list .channel'); @@ -54,45 +54,45 @@ export default class Actions { } }, errorIndicatorAddFunc: async (errorIndicatorElement) => { - await ui.setChannelsErrorIndicator(server, errorIndicatorElement); + await ui.setChannelsErrorIndicator(guild, errorIndicatorElement); }, errorContainer: q.$('#channel-list'), errorMessage: 'Error fetching channels' }); } - static async fetchAndUpdateMessagesRecent(q: Q, ui: UI, server: ClientController, channel: Channel | { id: string }) { + static async fetchAndUpdateMessagesRecent(q: Q, ui: UI, guild: CombinedGuild, channel: Channel | { id: string }) { await Util.withPotentialErrorWarnOnCancel(q, { taskFunc: async () => { - if (ui.activeServer === null || ui.activeServer.id !== server.id) return; + if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return; if (ui.activeChannel === null || ui.activeChannel.id !== channel.id) return; - let messages = await server.grabRecentMessages(channel.id, Globals.MESSAGES_PER_REQUEST); - await ui.setMessages(server, channel, messages, { atTop: messages.length < Globals.MESSAGES_PER_REQUEST, atBottom: true }); + let messages = await guild.fetchMessagesRecent(channel.id, Globals.MESSAGES_PER_REQUEST); + await ui.setMessages(guild, channel, messages, { atTop: messages.length < Globals.MESSAGES_PER_REQUEST, atBottom: true }); }, errorIndicatorAddFunc: async (errorIndicatorElement) => { - await ui.setMessagesErrorIndicator(server, channel, errorIndicatorElement); + await ui.setMessagesErrorIndicator(guild, channel, errorIndicatorElement); }, errorContainer: q.$('#channel-feed'), errorMessage: 'Error fetching messages' }); } - static async fetchAndUpdateMessagesBefore(q: Q, ui: UI, server: ClientController, channel: Channel) { + static async fetchAndUpdateMessagesBefore(q: Q, ui: UI, guild: CombinedGuild, channel: Channel) { await Util.withPotentialErrorWarnOnCancel(q, { taskFunc: async () => { - if (ui.activeServer === null || ui.activeServer.id !== server.id) return; + if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return; if (ui.activeChannel === null || ui.activeChannel.id !== channel.id) return; let topPair = ui.getTopMessagePair(); if (topPair == null) return; - let messages = await server.fetchMessagesBefore(channel.id, topPair.message.id, Globals.MESSAGES_PER_REQUEST); + let messages = await guild.fetchMessagesBefore(channel.id, topPair.message.id, Globals.MESSAGES_PER_REQUEST); if (messages && messages.length > 0) { - await ui.addMessagesBefore(server, channel, messages, topPair.message); + await ui.addMessagesBefore(guild, channel, messages, topPair.message); } else { ui.messagesAtTop = true; } }, errorIndicatorAddFunc: async (errorIndicatorElement) => { - await ui.addMessagesErrorIndicatorBefore(server, channel, errorIndicatorElement); + await ui.addMessagesErrorIndicatorBefore(guild, channel, errorIndicatorElement); }, errorContainer: q.$('#channel-feed'), errorClasses: [ 'before' ], @@ -100,22 +100,22 @@ export default class Actions { }); } - static async fetchAndUpdateMessagesAfter(q: Q, ui: UI, server: ClientController, channel: Channel) { + static async fetchAndUpdateMessagesAfter(q: Q, ui: UI, guild: CombinedGuild, channel: Channel) { await Util.withPotentialErrorWarnOnCancel(q, { taskFunc: async () => { - if (ui.activeServer === null || ui.activeServer.id !== server.id) return; + if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return; if (ui.activeChannel === null || ui.activeChannel.id !== channel.id) return; let bottomPair = ui.getBottomMessagePair(); if (bottomPair == null) return; - let messages = await server.fetchMessagesAfter(channel.id, bottomPair.message.id, Globals.MESSAGES_PER_REQUEST); + let messages = await guild.fetchMessagesAfter(channel.id, bottomPair.message.id, Globals.MESSAGES_PER_REQUEST); if (messages && messages.length > 0) { - await ui.addMessagesAfter(server, channel, messages, bottomPair.message); + await ui.addMessagesAfter(guild, channel, messages, bottomPair.message); } else { ui.messagesAtBottom = true; } }, errorIndicatorAddFunc: async (errorIndicatorElement) => { - await ui.addMessagesErrorIndicatorAfter(server, channel, errorIndicatorElement); + await ui.addMessagesErrorIndicatorAfter(guild, channel, errorIndicatorElement); }, errorContainer: q.$('#channel-feed'), errorClasses: [ 'after' ], diff --git a/client/webapp/auto-verifier.ts b/client/webapp/auto-verifier.ts index 3a885be..5158d7d 100644 --- a/client/webapp/auto-verifier.ts +++ b/client/webapp/auto-verifier.ts @@ -142,6 +142,9 @@ export class AutoVerifier { return await new Promise(async (resolve: (result: T | null) => void, reject: (error: Error) => void) => { let resolved = false; try { + let origTrustedStatus = this.trustedStatus; + let origTrustedPromise = this.trustedPromise; + let primaryPromise = this.primaryFunc(); if (this.trustedStatus === 'none') { @@ -154,6 +157,9 @@ export class AutoVerifier { if (primaryResult) { resolve(primaryResult); resolved = true; + } else if (origTrustedStatus === 'verified' && origTrustedPromise === this.trustedPromise) { + // Unverify if the primary returns null after it was verified + this.unverify(); } //@ts-ignore (could be changed by an unverify during primaryPromise) diff --git a/client/webapp/controller.ts b/client/webapp/controller.ts index 53c0291..c97a571 100644 --- a/client/webapp/controller.ts +++ b/client/webapp/controller.ts @@ -5,73 +5,87 @@ const electronConsole = electronRemote.getGlobal('console') as Console; import Logger from '../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); -import { EventEmitter } from "stream"; - -import ClientController from "./client-controller"; - import * as socketio from 'socket.io-client'; import * as crypto from 'crypto'; -import DBCache from './db-cache'; -import { ServerConfig } from './data-types'; +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 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 { - public servers: ClientController[] = []; +export default class Controller extends EventEmitter<{ + 'connect': (guild: CombinedGuild) => void; + 'disconnect': (guild: CombinedGuild) => void; + + 'update-metadata': (guild: CombinedGuild, guildMeta: GuildMetadata) => void; + + 'new-channels': (guild: CombinedGuild, channels: Channel[]) => void; + 'update-channels': (guild: CombinedGuild, updatedChannels: Channel[]) => void; + + 'new-members': (guild: CombinedGuild, members: Member[]) => void; + 'update-members': (guild: CombinedGuild, updatedMembers: Member[]) => void; + + 'new-messages': (guild: CombinedGuild, messages: Message[]) => void; + 'update-messages': (guild: CombinedGuild, updatedMessages: Message[]) => void; + + 'conflict-metadata': (guild: CombinedGuild, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => void; + 'conflict-channels': (guild: CombinedGuild, changes: Changes) => void; + 'conflict-members': (guild: CombinedGuild, changes: Changes) => void; + 'conflict-messages': (guild: CombinedGuild, changes: Changes) => void; + 'conflict-tokens': (guild: CombinedGuild, changes: Changes) => void; + 'conflict-resource': (guild: CombinedGuild, oldResource: Resource, newResource: Resource) => void; +}> { + public guilds: CombinedGuild[] = []; constructor( - private dbCache: DBCache + private messageRAMCache: MessageRAMCache, + private resourceRAMCache: ResourceRAMCache, + private personalDB: PersonalDB ) { super(); } - async _connectFromConfig(serverConfig: ServerConfig): Promise { - LOG.debug(`connecting to server#${serverConfig.guildId} at ${serverConfig.url}`); + async _connectFromConfig(guildMetadata: GuildMetadataWithIds, socketConfig: SocketConfig): Promise { + LOG.debug(`connecting to server#${guildMetadata.id} at ${socketConfig.url}`); - let server = new ClientController(this.dbCache, serverConfig); + let guild = await CombinedGuild.create( + guildMetadata, + socketConfig, + this.messageRAMCache, + this.resourceRAMCache, + this.personalDB + ); - await this.dbCache.clearAllMemberStatus(server.id); + await this.personalDB.clearAllMembersStatus(guild.id); - this.servers.push(server); + this.guilds.push(guild); - // Forward server events through this event emitter - let serverEvents = [ - 'connected', - 'disconnected', - 'verified', - 'new-message', - 'update-server', - 'deleted-members', - 'updated-members', - 'added-members', - 'deleted-channels', - 'updated-channels', - 'added-channels', - 'deleted-messages', - 'updated-messages', - 'added-messages', - ]; - for (let event of serverEvents) { - server.on(event, (...args) => { - this.emit(event, server, ...args); + // Forward guild events through this event emitter + for (let eventName of guild.eventNames()) { + guild.on(eventName as any, (...args: any) => { + this.emit(eventName as any, guild, ...args); }); } - return server; + + return guild; } async init(): Promise { - this.servers = []; + this.guilds = []; - let serverConfigs = await this.dbCache.getServerConfigs(); - - // TODO: HTML prompt if no server configs - if (serverConfigs.length == 0) { - LOG.warn('no server configs found in client-side db'); + // TODO: connect concurrently + for (let guildMeta of await this.personalDB.fetchGuilds()) { + for (let guildSocket of await this.personalDB.fetchGuildSockets(guildMeta.id)) { + await this._connectFromConfig(guildMeta, guildSocket); + } } - for (let serverConfig of serverConfigs) { - await this._connectFromConfig(serverConfig); + if (this.guilds.length === 0) { + LOG.warn('no guilds found in client-side db'); } } @@ -94,7 +108,7 @@ export default class Controller extends EventEmitter { }); } - async addNewServer(serverConfig: IAddServerData, displayName: string, avatarBuff: Buffer): Promise { + async addNewGuild(serverConfig: IAddServerData, displayName: string, avatarBuff: Buffer): Promise { const { name, url, cert, token } = serverConfig; LOG.debug('Adding new server', { name, url, cert, token, displayName, avatarBuff }); @@ -128,24 +142,25 @@ 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', - token, clientPublicKeyDerBuff, displayName, avatarBuff, async (errStr, member) => { + token, clientPublicKeyDerBuff, displayName, avatarBuff, async (errStr: string, dataMember: any, dataMetadata: any) => { if (errStr) { reject(new Error(errStr)); } else { try { - let serverConfig: ServerConfig | null = null; - await this.dbCache.queueTransaction(async () => { - let publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }); - let privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }); - let identityId = await this.dbCache.addIdentity(publicKeyPem, privateKeyPem); - let guildId = await this.dbCache.addServer(url, cert, name); - await this.dbCache.addServerIdentity(guildId, identityId); - serverConfig = await this.dbCache.getServerConfig(guildId, identityId); + const member = Member.fromDBData(dataMember); + const meta = GuildMetadata.fromDBData(dataMetadata); + let guildMeta: GuildMetadataWithIds | null = null; + let socketConfig: SocketConfig | null = null; + await this.personalDB.queueTransaction(async () => { + let guildId = await this.personalDB.addGuild(meta.name, meta.iconResourceId, member.id); + let guildSocketId = await this.personalDB.addGuildSocket(guildId, url, cert, publicKey, privateKey); + guildMeta = await this.personalDB.fetchGuild(guildId); + socketConfig = await this.personalDB.fetchGuildSocket(guildId, guildSocketId); }); - if (serverConfig == null) { - throw new Error('unable to get server config'); + if (!guildMeta || !socketConfig) { + throw new Error('unable to properly add guild'); } - let server = await this._connectFromConfig(serverConfig); + let server = await this._connectFromConfig(guildMeta, socketConfig); resolve(server); } catch (e) { reject(e); @@ -159,8 +174,11 @@ export default class Controller extends EventEmitter { } } - async removeServer(server: ClientController): Promise { - await this.dbCache.removeServer(server.id); - this.servers = this.servers.filter(s => s != server); + async removeServer(guild: CombinedGuild): Promise { + await this.personalDB.queueTransaction(async () => { + await this.personalDB.removeGuildSockets(guild.id); + await this.personalDB.removeGuild(guild.id); + }); + this.guilds = this.guilds.filter(g => g.id != guild.id); } } diff --git a/client/webapp/data-types.ts b/client/webapp/data-types.ts index 2a7ca6c..e1fca06 100644 --- a/client/webapp/data-types.ts +++ b/client/webapp/data-types.ts @@ -214,7 +214,8 @@ export class Message implements WithEquals { export class SocketConfig { private constructor( - public readonly id: number, + public readonly id: number | null, + public readonly guildId: number, public readonly url: string, public readonly cert: string, public readonly publicKey: crypto.KeyObject, @@ -225,6 +226,7 @@ export class SocketConfig { static fromDBData(data: any): SocketConfig { return new SocketConfig( data.id, + data.guild_id, data.url, data.cert, crypto.createPublicKey(data.public_key), diff --git a/client/webapp/elements/channel.ts b/client/webapp/elements/channel.ts index 31f010f..89bc030 100644 --- a/client/webapp/elements/channel.ts +++ b/client/webapp/elements/channel.ts @@ -7,7 +7,7 @@ import UI from '../ui'; import Actions from '../actions'; import Q from '../q-module'; -export default function createChannel(document: Document, q: Q, ui: UI, server: ClientController, channel: Channel) { +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: [ // Scraped directly from discord (#) { class: 'icon', content: BaseElements.TEXT_CHANNEL_ICON }, diff --git a/client/webapp/elements/context-menu-conn.ts b/client/webapp/elements/context-menu-conn.ts index 9ca3289..52eb2ce 100644 --- a/client/webapp/elements/context-menu-conn.ts +++ b/client/webapp/elements/context-menu-conn.ts @@ -6,7 +6,7 @@ import createPersonalizeOverlay from './overlay-personalize.js'; import Q from '../q-module.js'; import UI from '../ui.js'; -export default function createConnectionContextMenu(document: Document, q: Q, ui: UI, server: ClientController) { +export default function createConnectionContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild) { let statuses = [ 'online', 'away', 'busy', 'invisible' ]; let content: any[] = [ { class: 'item personalize', content: [ diff --git a/client/webapp/elements/context-menu-img.ts b/client/webapp/elements/context-menu-img.ts index e987d42..9d2fcf5 100644 --- a/client/webapp/elements/context-menu-img.ts +++ b/client/webapp/elements/context-menu-img.ts @@ -12,7 +12,7 @@ import Q from '../q-module'; export default function createImageContextMenu( document: Document, q: Q, - server: ClientController, + guild: CombinedGuild, resourceName: string, buffer: Buffer, mime: string, diff --git a/client/webapp/elements/context-menu-srv-title.ts b/client/webapp/elements/context-menu-srv-title.ts index 062e5d4..53e9c7c 100644 --- a/client/webapp/elements/context-menu-srv-title.ts +++ b/client/webapp/elements/context-menu-srv-title.ts @@ -16,7 +16,7 @@ import createCreateInviteTokenOverlay from './overlay-create-invite-token'; import createCreateChannelOverlay from './overlay-create-channel'; import createTokenLogOverlay from './overlay-token-log'; -export default function createServerTitleContextMenu(document: Document, q: Q, ui: UI, server: ClientController): HTMLElement { +export default function createServerTitleContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild): HTMLElement { if (ui.activeConnection === null) { LOG.warn('no active connection when creating server title context menu'); return q.create({}) as HTMLElement; diff --git a/client/webapp/elements/context-menu-srv.ts b/client/webapp/elements/context-menu-srv.ts index 57d9c70..29d290b 100644 --- a/client/webapp/elements/context-menu-srv.ts +++ b/client/webapp/elements/context-menu-srv.ts @@ -10,7 +10,7 @@ import Q from '../q-module'; import UI from '../ui'; import Controller from '../controller'; -export default function createServerContextMenu(document: Document, q: Q, ui: UI, controller: Controller, server: ClientController) { +export default function createServerContextMenu(document: Document, q: Q, ui: UI, controller: Controller, guild: CombinedGuild) { let element = BaseElements.createContextMenu(document, { class: 'server-context', content: [ { class: 'item red leave-server', content: 'Leave Server' } diff --git a/client/webapp/elements/events-connection.ts b/client/webapp/elements/events-connection.ts index e56c9b3..c765db2 100644 --- a/client/webapp/elements/events-connection.ts +++ b/client/webapp/elements/events-connection.ts @@ -6,10 +6,10 @@ import createConnectionContextMenu from './context-menu-conn'; export default function bindConnectionEvents(document: Document, q: Q, ui: UI): void { q.$('#connection').addEventListener('click', () => { - if (ui.activeServer === null) return; - if (!ui.activeServer.isVerified) return; + if (ui.activeGuild === null) return; + if (!ui.activeGuild.isVerified) return; - let contextMenu = createConnectionContextMenu(document, q, ui, ui.activeServer); + let contextMenu = createConnectionContextMenu(document, q, ui, ui.activeGuild); document.body.appendChild(contextMenu); ElementsUtil.alignContextElement(contextMenu, q.$('#connection'), { bottom: 'top', centerX: 'centerX' }); }); diff --git a/client/webapp/elements/events-infinite-scroll.ts b/client/webapp/elements/events-infinite-scroll.ts index dd23a91..cdadd54 100644 --- a/client/webapp/elements/events-infinite-scroll.ts +++ b/client/webapp/elements/events-infinite-scroll.ts @@ -11,18 +11,18 @@ export default function bindInfiniteScrollEvents(q: Q, ui: UI): void { let scrollHeight = q.$('#channel-feed-content-wrapper').scrollHeight; let clientHeight = q.$('#channel-feed-content-wrapper').clientHeight; - if (ui.activeServer === null) return; + if (ui.activeGuild === null) return; if (ui.activeChannel === null) return; if (!loadingBefore && !ui.messagesAtTop && scrollTop < 600) { // Approaching the unloaded top of the page // Fetch more messages to add above loadingBefore = true; - await Actions.fetchAndUpdateMessagesBefore(q, ui, ui.activeServer, ui.activeChannel); + await Actions.fetchAndUpdateMessagesBefore(q, ui, ui.activeGuild, ui.activeChannel); loadingBefore = false; } else if (!loadingAfter && !ui.messagesAtBottom && scrollHeight - clientHeight - scrollTop < 600) { // Approaching the unloaded bottom of the page // Fetch more messages to add below loadingAfter = true; - await Actions.fetchAndUpdateMessagesAfter(q, ui, ui.activeServer, ui.activeChannel); + await Actions.fetchAndUpdateMessagesAfter(q, ui, ui.activeGuild, ui.activeChannel); loadingAfter = false; } } diff --git a/client/webapp/elements/events-server-title.ts b/client/webapp/elements/events-server-title.ts index 0a68911..4343a82 100644 --- a/client/webapp/elements/events-server-title.ts +++ b/client/webapp/elements/events-server-title.ts @@ -6,14 +6,14 @@ 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.activeServer === null) return; - if (!ui.activeServer.isVerified) return; + if (ui.activeGuild === null) return; + if (!ui.activeGuild.isVerified) return; if ( !ui.activeConnection.privileges.includes('modify_profile') && !ui.activeConnection.privileges.includes('modify_members') ) return; - let contextMenu = createServerTitleContextMenu(document, q, ui, ui.activeServer); + let contextMenu = createServerTitleContextMenu(document, q, ui, ui.activeGuild); document.body.appendChild(contextMenu); ElementsUtil.alignContextElement(contextMenu, q.$('#server-name-container'), { top: 'bottom', centerX: 'centerX' }); }); diff --git a/client/webapp/elements/events-text-input.ts b/client/webapp/elements/events-text-input.ts index d043d68..39b9252 100644 --- a/client/webapp/elements/events-text-input.ts +++ b/client/webapp/elements/events-text-input.ts @@ -19,7 +19,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v let sendingMessage = false; async function sendCurrentTextMessage() { if (sendingMessage) return; - if (ui.activeServer === null) return; + if (ui.activeGuild === null) return; if (ui.activeChannel === null) return; let text = q.$('#text-input').innerText.trim(); // trimming is not done server-side, just a client-side 'feature' @@ -27,7 +27,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v sendingMessage = true; - let server = ui.activeServer as ClientController; + let server = ui.activeGuild as ClientController; let channel = ui.activeChannel as Channel; if (!server.isVerified) { @@ -92,7 +92,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v // Open resource select dialog when resource-input-button is clicked let selectingResources = false; q.$('#resource-input-button').addEventListener('click', async () => { - if (ui.activeServer === null) return; + if (ui.activeGuild === null) return; if (ui.activeChannel === null) return; if (selectingResources) { @@ -106,7 +106,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v }); // TODO: multiple files do consecutive overlays? if (!result.canceled) { - let element = createUploadOverlayFromPath(document, ui.activeServer, ui.activeChannel, result.filePaths[0]); + let element = createUploadOverlayFromPath(document, ui.activeGuild, ui.activeChannel, result.filePaths[0]); document.body.appendChild(element); q.$$$(element, '.text-input').focus(); } @@ -115,7 +115,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v // Open upload resource dialog when an image is pasted window.addEventListener('paste', (e) => { - if (ui.activeServer === null) return; + if (ui.activeGuild === null) return; if (ui.activeChannel === null) return; let fileTransferItem: DataTransferItem | null = null; @@ -127,7 +127,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v } } if (fileTransferItem) { - let element = createUploadOverlayFromDataTransferItem(document, ui.activeServer, ui.activeChannel, fileTransferItem); + let element = createUploadOverlayFromDataTransferItem(document, ui.activeGuild, ui.activeChannel, fileTransferItem); document.body.appendChild(element); q.$$$(element, '.text-input').focus(); } @@ -135,11 +135,11 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v // TODO: drag+drop new server files? document.addEventListener('dragenter', () => { - if (ui.activeServer === null) return; + if (ui.activeGuild === null) return; if (ui.activeChannel === null) return; if (q.$('.overlay .drop-target')) return; - let element = createUploadDropTarget(document, q, ui.activeServer, ui.activeChannel); + let element = createUploadDropTarget(document, q, ui.activeGuild, ui.activeChannel); if (!element) return; document.body.appendChild(element); }); diff --git a/client/webapp/elements/server-list-server.ts b/client/webapp/elements/guild-list-guild.ts similarity index 59% rename from client/webapp/elements/server-list-server.ts rename to client/webapp/elements/guild-list-guild.ts index ba65f71..7f4e3ed 100644 --- a/client/webapp/elements/server-list-server.ts +++ b/client/webapp/elements/guild-list-guild.ts @@ -6,41 +6,41 @@ const LOG = Logger.create(__filename, electronConsole); 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 UI from '../ui'; import Actions from '../actions'; -import createServerContextMenu from './context-menu-srv'; +import createGuildContextMenu from './context-menu-srv'; import Controller from '../controller'; +import CombinedGuild from '../guild-combined'; -export default function createServerListServer(document: Document, q: Q, ui: UI, controller: Controller, server: ClientController) { - let element = q.create({ class: 'server', 'meta-id': server.id, 'meta-name': server.id, content: [ +export default function createGuildListGuild(document: Document, q: Q, ui: UI, controller: Controller, 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: 'server' }, // src is set later by script.js + { tag: 'img', src: './img/loading.svg', alt: 'guild' }, // src is set later by script.js ] }) as HTMLElement; // Hover over for name + connection info (async () => { - let serverData: ServerMetaData | CacheServerData; + let guildData: GuildMetadata; try { - serverData = await server.grabMetadata(); - if (!serverData.iconResourceId) throw new Error('server icon not identified yet'); - let serverIcon = await server.fetchResource(serverData.iconResourceId); - let serverIconSrc = await ElementsUtil.getImageBufferSrc(serverIcon); - (q.$$$(element, 'img') as HTMLImageElement).src = serverIconSrc; + guildData = await guild.grabMetadata(); + if (!guildData.iconResourceId) throw new Error('guild icon not identified yet'); + let guildIcon = await guild.fetchResource(guildData.iconResourceId); + let guildIconSrc = await ElementsUtil.getImageBufferSrc(guildIcon); + (q.$$$(element, 'img') as HTMLImageElement).src = guildIconSrc; } catch (e) { - LOG.error('Error fetching server icon', e); + LOG.error('Error fetching guild icon', e); (q.$$$(element, 'img') as HTMLImageElement).src = './img/error.png'; return; } - element.setAttribute('meta-name', serverData.name); + element.setAttribute('meta-name', guildData.name); let contextElement = q.create({ class: 'context', content: { class: 'info', content: [ BaseElements.TAB_LEFT, - { class: 'content server' } // populated later + { class: 'content guild' } // populated later ] } }) as HTMLElement; @@ -52,7 +52,7 @@ export default function createServerListServer(document: Document, q: Q, ui: UI, document.body.appendChild(contextElement); ElementsUtil.alignContextElement(contextElement, element, { left: 'right', centerY: 'centerY' }); (async () => { - let connection = await server.fetchConnectionInfo(); + let connection = await guild.fetchConnectionInfo(); let connectionElement = q.create({ class: 'connection ' + connection.status, content: [ { class: 'status-circle' }, { class: 'display-name', content: connection.displayName } @@ -72,38 +72,38 @@ export default function createServerListServer(document: Document, q: Q, ui: UI, element.addEventListener('click', async () => { if (element.classList.contains('active')) return; - ui.setActiveServer(server); + ui.setActiveGuild(guild); // Connection information (async () => { - await Actions.fetchAndUpdateConnection(ui, server); + await Actions.fetchAndUpdateConnection(ui, guild); })(); - // Server Channel Name + // Guild Channel Name (async () => { // Explicitly not using a withPotentialError to make this simpler try { - let serverData = await server.grabMetadata(); - ui.updateServerName(server, serverData.name); + let guildData = await guild.grabMetadata(); + ui.updateGuildName(guild, guildData.name); } catch (e) { - LOG.error('Error fetching server name', e); - ui.updateServerName(server, 'ERROR'); + LOG.error('Error fetching guild name', e); + ui.updateGuildName(guild, 'ERROR'); } })(); - // Server Channel List + // Guild Channel List (async () => { - await Actions.fetchAndUpdateChannels(q, ui, server); + await Actions.fetchAndUpdateChannels(q, ui, guild); })(); - // Server Members + // Guild Members (async () => { - await Actions.fetchAndUpdateMembers(q, ui, server); + await Actions.fetchAndUpdateMembers(q, ui, guild); })(); }); element.addEventListener('contextmenu', (e) => { - let contextMenu = createServerContextMenu(document, q, ui, controller, server); + let contextMenu = createGuildContextMenu(document, q, ui, controller, 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 db36129..21e23ba 100644 --- a/client/webapp/elements/member.ts +++ b/client/webapp/elements/member.ts @@ -4,7 +4,7 @@ import Q from "../q-module"; import ElementsUtil from "./require/elements-util"; -export default function createMember(q: Q, server: ClientController, member: Member): HTMLElement { +export default function createMember(q: Q, guild: CombinedGuild, member: Member): HTMLElement { let nameStyle = member.roleColor ? 'color: ' + member.roleColor : ''; let element = q.create({ class: 'member ' + member.status, 'meta-id': member.id, content: [ { class: 'icon', content: [ diff --git a/client/webapp/elements/message.ts b/client/webapp/elements/message.ts index 18df3a2..8a7308b 100644 --- a/client/webapp/elements/message.ts +++ b/client/webapp/elements/message.ts @@ -8,7 +8,7 @@ import createResourceMessageContinued from './msg-res-cont'; import createTextMessage from './msg-txt'; import createTextMessageContinued from './msg-txt-cont'; -export default function createMessage(document: Document, q: Q, server: ClientController, message: Message, lastMessage: Message | null): HTMLElement { +export default function createMessage(document: Document, q: Q, guild: CombinedGuild, message: Message, lastMessage: Message | null): HTMLElement { let element: HTMLElement; if (message.hasResource()) { if (message.isImageResource()) { diff --git a/client/webapp/elements/msg-img-res-cont.ts b/client/webapp/elements/msg-img-res-cont.ts index 1975b37..f7e8695 100644 --- a/client/webapp/elements/msg-img-res-cont.ts +++ b/client/webapp/elements/msg-img-res-cont.ts @@ -14,7 +14,7 @@ import Q from '../q-module'; import createImageOverlay from './overlay-image'; import createImageContextMenu from './context-menu-img'; -export default function createImageResourceMessageContinued(document: Document, q: Q, server: ClientController, message: Message): HTMLElement { +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'); } diff --git a/client/webapp/elements/msg-img-res.ts b/client/webapp/elements/msg-img-res.ts index 86a9bbf..6350424 100644 --- a/client/webapp/elements/msg-img-res.ts +++ b/client/webapp/elements/msg-img-res.ts @@ -14,7 +14,7 @@ import Q from '../q-module'; import createImageOverlay from './overlay-image'; import createImageContextMenu from './context-menu-img'; -export default function createImageResourceMessage(document: Document, q: Q, server: ClientController, message: Message) { +export default function createImageResourceMessage(document: Document, q: Q, guild: CombinedGuild, message: Message) { if (!message.resourceId || !message.resourcePreviewId || !message.resourceName) { throw new ShouldNeverHappenError('Message is not a resource message'); } diff --git a/client/webapp/elements/msg-res-cont.ts b/client/webapp/elements/msg-res-cont.ts index 15219ce..6efd4f7 100644 --- a/client/webapp/elements/msg-res-cont.ts +++ b/client/webapp/elements/msg-res-cont.ts @@ -12,7 +12,7 @@ class ShouldNeverHappenError extends Error { } } -export default function createResourceMessageContinued(q: Q, server: ClientController, message: Message): HTMLElement { +export default function createResourceMessageContinued(q: Q, guild: CombinedGuild, message: Message): HTMLElement { if (!message.resourceId || !message.resourceName) { throw new ShouldNeverHappenError('Message is not a resource message'); } diff --git a/client/webapp/elements/msg-res.ts b/client/webapp/elements/msg-res.ts index 92f16e6..7d580d6 100644 --- a/client/webapp/elements/msg-res.ts +++ b/client/webapp/elements/msg-res.ts @@ -12,7 +12,7 @@ class ShouldNeverHappenError extends Error { } } -export default function createResourceMessage(q: Q, server: ClientController, message: Message): HTMLElement { +export default function createResourceMessage(q: Q, guild: CombinedGuild, message: Message): HTMLElement { if (!message.resourceId || !message.resourceName) { throw new ShouldNeverHappenError('Message is not a resource message'); } diff --git a/client/webapp/elements/msg-txt-cont.ts b/client/webapp/elements/msg-txt-cont.ts index 4f6dfd9..53759ac 100644 --- a/client/webapp/elements/msg-txt-cont.ts +++ b/client/webapp/elements/msg-txt-cont.ts @@ -5,7 +5,7 @@ import Q from '../q-module.js'; import ElementsUtil from './require/elements-util.js'; -export default function createTextMessageContinued(q: Q, server: ClientController, message: Message): HTMLElement { +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: [ { class: 'timestamp', content: moment(message.sent).format('HH:mm') }, { class: 'right', content: [ diff --git a/client/webapp/elements/msg-txt.ts b/client/webapp/elements/msg-txt.ts index eb880ea..24aad5e 100644 --- a/client/webapp/elements/msg-txt.ts +++ b/client/webapp/elements/msg-txt.ts @@ -6,7 +6,7 @@ import { Message, Member, IDummyTextMessage } from '../data-types'; import ClientController from '../client-controller'; import Q from '../q-module'; -export default function createTextMessage(q: Q, server: ClientController, message: Message | IDummyTextMessage): HTMLElement { +export default function createTextMessage(q: Q, guild: CombinedGuild, message: Message | IDummyTextMessage): HTMLElement { let memberInfo: { roleColor: string | null, displayName: string, diff --git a/client/webapp/elements/overlay-add-server.ts b/client/webapp/elements/overlay-add-server.ts index 114fb5e..8ceea46 100644 --- a/client/webapp/elements/overlay-add-server.ts +++ b/client/webapp/elements/overlay-add-server.ts @@ -177,7 +177,7 @@ export default function createAddServerOverlay(document: Document, q: Q, ui: UI, q.$$$(element, '.display-name-input').removeAttribute('contenteditable'); - let newServer: ClientController | null = null; + let newguild: CombinedGuild | null = null; if (addServerData == null) { q.$$$(element, '.error').innerText = 'Very bad server file'; q.$$$(element, '.submit').innerText = 'Try Again'; @@ -201,7 +201,7 @@ 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.addNewServer(addServerData, displayName, avatarBuff); + newServer = await controller.addNewGuild(addServerData, displayName, avatarBuff); } catch (e) { LOG.warn('error adding new server: ' + e.message, e); // explicitly not printing stack trace here q.$$$(element, '.error').innerText = e.message; diff --git a/client/webapp/elements/overlay-create-channel.ts b/client/webapp/elements/overlay-create-channel.ts index 5f16d9b..f519fa6 100644 --- a/client/webapp/elements/overlay-create-channel.ts +++ b/client/webapp/elements/overlay-create-channel.ts @@ -10,7 +10,7 @@ import ElementsUtil from "./require/elements-util"; import BaseElements from "./require/base-elements"; import Q from '../q-module'; -export default function createCreateChannelOverlay(document: Document, q: Q, server: ClientController): HTMLElement { +export default function createCreateChannelOverlay(document: Document, q: Q, guild: CombinedGuild): HTMLElement { // See also overlay-modify-channel diff --git a/client/webapp/elements/overlay-create-invite-token.ts b/client/webapp/elements/overlay-create-invite-token.ts index 15cd9cb..956c65d 100644 --- a/client/webapp/elements/overlay-create-invite-token.ts +++ b/client/webapp/elements/overlay-create-invite-token.ts @@ -2,7 +2,7 @@ import ClientController from "../client-controller"; import BaseElements from "./require/base-elements"; -export default function createCreateInviteTokenOverlay(document: Document, server: ClientController): HTMLElement { +export default function createCreateInviteTokenOverlay(document: Document, guild: CombinedGuild): HTMLElement { let element = BaseElements.createOverlay(document, { class: 'content submit-dialog', content: [ { class: 'role-select category-select', content: [ { class: 'label', content: 'Select Starting Roles' }, diff --git a/client/webapp/elements/overlay-image.ts b/client/webapp/elements/overlay-image.ts index 052346e..81338d5 100644 --- a/client/webapp/elements/overlay-image.ts +++ b/client/webapp/elements/overlay-image.ts @@ -12,7 +12,7 @@ import ClientController from '../client-controller'; import Q from '../q-module'; import createImageContextMenu from './context-menu-img'; -export default function createImageOverlay(document: Document, q: Q, server: ClientController, resourceId: string, resourceName: string): HTMLElement { +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: [ { tag: 'img', src: './img/loading.svg', alt: resourceName, title: resourceName }, { class: 'download', content: [ diff --git a/client/webapp/elements/overlay-modify-channel.ts b/client/webapp/elements/overlay-modify-channel.ts index 852344e..91b6ce4 100644 --- a/client/webapp/elements/overlay-modify-channel.ts +++ b/client/webapp/elements/overlay-modify-channel.ts @@ -11,7 +11,7 @@ import BaseElements from './require/base-elements.js'; import ElementsUtil from './require/elements-util.js'; import Q from '../q-module'; -export default function createModifyChannelOverlay(document: Document, q: Q, server: ClientController, channel: Channel): HTMLElement { +export default function createModifyChannelOverlay(document: Document, q: Q, guild: CombinedGuild, channel: Channel): HTMLElement { // See also overlay-create-channel let element = BaseElements.createOverlay(document, { class: 'content submit-dialog modify-channel', content: [ diff --git a/client/webapp/elements/overlay-personalize.ts b/client/webapp/elements/overlay-personalize.ts index a6428db..0c61e92 100644 --- a/client/webapp/elements/overlay-personalize.ts +++ b/client/webapp/elements/overlay-personalize.ts @@ -12,7 +12,7 @@ import ClientController from '../client-controller'; import Q from '../q-module'; import createTextMessage from './msg-txt'; -export default function createPersonalizeOverlay(document: Document, q: Q, server: ClientController, connection: any): HTMLElement { +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' }), diff --git a/client/webapp/elements/overlay-server-settings.ts b/client/webapp/elements/overlay-server-settings.ts index 1fc69ac..ae3e440 100644 --- a/client/webapp/elements/overlay-server-settings.ts +++ b/client/webapp/elements/overlay-server-settings.ts @@ -12,7 +12,7 @@ import ClientController from '../client-controller'; import { CacheServerData, ServerMetaData } from '../data-types'; import Q from '../q-module'; -export default function createServerSettingsOverlay(document: Document, q: Q, server: ClientController, serverMeta: ServerMetaData | CacheServerData): HTMLElement { +export default function createServerSettingsOverlay(document: Document, q: Q, guild: CombinedGuild, serverMeta: ServerMetaData | CacheServerData): HTMLElement { let element = BaseElements.createOverlay(document, { class: 'content submit-dialog server-settings', content: [ { class: 'server preview', content: [ diff --git a/client/webapp/elements/overlay-token-log.ts b/client/webapp/elements/overlay-token-log.ts index b57b88c..8c94685 100644 --- a/client/webapp/elements/overlay-token-log.ts +++ b/client/webapp/elements/overlay-token-log.ts @@ -13,7 +13,7 @@ import ClientController from '../client-controller'; import { Member } from '../data-types'; import Q from '../q-module'; -export default function createTokenLogOverlay(document: Document, q: Q, server: ClientController): HTMLElement { +export default function createTokenLogOverlay(document: Document, q: Q, guild: CombinedGuild): HTMLElement { let element = BaseElements.createOverlay(document, { class: 'content token-log', content: [ { class: 'tokens', content: { tag: 'img', src: './img/loading.svg', alt: 'loading...' } }, diff --git a/client/webapp/elements/overlay-upload-datatransfer.ts b/client/webapp/elements/overlay-upload-datatransfer.ts index 097fff9..12649f7 100644 --- a/client/webapp/elements/overlay-upload-datatransfer.ts +++ b/client/webapp/elements/overlay-upload-datatransfer.ts @@ -3,7 +3,7 @@ import BaseElements from './require/base-elements.js'; import { Channel, ShouldNeverHappenError } from '../data-types'; import ClientController from '../client-controller.js'; -export default function createUploadOverlayFromDataTransferItem(document: Document, server: ClientController, channel: Channel, dataTransferItem: DataTransferItem): HTMLElement { +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, { diff --git a/client/webapp/elements/overlay-upload-drop-target.ts b/client/webapp/elements/overlay-upload-drop-target.ts index 339ace2..f87f028 100644 --- a/client/webapp/elements/overlay-upload-drop-target.ts +++ b/client/webapp/elements/overlay-upload-drop-target.ts @@ -9,7 +9,7 @@ import ClientController from '../client-controller'; import Q from '../q-module'; import createUploadOverlayFromDataTransferItem from './overlay-upload-datatransfer'; -export default function createUploadDropTarget(document: Document, q: Q, server: ClientController, channel: Channel): HTMLElement { +export default function createUploadDropTarget(document: Document, q: Q, guild: CombinedGuild, channel: Channel): HTMLElement { let element = BaseElements.createOverlay(document, { class: 'content drop-target', content: [ // TODO: icon? { class: 'message', content: 'Upload to #' + channel.name } diff --git a/client/webapp/elements/overlay-upload-path.ts b/client/webapp/elements/overlay-upload-path.ts index 11f880b..a52d04a 100644 --- a/client/webapp/elements/overlay-upload-path.ts +++ b/client/webapp/elements/overlay-upload-path.ts @@ -6,7 +6,7 @@ import BaseElements from './require/base-elements'; import { Channel } from '../data-types'; import ClientController from '../client-controller'; -export default function createUploadOverlayFromPath(document: Document, server: ClientController, channel: Channel, resourcePath: string): HTMLElement { +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, diff --git a/client/webapp/elements/require/base-elements.ts b/client/webapp/elements/require/base-elements.ts index 0c968e9..52df774 100644 --- a/client/webapp/elements/require/base-elements.ts +++ b/client/webapp/elements/require/base-elements.ts @@ -18,7 +18,7 @@ interface HTMLElementWithRemoveSelf extends HTMLElement { } interface CreateUploadOverlayProps { - server: ClientController; + guild: CombinedGuild; channel: Channel; resourceName: string; resourceBuffFunc: (() => Promise); diff --git a/client/webapp/elements/require/elements-util.ts b/client/webapp/elements/require/elements-util.ts index 5e77732..9e44d75 100644 --- a/client/webapp/elements/require/elements-util.ts +++ b/client/webapp/elements/require/elements-util.ts @@ -99,7 +99,7 @@ export default class ElementsUtil { } } - static async getImageBufferFromResourceFailSoftly(server: ClientController, resourceId: string | null): Promise { + static async getImageBufferFromResourceFailSoftly(guild: CombinedGuild, resourceId: string | null): Promise { if (!resourceId) { LOG.warn('no server resource specified, showing error instead', new Error()); return './img/error.png'; diff --git a/client/webapp/fetchable-pair-verifier.ts b/client/webapp/fetchable-pair-verifier.ts index 3ac811c..2fd3b25 100644 --- a/client/webapp/fetchable-pair-verifier.ts +++ b/client/webapp/fetchable-pair-verifier.ts @@ -4,7 +4,7 @@ import { AutoVerifier, AutoVerifierChangesType } from './auto-verifier'; import { AutoVerifierWithArg, PartialMessageListQuery, IDQuery } from './auto-verifier-with-args'; import { EventEmitter } from 'tsee'; -export default class GuildPairVerifierGuild extends EventEmitter implements AsyncFetchable { +export default class PairVerifierFetchable extends EventEmitter implements AsyncFetchable { private readonly fetchMetadataVerifier: AutoVerifier; private readonly fetchMembersVerifier: AutoVerifier; diff --git a/client/webapp/guild-combined.ts b/client/webapp/guild-combined.ts index dcfd8bb..4be5a68 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, Member, Message, Resource, ServerMetaData, SocketConfig, Token } from './data-types'; +import { Changes, Channel, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, ServerMetaData, SocketConfig, Token } from './data-types'; import MessageRAMCache from "./message-ram-cache"; import PersonalDB from "./personal-db"; @@ -27,7 +27,7 @@ export default class CombinedGuild extends EventEmitter { if (this.ramGuild.getMembers().size === 0) { await this.fetchMembers(); } if (this.ramGuild.getMembers().size === 0) throw new Error('RAM Members was not updated through fetchMembers'); } - async ensureRAMChannels() { + private async ensureRAMChannels(): Promise { if (this.ramGuild.getChannels().size === 0) { await this.fetchChannels(); } if (this.ramGuild.getChannels().size === 0) throw new Error('RAM Channels was not updated through fetchChannels'); } + + public async grabRAMMembersMap(): Promise> { + await this.ensureRAMMembers(); + return this.ramGuild.getMembers(); + } + + public async grabRAMChannelsMap(): Promise> { + await this.ensureRAMChannels(); + return this.ramGuild.getChannels(); + } // Fetched through the triple-cache system (RAM -> Disk -> Server) async fetchMetadata(): Promise { diff --git a/client/webapp/message-ram-cache.ts b/client/webapp/message-ram-cache.ts index 188b545..cca7d62 100644 --- a/client/webapp/message-ram-cache.ts +++ b/client/webapp/message-ram-cache.ts @@ -28,7 +28,7 @@ export default class MessageRAMCache { } // Removes the oldest messages from the channel until the channel is under the max cached character limit - private trimOldChannelMessagesIfNeeded(guildId: string, channelId: string) { + private trimOldChannelMessagesIfNeeded(guildId: number, channelId: string) { let id = `g#${guildId}/c#${channelId}`; let value = this.data.get(id); if (!value) return; diff --git a/client/webapp/personal-db.ts b/client/webapp/personal-db.ts index 2af913c..779ce74 100644 --- a/client/webapp/personal-db.ts +++ b/client/webapp/personal-db.ts @@ -1,3 +1,5 @@ +import * as crypto from 'crypto'; + import ConcurrentQueue from "../../concurrent-queue/concurrent-queue"; import * as sqlite from 'sqlite'; @@ -150,10 +152,10 @@ export default class PersonalDB { // Guilds - async addGuild(name: string | null, icon: Resource | null): Promise { + async addGuild(name: string, iconResourceId: string, memberId: string): Promise { let result = await this.db.run( - `INSERT INTO guilds (name, icon_resource_id) VALUES (:name, :icon_resource_id)`, - { ':name': name, ':icon_resource_id': icon?.id ?? null } + `INSERT INTO guilds (name, icon_resource_id, member_id) VALUES (:name, :icon_resource_id, :member_id)`, + { ':name': name, ':icon_resource_id': iconResourceId, ':member_id': memberId } ); if (result.changes !== 1) throw new Error('unable to add guild'); if (result.lastID === undefined) throw new Error('unable to get guild last id'); @@ -200,24 +202,33 @@ export default class PersonalDB { // Guild Sockets - async addGuildSocket(guildId: number, socketConfig: SocketConfig): Promise { - let publicKeyPem = socketConfig.publicKey.export({ type: 'spki', format: 'pem' }); - let privateKeyPem = socketConfig.privateKey.export({ type: 'pkcs8', format: 'pem' }); - let result = await this.db.all( + async addGuildSocket(guildId: number, url: string, cert: string, publicKey: crypto.KeyObject, privateKey: crypto.KeyObject): Promise { + let publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }); + let privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }); + let result = await this.db.run( `INSERT INTO guild_sockets (guild_id, url, cert, public_key, private_key) VALUES (:guild_id, :url, :cert, :public_key, :private_key)`, - { ':guild_id': guildId, ':url': socketConfig.url, ':cert': socketConfig.cert, ':public_key': publicKeyPem, ':private_key': privateKeyPem } + { ':guild_id': guildId, ':url': url, ':cert': cert, ':public_key': publicKeyPem, ':private_key': privateKeyPem } ); - if (result.length !== 1) throw new Error('unable to add guild'); + if (result.changes !== 1) throw new Error('unable to add guild'); + return result.lastID as number; } async removeGuildSocket(guildId: number, guildSocketId: number): Promise { let result = await this.db.run( - `DELETE FROM guilds WHERE id=:guild_socket_id AND guild_id=:guild_id`, + `DELETE FROM guild_sockets WHERE id=:guild_socket_id AND guild_id=:guild_id`, { ':guild_id': guildId, ':guild_socket_id': guildSocketId } ); if (result?.changes !== 1) throw new Error('unable to remove guild'); } + async removeGuildSockets(guildId: number): Promise { + let result = await this.db.run( + `DELETE FROM guild_sockets WHERE guild_id=:guild_id`, + { ':guild_id': guildId } + ); + if (result?.changes !== 1) throw new Error('unable to remove guild'); + } + async fetchGuildSockets(guildId: number): Promise { let result = await this.db.all( `SELECT * FROM guild_sockets WHERE guild_id=:guild_id`, @@ -226,6 +237,15 @@ export default class PersonalDB { return result.map(dataGuildSocket => SocketConfig.fromDBData(dataGuildSocket)); } + async fetchGuildSocket(guildId: number, guildSocketId: number): Promise { + let result = await this.db.get( + `SELECT * FROM guild_sockets WHERE id=:guild_socket_id AND guild_id=:guild_id`, + { ':guild_socket_id': guildSocketId, ':guild_id': guildId } + ); + if (!result) throw new Error('unable to fetch specific guild socket'); + return SocketConfig.fromDBData(result); + } + // Members async addMembers(guildId: number, members: Member[]): Promise { @@ -285,6 +305,10 @@ export default class PersonalDB { return result.map(dataMember => Member.fromDBData(dataMember)); } + async clearAllMembersStatus(guildId: number): Promise { + await this.db.run(`UPDATE members SET status='unknown' WHERE guild_id=:guild_id`, { ':guild_id': guildId }); + } + // Channels async addChannels(guildId: number, channels: Channel[]) { diff --git a/client/webapp/script.ts b/client/webapp/script.ts index f0986a4..dc6a309 100644 --- a/client/webapp/script.ts +++ b/client/webapp/script.ts @@ -74,8 +74,8 @@ window.addEventListener('DOMContentLoaded', () => { } // Receive Current Channel Messages - controller.on('new-message', async (server: ClientController, message: Message) => { - if (ui.activeServer === null || ui.activeServer.id !== server.id) return; + 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 @@ -88,7 +88,7 @@ window.addEventListener('DOMContentLoaded', () => { } }); - controller.on('verified', async (server: ClientController) => { + controller.on('verified', async (guild: CombinedGuild) => { (async () => { // update connection info await Actions.fetchAndUpdateConnection(ui, server); })(); @@ -120,7 +120,7 @@ window.addEventListener('DOMContentLoaded', () => { })(); }); - controller.on('disconnected', (server: ClientController) => { + controller.on('disconnected', (guild: CombinedGuild) => { (async () => { await Actions.fetchAndUpdateConnection(ui, server); })(); @@ -129,7 +129,7 @@ window.addEventListener('DOMContentLoaded', () => { })(); }); - controller.on('update-server', async (server: ClientController, serverData: ServerMetaData | CacheServerData) => { + controller.on('update-server', async (guild: CombinedGuild, serverData: ServerMetaData | CacheServerData) => { LOG.debug(`s#${server.id} metadata updated`) await ui.updateServerName(server, serverData.name); @@ -146,12 +146,12 @@ window.addEventListener('DOMContentLoaded', () => { } }); - controller.on('deleted-members', async (server: ClientController, members: Member[]) => { + 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 (server: ClientController, data: { oldMember: Member, newMember: Member }[]) => { + 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 ( @@ -162,40 +162,40 @@ window.addEventListener('DOMContentLoaded', () => { } }); - controller.on('added-members', async (server: ClientController, members: Member[]) => { + 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 (server: ClientController, channels: Channel[]) => { + 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 (server: ClientController, data: { oldChannel: Channel, newChannel: Channel }[]) => { + 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 (server: ClientController, channels: Channel[]) => { + 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 (server: ClientController, channel: Channel, messages: Message[]) => { + 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 (server: ClientController, channel: Channel, data: { oldMessage: Message, newMessage: Message }[]) => { + 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 (server: ClientController, channel: Channel, addedAfter: Map, addedBefore: Map) => { + 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 diff --git a/client/webapp/socket-verifier.ts b/client/webapp/socket-verifier.ts index 8e7ab1e..f7d4812 100644 --- a/client/webapp/socket-verifier.ts +++ b/client/webapp/socket-verifier.ts @@ -4,6 +4,7 @@ import * as socketio from 'socket.io-client'; import DedupAwaiter from "./dedup-awaiter"; import Util from './util'; +// Automatically re-verifies the socket when connected export default class SocketVerifier { public isVerified = false; private memberId: string | null = null; diff --git a/client/webapp/ui.ts b/client/webapp/ui.ts index f8de9be..4deea0d 100644 --- a/client/webapp/ui.ts +++ b/client/webapp/ui.ts @@ -9,13 +9,12 @@ import ElementsUtil from './elements/require/elements-util'; import Globals from './globals'; import Util from './util'; -import ClientController from './client-controller'; +import CombinedGuild from './guild-combined'; import { Message, Member, Channel, ConnectionInfo, ShouldNeverHappenError } from './data-types'; import Q from './q-module'; -import createServerListServer from './elements/server-list-server'; +import createGuildListGuild from './elements/guild-list-guild'; import createChannel from './elements/channel'; import createMember from './elements/member'; -import Actions from './actions'; import Controller from './controller'; import createMessage from './elements/message'; @@ -25,14 +24,14 @@ interface SetMessageProps { } export default class UI { - public activeServer: ClientController | null = null; + public activeGuild: CombinedGuild | null = null; public activeChannel: Channel | null = null; public activeConnection: ConnectionInfo | null = null; public messagesAtTop = false; public messagesAtBottom = false; - public messagePairsServer: ClientController | null = null; + public messagePairsGuild: CombinedGuild | null = null; public messagePairsChannel: Channel | { id: string } | null = null; public messagePairs = new Map(); // messageId -> { message: Message, element: HTMLElement } @@ -44,8 +43,8 @@ export default class UI { this.q = q; } - public isMessagePairsServer(server: ClientController): boolean { - return this.messagePairsServer !== null && server.id === this.messagePairsServer.id; + public isMessagePairsGuild(guild: CombinedGuild): boolean { + return this.messagePairsGuild !== null && guild.id === this.messagePairsGuild.id; } public isMessagePairsChannel(channel: Channel): boolean { @@ -55,66 +54,66 @@ export default class UI { // Use non-concurrent queues to prevent concurrent updates to parts of the view // This is effectively a javascript version of a 'lock' // These 'locks' should be called from working code rather than the updating functions themselves to work properly - private _serversLock = new ConcurrentQueue(1); - private _serverNameLock = new ConcurrentQueue(1); + private _guildsLock = new ConcurrentQueue(1); + private _guildNameLock = new ConcurrentQueue(1); private _connectionLock = new ConcurrentQueue(1); private _channelsLock = new ConcurrentQueue(1); private _membersLock = new ConcurrentQueue(1); private _messagesLock = new ConcurrentQueue(1); - private async _lockWithServer(server: ClientController, task: (() => Promise | void), lock: ConcurrentQueue): Promise { - if (this.activeServer === null || this.activeServer.id !== server.id) return; + private async _lockWithGuild(guild: CombinedGuild, task: (() => Promise | void), lock: ConcurrentQueue): Promise { + if (this.activeGuild === null || this.activeGuild.id !== guild.id) return; await lock.push(async () => { - if (this.activeServer === null || this.activeServer.id !== server.id) return; + if (this.activeGuild === null || this.activeGuild.id !== guild.id) return; await task(); }); } - private async _lockWithServerChannel(server: ClientController, channel: Channel | { id: string }, task: (() => Promise | void), lock: ConcurrentQueue): Promise { - if (this.activeServer === null || this.activeServer.id !== server.id) return; + private async _lockWithGuildChannel(guild: CombinedGuild, channel: Channel | { id: string }, task: (() => Promise | void), lock: ConcurrentQueue): Promise { + if (this.activeGuild === null || this.activeGuild.id !== guild.id) return; if (this.activeChannel === null || this.activeChannel.id !== channel.id) return; await lock.push(async () => { - if (this.activeServer === null || this.activeServer.id !== server.id) return; + if (this.activeGuild === null || this.activeGuild.id !== guild.id) return; if (this.activeChannel === null || this.activeChannel.id !== channel.id) return; await task(); }); } - public async lockServerName(server: ClientController, task: (() => Promise | void)): Promise { - await this._lockWithServer(server, task, this._serverNameLock); + public async lockGuildName(guild: CombinedGuild, task: (() => Promise | void)): Promise { + await this._lockWithGuild(guild, task, this._guildNameLock); } - public async lockConnection(server: ClientController, task: (() => Promise | void)): Promise { - await this._lockWithServer(server, task, this._connectionLock); + public async lockConnection(guild: CombinedGuild, task: (() => Promise | void)): Promise { + await this._lockWithGuild(guild, task, this._connectionLock); } - public async lockChannels(server: ClientController, task: (() => Promise | void)): Promise { - await this._lockWithServer(server, task, this._channelsLock); + public async lockChannels(guild: CombinedGuild, task: (() => Promise | void)): Promise { + await this._lockWithGuild(guild, task, this._channelsLock); } - public async lockMembers(server: ClientController, task: (() => Promise | void)): Promise { - await this._lockWithServer(server, task, this._membersLock); + public async lockMembers(guild: CombinedGuild, task: (() => Promise | void)): Promise { + await this._lockWithGuild(guild, task, this._membersLock); } - public async lockMessages(server: ClientController, channel: Channel | { id: string }, task: (() => Promise | void)): Promise { - await this._lockWithServerChannel(server, channel, task, this._messagesLock); + public async lockMessages(guild: CombinedGuild, channel: Channel | { id: string }, task: (() => Promise | void)): Promise { + await this._lockWithGuildChannel(guild, channel, task, this._messagesLock); } - public setActiveServer(server: ClientController): void { - if (this.activeServer !== null) { - let prev = this.q.$_('#server-list .server[meta-id="' + this.activeServer.id + '"]'); + public setActiveGuild(guild: CombinedGuild): void { + if (this.activeGuild !== null) { + let prev = this.q.$_('#guild-list .guild[meta-id="' + this.activeGuild.id + '"]'); if (prev) { prev.classList.remove('active'); } } - let next = this.q.$('#server-list .server[meta-id="' + server.id + '"]'); + let next = this.q.$('#guild-list .guild[meta-id="' + guild.id + '"]'); next.classList.add('active'); - this.q.$('#server').setAttribute('meta-id', server.id); - this.activeServer = server; + this.q.$('#guild').setAttribute('meta-id', guild.id + ''); + this.activeGuild = guild; } - public async setActiveChannel(server: ClientController, channel: Channel): Promise { - await this.lockChannels(server, () => { + public async setActiveChannel(guild: CombinedGuild, channel: Channel): Promise { + await this.lockChannels(guild, () => { // Channel List Highlight if (this.activeChannel !== null) { let prev = this.q.$_('#channel-list .channel[meta-id="' + this.activeChannel.id + '"]'); @@ -135,25 +134,25 @@ export default class UI { }); } - public async setActiveConnection(server: ClientController, connection: ConnectionInfo): Promise { - await this.lockConnection(server, () => { + public async setActiveConnection(guild: CombinedGuild, connection: ConnectionInfo): Promise { + await this.lockConnection(guild, () => { this.activeConnection = connection; this.q.$('#connection').className = 'member ' + connection.status; this.q.$('#member-name').innerText = connection.displayName; this.q.$('#member-status-text').innerText = connection.status; - this.q.$('#server').className = ''; + this.q.$('#guild').className = ''; for (let privilege of connection.privileges) { - this.q.$('#server').classList.add('privilege-' + privilege); + this.q.$('#guild').classList.add('privilege-' + privilege); } if (connection.avatarResourceId) { (async () => { // Update avatar - if (this.activeServer === null || this.activeServer.id !== server.id) return; - let src = await ElementsUtil.getImageBufferFromResourceFailSoftly(server, connection.avatarResourceId); - if (this.activeServer === null || this.activeServer.id !== server.id) return; + if (this.activeGuild === null || this.activeGuild.id !== guild.id) return; + let src = await ElementsUtil.getImageBufferFromResourceFailSoftly(guild, connection.avatarResourceId); + if (this.activeGuild === null || this.activeGuild.id !== guild.id) return; (this.q.$('#member-avatar') as HTMLImageElement).src = src; })(); } else { @@ -162,55 +161,55 @@ export default class UI { }); } - public async setServers(controller: Controller, servers: ClientController[]): Promise { - await this._serversLock.push(() => { - Q.clearChildren(this.q.$('#server-list')); - for (let server of servers) { - let element = createServerListServer(this.document, this.q, this, controller, server); - this.q.$('#server-list').appendChild(element); + public async setGuilds(controller: Controller, 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); + this.q.$('#guild-list').appendChild(element); } }); } - public async addServer(controller: Controller, server: ClientController): Promise { + public async addGuild(controller: Controller, guild: CombinedGuild): Promise { let element: HTMLElement | null = null; - await this._serversLock.push(() => { - element = createServerListServer(this.document, this.q, this, controller, server) as HTMLElement; - this.q.$('#server-list').appendChild(element); + await this._guildsLock.push(() => { + element = createGuildListGuild(this.document, this.q, this, controller, guild) as HTMLElement; + this.q.$('#guild-list').appendChild(element); }); if (element == null) throw new ShouldNeverHappenError('element was not set'); return element; } - public async removeServer(server: ClientController): Promise { - await this._serversLock.push(() => { - let element = this.q.$_('#server-list .server[meta-id="' + server.id + '"]'); + public async removeGuild(guild: CombinedGuild): Promise { + await this._guildsLock.push(() => { + let element = this.q.$_('#guild-list .guild[meta-id="' + guild.id + '"]'); element?.parentElement?.removeChild(element); }); } - public async updateServerIcon(server: ClientController, iconBuff: Buffer): Promise { - await this._serversLock.push(async () => { - let iconElement = this.q.$('#server-list .server[meta-id="' + server.id + '"] img') as HTMLImageElement; + public async updateGuildIcon(guild: CombinedGuild, iconBuff: Buffer): Promise { + await this._guildsLock.push(async () => { + let iconElement = this.q.$('#guild-list .guild[meta-id="' + guild.id + '"] img') as HTMLImageElement; iconElement.src = await ElementsUtil.getImageBufferSrc(iconBuff); }); } - public async updateServerName(server: ClientController, name: string): Promise{ - await this.lockServerName(server, () => { - this.q.$('#server-name').innerText = name; - let baseElement = this.q.$('#server-list .server[meta-id="' + server.id + '"]'); + public async updateGuildName(guild: CombinedGuild, name: string): Promise{ + await this.lockGuildName(guild, () => { + this.q.$('#guild-name').innerText = name; + let baseElement = this.q.$('#guild-list .guild[meta-id="' + guild.id + '"]'); baseElement.setAttribute('meta-name', name); }); } - private _updatePosition(element: HTMLElement, serverCacheMap: Map, getDirection: ((prevData: T, data: T) => number)) { - let data = serverCacheMap.get(element.getAttribute('meta-id')); + private _updatePosition(element: HTMLElement, guildCacheMap: Map, getDirection: ((prevData: T, data: T) => number)) { + let data = guildCacheMap.get(element.getAttribute('meta-id')); if (!data) throw new ShouldNeverHappenError('unable to get data from cache map'); // TODO: do-while may be a bit cleaner? let prev = Q.previousElement(element); while (prev != null) { - let prevData = serverCacheMap.get(prev.getAttribute('meta-id')); + let prevData = guildCacheMap.get(prev.getAttribute('meta-id')); if (!prevData) throw new ShouldNeverHappenError('unable to get prevData from cache map'); if (getDirection(prevData, data) > 0) { // this element comes before previous element prev.parentElement?.insertBefore(element, prev); @@ -221,7 +220,7 @@ export default class UI { } let next = Q.nextElement(element); while (next != null) { - let nextData = serverCacheMap.get(next.getAttribute('meta-id')); + let nextData = guildCacheMap.get(next.getAttribute('meta-id')); if (!nextData) throw new ShouldNeverHappenError('unable to get nextData from cache map'); if (getDirection(data, nextData) > 0) { // this element comes after next element next.parentElement?.insertBefore(next, element); @@ -232,27 +231,27 @@ export default class UI { } } - public updateChannelPosition(server: ClientController, channelElement: HTMLElement): void { - this._updatePosition(channelElement, server.channels, (a, b) => { + public async updateChannelPosition(guild: CombinedGuild, channelElement: HTMLElement): Promise { + this._updatePosition(channelElement, await guild.grabRAMChannelsMap(), (a, b) => { return a.index - b.index; }); } - public async addChannels(server: ClientController, channels: Channel[], options?: { clear: boolean }): Promise { - await this.lockChannels(server, () => { + public async addChannels(guild: CombinedGuild, channels: Channel[], options?: { clear: boolean }): Promise { + await this.lockChannels(guild, async () => { if (options?.clear) { Q.clearChildren(this.q.$('#channel-list')); } for (let channel of channels) { - let element = createChannel(this.document, this.q, this, server, channel); + let element = createChannel(this.document, this.q, this, guild, channel); this.q.$('#channel-list').appendChild(element); - this.updateChannelPosition(server, element); + await this.updateChannelPosition(guild, element); } }); } - public async deleteChannels(server: ClientController, channels: Channel[]): Promise { - await this.lockChannels(server, () => { + public async deleteChannels(guild: CombinedGuild, channels: Channel[]): Promise { + await this.lockChannels(guild, () => { for (let channel of channels) { let element = this.q.$_('#channel-list .channel[meta-id="' + channel.id + '"]'); element?.parentElement?.removeChild(element); @@ -263,13 +262,13 @@ export default class UI { }); } - public async updateChannels(server: ClientController, data: { oldChannel: Channel, newChannel: Channel }[]): Promise { - await this.lockChannels(server, () => { + public async updateChannels(guild: CombinedGuild, data: { oldChannel: Channel, newChannel: 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, server, newChannel); + let newElement = createChannel(this.document, this.q, this, guild, newChannel); oldElement.parentElement?.replaceChild(newElement, oldElement); - this.updateChannelPosition(server, newElement); + await this.updateChannelPosition(guild, newElement); if (this.activeChannel !== null && this.activeChannel.id === newChannel.id) { newElement.classList.add('active'); @@ -284,38 +283,39 @@ export default class UI { }); } - public async setChannels(server: ClientController, channels: Channel[]): Promise { - // check if an element with the same channel and server exists before adding the new channels - // this is nescessary to make sure that if two servers have channels with the same id, the channel list is still + public async setChannels(guild: CombinedGuild, channels: Channel[]): Promise { + // check if an element with the same channel and guild exists before adding the new channels + // this is nescessary to make sure that if two guilds have channels with the same id, the channel list is still // properly refreshed and the active channel is not improperly set. let oldMatchingElement: HTMLElement | null = null; - if (this.activeServer !== null && this.activeChannel !== null) { - oldMatchingElement = this.q.$_('#channel-list .channel[meta-id="' + this.activeChannel.id + '"][meta-server-id="' + this.activeServer.id + '"]'); + if (this.activeGuild !== null && this.activeChannel !== null) { + oldMatchingElement = this.q.$_('#channel-list .channel[meta-id="' + this.activeChannel.id + '"][meta-guild-id="' + this.activeGuild.id + '"]'); } - await this.addChannels(server, channels, { clear: true }); + await this.addChannels(guild, channels, { clear: true }); - if (this.activeServer !== null && this.activeServer.id === server.id && this.activeChannel !== null) { - let newActiveElement = this.q.$_('#channel-list .channel[meta-id="' + this.activeChannel.id + '"][meta-server-id="' + this.activeServer.id + '"]'); + if (this.activeGuild !== null && this.activeGuild.id === guild.id && this.activeChannel !== null) { + let newActiveElement = this.q.$_('#channel-list .channel[meta-id="' + this.activeChannel.id + '"][meta-guild-id="' + this.activeGuild.id + '"]'); if (newActiveElement && oldMatchingElement) { let activeChannelId = this.activeChannel.id; let channel = channels.find(channel => channel.id === activeChannelId); if (channel === undefined) throw new ShouldNeverHappenError('current channel does not exist in channels list') - this.setActiveChannel(server, channel); + this.setActiveChannel(guild, channel); } else { this.activeChannel = null; // the active channel was removed } } } - public async setChannelsErrorIndicator(server: ClientController, errorIndicatorElement: HTMLElement): Promise { - await this.lockChannels(server, () => { + public async setChannelsErrorIndicator(guild: CombinedGuild, errorIndicatorElement: HTMLElement): Promise { + await this.lockChannels(guild, () => { Q.clearChildren(this.q.$('#channel-list')); this.q.$('#channel-list').appendChild(errorIndicatorElement); }); } - public updateMemberPosition(server: ClientController, memberElement: HTMLElement): void { + public async updateMemberPosition(guild: CombinedGuild, memberElement: HTMLElement): Promise { + // TODO: Change 100 to a constant? let statusOrder = { 'online': 0, 'away': 1, @@ -324,7 +324,7 @@ export default class UI { 'invisible': 3, // this would only be shown in the case of the current member. 'unknown': 100, }; - this._updatePosition(memberElement, server.members, (a, b) => { + this._updatePosition(memberElement, await guild.grabRAMMembersMap(), (a, b) => { let onlineCmp = (a.status == 'offline' ? 1 : 0) - (b.status == 'offline' ? 1 : 0); if (onlineCmp != 0) return onlineCmp; let rolePriorityCmp = (a.rolePriority == null ? 100 : a.rolePriority) - (b.rolePriority == null ? 100 : b.rolePriority); @@ -336,41 +336,41 @@ export default class UI { }); } - public async addMembers(server: ClientController, members: Member[], options?: { clear: boolean }): Promise { - await this.lockMembers(server, () => { + public async addMembers(guild: CombinedGuild, members: Member[], options?: { clear: boolean }): Promise { + await this.lockMembers(guild, async () => { if (options?.clear) { - Q.clearChildren(this.q.$('#server-members')); + Q.clearChildren(this.q.$('#guild-members')); } for (let member of members) { - let element = createMember(this.q, server, member); - this.q.$('#server-members').appendChild(element); - this.updateMemberPosition(server, element); + let element = createMember(this.q, guild, member); + this.q.$('#guild-members').appendChild(element); + await this.updateMemberPosition(guild, element); } }); } - public async deleteMembers(server: ClientController, members: Member[]): Promise { - await this.lockMembers(server, () => { + public async deleteMembers(guild: CombinedGuild, members: Member[]): Promise { + await this.lockMembers(guild, () => { for (let member of members) { - let element = this.q.$_('#server-members .member[meta-id="' + member.id + '"]'); + let element = this.q.$_('#guild-members .member[meta-id="' + member.id + '"]'); element?.parentElement?.removeChild(element); } }); } - public async updateMembers(server: ClientController, data: { oldMember: Member, newMember: Member }[]): Promise { - await this.lockMembers(server, () => { + public async updateMembers(guild: CombinedGuild, data: { oldMember: Member, newMember: Member }[]): Promise { + await this.lockMembers(guild, async () => { for (const { oldMember, newMember } of data) { - let oldElement = this.q.$_('#server-members .member[meta-id="' + newMember.id + '"]'); + let oldElement = this.q.$_('#guild-members .member[meta-id="' + newMember.id + '"]'); if (oldElement) { - let newElement = createMember(this.q, server, newMember); + let newElement = createMember(this.q, guild, newMember); oldElement.parentElement?.replaceChild(newElement, oldElement); - this.updateMemberPosition(server, newElement); + await this.updateMemberPosition(guild, newElement); } } }); if (this.activeChannel === null) return; - await this.lockMessages(server, this.activeChannel, () => { + await this.lockMessages(guild, this.activeChannel, () => { for (const { oldMember, newMember } of data) { let newStyle = newMember.roleColor ? 'color: ' + newMember.roleColor : null; let newName = newMember.displayName; @@ -386,14 +386,14 @@ export default class UI { }); } - public async setMembers(server: ClientController, members: Member[]): Promise { - await this.addMembers(server, members, { clear: true }); + public async setMembers(guild: CombinedGuild, members: Member[]): Promise { + await this.addMembers(guild, members, { clear: true }); } - public async setMembersErrorIndicator(server: ClientController, errorIndicatorElement: HTMLElement): Promise { - await this.lockMembers(server, () => { - Q.clearChildren(this.q.$('#server-members')); - this.q.$('#server-members').appendChild(errorIndicatorElement); + public async setMembersErrorIndicator(guild: CombinedGuild, errorIndicatorElement: HTMLElement): Promise { + await this.lockMembers(guild, () => { + Q.clearChildren(this.q.$('#guild-members')); + this.q.$('#guild-members').appendChild(errorIndicatorElement); }); } @@ -408,8 +408,8 @@ export default class UI { return element && this.messagePairs.get(element.getAttribute('meta-id')) || null; } - public async addMessagesBefore(server: ClientController, channel: Channel, messages: Message[], prevTopMessage: Message | null): Promise { - this.lockMessages(server, channel, () => { + public async addMessagesBefore(guild: CombinedGuild, channel: Channel, messages: Message[], prevTopMessage: Message | null): Promise { + this.lockMessages(guild, channel, () => { if (prevTopMessage && this.getTopMessagePair()?.message.id !== prevTopMessage.id) return; this.messagesAtTop = false; @@ -441,22 +441,22 @@ export default class UI { for (let i = messages.length - 1; i >= 0; --i) { let message = messages[i]; let priorMessage = messages[i - 1] || null; - let element = createMessage(this.document, this.q, server, message, priorMessage); + let element = createMessage(this.document, this.q, guild, message, priorMessage); this.messagePairs.set(message.id, { message: message, element: element }); this.q.$('#channel-feed').prepend(element); } if (messages.length > 0 && prevTopPair) { // Update the previous top message since it may have changed format - let newPrevTopElement = createMessage(this.document, this.q, server, prevTopPair.message, messages[messages.length - 1]); + let newPrevTopElement = createMessage(this.document, this.q, guild, prevTopPair.message, messages[messages.length - 1]); prevTopPair.element.parentElement?.replaceChild(newPrevTopElement, prevTopPair.element); this.messagePairs.set(prevTopPair.message.id, { message: prevTopPair.message, element: newPrevTopElement }); } }); } - public async addMessagesAfter(server: ClientController, channel: Channel | { id: string }, messages: Message[], prevBottomMessage: Message | null): Promise { - await this.lockMessages(server, channel, () => { + public async addMessagesAfter(guild: CombinedGuild, channel: Channel | { id: string }, messages: Message[], prevBottomMessage: Message | null): Promise { + await this.lockMessages(guild, channel, () => { if (prevBottomMessage && this.getBottomMessagePair()?.message.id !== prevBottomMessage.id) return; this.messagesAtBottom = false; @@ -487,7 +487,7 @@ export default class UI { for (let i = 0; i < messages.length; ++i) { // add in-order since we will be scrolling from oldest to newest let message = messages[i]; let priorMessage = messages[i - 1] || (prevBottomPair && prevBottomPair.message); - let element = createMessage(this.document, this.q, server, message, priorMessage); + let element = createMessage(this.document, this.q, guild, message, priorMessage); this.messagePairs.set(message.id, { message: message, element: element }); this.q.$('#channel-feed').appendChild(element); } @@ -495,8 +495,8 @@ export default class UI { } // TODO: use topMessage, bottomMessage / topMessageId, bottomMessageId instead? - public async addMessagesBetween(server: ClientController, channel: Channel, messages: Message[], topElement: HTMLElement, bottomElement: HTMLElement): Promise { - await this.lockMessages(server, channel, () => { + public async addMessagesBetween(guild: CombinedGuild, channel: Channel, 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 }); throw new Error('invalid messages between'); @@ -566,7 +566,7 @@ export default class UI { for (let i = 0; i < messages.length; ++i) { let message = messages[i]; let priorMessage = messages[i - 1] || topMessage; - let element = createMessage(this.document, this.q, server, message, priorMessage); + let element = createMessage(this.document, this.q, guild, message, priorMessage); this.messagePairs.set(message.id, { message: message, element: element }); this.q.$('#channel-feed').insertBefore(element, bottomElement); } @@ -575,16 +575,16 @@ export default class UI { // update the bottom element since the element above it changed let bottomMessage = this.messagePairs.get(bottomElement.getAttribute('meta-id'))?.message; if (!bottomMessage) throw new ShouldNeverHappenError('could not find bottom message'); - let newBottomElement = createMessage(this.document, this.q, server, bottomMessage, messages[messages.length - 1]); + let newBottomElement = createMessage(this.document, this.q, guild, bottomMessage, messages[messages.length - 1]); bottomElement.parentElement?.replaceChild(newBottomElement, bottomElement); this.messagePairs.set(bottomMessage.id, { element: newBottomElement, message: bottomMessage }); } }); } - public async setMessages(server: ClientController, channel: Channel | { id: string }, messages: Message[], props: SetMessageProps): Promise { + public async setMessages(guild: CombinedGuild, channel: Channel | { id: string }, messages: Message[], props: SetMessageProps): Promise { const { atTop, atBottom } = props; - await this.lockMessages(server, channel, () => { + await this.lockMessages(guild, channel, () => { this.messagesAtTop = atTop; this.messagesAtBottom = atBottom; @@ -592,7 +592,7 @@ export default class UI { Util.removeErrorIndicators(this.q, this.q.$('#channel-feed'), [ 'before' ]); Util.removeErrorIndicators(this.q, this.q.$('#channel-feed'), [ 'after' ]); - this.messagePairsServer = server; + this.messagePairsGuild = guild; this.messagePairsChannel = channel; this.messagePairs.clear(); @@ -604,7 +604,7 @@ export default class UI { for (let i = messages.length - 1; i >= 0; --i) { let message = messages[i]; let priorMessage = messages[i - 1] || null; - let element = createMessage(this.document, this.q, server, message, priorMessage); + let element = createMessage(this.document, this.q, guild, message, priorMessage); this.messagePairs.set(message.id, { message: message, element: element }); this.q.$('#channel-feed').prepend(element); } @@ -618,8 +618,8 @@ export default class UI { this.messagesAtBottom = true; } - public async deleteMessages(server: ClientController, channel: Channel, messages: Message[]) { - await this.lockMessages(server, channel, () => { + 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 }; @@ -632,14 +632,14 @@ export default class UI { }); } - public async updateMessages(server: ClientController, channel: Channel, data: { oldMessage: Message, newMessage: Message }[]): Promise { - await this.lockMessages(server, channel, () => { + 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, server, newMessage, prevMessage); + 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 @@ -649,20 +649,20 @@ export default class UI { }); } - public async addMessagesErrorIndicatorBefore(server: ClientController, channel: Channel, errorIndicatorElement: HTMLElement): Promise { - await this.lockMessages(server, channel, () => { + public async addMessagesErrorIndicatorBefore(guild: CombinedGuild, channel: Channel, errorIndicatorElement: HTMLElement): Promise { + await this.lockMessages(guild, channel, () => { this.q.$('#channel-feed').prepend(errorIndicatorElement); }); } - public async addMessagesErrorIndicatorAfter(server: ClientController, channel: Channel, errorIndicatorElement: HTMLElement): Promise { - await this.lockMessages(server, channel, () => { + public async addMessagesErrorIndicatorAfter(guild: CombinedGuild, channel: Channel, errorIndicatorElement: HTMLElement): Promise { + await this.lockMessages(guild, channel, () => { this.q.$('#channel-feed').appendChild(errorIndicatorElement); }); } - public async setMessagesErrorIndicator(server: ClientController, channel: Channel | { id: string }, errorIndicatorElement: HTMLElement): Promise { - await this.lockMessages(server, channel, () => { + public async setMessagesErrorIndicator(guild: CombinedGuild, channel: Channel | { id: string }, errorIndicatorElement: HTMLElement): Promise { + await this.lockMessages(guild, channel, () => { Q.clearChildren(this.q.$('#channel-feed')); this.q.$('#channel-feed').appendChild(errorIndicatorElement); }); diff --git a/server/server-controller.ts b/server/server-controller.ts index e765309..63b6eeb 100644 --- a/server/server-controller.ts +++ b/server/server-controller.ts @@ -178,10 +178,11 @@ function bindRegistrationEvents(io: socketio.Server, client: socketio.Socket): v const { guildId, memberId } = await DB.registerWithToken(token, publicKeyBuff, displayName, avatarBuff); const member = await DB.getMember(guildId, memberId); + const meta = await DB.getGuild(guildId); LOG.info(`c#${client.id}: registered with t#${token} as u#${member.id} / ${member.display_name}`); - respond(null, member); + respond(null, member, meta); io.to(guildId).emit('new-member', member); }