combined guild mostly implemented

This commit is contained in:
Michael Peters 2021-11-21 20:47:29 -06:00
parent 64490d027f
commit 87a4d8584f
42 changed files with 626 additions and 563 deletions

View File

@ -35,7 +35,7 @@ electronMain.initialize();
webPreferences: {
//@ts-ignore enableRemoteModule is enabled with @electron/remote and not included in electron's typing
enableRemoteModule: true, // so we can get console logs properly
preload: path.join(__dirname, 'webapp', 'script.js')
preload: path.join(__dirname, 'webapp', 'entrypoint.js')
}
});

View File

@ -1,14 +1,14 @@
import ElementsUtil from './require/elements-util';
import BaseElements from './require/base-elements';
import ClientController from '../client-controller';
import { Channel } from '../data-types';
import createModifyChannelOverlay from './overlay-modify-channel';
import UI from '../ui';
import Actions from '../actions';
import Q from '../q-module';
import CombinedGuild from '../guild-combined';
export default function createChannel(document: Document, q: Q, ui: UI, guild: CombinedGuild, channel: Channel) {
let element = q.create({ class: 'channel text', 'meta-id': channel.id, 'meta-server-id': server.id, content: [
let element = q.create({ class: 'channel text', 'meta-id': channel.id, 'meta-server-id': guild.id, content: [
// Scraped directly from discord (#)
{ class: 'icon', content: BaseElements.TEXT_CHANNEL_ICON },
{ class: 'name', content: channel.name },
@ -17,8 +17,8 @@ export default function createChannel(document: Document, q: Q, ui: UI, guild: C
element.addEventListener('click', async () => {
if (element.classList.contains('active')) return;
await ui.setActiveChannel(server, channel);
await Actions.fetchAndUpdateMessagesRecent(q, ui, server, channel);
await ui.setActiveChannel(guild, channel);
await Actions.fetchAndUpdateMessagesRecent(q, ui, guild, channel);
q.$('#text-input').focus();
});
@ -34,7 +34,7 @@ export default function createChannel(document: Document, q: Q, ui: UI, guild: C
if (modifyContextElement.parentElement) {
modifyContextElement.parentElement.removeChild(modifyContextElement);
}
let modifyOverlay = createModifyChannelOverlay(document, q, server, channel);
let modifyOverlay = createModifyChannelOverlay(document, q, guild, channel);
document.body.appendChild(modifyOverlay);
q.$$$(modifyOverlay, '.text-input.channel-name').focus();
ElementsUtil.setCursorToEnd(q.$$$(modifyOverlay, '.text-input.channel-name'));

View File

@ -1,10 +1,10 @@
import ElementsUtil from './require/elements-util.js';
import BaseElements from './require/base-elements.js';
import ClientController from '../client-controller';
import createPersonalizeOverlay from './overlay-personalize.js';
import Q from '../q-module.js';
import UI from '../ui.js';
import CombinedGuild from '../guild-combined.js';
export default function createConnectionContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild) {
let statuses = [ 'online', 'away', 'busy', 'invisible' ];
@ -26,7 +26,7 @@ export default function createConnectionContextMenu(document: Document, q: Q, ui
q.$$$(element, '.personalize').addEventListener('click', async () => {
element.removeSelf();
if (ui.activeConnection === null) return;
let overlayElement = createPersonalizeOverlay(document, q, server, ui.activeConnection);
let overlayElement = createPersonalizeOverlay(document, q, guild, ui.activeConnection);
document.body.appendChild(overlayElement);
q.$$$(overlayElement, '.text-input').focus();
ElementsUtil.setCursorToEnd(q.$$$(overlayElement, '.text-input'));
@ -35,9 +35,9 @@ export default function createConnectionContextMenu(document: Document, q: Q, ui
for (let status of statuses) {
q.$$$(element, '.' + status).addEventListener('click', async () => {
element.removeSelf();
let currentConnection = await server.fetchConnectionInfo();
let currentConnection = await guild.fetchConnectionInfo();
if (status != currentConnection.status) {
await server.setStatus(status);
await guild.requestSetStatus(status);
}
});
}

View File

@ -5,29 +5,29 @@ const LOG = Logger.create(__filename, electronConsole);
import ElementsUtil from './require/elements-util.js';
import BaseElements from './require/base-elements.js';
import ClientController from '../client-controller';
import { CacheServerData, ServerMetaData } from '../data-types';
import { GuildMetadata } from '../data-types';
import Q from '../q-module';
import UI from '../ui';
import createErrorMessageOverlay from './overlay-error-message';
import createServerSettingsOverlay from './overlay-server-settings';
import createGuildSettingsOverlay from './overlay-guild-settings';
import createCreateInviteTokenOverlay from './overlay-create-invite-token';
import createCreateChannelOverlay from './overlay-create-channel';
import createTokenLogOverlay from './overlay-token-log';
import CombinedGuild from '../guild-combined';
export default function createServerTitleContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild): HTMLElement {
export default function createGuildTitleContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild): HTMLElement {
if (ui.activeConnection === null) {
LOG.warn('no active connection when creating server title context menu');
LOG.warn('no active connection when creating guild title context menu');
return q.create({}) as HTMLElement;
}
let menuItems: any[] = [];
if (ui.activeConnection.privileges.includes('modify_profile')) {
menuItems.push({ class: 'item server-settings', content: [
menuItems.push({ class: 'item guild-settings', content: [
{ class: 'icon', content: BaseElements.COG },
'Server Settings'
'Guild Settings'
] });
}
@ -59,23 +59,23 @@ export default function createServerTitleContextMenu(document: Document, q: Q, u
}
let element = BaseElements.createContextMenu(document, {
class: 'server-title-context', content: menuItems
class: 'guild-title-context', content: menuItems
});
if (ui.activeConnection.privileges.includes('modify_profile')) {
q.$$$(element, '.item.server-settings').addEventListener('click', async () => {
q.$$$(element, '.item.guild-settings').addEventListener('click', async () => {
element.removeSelf();
let serverMeta: ServerMetaData | CacheServerData | null = null;
let guildMeta: GuildMetadata | null = null;
try {
serverMeta = await server.grabMetadata();
guildMeta = await guild.fetchMetadata();
} catch (e) {
LOG.error('error fetching server info', e);
LOG.error('error fetching guild info', e);
}
if (serverMeta === null) {
let overlay = createErrorMessageOverlay(document, 'Error Opening Settings', 'Could not load server information');
if (guildMeta === null) {
let overlay = createErrorMessageOverlay(document, 'Error Opening Settings', 'Could not load guild information');
document.body.appendChild(overlay);
} else {
let overlay = createServerSettingsOverlay(document, q, server, serverMeta);
let overlay = createGuildSettingsOverlay(document, q, guild, guildMeta);
document.body.appendChild(overlay);
q.$$$(overlay, '.text-input').focus();
ElementsUtil.setCursorToEnd(q.$$$(overlay, '.text-input'));
@ -86,7 +86,7 @@ export default function createServerTitleContextMenu(document: Document, q: Q, u
if (ui.activeConnection.privileges.includes('modify_channels')) {
q.$$$(element, '.item.create-channel').addEventListener('click', () => {
element.removeSelf();
let overlay = createCreateChannelOverlay(document, q, server);
let overlay = createCreateChannelOverlay(document, q, guild);
document.body.appendChild(overlay);
q.$$$(overlay, '.text-input.channel-name').focus();
ElementsUtil.setCursorToEnd(q.$$$(overlay, '.text-input.channel-name'));
@ -96,13 +96,13 @@ export default function createServerTitleContextMenu(document: Document, q: Q, u
if (ui.activeConnection.privileges.includes('modify_members')) {
q.$$$(element, '.item.create-invite-token').addEventListener('click', () => {
element.removeSelf();
let overlay = createCreateInviteTokenOverlay(document, server);
let overlay = createCreateInviteTokenOverlay(document, guild);
document.body.appendChild(overlay);
//LOG.info('create invite token clicked');
});
q.$$$(element, '.item.token-log').addEventListener('click', () => {
element.removeSelf();
let overlay = createTokenLogOverlay(document, q, server);
let overlay = createTokenLogOverlay(document, q, guild);
document.body.appendChild(overlay);
});
}

View File

@ -5,12 +5,12 @@ const LOG = Logger.create(__filename, electronConsole);
import BaseElements from './require/base-elements.js';
import ClientController from '../client-controller.js';
import Q from '../q-module';
import UI from '../ui';
import Controller from '../controller';
import GuildsManager from '../guilds-manager';
import CombinedGuild from '../guild-combined';
export default function createServerContextMenu(document: Document, q: Q, ui: UI, controller: Controller, guild: CombinedGuild) {
export default function createServerContextMenu(document: Document, q: Q, ui: UI, guildsManager: GuildsManager, guild: CombinedGuild) {
let element = BaseElements.createContextMenu(document, {
class: 'server-context', content: [
{ class: 'item red leave-server', content: 'Leave Server' }
@ -19,9 +19,9 @@ export default function createServerContextMenu(document: Document, q: Q, ui: UI
q.$$$(element, '.leave-server').addEventListener('click', async () => {
element.removeSelf();
await server.disconnect();
await controller.removeServer(server);
await ui.removeServer(server);
guild.disconnect();
await guildsManager.removeServer(guild);
await ui.removeGuild(guild);
let firstServerElement = q.$_('#server-list .server');
if (firstServerElement) {
firstServerElement.click();

View File

@ -6,8 +6,8 @@ import * as electron from 'electron';
import BaseElements from './require/base-elements';
import ElementsUtil from './require/elements-util';
import ClientController from '../client-controller';
import Q from '../q-module';
import CombinedGuild from '../guild-combined';
export default function createImageContextMenu(
document: Document,
@ -28,7 +28,7 @@ export default function createImageContextMenu(
q.$$$(contextMenu, '.copy-image').innerText = 'Copying...';
let nativeImage: electron.NativeImage;
if (mime != 'image/png' && mime != 'image/jpeg' && mime != 'image/jpg') {
// use sharp to convert to png since nativeImage only supports jpeg/png
// use sharp to convserver: serverrt to png since nativeImage only supports jpeg/png
nativeImage = electron.nativeImage.createFromBuffer(await sharp(buffer).png().toBuffer());
} else {
nativeImage = electron.nativeImage.createFromBuffer(buffer);
@ -37,7 +37,7 @@ export default function createImageContextMenu(
q.$$$(contextMenu, '.copy-image').innerText = 'Copied to Clipboard';
});
q.$$$(contextMenu, '.save-image').addEventListener('click', ElementsUtil.createDownloadListener({
downloadBuff: buffer, server: server,
downloadBuff: buffer, guild: guild,
resourceName: path.basename(resourceName, '.' + ext) + (isPreview ? '-preview.' : '.') + ext,
downloadStartFunc: () => {},
writeStartFunc: () => { q.$$$(contextMenu, '.save-image').innerText = 'Writing...'; },

View File

@ -7,25 +7,24 @@ import * as fs from 'fs/promises';
import ElementsUtil from './require/elements-util';
import createAddServerOverlay, { IAddServerData } from './overlay-add-server';
import createAddGuildOverlay, { IAddGuildData } from './overlay-add-guild';
import Q from '../q-module';
import UI from '../ui';
import Controller from '../controller';
import GuildsManager from '../guilds-manager';
import createErrorMessageOverlay from './overlay-error-message';
import Actions from '../actions';
export default function bindAddServerEvents(document: Document, q: Q, ui: UI, controller: Controller): void {
export default function bindAddGuildEvents(document: Document, q: Q, ui: UI, guildsManager: GuildsManager): void {
let choosingFile = false;
q.$('#add-server').addEventListener('click', async () => {
q.$('#add-guild').addEventListener('click', async () => {
if (choosingFile) return;
choosingFile = true;
let result = await electronRemote.dialog.showOpenDialog({
title: 'Select Server File',
title: 'Select Guild File',
defaultPath: '.', // TODO: better path name
properties: [ 'openFile' ],
filters: [
{ name: 'Cordis Server Files', extensions: [ 'cordis' ] }
{ name: 'Cordis Guild Files', extensions: [ 'cordis' ] }
]
});
@ -37,26 +36,26 @@ export default function bindAddServerEvents(document: Document, q: Q, ui: UI, co
let filePath = result.filePaths[0];
let fileText = (await fs.readFile(filePath)).toString('utf-8'); // TODO: try/catch?
let addServerData: any | null = null;
let addGuildData: any | null = null;
try {
addServerData = JSON.parse(fileText);
addGuildData = JSON.parse(fileText);
if (
typeof addServerData !== 'object' ||
typeof addServerData?.name !== 'string' ||
typeof addServerData?.url !== 'string' ||
typeof addServerData?.cert !== 'string' ||
typeof addServerData?.token !== 'string' ||
typeof addServerData?.expires !== 'number' ||
typeof addServerData?.iconSrc !== 'string'
typeof addGuildData !== 'object' ||
typeof addGuildData?.name !== 'string' ||
typeof addGuildData?.url !== 'string' ||
typeof addGuildData?.cert !== 'string' ||
typeof addGuildData?.token !== 'string' ||
typeof addGuildData?.expires !== 'number' ||
typeof addGuildData?.iconSrc !== 'string'
) {
LOG.debug('bad server data:', { addServerData, fileText })
throw new Error('bad server data');
LOG.debug('bad guild data:', { addGuildData, fileText })
throw new Error('bad guild data');
}
let overlayElement = createAddServerOverlay(document, q, ui, controller, addServerData as IAddServerData);
let overlayElement = createAddGuildOverlay(document, q, ui, guildsManager, addGuildData as IAddGuildData);
document.body.appendChild(overlayElement);
} catch (e) {
LOG.error('Unable to parse server data', e);
let errorOverlayElement = createErrorMessageOverlay(document, 'Unable to parse server file', e.message);
LOG.error('Unable to parse guild data', e);
let errorOverlayElement = createErrorMessageOverlay(document, 'Unable to parse guild file', e.message);
document.body.appendChild(errorOverlayElement);
}
@ -72,14 +71,14 @@ export default function bindAddServerEvents(document: Document, q: Q, ui: UI, co
'L 8,0 ' +
'Z' }
] },
{ class: 'content', content: 'Add a Server' }
{ class: 'content', content: 'Add a Guild' }
]
} }) as HTMLElement;
q.$('#add-server').addEventListener('mouseenter', () => {
q.$('#add-guild').addEventListener('mouseenter', () => {
document.body.appendChild(contextElement);
ElementsUtil.alignContextElement(contextElement, q.$('#add-server'), { left: 'right', centerY: 'centerY' })
ElementsUtil.alignContextElement(contextElement, q.$('#add-guild'), { left: 'right', centerY: 'centerY' })
});
q.$('#add-server').addEventListener('mouseleave', () => {
q.$('#add-guild').addEventListener('mouseleave', () => {
if (contextElement.parentElement) {
contextElement.parentElement.removeChild(contextElement);
}

View File

@ -7,7 +7,7 @@ import createConnectionContextMenu from './context-menu-conn';
export default function bindConnectionEvents(document: Document, q: Q, ui: UI): void {
q.$('#connection').addEventListener('click', () => {
if (ui.activeGuild === null) return;
if (!ui.activeGuild.isVerified) return;
if (!ui.activeGuild.isSocketVerified()) return;
let contextMenu = createConnectionContextMenu(document, q, ui, ui.activeGuild);
document.body.appendChild(contextMenu);

View File

@ -1,13 +1,13 @@
import Q from '../q-module';
import UI from '../ui';
import createServerTitleContextMenu from './context-menu-srv-title';
import createServerTitleContextMenu from './context-menu-guild-title';
import ElementsUtil from './require/elements-util';
export default function bindAddServerTitleEvents(document: Document, q: Q, ui: UI) {
q.$('#server-name-container').addEventListener('click', () => {
if (ui.activeConnection === null) return;
if (ui.activeGuild === null) return;
if (!ui.activeGuild.isVerified) return;
if (!ui.activeGuild.isSocketVerified()) return;
if (
!ui.activeConnection.privileges.includes('modify_profile') &&
!ui.activeConnection.privileges.includes('modify_members')

View File

@ -7,12 +7,12 @@ import ElementsUtil from './require/elements-util.js';
import Globals from '../globals';
import { Channel } from '../data-types';
import ClientController from '../client-controller';
import Q from '../q-module';
import UI from '../ui';
import createUploadOverlayFromPath from './overlay-upload-path';
import createUploadOverlayFromDataTransferItem from './overlay-upload-datatransfer';
import createUploadDropTarget from './overlay-upload-drop-target';
import CombinedGuild from '../guild-combined';
export default function bindTextInputEvents(document: Document, q: Q, ui: UI): void {
// Send Current Channel Messages
@ -27,10 +27,10 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v
sendingMessage = true;
let server = ui.activeGuild as ClientController;
let server = ui.activeGuild as CombinedGuild;
let channel = ui.activeChannel as Channel;
if (!server.isVerified) {
if (!server.isSocketVerified()) {
LOG.warn('client attempted to send message while not verified');
q.$('#send-error').innerText = 'Not Connected to Server';
await ElementsUtil.shakeElement(q.$('#send-error'), 400);
@ -50,7 +50,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v
q.$('#text-input').removeAttribute('contenteditable');
q.$('#text-input').classList.add('sending');
try {
await server.sendMessage(channel.id, text);
await server.requestSendMessage(channel.id, text);
q.$('#send-error').innerText = '';
q.$('#text-input').innerText = '';

View File

@ -10,11 +10,11 @@ import { GuildMetadata } from '../data-types';
import Q from '../q-module';
import UI from '../ui';
import Actions from '../actions';
import createGuildContextMenu from './context-menu-srv';
import Controller from '../controller';
import createGuildContextMenu from './context-menu-guild';
import GuildsManager from '../guilds-manager';
import CombinedGuild from '../guild-combined';
export default function createGuildListGuild(document: Document, q: Q, ui: UI, controller: Controller, guild: CombinedGuild) {
export default function createGuildListGuild(document: Document, q: Q, ui: UI, guildsManager: GuildsManager, guild: CombinedGuild) {
let element = q.create({ class: 'guild', 'meta-id': guild.id, 'meta-name': guild.id, content: [
{ class: 'pill' },
{ tag: 'img', src: './img/loading.svg', alt: 'guild' }, // src is set later by script.js
@ -24,10 +24,10 @@ export default function createGuildListGuild(document: Document, q: Q, ui: UI, c
(async () => {
let guildData: GuildMetadata;
try {
guildData = await guild.grabMetadata();
guildData = await guild.fetchMetadata();
if (!guildData.iconResourceId) throw new Error('guild icon not identified yet');
let guildIcon = await guild.fetchResource(guildData.iconResourceId);
let guildIconSrc = await ElementsUtil.getImageBufferSrc(guildIcon);
let guildIconSrc = await ElementsUtil.getImageBufferSrc(guildIcon.data);
(q.$$$(element, 'img') as HTMLImageElement).src = guildIconSrc;
} catch (e) {
LOG.error('Error fetching guild icon', e);
@ -83,7 +83,7 @@ export default function createGuildListGuild(document: Document, q: Q, ui: UI, c
(async () => {
// Explicitly not using a withPotentialError to make this simpler
try {
let guildData = await guild.grabMetadata();
let guildData = await guild.fetchMetadata();
ui.updateGuildName(guild, guildData.name);
} catch (e) {
LOG.error('Error fetching guild name', e);
@ -103,7 +103,7 @@ export default function createGuildListGuild(document: Document, q: Q, ui: UI, c
});
element.addEventListener('contextmenu', (e) => {
let contextMenu = createGuildContextMenu(document, q, ui, controller, guild);
let contextMenu = createGuildContextMenu(document, q, ui, guildsManager, guild);
document.body.appendChild(contextMenu);
let relativeTo = { x: e.pageX, y: e.pageY };
ElementsUtil.alignContextElement(contextMenu, relativeTo, { top: 'centerY', left: 'centerX' });

View File

@ -1,5 +1,5 @@
import ClientController from "../client-controller";
import { Member } from "../data-types";
import CombinedGuild from "../guild-combined";
import Q from "../q-module";
import ElementsUtil from "./require/elements-util";
@ -18,7 +18,7 @@ export default function createMember(q: Q, guild: CombinedGuild, member: Member)
] }) as HTMLElement;
(async () => {
(q.$$$(element, 'img.avatar') as HTMLImageElement).src =
await ElementsUtil.getImageBufferFromResourceFailSoftly(server, member.avatarResourceId);
await ElementsUtil.getImageBufferFromResourceFailSoftly(guild, member.avatarResourceId);
})();
return element;
}

View File

@ -1,5 +1,5 @@
import ClientController from '../client-controller';
import { Message } from '../data-types';
import CombinedGuild from '../guild-combined';
import Q from '../q-module';
import createImageResourceMessage from './msg-img-res';
import createImageResourceMessageContinued from './msg-img-res-cont';
@ -13,22 +13,22 @@ export default function createMessage(document: Document, q: Q, guild: CombinedG
if (message.hasResource()) {
if (message.isImageResource()) {
if (message.isContinued(lastMessage)) {
element = createImageResourceMessageContinued(document, q, server, message);
element = createImageResourceMessageContinued(document, q, guild, message);
} else {
element = createImageResourceMessage(document, q, server, message);
element = createImageResourceMessage(document, q, guild, message);
}
} else {
if (message.isContinued(lastMessage)) {
element = createResourceMessageContinued(q, server, message);
element = createResourceMessageContinued(q, guild, message);
} else {
element = createResourceMessage(q, server, message);
element = createResourceMessage(q, guild, message);
}
}
} else {
if (message.isContinued(lastMessage)) {
element = createTextMessageContinued(q, server, message);
element = createTextMessageContinued(q, guild, message);
} else {
element = createTextMessage(q, server, message);
element = createTextMessage(q, guild, message);
}
}
return element;

View File

@ -9,17 +9,17 @@ import * as FileType from 'file-type';
import ElementsUtil from './require/elements-util.js';
import { Message, ShouldNeverHappenError } from '../data-types';
import ClientController from '../client-controller';
import Q from '../q-module';
import createImageOverlay from './overlay-image';
import createImageContextMenu from './context-menu-img';
import CombinedGuild from '../guild-combined';
export default function createImageResourceMessageContinued(document: Document, q: Q, guild: CombinedGuild, message: Message): HTMLElement {
if (!message.resourceId || !message.resourcePreviewId || !message.resourceName) {
throw new ShouldNeverHappenError('Message is not a resource message');
}
let element = q.create({ class: 'message continued', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-server-id': server.id, content: [
let element = q.create({ class: 'message continued', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-guild-id': guild.id, content: [
{ class: 'timestamp', content: moment(message.sent).format('HH:mm') },
{ class: 'right', content: [
{ class: 'content image', style: `width: ${message.previewWidth}px; height: ${message.previewHeight}px;`, content:
@ -28,20 +28,20 @@ export default function createImageResourceMessageContinued(document: Document,
] }
] }) as HTMLElement;
q.$$$(element, '.content.image').addEventListener('click', () => {
document.body.appendChild(createImageOverlay(document, q, server, message.resourceId as string, message.resourceName as string));
document.body.appendChild(createImageOverlay(document, q, guild, message.resourceId as string, message.resourceName as string));
});
(async () => {
try {
let buffer = await server.fetchResource(message.resourcePreviewId as string);
let src = await ElementsUtil.getImageBufferSrc(buffer);
let resource = await guild.fetchResource(message.resourcePreviewId as string);
let src = await ElementsUtil.getImageBufferSrc(resource.data);
(q.$$$(element, '.content.image img') as HTMLImageElement).src = src;
let { mime, ext } = (await FileType.fromBuffer(buffer)) ?? { mime: null, ext: null };
let { mime, ext } = (await FileType.fromBuffer(resource.data)) ?? { mime: null, ext: null };
if (mime === null || ext === null) throw new Error('unable to get mime/ext');
q.$$$(element, '.content.image').addEventListener('contextmenu', (e) => {
let contextMenu = createImageContextMenu(document, q, server, message.resourceName as string, buffer, mime as string, ext as string, true);
let contextMenu = createImageContextMenu(document, q, guild, message.resourceName as string, resource.data, mime as string, ext as string, true);
document.body.appendChild(contextMenu);
let relativeTo = { x: e.pageX, y: e.pageY };
ElementsUtil.alignContextElement(contextMenu, relativeTo, { top: 'centerY', left: 'right' });

View File

@ -9,10 +9,10 @@ import * as FileType from 'file-type';
import ElementsUtil from './require/elements-util.js';
import { Message, Member, ShouldNeverHappenError } from '../data-types';
import ClientController from '../client-controller';
import Q from '../q-module';
import createImageOverlay from './overlay-image';
import createImageContextMenu from './context-menu-img';
import CombinedGuild from '../guild-combined';
export default function createImageResourceMessage(document: Document, q: Q, guild: CombinedGuild, message: Message) {
if (!message.resourceId || !message.resourcePreviewId || !message.resourceName) {
@ -39,7 +39,7 @@ export default function createImageResourceMessage(document: Document, q: Q, gui
}
let nameStyle = memberInfo.roleColor != null ? 'color: ' + memberInfo.roleColor : '';
let element = q.create({ class: 'message', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-server-id': server.id, content: [
let element = q.create({ class: 'message', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-guild-id': guild.id, content: [
{ class: 'member-avatar', content: { tag: 'img', src: './img/loading.svg', alt: memberInfo.displayName } },
{ class: 'right', content: [
{ class: 'header', content: [
@ -52,23 +52,23 @@ export default function createImageResourceMessage(document: Document, q: Q, gui
] }
] }) as HTMLElement;
q.$$$(element, '.content.image').addEventListener('click', (e) => {
document.body.appendChild(createImageOverlay(document, q, server, message.resourceId as string, message.resourceName as string));
document.body.appendChild(createImageOverlay(document, q, guild, message.resourceId as string, message.resourceName as string));
});
(async () => {
(q.$$$(element, '.member-avatar img') as HTMLImageElement).src =
await ElementsUtil.getImageBufferFromResourceFailSoftly(server, memberInfo.avatarResourceId);
await ElementsUtil.getImageBufferFromResourceFailSoftly(guild, memberInfo.avatarResourceId);
})();
(async () => {
try {
let buffer = await server.fetchResource(message.resourcePreviewId as string);
let src = await ElementsUtil.getImageBufferSrc(buffer);
let resource = await guild.fetchResource(message.resourcePreviewId as string);
let src = await ElementsUtil.getImageBufferSrc(resource.data);
(q.$$$(element, '.content.image img') as HTMLImageElement).src = src;
let { mime, ext } = (await FileType.fromBuffer(buffer)) ?? { mime: null, ext: null };
let { mime, ext } = (await FileType.fromBuffer(resource.data)) ?? { mime: null, ext: null };
if (mime === null || ext === null) throw new Error('unable to get mime/ext');
q.$$$(element, '.content.image').addEventListener('contextmenu', (e) => {
let contextMenu = createImageContextMenu(document, q, server, message.resourceName as string, buffer, mime as string, ext as string, true);
let contextMenu = createImageContextMenu(document, q, guild, message.resourceName as string, resource.data, mime as string, ext as string, true);
document.body.appendChild(contextMenu);
let relativeTo = { x: e.pageX, y: e.pageY };
ElementsUtil.alignContextElement(contextMenu, relativeTo, { top: 'centerY', left: 'right' });

View File

@ -1,6 +1,6 @@
import * as moment from "moment";
import ClientController from "../client-controller";
import { Message } from '../data-types';
import CombinedGuild from "../guild-combined";
import Q from "../q-module";
import ElementsUtil from "./require/elements-util";
@ -17,7 +17,7 @@ export default function createResourceMessageContinued(q: Q, guild: CombinedGuil
throw new ShouldNeverHappenError('Message is not a resource message');
}
let element = q.create({ class: 'message continued', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-server-id': server.id, content: [
let element = q.create({ class: 'message continued', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-guild-id': guild.id, content: [
{ class: 'timestamp', content: moment(message.sent).format('HH:mm') },
{ class: 'right', content: [
{ class: 'content resource', content: [
@ -31,7 +31,7 @@ export default function createResourceMessageContinued(q: Q, guild: CombinedGuil
] }
] }) as HTMLElement;
q.$$$(element, '.resource').addEventListener('click', ElementsUtil.createDownloadListener({
server: server, resourceId: message.resourceId, resourceName: message.resourceName,
guild: guild, resourceId: message.resourceId, resourceName: message.resourceName,
downloadStartFunc: () => {
q.$$$(element, '.resource .download-status').innerText = 'Downloading...';
},

View File

@ -1,6 +1,6 @@
import * as moment from 'moment';
import ClientController from "../client-controller";
import { Message, Member } from '../data-types';
import CombinedGuild from '../guild-combined';
import Q from '../q-module';
import ElementsUtil from './require/elements-util';
@ -37,7 +37,7 @@ export default function createResourceMessage(q: Q, guild: CombinedGuild, messag
}
let nameStyle = memberInfo.roleColor ? 'color: ' + memberInfo.roleColor : '';
let element = q.create({ class: 'message', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-server-id': server.id, content: [
let element = q.create({ class: 'message', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-guild-id': guild.id, content: [
{ class: 'member-avatar', content: { tag: 'img', src: './img/loading.svg', alt: memberInfo.displayName } },
{ class: 'right', content: [
{ class: 'header', content: [
@ -55,7 +55,7 @@ export default function createResourceMessage(q: Q, guild: CombinedGuild, messag
] }
] }) as HTMLElement;
q.$$$(element, '.resource').addEventListener('click', ElementsUtil.createDownloadListener({
server: server, resourceId: message.resourceId, resourceName: message.resourceName,
guild: guild, resourceId: message.resourceId, resourceName: message.resourceName,
downloadStartFunc: () => {
q.$$$(element, '.resource .download-status').innerText = 'Downloading...';
},
@ -76,7 +76,7 @@ export default function createResourceMessage(q: Q, guild: CombinedGuild, messag
}));
(async () => {
(q.$$$(element, '.member-avatar img') as HTMLImageElement).src =
await ElementsUtil.getImageBufferFromResourceFailSoftly(server, memberInfo.avatarResourceId);
await ElementsUtil.getImageBufferFromResourceFailSoftly(guild, memberInfo.avatarResourceId);
})();
return element;
}

View File

@ -1,12 +1,12 @@
import * as moment from 'moment';
import ClientController from '../client-controller.js';
import { Message } from '../data-types';
import CombinedGuild from '../guild-combined';
import Q from '../q-module.js';
import ElementsUtil from './require/elements-util.js';
export default function createTextMessageContinued(q: Q, guild: CombinedGuild, message: Message): HTMLElement {
return q.create({ class: 'message continued', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-server-id': server.id, content: [
return q.create({ class: 'message continued', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-guild-id': guild.id, content: [
{ class: 'timestamp', content: moment(message.sent).format('HH:mm') },
{ class: 'right', content: [
{ class: 'content text', content: ElementsUtil.parseMessageText(message.text ?? '') }

View File

@ -3,8 +3,8 @@ import * as moment from 'moment';
import ElementsUtil from './require/elements-util';
import { Message, Member, IDummyTextMessage } from '../data-types';
import ClientController from '../client-controller';
import Q from '../q-module';
import CombinedGuild from '../guild-combined';
export default function createTextMessage(q: Q, guild: CombinedGuild, message: Message | IDummyTextMessage): HTMLElement {
let memberInfo: {
@ -35,7 +35,7 @@ export default function createTextMessage(q: Q, guild: CombinedGuild, message: M
}
let nameStyle = memberInfo.roleColor ? 'color: ' + memberInfo.roleColor : '';
let element = q.create({ class: 'message', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-server-id': server.id, content: [
let element = q.create({ class: 'message', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-guild-id': guild.id, content: [
{ class: 'member-avatar', content: { tag: 'img', src: './img/loading.svg', alt: memberInfo.displayName } },
{ class: 'right', content: [
{ class: 'header', content: [
@ -47,7 +47,7 @@ export default function createTextMessage(q: Q, guild: CombinedGuild, message: M
] }) as HTMLElement;
(async () => {
(q.$$$(element, '.member-avatar img') as HTMLImageElement).src =
await ElementsUtil.getImageBufferFromResourceFailSoftly(server, memberInfo.avatarResourceId);
await ElementsUtil.getImageBufferFromResourceFailSoftly(guild, memberInfo.avatarResourceId);
})();
return element;
}

View File

@ -14,13 +14,12 @@ import Globals from '../globals';
import BaseElements from './require/base-elements';
import ElementsUtil from './require/elements-util';
import ClientController from '../client-controller';
import Q from '../q-module';
import UI from '../ui';
import Controller from '../controller';
import Actions from '../actions';
import GuildsManager from '../guilds-manager';
import CombinedGuild from '../guild-combined';
export interface IAddServerData {
export interface IAddGuildData {
name: string,
url: string,
cert: string,
@ -51,23 +50,23 @@ function getExampleAvatarPath(): string {
return paths[Math.floor(Math.random() * paths.length)];
}
export default function createAddServerOverlay(document: Document, q: Q, ui: UI, controller: Controller, addServerData: IAddServerData): HTMLElement {
let expired = addServerData.expires < new Date().getTime();
export default function createAddGuildOverlay(document: Document, q: Q, ui: UI, guildsManager: GuildsManager, addGuildData: IAddGuildData): HTMLElement {
let expired = addGuildData.expires < new Date().getTime();
let displayName = getExampleDisplayName();
let avatarPath = getExampleAvatarPath();
//LOG.debug('addserverdata:', { addServerData });
//LOG.debug('addguilddata:', { addGuildData });
let element = BaseElements.createOverlay(document, {
class: 'content add-server', content: [
class: 'content add-guild', content: [
{ class: 'preview', content: [
{ tag: 'img', class: 'icon', src: addServerData.iconSrc, alt: 'icon' },
{ tag: 'img', class: 'icon', src: addGuildData.iconSrc, alt: 'icon' },
{ content: [
{ class: 'name', content: addServerData.name },
{ class: 'url', content: addServerData.url },
{ class: 'name', content: addGuildData.name },
{ class: 'url', content: addGuildData.url },
{ class: 'expires',
content: (expired ? 'Invite Expired ' : 'Invite Expires ') + moment(addServerData.expires).fromNow() }
content: (expired ? 'Invite Expired ' : 'Invite Expires ') + moment(addGuildData.expires).fromNow() }
] }
] },
{ class: 'divider' },
@ -93,7 +92,7 @@ export default function createAddServerOverlay(document: Document, q: Q, ui: UI,
{ class: 'lower', content: [
{ class: 'error' },
{ class: 'buttons', content: [
{ class: 'button submit', content: 'Add Server' }
{ class: 'button submit', content: 'Add Guild' }
] }
] }
]
@ -177,12 +176,12 @@ export default function createAddServerOverlay(document: Document, q: Q, ui: UI,
q.$$$(element, '.display-name-input').removeAttribute('contenteditable');
let newguild: CombinedGuild | null = null;
if (addServerData == null) {
q.$$$(element, '.error').innerText = 'Very bad server file';
let newGuild: CombinedGuild | null = null;
if (addGuildData == null) {
q.$$$(element, '.error').innerText = 'Very bad guild file';
q.$$$(element, '.submit').innerText = 'Try Again';
await ElementsUtil.shakeElement(q.$$$(element, '.submit'), 400);
} else if (addServerData.expires < new Date().getTime()) {
} else if (addGuildData.expires < new Date().getTime()) {
q.$$$(element, '.error').innerText = 'Token expired';
q.$$$(element, '.submit').innerText = 'Try Again';
await ElementsUtil.shakeElement(q.$$$(element, '.submit'), 400);
@ -201,20 +200,20 @@ export default function createAddServerOverlay(document: Document, q: Q, ui: UI,
} else { // NOTE: Avatar size is checked above
q.$$$(element, '.submit').innerText = 'Registering...';
try {
newServer = await controller.addNewGuild(addServerData, displayName, avatarBuff);
newGuild = await guildsManager.addNewGuild(addGuildData, displayName, avatarBuff);
} catch (e) {
LOG.warn('error adding new server: ' + e.message, e); // explicitly not printing stack trace here
LOG.warn('error adding new guild: ' + e.message, e); // explicitly not printing stack trace here
q.$$$(element, '.error').innerText = e.message;
q.$$$(element, '.submit').innerText = 'Try Again';
await ElementsUtil.shakeElement(q.$$$(element, '.submit'), 400);
newServer = null;
newGuild = null;
}
}
if (newServer !== null) {
let serverElement = await ui.addServer(controller, newServer);
element.removeSelf(); // close the overlay since we got a new server
serverElement.click(); // click on the new server
if (newGuild !== null) {
let guildElement = await ui.addGuild(guildsManager, newGuild);
element.removeSelf(); // close the overlay since we got a new guild
guildElement.click(); // click on the new guild
}
q.$$$(element, '.display-name-input').setAttribute('contenteditable', 'plaintext-only');

View File

@ -3,12 +3,12 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import ClientController from "../client-controller";
import Globals from "../globals";
import ElementsUtil from "./require/elements-util";
import BaseElements from "./require/base-elements";
import Q from '../q-module';
import CombinedGuild from '../guild-combined';
export default function createCreateChannelOverlay(document: Document, q: Q, guild: CombinedGuild): HTMLElement {
@ -83,7 +83,7 @@ export default function createCreateChannelOverlay(document: Document, q: Q, gui
newFlavorText = null;
}
try {
await server.createChannel(newName, newFlavorText);
await guild.requestDoCreateChannel(newName, newFlavorText);
success = true;
} catch (e) {
LOG.error('error updating channel', e);

View File

@ -1,5 +1,4 @@
import ClientController from "../client-controller";
import CombinedGuild from "../guild-combined";
import BaseElements from "./require/base-elements";
export default function createCreateInviteTokenOverlay(document: Document, guild: CombinedGuild): HTMLElement {

View File

@ -8,20 +8,20 @@ import Globals from '../globals';
import BaseElements from './require/base-elements';
import ElementsUtil from './require/elements-util';
import ClientController from '../client-controller';
import { CacheServerData, ServerMetaData } from '../data-types';
import { GuildMetadata } from '../data-types';
import Q from '../q-module';
import CombinedGuild from '../guild-combined';
export default function createServerSettingsOverlay(document: Document, q: Q, guild: CombinedGuild, serverMeta: ServerMetaData | CacheServerData): HTMLElement {
export default function createGuildSettingsOverlay(document: Document, q: Q, guild: CombinedGuild, guildMeta: GuildMetadata): HTMLElement {
let element = BaseElements.createOverlay(document, {
class: 'content submit-dialog server-settings', content: [
{ class: 'server preview', content: [
class: 'content submit-dialog guild-settings', content: [
{ class: 'guild preview', content: [
{ class: 'icon', content: { tag: 'img', src: './img/loading.svg', alt: 'icon' } },
{ class: 'name', content: serverMeta.name }
{ class: 'name', content: guildMeta.name }
] },
{ class: 'text-input server-name', placeholder: 'New Server Name',
contenteditable: 'plaintext-only', content: serverMeta.name },
{ class: 'image-input server-icon', content: [
{ class: 'text-input guild-name', placeholder: 'New Guild Name',
contenteditable: 'plaintext-only', content: guildMeta.name },
{ class: 'image-input guild-icon', content: [
{ tag: 'label', class: 'image-input-label button', content: [
'Select New Icon',
{ class: 'image-input-upload', tag: 'input', type: 'file', accept: '.png,.jpg,.jpeg', style: 'display: none;' }
@ -37,7 +37,7 @@ export default function createServerSettingsOverlay(document: Document, q: Q, gu
});
(async () => {
(q.$$$(element, '.icon img') as HTMLImageElement).src = await ElementsUtil.getImageBufferFromResourceFailSoftly(server, serverMeta.iconResourceId);
(q.$$$(element, '.icon img') as HTMLImageElement).src = await ElementsUtil.getImageBufferFromResourceFailSoftly(guild, guildMeta.iconResourceId);
})();
let newIconBuff: Buffer | null = null;
@ -48,11 +48,11 @@ export default function createServerSettingsOverlay(document: Document, q: Q, gu
onCleared: () => {},
onError: async (errMsg) => {
q.$$$(element, '.error').innerText = errMsg;
await ElementsUtil.shakeElement(q.$$$(element, '.image-input-upload.server-icon'), 400);
await ElementsUtil.shakeElement(q.$$$(element, '.image-input-upload.guild-icon'), 400);
},
onLoaded: (buff, src) => {
newIconBuff = buff;
(q.$$$(element, '.server .icon img') as HTMLImageElement).src = src;
(q.$$$(element, '.guild .icon img') as HTMLImageElement).src = src;
}
});
@ -64,7 +64,7 @@ export default function createServerSettingsOverlay(document: Document, q: Q, gu
});
q.$$$(element, '.text-input').addEventListener('input', () => {
q.$$$(element, '.server.preview .name').innerText = q.$$$(element, '.text-input').innerText;
q.$$$(element, '.guild.preview .name').innerText = q.$$$(element, '.text-input').innerText;
});
let submitting = false;
@ -76,33 +76,34 @@ export default function createServerSettingsOverlay(document: Document, q: Q, gu
let newName = q.$$$(element, '.text-input').innerText;
if (newName == serverMeta.name && newIconBuff == null) {
if (newName == guildMeta.name && newIconBuff == null) {
// nothing changed, close the dialog
element.removeSelf();
return;
}
let success = false;
if (newName != serverMeta.name && newName.length == 0) {
LOG.warn('attempted to set empty server name');
if (newName != guildMeta.name && newName.length == 0) {
LOG.warn('attempted to set empty guild name');
q.$$$(element, '.button.submit').innerText = 'Try Again';
q.$$$(element, '.error').innerText = 'New name is empty';
await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400);
} else if (newName != serverMeta.name && newName.length > Globals.MAX_SERVER_NAME_LENGTH) {
LOG.warn('attempted to oversized server name');
} else if (newName != guildMeta.name && newName.length > Globals.MAX_GUILD_NAME_LENGTH) {
LOG.warn('attempted to oversized guild name');
q.$$$(element, '.button.submit').innerText = 'Try Again';
q.$$$(element, '.error').innerText = 'New name is too long. ' + newName.length + ' > ' + Globals.MAX_SERVER_NAME_LENGTH;
q.$$$(element, '.error').innerText = 'New name is too long. ' + newName.length + ' > ' + Globals.MAX_GUILD_NAME_LENGTH;
await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400);
} else { // client-size icon size checks are handled above
let failed = false;
// Set Name
if (newName != serverMeta.name) {
if (newName != guildMeta.name) {
try {
serverMeta = await server.setName(newName);
await guild.requestSetServerName(newName);
guildMeta = await guild.fetchMetadata();
} catch (e) {
LOG.error('error setting new server name', e);
LOG.error('error setting new guild name', e);
q.$$$(element, '.button.submit').innerText = 'Try Again';
q.$$$(element, '.error').innerText = 'Error setting new server name';
q.$$$(element, '.error').innerText = 'Error setting new guild name';
await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400);
failed = true;
}
@ -111,12 +112,12 @@ export default function createServerSettingsOverlay(document: Document, q: Q, gu
// Set Icon
if (!failed && newIconBuff != null) {
try {
await server.setIcon(newIconBuff);
await guild.requestSetServerIcon(newIconBuff);
newIconBuff = null; // prevent resubmit
} catch (e) {
LOG.error('error setting new server icon', e);
LOG.error('error setting new guild icon', e);
q.$$$(element, '.button.submit').innerText = 'Try Again';
q.$$$(element, '.error').innerText = 'Error setting new server icon';
q.$$$(element, '.error').innerText = 'Error setting new guild icon';
await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400);
failed = true;
}

View File

@ -8,9 +8,9 @@ import * as FileType from 'file-type'
import BaseElements from './require/base-elements';
import ElementsUtil from './require/elements-util';
import ClientController from '../client-controller';
import Q from '../q-module';
import createImageContextMenu from './context-menu-img';
import CombinedGuild from '../guild-combined';
export default function createImageOverlay(document: Document, q: Q, guild: CombinedGuild, resourceId: string, resourceName: string): HTMLElement {
let element = BaseElements.createOverlay(document, { class: 'content popup-image', content: [
@ -26,16 +26,16 @@ export default function createImageOverlay(document: Document, q: Q, guild: Comb
(async () => {
try {
let resourceBuff = await server.fetchResource(resourceId);
let src = await ElementsUtil.getImageBufferSrc(resourceBuff);
let resource = await guild.fetchResource(resourceId);
let src = await ElementsUtil.getImageBufferSrc(resource.data);
(q.$$$(element, '.content img') as HTMLImageElement).src = src;
q.$$$(element, '.download .size').innerText = ElementsUtil.humanSize(resourceBuff.length);
q.$$$(element, '.download .size').innerText = ElementsUtil.humanSize(resource.data.length);
let { mime, ext } = (await FileType.fromBuffer(resourceBuff)) ?? { mime: null, ext: null };
let { mime, ext } = (await FileType.fromBuffer(resource.data)) ?? { mime: null, ext: null };
if (mime === null || ext === null) throw new Error('unable to get mime/ext');
q.$$$(element, '.content img').addEventListener('contextmenu', (e) => {
let contextMenu = createImageContextMenu(document, q, server, resourceName, resourceBuff, mime as string, ext as string, false);
let contextMenu = createImageContextMenu(document, q, guild, resourceName, resource.data, mime as string, ext as string, false);
document.body.appendChild(contextMenu);
let relativeTo = { x: e.pageX, y: e.pageY };
ElementsUtil.alignContextElement(contextMenu, relativeTo, { top: 'centerY', left: 'right' });
@ -43,7 +43,7 @@ export default function createImageOverlay(document: Document, q: Q, guild: Comb
q.$$$(element, '.button').innerText = 'Save';
q.$$$(element, '.button').addEventListener('click', ElementsUtil.createDownloadListener({
downloadBuff: resourceBuff,
downloadBuff: resource.data,
resourceName: resourceName,
downloadStartFunc: () => {
q.$$$(element, '.button').innerText = 'Downloading...';

View File

@ -4,12 +4,12 @@ import Logger from '../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import { Channel } from '../data-types';
import ClientController from '../client-controller.js';
import Globals from '../globals.js';
import BaseElements from './require/base-elements.js';
import ElementsUtil from './require/elements-util.js';
import Q from '../q-module';
import CombinedGuild from '../guild-combined';
export default function createModifyChannelOverlay(document: Document, q: Q, guild: CombinedGuild, channel: Channel): HTMLElement {
// See also overlay-create-channel
@ -85,7 +85,7 @@ export default function createModifyChannelOverlay(document: Document, q: Q, gui
newFlavorText = null;
}
try {
await server.updateChannel(channel.id, newName, newFlavorText);
await guild.requestDoUpdateChannel(channel.id, newName, newFlavorText);
success = true;
} catch (e) {
LOG.error('error updating channel', e);

View File

@ -8,14 +8,14 @@ import ElementsUtil from './require/elements-util';
import Globals from '../globals';
import ClientController from '../client-controller';
import Q from '../q-module';
import createTextMessage from './msg-txt';
import CombinedGuild from '../guild-combined';
export default function createPersonalizeOverlay(document: Document, q: Q, guild: CombinedGuild, connection: any): HTMLElement {
let element = BaseElements.createOverlay(document, {
class: 'content submit-dialog personalize', content: [
createTextMessage(q, server, { id: 'test-message', member: connection, sent: new Date(), text: 'Example Message' }),
createTextMessage(q, guild, { id: 'test-message', member: connection, sent: new Date(), text: 'Example Message' }),
{ class: 'text-input', placeholder: 'New Display Name',
spellcheck: 'false', contenteditable: 'plaintext-only', content: connection.display_name },
{ class: 'image-input avatar-input', content: [
@ -95,7 +95,7 @@ export default function createPersonalizeOverlay(document: Document, q: Q, guild
let failed = false;
if (newDisplayName != connection.display_name) {
try {
await server.setDisplayName(newDisplayName);
await guild.requestSetDisplayName(newDisplayName);
connection.display_name = newDisplayName; // prevent resubmit
} catch (e) {
LOG.error('error setting display name', e);
@ -109,7 +109,7 @@ export default function createPersonalizeOverlay(document: Document, q: Q, guild
// Set New Avatar
if (!failed && newAvatarBuffer != null) {
try {
await server.setAvatar(newAvatarBuffer);
await guild.requestSetAvatar(newAvatarBuffer);
newAvatarBuffer = null; // prevent resubmit
} catch (e) {
LOG.error('error setting avatar buffer', e);

View File

@ -9,9 +9,9 @@ import BaseElements from './require/base-elements';
import ElementsUtil from './require/elements-util';
import Util from '../util';
import ClientController from '../client-controller';
import { Member } from '../data-types';
import Q from '../q-module';
import CombinedGuild from '../guild-combined';
export default function createTokenLogOverlay(document: Document, q: Q, guild: CombinedGuild): HTMLElement {
let element = BaseElements.createOverlay(document, {
@ -23,7 +23,7 @@ export default function createTokenLogOverlay(document: Document, q: Q, guild: C
(async () => {
Util.withPotentialErrorWarnOnCancel(q, {
taskFunc: async () => {
let tokens = await server.queryTokens();
let tokens = await guild.fetchTokens();
Q.clearChildren(q.$$$(element, '.tokens'));
let displayed = 0;
for (let token of tokens) {
@ -81,7 +81,7 @@ export default function createTokenLogOverlay(document: Document, q: Q, guild: C
q.$$$(contextElement, '.content').innerText = 'Revoking...';
ElementsUtil.alignContextElement(contextElement, revokeElement, alignment);
try {
await server.revokeToken(token.token);
await guild.requestDoRevokeToken(token.token);
} catch (e) {
LOG.error('unable to revoke token', e);
q.$$$(contextElement, '.content').innerText = 'Unable to Revoke';

View File

@ -1,13 +1,13 @@
import BaseElements from './require/base-elements.js';
import { Channel, ShouldNeverHappenError } from '../data-types';
import ClientController from '../client-controller.js';
import CombinedGuild from '../guild-combined.js';
export default function createUploadOverlayFromDataTransferItem(document: Document, guild: CombinedGuild, channel: Channel, dataTransferItem: DataTransferItem): HTMLElement {
let file = dataTransferItem.getAsFile();
if (file === null) throw new ShouldNeverHappenError('no file in the data transfer item');
let element = BaseElements.createUploadOverlay(document, {
server: server, channel: channel, resourceName: file.name,
guild: guild, channel: channel, resourceName: file.name,
resourceBuffFunc: async () => {
if (file === null) throw new ShouldNeverHappenError('no file in the data transfer item');
return Buffer.from(await file.arrayBuffer());

View File

@ -5,9 +5,9 @@ const LOG = Logger.create(__filename, electronConsole);
import { Channel } from '../data-types';
import BaseElements from './require/base-elements';
import ClientController from '../client-controller';
import Q from '../q-module';
import createUploadOverlayFromDataTransferItem from './overlay-upload-datatransfer';
import CombinedGuild from '../guild-combined';
export default function createUploadDropTarget(document: Document, q: Q, guild: CombinedGuild, channel: Channel): HTMLElement {
let element = BaseElements.createOverlay(document, { class: 'content drop-target', content: [
@ -37,7 +37,7 @@ export default function createUploadDropTarget(document: Document, q: Q, guild:
}
}
if (fileTransferItem) {
let element = createUploadOverlayFromDataTransferItem(document, server, channel, fileTransferItem);
let element = createUploadOverlayFromDataTransferItem(document, guild, channel, fileTransferItem);
document.body.appendChild(element);
q.$$$(element, '.text-input').focus();
} else {

View File

@ -4,12 +4,12 @@ import * as path from 'path';
import BaseElements from './require/base-elements';
import { Channel } from '../data-types';
import ClientController from '../client-controller';
import CombinedGuild from '../guild-combined';
export default function createUploadOverlayFromPath(document: Document, guild: CombinedGuild, channel: Channel, resourcePath: string): HTMLElement {
let resourceName = path.basename(resourcePath);
let element = BaseElements.createUploadOverlay(document, {
server: server, channel: channel, resourceName: resourceName,
guild: guild, channel: channel, resourceName: resourceName,
resourceBuffFunc: async () => {
return await fs.readFile(resourcePath);
},

View File

@ -10,7 +10,7 @@ import Globals from '../../globals';
import ElementsUtil from './elements-util';
import { Channel } from '../../data-types';
import ClientController from '../../client-controller';
import CombinedGuild from '../../guild-combined';
import Q from '../../q-module';
interface HTMLElementWithRemoveSelf extends HTMLElement {
@ -209,7 +209,7 @@ export default class BaseElements {
static createUploadOverlay(document: Document, props: CreateUploadOverlayProps): HTMLElementWithRemoveSelf {
const q = new Q(document);
const { server, channel, resourceName, resourceBuffFunc, resourceSizeFunc } = props;
const { guild, channel, resourceName, resourceBuffFunc, resourceSizeFunc } = props;
let element = BaseElements.createOverlay(document, { class: 'content upload', content: [
{ class: 'title', content: [
@ -248,7 +248,7 @@ export default class BaseElements {
return;
}
sending = true;
if (!server.isVerified) {
if (!guild.isSocketVerified()) {
LOG.warn('client attempted to send message with resource while not verified');
q.$$$(element, '.error').innerText = 'Not Connected to Server';
q.$$$(element, '.button.upload').innerText = 'Try Again';
@ -290,7 +290,7 @@ export default class BaseElements {
return;
}
try {
await server.sendMessageWithResource(channel.id, text, resourceBuff, resourceName);
await guild.requestSendMessageWithResource(channel.id, text, resourceBuff, resourceName);
} catch (e) {
q.$$$(element, '.error').innerText = 'Error uploading resource.';
q.$$$(element, '.button.upload').innerText = 'Try Again';

View File

@ -13,7 +13,7 @@ import * as FileType from 'file-type';
import Util from '../../util';
import Globals from '../../globals';
import ClientController from '../../client-controller';
import CombinedGuild from '../../guild-combined';
import { ShouldNeverHappenError } from '../../data-types';
// TODO: pass-through Globals in init function
@ -36,7 +36,7 @@ interface IHTMLElementWithRemovalType extends HTMLElement {
interface CreateDownloadListenerProps {
downloadBuff?: Buffer;
server?: ClientController;
guild?: CombinedGuild;
resourceId?: string;
resourceName: string;
downloadStartFunc: (() => Promise<void> | void);
@ -101,15 +101,15 @@ export default class ElementsUtil {
static async getImageBufferFromResourceFailSoftly(guild: CombinedGuild, resourceId: string | null): Promise<string> {
if (!resourceId) {
LOG.warn('no server resource specified, showing error instead', new Error());
LOG.warn('no guild resource specified, showing error instead', new Error());
return './img/error.png';
}
try {
let resourceBuff = await server.fetchResource(resourceId);
let src = await ElementsUtil.getImageBufferSrc(resourceBuff);
let resource = await guild.fetchResource(resourceId);
let src = await ElementsUtil.getImageBufferSrc(resource.data);
return src;
} catch (e) {
LOG.warn('unable to fetch server resource, showing error instead', e);
LOG.warn('unable to fetch guild resource, showing error instead', e);
return './img/error.png';
}
}
@ -340,7 +340,7 @@ export default class ElementsUtil {
static createDownloadListener(props: CreateDownloadListenerProps): (() => Promise<void>) {
const {
downloadBuff, // pre-downloaded buffer to save rather than submit a download request (downloadStartFunc still required)
server, resourceId, resourceName,
guild, resourceId, resourceName,
downloadStartFunc, downloadFailFunc,
writeStartFunc, writeFailFunc,
successFunc
@ -362,10 +362,10 @@ export default class ElementsUtil {
if (downloadBuff) {
resourceBuff = downloadBuff;
} else {
if (!server) throw new ShouldNeverHappenError('server is null and we are not using a pre-download');
if (!guild) throw new ShouldNeverHappenError('guild is null and we are not using a pre-download');
if (!resourceId) throw new ShouldNeverHappenError('resourceId is null and we are not using a pre-download');
try {
resourceBuff = await server.fetchResource(resourceId);
resourceBuff = (await guild.fetchResource(resourceId)).data;
} catch (e) {
LOG.error('Error downloading resource', e);
if (downloadFailFunc) await downloadFailFunc(e);

257
client/webapp/entrypoint.ts Normal file
View File

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

View File

@ -1,26 +1,27 @@
export default class Globals {
static DOWNLOAD_DIR = '/home/michael/Downloads'; // TODO: not hard coded
static PERSONALDB_FILE = './db/personal.db';
static DEFAULT_SOCKET_TIMEOUT = 5000; // Wait up to 5000ms for the server to respond before rejecting/throwing an error
static DEFAULT_SOCKET_TIMEOUT = 5000; // Wait up to 5000ms for the guild to respond before rejecting/throwing an error
static MAX_CURRENT_MESSAGES = 300;
static MESSAGES_PER_REQUEST = 100;
static MAX_TEXT_MESSAGE_LENGTH = 1024 * 2; // 2 KB character message max (for readability)
static MAX_RESOURCE_SIZE = 1024 * 1024 * 10; // 10 MB max resource size (server is 50, client-side is limited until socket.io-stream)
static MAX_RESOURCE_SIZE = 1024 * 1024 * 10; // 10 MB max resource size (guild is 50, client-side is limited until socket.io-stream)
static MAX_AVATAR_SIZE = 1024 * 128; // 128 KB max avatar size
static MAX_DISPLAY_NAME_LENGTH = 32; // 32 char max display name length
static MAX_ICON_SIZE = 1024 * 128; // 128 KB max server icon size
static MAX_SERVER_NAME_LENGTH = 64; // 64 char max server name length
static MAX_ICON_SIZE = 1024 * 128; // 128 KB max guild icon size
static MAX_GUILD_NAME_LENGTH = 64; // 64 char max guild name length
static MAX_CHANNEL_NAME_LENGTH = 32; // 32 char max channel name length
static MAX_CHANNEL_FLAVOR_TEXT_LENGTH = 256; // 256 char max channel flavor text length
static MAX_CACHED_CHANNEL_MESSAGES = 1000; // the 1000 most recent messages in each text channel are cached (in the sqlite db)
static MAX_SERVER_RESOURCE_CACHE_SIZE = 1024 * 1024 * 1024; // 1 GB max resource cache per server
static MAX_SERVER_RESOURCE_CACHE_SIZE = 1024 * 1024 * 1024; // 1 GB max resource cache per guild
static MAX_CACHED_RESOURCE_SIZE = 1024 * 1024 * 4; // 4 MB is the biggest resource that will be cached
static MAX_RAM_CACHED_MESSAGES_CHANNEL_CHARACTERS = 1024 * 1024 * 64; // at most, 64 MB of channel

View File

@ -7,7 +7,7 @@ import * as socketio from 'socket.io-client';
import PersonalDBGuild from './guild-personal-db';
import RAMGuild from './guild-ram';
import SocketGuild from './guild-socket';
import { Changes, Channel, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, ServerMetaData, SocketConfig, Token } from './data-types';
import { Changes, Channel, ConnectionInfo, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, ServerMetaData, SocketConfig, Token } from './data-types';
import MessageRAMCache from "./message-ram-cache";
import PersonalDB from "./personal-db";
@ -28,7 +28,7 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
constructor(
public readonly id: number,
private readonly memberId: string,
public readonly memberId: string,
socket: socketio.Socket,
socketVerifier: SocketVerifier,
messageRAMCache: MessageRAMCache,
@ -41,16 +41,15 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
this.personalDBGuild = new PersonalDBGuild(personalDB, this.id, this.memberId);
this.socketGuild = new SocketGuild(socket, socketVerifier);
// TODO: Only unverify the personaldb->socket connection on d/c?
// Connect/Disconnect
this.socketGuild.on('connect', () => {
LOG.info(`g#${this.id} connected`);
this.emit('connect');
});
this.socketGuild.on('disconnect', () => {
this.socketGuild.on('disconnect', async () => {
LOG.info(`g#${this.id} disconnected`);
this.unverify();
await personalDB.clearAllMembersStatus(this.id);
this.emit('disconnect');
});
@ -175,12 +174,20 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
);
}
public isSocketVerified(): boolean {
return this.socketGuild.verifier.isVerified;
}
private unverify(): void {
for (let pairVerifier of this.pairVerifiers) {
pairVerifier.unverify();
}
}
public disconnect(): void {
this.socketGuild.disconnect();
}
private async ensureRAMMembers(): Promise<void> {
if (this.ramGuild.getMembers().size === 0) {
await this.fetchMembers();
@ -195,15 +202,59 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
if (this.ramGuild.getChannels().size === 0) throw new Error('RAM Channels was not updated through fetchChannels');
}
public async grabRAMMembersMap(): Promise<Map<string, Member>> {
async grabRAMMembersMap(): Promise<Map<string, Member>> {
await this.ensureRAMMembers();
return this.ramGuild.getMembers();
}
public async grabRAMChannelsMap(): Promise<Map<string, Channel>> {
async grabRAMChannelsMap(): Promise<Map<string, Channel>> {
await this.ensureRAMChannels();
return this.ramGuild.getChannels();
}
async fetchConnectionInfo(): Promise<ConnectionInfo> {
let connection: ConnectionInfo = {
id: null,
avatarResourceId: null,
displayName: 'Connecting...',
status: '',
privileges: [],
roleName: null,
roleColor: null,
rolePriority: null
};
if (this.socketGuild.verifier.isVerified) {
let members = await this.grabRAMMembersMap();
let member = members.get(this.memberId);
if (member) {
connection.id = member.id;
connection.avatarResourceId = member.avatarResourceId;
connection.displayName = member.displayName;
connection.status = member.status;
connection.roleName = member.roleName;
connection.roleColor = member.roleColor;
connection.rolePriority = member.rolePriority;
connection.privileges = member.privileges;
} else {
LOG.warn('unable to find self in members');
}
} else {
let members = await this.personalDBGuild.fetchMembers();
if (members) {
let member = members.find(m => m.id === this.memberId);
if (member) {
connection.id = member.id;
connection.avatarResourceId = member.avatarResourceId;
connection.displayName = member.displayName;
connection.status = 'connecting';
connection.privileges = [];
} else {
LOG.warn('unable to find self in cached members');
}
}
}
return connection;
}
// Fetched through the triple-cache system (RAM -> Disk -> Server)
async fetchMetadata(): Promise<GuildMetadata> {
@ -247,6 +298,7 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
async requestSetAvatar(avatar: Buffer): Promise<void> {
await this.socketGuild.requestSetAvatar(avatar);
}
// TODO: Rename Server -> Guild
async requestSetServerName(serverName: string): Promise<void> {
await this.socketGuild.requestSetServerName(serverName);
}

View File

@ -18,7 +18,7 @@ export default class SocketGuild extends EventEmitter<Connectable> implements As
constructor(
private socket: socketio.Socket,
private verifier: SocketVerifier
public verifier: SocketVerifier
) {
super();
this.socket.on('connect', async () => {
@ -63,6 +63,10 @@ export default class SocketGuild extends EventEmitter<Connectable> implements As
});
}
public disconnect() {
this.socket.disconnect();
}
// server helper functions
private async query(timeout: number, endpoint: string, ...args: any[]): Promise<any> {

View File

@ -107,23 +107,25 @@ export interface SyncLackable {
export type Lackable = AsyncLackable | SyncLackable;
// A Connectable can emit server-like events
export type Connectable = {
'connect': () => void;
'disconnect': () => void;
'verified': () => void;
'update-metadata': (guildMeta: GuildMetadata) => void;
'new-channels': (channels: Channel[]) => void;
'update-channels': (updatedChannels: Channel[]) => void;
'remove-channels': (removedChannels: Channel[]) => void;
'new-members': (members: Member[]) => void;
'update-members': (updatedMembers: Member[]) => void;
'remove-members': (removedMembers: Member[]) => void;
'new-messages': (messages: Message[]) => void;
'update-messages': (updatedMessages: Message[]) => void;
'remove-messages': (removedMessages: Message[]) => void;
}
// A Conflictable could emit conflict-based events if data changed based on verification

View File

@ -11,26 +11,30 @@ import * as crypto from 'crypto';
import { Changes, Channel, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, ServerConfig, SocketConfig, Token } from './data-types';
import { IAddServerData } from './elements/overlay-add-server';
import { DefaultEventMap, EventEmitter } from 'tsee';
import { EventEmitter } from 'tsee';
import CombinedGuild from './guild-combined';
import PersonalDB from './personal-db';
import MessageRAMCache from './message-ram-cache';
import ResourceRAMCache from './resource-ram-cache';
export default class Controller extends EventEmitter<{
export default class GuildsManager extends EventEmitter<{
'connect': (guild: CombinedGuild) => void;
'disconnect': (guild: CombinedGuild) => void;
'verified': (guild: CombinedGuild) => void;
'update-metadata': (guild: CombinedGuild, guildMeta: GuildMetadata) => void;
'new-channels': (guild: CombinedGuild, channels: Channel[]) => void;
'update-channels': (guild: CombinedGuild, updatedChannels: Channel[]) => void;
'remove-channels': (guild: CombinedGuild, removedChannels: Channel[]) => void;
'new-members': (guild: CombinedGuild, members: Member[]) => void;
'update-members': (guild: CombinedGuild, updatedMembers: Member[]) => void;
'remove-members': (guild: CombinedGuild, removedMembers: Member[]) => void;
'new-messages': (guild: CombinedGuild, messages: Message[]) => void;
'update-messages': (guild: CombinedGuild, updatedMessages: Message[]) => void;
'remove-messages': (guild: CombinedGuild, removedMessages: Message[]) => void;
'conflict-metadata': (guild: CombinedGuild, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => void;
'conflict-channels': (guild: CombinedGuild, changes: Changes<Channel>) => void;
@ -141,7 +145,7 @@ export default class Controller extends EventEmitter<{
return await new Promise((resolve, reject) => {
let clientPublicKeyDerBuff = publicKey.export({ type: 'spki', format: 'der' });
Controller._socketEmitTimeout(socket, 5000, 'register-with-token',
GuildsManager._socketEmitTimeout(socket, 5000, 'register-with-token',
token, clientPublicKeyDerBuff, displayName, avatarBuff, async (errStr: string, dataMember: any, dataMetadata: any) => {
if (errStr) {
reject(new Error(errStr));

View File

@ -14,7 +14,7 @@ export default class PersonalDB {
private readonly db: sqlite.Database
) {}
public async create(filePath: string): Promise<PersonalDB> {
public static async create(filePath: string): Promise<PersonalDB> {
return new PersonalDB(
await sqlite.open({ driver: sqlite3.Database, filename: filePath })
);

View File

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

View File

@ -1,11 +1,12 @@
import * as crypto from 'crypto';
import * as socketio from 'socket.io-client';
import { EventEmitter } from 'tsee';
import DedupAwaiter from "./dedup-awaiter";
import Util from './util';
// Automatically re-verifies the socket when connected
export default class SocketVerifier {
export default class SocketVerifier extends EventEmitter<{ 'verified': () => void }> {
public isVerified = false;
private memberId: string | null = null;
private verifyDedup = new DedupAwaiter(async () => { return await this.doVerify(); });
@ -15,6 +16,7 @@ export default class SocketVerifier {
private publicKey: crypto.KeyObject,
private privateKey: crypto.KeyObject
) {
super();
socket.on('connect', async () => {
await this.verify();
});
@ -23,7 +25,6 @@ export default class SocketVerifier {
});
}
// TODO: Move this to a "query/queryDedup" request
/** Verifies this client with the server. This function must be called before the server will send messages or give results */
private async doVerify(): Promise<string> {
if (this.socket.disconnected) throw new Error('socket is disconnected');
@ -48,6 +49,7 @@ export default class SocketVerifier {
}
this.memberId = null;
resolve(memberId);
this.emit('verified');
});
});
});

View File

@ -15,7 +15,7 @@ import Q from './q-module';
import createGuildListGuild from './elements/guild-list-guild';
import createChannel from './elements/channel';
import createMember from './elements/member';
import Controller from './controller';
import GuildsManager from './guilds-manager';
import createMessage from './elements/message';
interface SetMessageProps {
@ -161,20 +161,20 @@ export default class UI {
});
}
public async setGuilds(controller: Controller, guilds: CombinedGuild[]): Promise<void> {
public async setGuilds(guildsManager: GuildsManager, guilds: CombinedGuild[]): Promise<void> {
await this._guildsLock.push(() => {
Q.clearChildren(this.q.$('#guild-list'));
for (let guild of guilds) {
let element = createGuildListGuild(this.document, this.q, this, controller, guild);
let element = createGuildListGuild(this.document, this.q, this, guildsManager, guild);
this.q.$('#guild-list').appendChild(element);
}
});
}
public async addGuild(controller: Controller, guild: CombinedGuild): Promise<HTMLElement> {
public async addGuild(guildsManager: GuildsManager, guild: CombinedGuild): Promise<HTMLElement> {
let element: HTMLElement | null = null;
await this._guildsLock.push(() => {
element = createGuildListGuild(this.document, this.q, this, controller, guild) as HTMLElement;
element = createGuildListGuild(this.document, this.q, this, guildsManager, guild) as HTMLElement;
this.q.$('#guild-list').appendChild(element);
});
if (element == null) throw new ShouldNeverHappenError('element was not set');
@ -262,22 +262,22 @@ export default class UI {
});
}
public async updateChannels(guild: CombinedGuild, data: { oldChannel: Channel, newChannel: Channel }[]): Promise<void> {
public async updateChannels(guild: CombinedGuild, updatedChannels: Channel[]): Promise<void> {
await this.lockChannels(guild, async () => {
for (const { oldChannel, newChannel } of data) {
let oldElement = this.q.$('#channel-list .channel[meta-id="' + newChannel.id + '"]');
let newElement = createChannel(this.document, this.q, this, guild, newChannel);
for (const channel of updatedChannels) {
let oldElement = this.q.$('#channel-list .channel[meta-id="' + channel.id + '"]');
let newElement = createChannel(this.document, this.q, this, guild, channel);
oldElement.parentElement?.replaceChild(newElement, oldElement);
await this.updateChannelPosition(guild, newElement);
if (this.activeChannel !== null && this.activeChannel.id === newChannel.id) {
if (this.activeChannel !== null && this.activeChannel.id === channel.id) {
newElement.classList.add('active');
// See also setActiveChannel
this.q.$('#channel-name').innerText = newChannel.name;
this.q.$('#channel-flavor-text').innerText = newChannel.flavorText ?? '';
this.q.$('#channel-flavor-divider').style.visibility = newChannel.flavorText ? 'visible' : 'hidden';
this.q.$('#text-input').setAttribute('placeholder', 'Message #' + newChannel.name);
this.q.$('#channel-name').innerText = channel.name;
this.q.$('#channel-flavor-text').innerText = channel.flavorText ?? '';
this.q.$('#channel-flavor-divider').style.visibility = channel.flavorText ? 'visible' : 'hidden';
this.q.$('#text-input').setAttribute('placeholder', 'Message #' + channel.name);
}
}
});
@ -358,24 +358,26 @@ export default class UI {
});
}
public async updateMembers(guild: CombinedGuild, data: { oldMember: Member, newMember: Member }[]): Promise<void> {
public async updateMembers(guild: CombinedGuild, updatedMembers: Member[]): Promise<void> {
await this.lockMembers(guild, async () => {
for (const { oldMember, newMember } of data) {
let oldElement = this.q.$_('#guild-members .member[meta-id="' + newMember.id + '"]');
for (const member of updatedMembers) {
let oldElement = this.q.$_('#guild-members .member[meta-id="' + member.id + '"]');
if (oldElement) {
let newElement = createMember(this.q, guild, newMember);
let newElement = createMember(this.q, guild, member);
oldElement.parentElement?.replaceChild(newElement, oldElement);
await this.updateMemberPosition(guild, newElement);
}
}
});
// Update the messages too
if (this.activeChannel === null) return;
await this.lockMessages(guild, this.activeChannel, () => {
for (const { oldMember, newMember } of data) {
let newStyle = newMember.roleColor ? 'color: ' + newMember.roleColor : null;
let newName = newMember.displayName;
for (const member of updatedMembers) {
let newStyle = member.roleColor ? 'color: ' + member.roleColor : null;
let newName = member.displayName;
// the extra query selectors may be overkill
for (let messageElement of this.q.$$(`.message[meta-member-id="${newMember.id}"]`)) {
for (let messageElement of this.q.$$(`.message[meta-member-id="${member.id}"]`)) {
let nameElement = this.q.$$$_(messageElement, '.member-name');
if (nameElement) { // continued messages will still show up but need to be skipped
if (newStyle) nameElement.setAttribute('style', newStyle);
@ -408,7 +410,32 @@ export default class UI {
return element && this.messagePairs.get(element.getAttribute('meta-id')) || null;
}
public async addMessagesBefore(guild: CombinedGuild, channel: Channel, messages: Message[], prevTopMessage: Message | null): Promise<void> {
public async addMessages(guild: CombinedGuild, messages: Message[]) {
let channelIds = new Set(messages.map(message => message.channel.id));
for (let channelId of channelIds) {
let channelMessages = messages.filter(message => message.channel.id === channelId);
channelMessages = channelMessages.sort((a, b) => a.sent.getTime() - b.sent.getTime());
// No Previous Messages is an easy case
if (this.messagePairs.size === 0) {
await this.addMessagesBefore(guild, { id: channelId }, channelMessages, null);
continue;
}
let topMessagePair = this.getTopMessagePair() as { message: Message, element: HTMLElement };
let bottomMessagePair = this.getBottomMessagePair() as { message: Message, element: HTMLElement };
let aboveMessages = messages.filter(message => message.sent < topMessagePair.message.sent);
let belowMessages = messages.filter(message => message.sent > bottomMessagePair.message.sent);
let betweenMessages = messages.filter(message => message.sent >= topMessagePair.message.sent && message.sent <= bottomMessagePair.message.sent);
if (aboveMessages.length > 0) await this.addMessagesBefore(guild, { id: channelId }, aboveMessages, topMessagePair.message);
if (belowMessages.length > 0) await this.addMessagesAfter(guild, { id: channelId }, belowMessages, bottomMessagePair.message);
if (betweenMessages.length > 0) await this.addMessagesBetween(guild, { id: channelId }, betweenMessages, topMessagePair.element, bottomMessagePair.element);
}
}
public async addMessagesBefore(guild: CombinedGuild, channel: Channel | { id: string }, messages: Message[], prevTopMessage: Message | null): Promise<void> {
this.lockMessages(guild, channel, () => {
if (prevTopMessage && this.getTopMessagePair()?.message.id !== prevTopMessage.id) return;
@ -495,7 +522,7 @@ export default class UI {
}
// TODO: use topMessage, bottomMessage / topMessageId, bottomMessageId instead?
public async addMessagesBetween(guild: CombinedGuild, channel: Channel, messages: Message[], topElement: HTMLElement, bottomElement: HTMLElement): Promise<void> {
private async addMessagesBetween(guild: CombinedGuild, channel: Channel | { id: string }, messages: Message[], topElement: HTMLElement, bottomElement: HTMLElement): Promise<void> {
await this.lockMessages(guild, channel, () => {
if (!(messages.length > 0 && topElement != null && bottomElement != null && bottomElement == Q.nextElement(topElement))) {
LOG.error('invalid messages between', { messages, top: topElement.innerText, bottom: bottomElement.innerText, afterTop: Q.nextElement(topElement)?.innerText });
@ -618,35 +645,43 @@ export default class UI {
this.messagesAtBottom = true;
}
public async deleteMessages(guild: CombinedGuild, channel: Channel, messages: Message[]) {
await this.lockMessages(guild, channel, () => {
for (let message of messages) {
if (this.messagePairs.has(message.id)) {
let messagePair = this.messagePairs.get(message.id) as { message: Message, element: HTMLElement };
messagePair.element.parentElement?.removeChild(messagePair.element);
// TODO: we should be updating messages sent below this message
// however, these events should be relatively rare so that's for the future
this.messagePairs.delete(message.id);
public async deleteMessages(guild: CombinedGuild, messages: Message[]) {
let channelIds = new Set(messages.map(message => message.channel.id));
for (let channelId of channelIds) {
let channelMessages = messages.filter(message => message.channel.id === channelId);
await this.lockMessages(guild, { id: channelId }, () => {
for (let message of channelMessages) {
if (this.messagePairs.has(message.id)) {
let messagePair = this.messagePairs.get(message.id) as { message: Message, element: HTMLElement };
messagePair.element.parentElement?.removeChild(messagePair.element);
// TODO: we should be updating messages sent below this message
// however, these events should be relatively rare so that's for the future
this.messagePairs.delete(message.id);
}
}
}
});
});
}
}
public async updateMessages(guild: CombinedGuild, channel: Channel, data: { oldMessage: Message, newMessage: Message }[]): Promise<void> {
await this.lockMessages(guild, channel, () => {
for (const { oldMessage, newMessage } of data) {
if (this.messagePairs.has(oldMessage.id)) {
let oldElement = (this.messagePairs.get(oldMessage.id) as { message: Message, element: HTMLElement }).element;
let prevElement = Q.previousElement(oldElement);
let prevMessage = prevElement && (this.messagePairs.get(prevElement.getAttribute('meta-id')) as { message: Message, element: HTMLElement }).message;
let newElement = createMessage(this.document, this.q, guild, newMessage, prevMessage);
oldElement.parentElement?.replaceChild(newElement, oldElement);
// TODO: we should be updating messages sent below this message
// however, these events should be relatively rare so that's for the future
this.messagePairs.set(oldMessage.id, { message: newMessage, element: newElement });
public async updateMessages(guild: CombinedGuild, updatedMessages: Message[]): Promise<void> {
let channelIds = new Set(updatedMessages.map(message => message.channel.id));
for (let channelId of channelIds) {
let channelMessages = updatedMessages.filter(message => message.channel.id === channelId);
await this.lockMessages(guild, { id: channelId }, () => {
for (const message of channelMessages) {
if (this.messagePairs.has(message.id)) {
let oldElement = (this.messagePairs.get(message.id) as { message: Message, element: HTMLElement }).element;
let prevElement = Q.previousElement(oldElement);
let prevMessage = prevElement && (this.messagePairs.get(prevElement.getAttribute('meta-id')) as { message: Message, element: HTMLElement }).message;
let newElement = createMessage(this.document, this.q, guild, message, prevMessage);
oldElement.parentElement?.replaceChild(newElement, oldElement);
// TODO: we should be updating messages sent below this message
// however, these events should be relatively rare so that's for the future
this.messagePairs.set(message.id, { message: message, element: newElement });
}
}
}
});
});
}
}
public async addMessagesErrorIndicatorBefore(guild: CombinedGuild, channel: Channel, errorIndicatorElement: HTMLElement): Promise<void> {