cordis/client/webapp/script.ts

291 lines
12 KiB
TypeScript

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<string, Message>, addedBefore: Map<string, Message>) => {
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);
}
});
})();
});