import * as electronRemote from '@electron/remote'; const electronConsole = electronRemote.getGlobal('console') as Console; import Logger from '../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); LOG.silly('preload begins'); import * as path from 'path'; import * as fs from 'fs/promises'; import GuildsManager from './guilds-manager'; import Globals from './globals'; import UI from './ui'; import Actions from './actions'; import { Changes, Channel, ConnectionInfo, GuildMetadata, Member, Message, Resource, Token } from './data-types'; import Q from './q-module'; import bindWindowButtonEvents from './elements/events-window-buttons'; import bindTextInputEvents from './elements/events-text-input'; import bindInfiniteScrollEvents from './elements/events-infinite-scroll'; import bindConnectionEvents from './elements/events-connection'; import bindAddGuildTitleEvents from './elements/events-guild-title'; import bindAddGuildEvents from './elements/events-add-guild'; import PersonalDB from './personal-db'; import MessageRAMCache from './message-ram-cache'; import ResourceRAMCache from './resource-ram-cache'; import CombinedGuild from './guild-combined'; import { AutoVerifierChangesType } from './auto-verifier'; LOG.silly('modules loaded'); if (Globals.MESSAGES_PER_REQUEST >= Globals.MAX_CURRENT_MESSAGES) throw new Error('messages per request must be less than max current messages'); window.addEventListener('unhandledrejection', (e) => { LOG.error('Unhandled Promise Rejection', e.reason); }); window.addEventListener('error', (e) => { LOG.error('Uncaught Error', e.error); }); window.addEventListener('DOMContentLoaded', () => { document.body.classList.remove('preload'); (async () => { // Wait for the log to load the typescript source maps so that // logs will include typescript files+line numbers instead of // compiled javascript ones. await LOG.ensureSourceMaps(); LOG.silly('web client log source maps loaded'); // make sure the personaldb directory exists await fs.mkdir(path.dirname(Globals.PERSONALDB_FILE), { recursive: true }); const personalDB = await PersonalDB.create(Globals.PERSONALDB_FILE); await personalDB.init(); LOG.silly('personal db initialized'); let messageRAMCache = new MessageRAMCache(); let resourceRAMCache = new ResourceRAMCache(); LOG.silly('ram caches initialized'); const guildsManager = new GuildsManager(messageRAMCache, resourceRAMCache, personalDB); await guildsManager.init(); LOG.silly('controller initialized'); const q = new Q(document); const ui = new UI(document, q); LOG.silly('action classes initialized'); bindWindowButtonEvents(q); bindTextInputEvents(document, q, ui); bindInfiniteScrollEvents(q, ui); bindConnectionEvents(document, q, ui); bindAddGuildTitleEvents(document, q, ui); bindAddGuildEvents(document, q, ui, guildsManager); LOG.silly('events bound'); // Add guild icons await ui.setGuilds(guildsManager, guildsManager.guilds); if (guildsManager.guilds.length > 0) { // Click on the first guild in the list q.$('#guild-list .guild').click(); } // Connection Events guildsManager.on('verified', async (guild: CombinedGuild) => { (async () => { // update connection info await Actions.fetchAndUpdateConnection(ui, guild); })(); (async () => { // refresh members list await Actions.fetchAndUpdateMembers(q, ui, guild); })(); (async () => { // refresh channels list await Actions.fetchAndUpdateChannels(q, ui, guild); })(); (async () => { // refresh current channel messages if (ui.activeChannel === null) return; if (ui.messagePairs.size == 0) { // fetch messages again since there are no messages yet await Actions.fetchAndUpdateMessagesRecent(q, ui, guild, ui.activeChannel); } else { // If we already have messages, just update the infinite scroll. // NOTE: this will not add/remove new/deleted messages ui.messagesAtTop = false; ui.messagesAtBottom = false; (q.$('#channel-feed-content-wrapper') as any).updateInfiniteScroll(); } })(); }); guildsManager.on('disconnect', (guild: CombinedGuild) => { // Update everyone with the 'unknown' status (async () => { await Actions.fetchAndUpdateConnection(ui, guild); })(); (async () => { await Actions.fetchAndUpdateMembers(q, ui, guild); })(); }); // Change Events guildsManager.on('update-metadata', async (guild: CombinedGuild, guildMeta: GuildMetadata) => { LOG.debug(`g#${guild.id} metadata updated`) await ui.updateGuildName(guild, guildMeta.name); // Not using withPotentialError since keeping the old icon is a fine fallback if (guildMeta.iconResourceId) { try { let icon = await guild.fetchResource(guildMeta.iconResourceId); await ui.updateGuildIcon(guild, icon.data); } catch (e) { LOG.error('Error fetching new guild icon', e); // Keep the old guild icon, just log an error. // Should go through another try after a restart } } }); guildsManager.on('remove-members', async (guild: CombinedGuild, members: Member[]) => { LOG.debug(members.length + ' removed members'); await ui.deleteMembers(guild, members); }); guildsManager.on('update-members', async (guild: CombinedGuild, updatedMembers: Member[]) => { LOG.debug(updatedMembers.length + ' updated members g#' + guild.id); await ui.updateMembers(guild, updatedMembers); if ( ui.activeConnection !== null && updatedMembers.find(member => member.id === (ui.activeConnection as ConnectionInfo).id) ) { await Actions.fetchAndUpdateConnection(ui, guild); } }); guildsManager.on('new-members', async (guild: CombinedGuild, members: Member[]) => { LOG.debug(members.length + ' new members'); await ui.addMembers(guild, members); }); guildsManager.on('remove-channels', async (guild: CombinedGuild, channels: Channel[]) => { LOG.debug(channels.length + ' removed channels'); await ui.deleteChannels(guild, channels); }); guildsManager.on('update-channels', async (guild: CombinedGuild, updatedChannels: Channel[]) => { LOG.debug(updatedChannels.length + ' updated channels'); await ui.updateChannels(guild, updatedChannels); }); guildsManager.on('new-channels', async (guild: CombinedGuild, channels: Channel[]) => { LOG.debug(channels.length + ' added channels'); await ui.addChannels(guild, channels); }); guildsManager.on('remove-messages', async (guild: CombinedGuild, messages: Message[]) => { LOG.debug(messages.length + ' deleted messages'); await ui.deleteMessages(guild, messages); }); guildsManager.on('update-messages', async (guild: CombinedGuild, updatedMessages: Message[]) => { LOG.debug(updatedMessages.length + ' updated messages'); await ui.updateMessages(guild, updatedMessages); }); guildsManager.on('new-messages', async (guild: CombinedGuild, messages: Message[]) => { if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return; for (let message of messages) { if (ui.activeChannel === null || ui.activeChannel.id !== message.channel.id) return; if (ui.messagesAtBottom) { // add the message to the bottom of the message feed await ui.addMessages(guild, [ message ]); ui.jumpMessagesToBottom(); } else if (message.member.id == guild.memberId) { // this set of messages will include the new messageguildId LOG.debug('not at bottom, jumping down since message was sent by the current user'); await Actions.fetchAndUpdateMessagesRecent(q, ui, guild, message.channel); } } }); // Conflict Events guildsManager.on('conflict-metadata', async (guild: CombinedGuild, changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => { LOG.debug('metadata conflict', { newGuildMeta: newGuildMeta }); (async () => { await ui.updateGuildName(guild, newGuildMeta.name); })(); (async () => { let icon = await guild.fetchResource(newGuildMeta.iconResourceId); await ui.updateGuildIcon(guild, icon.data); })(); }); guildsManager.on('conflict-channels', async (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes) => { LOG.debug('channels conflict', { changes }); if (changes.deleted.length > 0) await ui.deleteChannels(guild, changes.deleted); if (changes.added.length > 0) await ui.addChannels(guild, changes.added); if (changes.updated.length > 0) await ui.updateChannels(guild, changes.updated.map(pair => pair.newDataPoint)); }); guildsManager.on('conflict-members', async (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes) => { //LOG.debug('members conflict', { changes }); if (changes.deleted.length > 0) await ui.deleteMembers(guild, changes.deleted); if (changes.added.length > 0) await ui.addMembers(guild, changes.added); if (changes.updated.length > 0) { (async () => { await ui.updateMembers(guild, changes.updated.map(pair => pair.newDataPoint)); })(); (async () => { LOG.debug('updating conflict members connection...'); // Likely getting called before the ram is updated await Actions.fetchAndUpdateConnection(ui, guild); })(); } }); guildsManager.on('conflict-messages', async (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes) => { //LOG.debug('messages conflict', { changes }); if (changes.deleted.length > 0) await ui.deleteMessages(guild, changes.deleted); if (changes.added.length > 0) await ui.addMessages(guild, changes.added); if (changes.updated.length > 0) await ui.updateMessages(guild, changes.updated.map(pair => pair.newDataPoint)); }); guildsManager.on('conflict-tokens', async (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes) => { LOG.debug('tokens conflict', { changes }); // TODO }); guildsManager.on('conflict-resource', async (guild: CombinedGuild, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => { LOG.debug('resource conflict', { oldResource, newResource }); // TODO (these changes should not happen often if at all) }); })(); });