import * as electronRemote from '@electron/remote'; const electronConsole = electronRemote.getGlobal('console') as Console; import Logger from '../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); LOG.silly('script begins'); import Controller from './controller'; import DBCache from './db-cache'; import Globals from './globals'; import UI from './ui'; import Actions from './actions'; import { CacheServerData, Channel, ConnectionInfo, Member, Message, ServerMetaData } from './data-types'; import ClientController from './client-controller'; import Q from './q-module'; import bindWindowButtonEvents from './elements/events-window-buttons'; import bindTextInputEvents from './elements/events-text-input'; import bindInfiniteScrollEvents from './elements/events-infinite-scroll'; import bindConnectionEvents from './elements/events-connection'; import bindAddServerTitleEvents from './elements/events-server-title'; import bindAddServerEvents from './elements/events-add-server'; LOG.silly('modules loaded'); if (Globals.MESSAGES_PER_REQUEST >= Globals.MAX_CURRENT_MESSAGES) throw new Error('messages per request must be less than max current messages'); window.addEventListener('unhandledrejection', (e) => { LOG.error('Unhandled Promise Rejection', e.reason); }); window.addEventListener('error', (e) => { LOG.error('Uncaught Error', e.error); }); window.addEventListener('DOMContentLoaded', () => { document.body.classList.remove('preload'); (async () => { await DBCache.connect(); await DBCache.init(); LOG.silly('cache initialized'); const controller = new Controller(); await controller.init(); LOG.silly('controller initialized'); const q = new Q(document); const ui = new UI(document, q); const actions = new Actions(ui); LOG.silly('action classes initialized'); bindWindowButtonEvents(q); bindTextInputEvents(document, q, ui); bindInfiniteScrollEvents(q, ui, actions); bindConnectionEvents(document, q, ui); bindAddServerTitleEvents(document, q, ui); bindAddServerEvents(document, q, ui, actions, controller); LOG.silly('events bound'); // Add server icons await ui.setServers(actions, controller, controller.servers); if (controller.servers.length > 0) { // Click on the first server in the list q.$('#server-list .server').click(); } // Receive Current Channel Messages controller.on('new-message', async (server: ClientController, message: Message) => { if (ui.activeServer === null || ui.activeServer.id !== server.id) return; if (ui.activeChannel === null || ui.activeChannel.id !== message.channel.id) return; if (ui.messagesAtBottom) { // add the message to the bottom of the message feed await ui.addMessagesAfter(server, message.channel, [ message ], null); ui.jumpMessagesToBottom(); } else if (message.member.id == server.memberId) { // this set of messages will include the new messageserverId LOG.debug('not at bottom, jumping down since message was sent by the current user'); await actions.fetchAndUpdateMessagesRecent(q, server, message.channel); } }); controller.on('verified', async (server: ClientController) => { (async () => { // update connection info await actions.fetchAndUpdateConnection(server); })(); (async () => { // refresh members cache if (server.members) { await server.fetchMembers(); } else { await actions.fetchAndUpdateMembers(q, server); } })(); (async () => { // refresh channels cache if (server.channels) { await server.fetchChannels(); } else { await actions.fetchAndUpdateChannels(q, server); } })(); (async () => { // refresh current channel messages if (ui.activeChannel === null) return; if (ui.messagePairs.size == 0) { // fetch messages again since there are no messages yet await actions.fetchAndUpdateMessagesRecent(q, server, ui.activeChannel); } else { // Just update the infinite scroll. NOTE: this will not remove deleted messages ui.messagesAtTop = false; ui.messagesAtBottom = false; (q.$('#channel-feed-content-wrapper') as any).updateInfiniteScroll(); } })(); }); controller.on('disconnected', (server: ClientController) => { (async () => { await actions.fetchAndUpdateConnection(server); })(); (async () => { await actions.fetchAndUpdateMembers(q, server); })(); }); controller.on('update-server', async (server: ClientController, serverData: ServerMetaData | CacheServerData) => { LOG.debug(`s#${server.id} metadata updated`) await ui.updateServerName(server, serverData.name); // Not using withPotentialError since keeping the old icon is a fine fallback if (serverData.iconResourceId) { try { let iconBuff = await server.fetchResource(serverData.iconResourceId); await ui.updateServerIcon(server, iconBuff); } catch (e) { LOG.error('Error fetching new server icon', e); // Keep the old server icon, just log an error. // Should go through another try after a restart } } }); controller.on('deleted-members', async (server: ClientController, members: Member[]) => { LOG.debug(members.length + ' deleted members'); await ui.deleteMembers(server, members); }); controller.on('updated-members', async (server: ClientController, data: { oldDataPoint: Member, newDataPoint: Member }[]) => { LOG.debug(data.length + ' updated members s#' + server.id); await ui.updateMembers(server, data); if ( ui.activeConnection !== null && data.find((c: any) => c.newDataPoint.id === (ui.activeConnection as ConnectionInfo).id) ) { await actions.fetchAndUpdateConnection(server); } }); controller.on('added-members', async (server: ClientController, members: Member[]) => { LOG.debug(members.length + ' added members'); await ui.addMembers(server, members); }); controller.on('deleted-channels', async (server: ClientController, channels: Channel[]) => { LOG.debug(channels.length + ' deleted channels'); await ui.deleteChannels(server, channels); }); controller.on('updated-channels', async (server: ClientController, data: { oldDataPoint: Channel, newDataPoint: Channel }[]) => { LOG.debug(data.length + ' updated channels'); await ui.updateChannels(actions, server, data); }); controller.on('added-channels', async (server: ClientController, channels: Channel[]) => { LOG.debug(channels.length + ' added channels'); await ui.addChannels(actions, server, channels); }); controller.on('deleted-messages', async (server: ClientController, 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: { oldDataPoint: Message, newDataPoint: 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) => { LOG.debug(addedAfter.size + ' added messages'); // addedBefore.size should equal addedAfter.size //LOG.debug('added messages', { messages: Array.from(addedAfter.values()).map(message => message.text) }); // messages were added in a place that the cache did not have them if (!ui.isMessagePairsServer(server)) return; // these messages are not from the ones in the feed if (!ui.isMessagePairsChannel(channel)) return; // these messages are not from the ones in the feed let currentMessagesSorted = Array.from(ui.messagePairs.values()).sort((a, b) => { return a.message.sent.getTime() - b.message.sent.getTime(); }); // length could be 0 if all previous messages were 'deleted' if (currentMessagesSorted.length == 0) { // Simply set the text channel messages rather than calculating where to add them ui.setMessages(server, channel, Array.from(addedAfter.values()), { atTop: false, atBottom: true }); return; } let firstMessage = currentMessagesSorted[0].message; let lastMessage = currentMessagesSorted[currentMessagesSorted.length - 1].message; // messages that were updated (currently extraneous) // Note: this should never happen since these should be handled by updated-messages // Note: this may be used to replace dummy pre-sent messages let toUpdate: { newDataPoint: Message, oldDataPoint: Message }[] = []; for (let addedMessage of addedBefore.values()) { if (ui.messagePairs.has(addedMessage.id)) { toUpdate.push({ newDataPoint: addedMessage, oldDataPoint: (ui.messagePairs.get(addedMessage.id) as { message: Message, element: HTMLElement }).message }); } } if (toUpdate.length > 0) { LOG.warn('updating messages in added-messages... this was intended to be extraneous...', { toUpdate: toUpdate, toUpdateLength: toUpdate.length }); await ui.updateMessages(server, channel, toUpdate); } // messages before the first message let toAddBefore: Message[] = []; let nextFirstMessage = firstMessage; while (addedBefore.has(nextFirstMessage.id)) { nextFirstMessage = addedBefore.get(nextFirstMessage.id) as Message; toAddBefore.unshift(nextFirstMessage); } if (toAddBefore.length > 0) { LOG.debug('adding ' + toAddBefore.length + ' before'); await ui.addMessagesBefore(server, channel, toAddBefore, firstMessage); } // messages after the last message let toAddAfter: Message[] = []; let nextLastMessage = lastMessage; while (addedAfter.has(nextLastMessage.id)) { nextLastMessage = addedAfter.get(nextLastMessage.id) as Message; toAddAfter.push(nextLastMessage); } if (toAddAfter.length > 0) { LOG.debug('adding ' + toAddAfter.length + ' after'); await ui.addMessagesAfter(server, channel, toAddAfter, lastMessage); } // messages added between messages already in the feed let toAddBetween: { messageTop: HTMLElement, messageBottom: HTMLElement, betweenMessages: Message[] }[] = []; for (let i = 1; i < currentMessagesSorted.length; ++i) { let messageTop = currentMessagesSorted[i - 1].message; let messageBottom = currentMessagesSorted[i].message; let betweenMessages: Message[] = []; let followMessage = messageTop; // this should never be null as long as there is an addedAfter (will throw error if this is not the case) let lastFollowMessage = addedBefore.get(messageBottom.id) as Message; while (addedAfter.has(followMessage.id) && followMessage.id != lastFollowMessage.id) { followMessage = addedAfter.get(followMessage.id) as Message; betweenMessages.push(followMessage); } if (betweenMessages.length > 0) { toAddBetween.push({ messageTop: (ui.messagePairs.get(messageTop.id) as { message: Message, element: HTMLElement }).element, messageBottom: (ui.messagePairs.get(messageBottom.id) as { message: Message, element: HTMLElement }).element, betweenMessages: betweenMessages }); } } // add messages in between if (toAddBetween.length > 0) { LOG.debug('adding ' + toAddBetween.length + ' between sets'); } for (let messageSet of toAddBetween) { await ui.addMessagesBetween(server, channel, messageSet.betweenMessages, messageSet.messageTop, messageSet.messageBottom); } }); })(); });