controller is guildized

This commit is contained in:
Michael Peters 2021-11-21 12:29:42 -06:00
parent a6097f9ccb
commit 64490d027f
43 changed files with 398 additions and 333 deletions

View File

@ -7,44 +7,44 @@ import Util from './util';
import Globals from './globals'; import Globals from './globals';
import UI from './ui'; import UI from './ui';
import ClientController from './client-controller'; import CombinedGuild from './guild-combined';
import { Channel } from './data-types'; import { Channel } from './data-types';
import Q from './q-module'; import Q from './q-module';
export default class Actions { export default class Actions {
static async fetchAndUpdateConnection(ui: UI, server: ClientController) { static async fetchAndUpdateConnection(ui: UI, guild: CombinedGuild) {
// Explicitly not using withPotentialError to make this simpler // Explicitly not using withPotentialError to make this simpler
try { try {
let connection = await server.fetchConnectionInfo(); let connection = await guild.fetchConnectionInfo();
ui.setActiveConnection(server, connection); ui.setActiveConnection(guild, connection);
} catch (e) { } catch (e) {
LOG.error('Error updating current connection', e); LOG.error('Error updating current connection', e);
ui.setActiveConnection(server, { id: null, avatarResourceId: null, displayName: 'Error', status: 'Error', privileges: [], roleName: null, roleColor: null, rolePriority: null }); ui.setActiveConnection(guild, { id: null, avatarResourceId: null, displayName: 'Error', status: 'Error', privileges: [], roleName: null, roleColor: null, rolePriority: null });
} }
} }
static async fetchAndUpdateMembers(q: Q, ui: UI, server: ClientController) { static async fetchAndUpdateMembers(q: Q, ui: UI, guild: CombinedGuild) {
await Util.withPotentialErrorWarnOnCancel(q, { await Util.withPotentialErrorWarnOnCancel(q, {
taskFunc: async () => { taskFunc: async () => {
if (ui.activeServer === null || ui.activeServer.id !== server.id) return; if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
let members = await server.grabMembers(); let members = await guild.fetchMembers();
await ui.setMembers(server, members); await ui.setMembers(guild, members);
}, },
errorIndicatorAddFunc: async (errorIndicatorElement) => { errorIndicatorAddFunc: async (errorIndicatorElement) => {
await ui.setMembersErrorIndicator(server, errorIndicatorElement); await ui.setMembersErrorIndicator(guild, errorIndicatorElement);
}, },
errorContainer: q.$('#server-members'), errorContainer: q.$('#server-members'),
errorMessage: 'Error loading members' errorMessage: 'Error loading members'
}); });
} }
static async fetchAndUpdateChannels(q: Q, ui: UI, server: ClientController) { static async fetchAndUpdateChannels(q: Q, ui: UI, guild: CombinedGuild) {
await Util.withPotentialErrorWarnOnCancel(q, { await Util.withPotentialErrorWarnOnCancel(q, {
taskFunc: async () => { taskFunc: async () => {
if (ui.activeServer === null || ui.activeServer.id !== server.id) return; if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
let channels = await server.grabChannels(); let channels = await guild.fetchChannels();
await ui.setChannels(server, channels); await ui.setChannels(guild, channels);
if (ui.activeServer === null || ui.activeServer.id !== server.id) return; if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
if (ui.activeChannel === null) { if (ui.activeChannel === null) {
// click on the first channel in the list if no channel is active yet // click on the first channel in the list if no channel is active yet
let element = q.$_('#channel-list .channel'); let element = q.$_('#channel-list .channel');
@ -54,45 +54,45 @@ export default class Actions {
} }
}, },
errorIndicatorAddFunc: async (errorIndicatorElement) => { errorIndicatorAddFunc: async (errorIndicatorElement) => {
await ui.setChannelsErrorIndicator(server, errorIndicatorElement); await ui.setChannelsErrorIndicator(guild, errorIndicatorElement);
}, },
errorContainer: q.$('#channel-list'), errorContainer: q.$('#channel-list'),
errorMessage: 'Error fetching channels' errorMessage: 'Error fetching channels'
}); });
} }
static async fetchAndUpdateMessagesRecent(q: Q, ui: UI, server: ClientController, channel: Channel | { id: string }) { static async fetchAndUpdateMessagesRecent(q: Q, ui: UI, guild: CombinedGuild, channel: Channel | { id: string }) {
await Util.withPotentialErrorWarnOnCancel(q, { await Util.withPotentialErrorWarnOnCancel(q, {
taskFunc: async () => { taskFunc: async () => {
if (ui.activeServer === null || ui.activeServer.id !== server.id) return; if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
if (ui.activeChannel === null || ui.activeChannel.id !== channel.id) return; if (ui.activeChannel === null || ui.activeChannel.id !== channel.id) return;
let messages = await server.grabRecentMessages(channel.id, Globals.MESSAGES_PER_REQUEST); let messages = await guild.fetchMessagesRecent(channel.id, Globals.MESSAGES_PER_REQUEST);
await ui.setMessages(server, channel, messages, { atTop: messages.length < Globals.MESSAGES_PER_REQUEST, atBottom: true }); await ui.setMessages(guild, channel, messages, { atTop: messages.length < Globals.MESSAGES_PER_REQUEST, atBottom: true });
}, },
errorIndicatorAddFunc: async (errorIndicatorElement) => { errorIndicatorAddFunc: async (errorIndicatorElement) => {
await ui.setMessagesErrorIndicator(server, channel, errorIndicatorElement); await ui.setMessagesErrorIndicator(guild, channel, errorIndicatorElement);
}, },
errorContainer: q.$('#channel-feed'), errorContainer: q.$('#channel-feed'),
errorMessage: 'Error fetching messages' errorMessage: 'Error fetching messages'
}); });
} }
static async fetchAndUpdateMessagesBefore(q: Q, ui: UI, server: ClientController, channel: Channel) { static async fetchAndUpdateMessagesBefore(q: Q, ui: UI, guild: CombinedGuild, channel: Channel) {
await Util.withPotentialErrorWarnOnCancel(q, { await Util.withPotentialErrorWarnOnCancel(q, {
taskFunc: async () => { taskFunc: async () => {
if (ui.activeServer === null || ui.activeServer.id !== server.id) return; if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
if (ui.activeChannel === null || ui.activeChannel.id !== channel.id) return; if (ui.activeChannel === null || ui.activeChannel.id !== channel.id) return;
let topPair = ui.getTopMessagePair(); let topPair = ui.getTopMessagePair();
if (topPair == null) return; if (topPair == null) return;
let messages = await server.fetchMessagesBefore(channel.id, topPair.message.id, Globals.MESSAGES_PER_REQUEST); let messages = await guild.fetchMessagesBefore(channel.id, topPair.message.id, Globals.MESSAGES_PER_REQUEST);
if (messages && messages.length > 0) { if (messages && messages.length > 0) {
await ui.addMessagesBefore(server, channel, messages, topPair.message); await ui.addMessagesBefore(guild, channel, messages, topPair.message);
} else { } else {
ui.messagesAtTop = true; ui.messagesAtTop = true;
} }
}, },
errorIndicatorAddFunc: async (errorIndicatorElement) => { errorIndicatorAddFunc: async (errorIndicatorElement) => {
await ui.addMessagesErrorIndicatorBefore(server, channel, errorIndicatorElement); await ui.addMessagesErrorIndicatorBefore(guild, channel, errorIndicatorElement);
}, },
errorContainer: q.$('#channel-feed'), errorContainer: q.$('#channel-feed'),
errorClasses: [ 'before' ], errorClasses: [ 'before' ],
@ -100,22 +100,22 @@ export default class Actions {
}); });
} }
static async fetchAndUpdateMessagesAfter(q: Q, ui: UI, server: ClientController, channel: Channel) { static async fetchAndUpdateMessagesAfter(q: Q, ui: UI, guild: CombinedGuild, channel: Channel) {
await Util.withPotentialErrorWarnOnCancel(q, { await Util.withPotentialErrorWarnOnCancel(q, {
taskFunc: async () => { taskFunc: async () => {
if (ui.activeServer === null || ui.activeServer.id !== server.id) return; if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
if (ui.activeChannel === null || ui.activeChannel.id !== channel.id) return; if (ui.activeChannel === null || ui.activeChannel.id !== channel.id) return;
let bottomPair = ui.getBottomMessagePair(); let bottomPair = ui.getBottomMessagePair();
if (bottomPair == null) return; if (bottomPair == null) return;
let messages = await server.fetchMessagesAfter(channel.id, bottomPair.message.id, Globals.MESSAGES_PER_REQUEST); let messages = await guild.fetchMessagesAfter(channel.id, bottomPair.message.id, Globals.MESSAGES_PER_REQUEST);
if (messages && messages.length > 0) { if (messages && messages.length > 0) {
await ui.addMessagesAfter(server, channel, messages, bottomPair.message); await ui.addMessagesAfter(guild, channel, messages, bottomPair.message);
} else { } else {
ui.messagesAtBottom = true; ui.messagesAtBottom = true;
} }
}, },
errorIndicatorAddFunc: async (errorIndicatorElement) => { errorIndicatorAddFunc: async (errorIndicatorElement) => {
await ui.addMessagesErrorIndicatorAfter(server, channel, errorIndicatorElement); await ui.addMessagesErrorIndicatorAfter(guild, channel, errorIndicatorElement);
}, },
errorContainer: q.$('#channel-feed'), errorContainer: q.$('#channel-feed'),
errorClasses: [ 'after' ], errorClasses: [ 'after' ],

View File

@ -142,6 +142,9 @@ export class AutoVerifier<T> {
return await new Promise<T | null>(async (resolve: (result: T | null) => void, reject: (error: Error) => void) => { return await new Promise<T | null>(async (resolve: (result: T | null) => void, reject: (error: Error) => void) => {
let resolved = false; let resolved = false;
try { try {
let origTrustedStatus = this.trustedStatus;
let origTrustedPromise = this.trustedPromise;
let primaryPromise = this.primaryFunc(); let primaryPromise = this.primaryFunc();
if (this.trustedStatus === 'none') { if (this.trustedStatus === 'none') {
@ -154,6 +157,9 @@ export class AutoVerifier<T> {
if (primaryResult) { if (primaryResult) {
resolve(primaryResult); resolve(primaryResult);
resolved = true; resolved = true;
} else if (origTrustedStatus === 'verified' && origTrustedPromise === this.trustedPromise) {
// Unverify if the primary returns null after it was verified
this.unverify();
} }
//@ts-ignore (could be changed by an unverify during primaryPromise) //@ts-ignore (could be changed by an unverify during primaryPromise)

View File

@ -5,73 +5,87 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../logger/logger'; import Logger from '../../logger/logger';
const LOG = Logger.create(__filename, electronConsole); const LOG = Logger.create(__filename, electronConsole);
import { EventEmitter } from "stream";
import ClientController from "./client-controller";
import * as socketio from 'socket.io-client'; import * as socketio from 'socket.io-client';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import DBCache from './db-cache'; import { Changes, Channel, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, ServerConfig, SocketConfig, Token } from './data-types';
import { ServerConfig } from './data-types';
import { IAddServerData } from './elements/overlay-add-server'; import { IAddServerData } from './elements/overlay-add-server';
import { DefaultEventMap, EventEmitter } from 'tsee';
import CombinedGuild from './guild-combined';
import PersonalDB from './personal-db';
import MessageRAMCache from './message-ram-cache';
import ResourceRAMCache from './resource-ram-cache';
export default class Controller extends EventEmitter { export default class Controller extends EventEmitter<{
public servers: ClientController[] = []; 'connect': (guild: CombinedGuild) => void;
'disconnect': (guild: CombinedGuild) => void;
'update-metadata': (guild: CombinedGuild, guildMeta: GuildMetadata) => void;
'new-channels': (guild: CombinedGuild, channels: Channel[]) => void;
'update-channels': (guild: CombinedGuild, updatedChannels: Channel[]) => void;
'new-members': (guild: CombinedGuild, members: Member[]) => void;
'update-members': (guild: CombinedGuild, updatedMembers: Member[]) => void;
'new-messages': (guild: CombinedGuild, messages: Message[]) => void;
'update-messages': (guild: CombinedGuild, updatedMessages: Message[]) => void;
'conflict-metadata': (guild: CombinedGuild, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => void;
'conflict-channels': (guild: CombinedGuild, changes: Changes<Channel>) => void;
'conflict-members': (guild: CombinedGuild, changes: Changes<Member>) => void;
'conflict-messages': (guild: CombinedGuild, changes: Changes<Message>) => void;
'conflict-tokens': (guild: CombinedGuild, changes: Changes<Token>) => void;
'conflict-resource': (guild: CombinedGuild, oldResource: Resource, newResource: Resource) => void;
}> {
public guilds: CombinedGuild[] = [];
constructor( constructor(
private dbCache: DBCache private messageRAMCache: MessageRAMCache,
private resourceRAMCache: ResourceRAMCache,
private personalDB: PersonalDB
) { ) {
super(); super();
} }
async _connectFromConfig(serverConfig: ServerConfig): Promise<ClientController> { async _connectFromConfig(guildMetadata: GuildMetadataWithIds, socketConfig: SocketConfig): Promise<CombinedGuild> {
LOG.debug(`connecting to server#${serverConfig.guildId} at ${serverConfig.url}`); LOG.debug(`connecting to server#${guildMetadata.id} at ${socketConfig.url}`);
let server = new ClientController(this.dbCache, serverConfig); let guild = await CombinedGuild.create(
guildMetadata,
socketConfig,
this.messageRAMCache,
this.resourceRAMCache,
this.personalDB
);
await this.dbCache.clearAllMemberStatus(server.id); await this.personalDB.clearAllMembersStatus(guild.id);
this.servers.push(server); this.guilds.push(guild);
// Forward server events through this event emitter // Forward guild events through this event emitter
let serverEvents = [ for (let eventName of guild.eventNames()) {
'connected', guild.on(eventName as any, (...args: any) => {
'disconnected', this.emit(eventName as any, guild, ...args);
'verified',
'new-message',
'update-server',
'deleted-members',
'updated-members',
'added-members',
'deleted-channels',
'updated-channels',
'added-channels',
'deleted-messages',
'updated-messages',
'added-messages',
];
for (let event of serverEvents) {
server.on(event, (...args) => {
this.emit(event, server, ...args);
}); });
} }
return server;
return guild;
} }
async init(): Promise<void> { async init(): Promise<void> {
this.servers = []; this.guilds = [];
let serverConfigs = await this.dbCache.getServerConfigs(); // TODO: connect concurrently
for (let guildMeta of await this.personalDB.fetchGuilds()) {
// TODO: HTML prompt if no server configs for (let guildSocket of await this.personalDB.fetchGuildSockets(guildMeta.id)) {
if (serverConfigs.length == 0) { await this._connectFromConfig(guildMeta, guildSocket);
LOG.warn('no server configs found in client-side db'); }
} }
for (let serverConfig of serverConfigs) { if (this.guilds.length === 0) {
await this._connectFromConfig(serverConfig); LOG.warn('no guilds found in client-side db');
} }
} }
@ -94,7 +108,7 @@ export default class Controller extends EventEmitter {
}); });
} }
async addNewServer(serverConfig: IAddServerData, displayName: string, avatarBuff: Buffer): Promise<ClientController> { async addNewGuild(serverConfig: IAddServerData, displayName: string, avatarBuff: Buffer): Promise<CombinedGuild> {
const { name, url, cert, token } = serverConfig; const { name, url, cert, token } = serverConfig;
LOG.debug('Adding new server', { name, url, cert, token, displayName, avatarBuff }); LOG.debug('Adding new server', { name, url, cert, token, displayName, avatarBuff });
@ -128,24 +142,25 @@ export default class Controller extends EventEmitter {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
let clientPublicKeyDerBuff = publicKey.export({ type: 'spki', format: 'der' }); let clientPublicKeyDerBuff = publicKey.export({ type: 'spki', format: 'der' });
Controller._socketEmitTimeout(socket, 5000, 'register-with-token', Controller._socketEmitTimeout(socket, 5000, 'register-with-token',
token, clientPublicKeyDerBuff, displayName, avatarBuff, async (errStr, member) => { token, clientPublicKeyDerBuff, displayName, avatarBuff, async (errStr: string, dataMember: any, dataMetadata: any) => {
if (errStr) { if (errStr) {
reject(new Error(errStr)); reject(new Error(errStr));
} else { } else {
try { try {
let serverConfig: ServerConfig | null = null; const member = Member.fromDBData(dataMember);
await this.dbCache.queueTransaction(async () => { const meta = GuildMetadata.fromDBData(dataMetadata);
let publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }); let guildMeta: GuildMetadataWithIds | null = null;
let privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }); let socketConfig: SocketConfig | null = null;
let identityId = await this.dbCache.addIdentity(publicKeyPem, privateKeyPem); await this.personalDB.queueTransaction(async () => {
let guildId = await this.dbCache.addServer(url, cert, name); let guildId = await this.personalDB.addGuild(meta.name, meta.iconResourceId, member.id);
await this.dbCache.addServerIdentity(guildId, identityId); let guildSocketId = await this.personalDB.addGuildSocket(guildId, url, cert, publicKey, privateKey);
serverConfig = await this.dbCache.getServerConfig(guildId, identityId); guildMeta = await this.personalDB.fetchGuild(guildId);
socketConfig = await this.personalDB.fetchGuildSocket(guildId, guildSocketId);
}); });
if (serverConfig == null) { if (!guildMeta || !socketConfig) {
throw new Error('unable to get server config'); throw new Error('unable to properly add guild');
} }
let server = await this._connectFromConfig(serverConfig); let server = await this._connectFromConfig(guildMeta, socketConfig);
resolve(server); resolve(server);
} catch (e) { } catch (e) {
reject(e); reject(e);
@ -159,8 +174,11 @@ export default class Controller extends EventEmitter {
} }
} }
async removeServer(server: ClientController): Promise<void> { async removeServer(guild: CombinedGuild): Promise<void> {
await this.dbCache.removeServer(server.id); await this.personalDB.queueTransaction(async () => {
this.servers = this.servers.filter(s => s != server); await this.personalDB.removeGuildSockets(guild.id);
await this.personalDB.removeGuild(guild.id);
});
this.guilds = this.guilds.filter(g => g.id != guild.id);
} }
} }

View File

@ -214,7 +214,8 @@ export class Message implements WithEquals<Message> {
export class SocketConfig { export class SocketConfig {
private constructor( private constructor(
public readonly id: number, public readonly id: number | null,
public readonly guildId: number,
public readonly url: string, public readonly url: string,
public readonly cert: string, public readonly cert: string,
public readonly publicKey: crypto.KeyObject, public readonly publicKey: crypto.KeyObject,
@ -225,6 +226,7 @@ export class SocketConfig {
static fromDBData(data: any): SocketConfig { static fromDBData(data: any): SocketConfig {
return new SocketConfig( return new SocketConfig(
data.id, data.id,
data.guild_id,
data.url, data.url,
data.cert, data.cert,
crypto.createPublicKey(data.public_key), crypto.createPublicKey(data.public_key),

View File

@ -7,7 +7,7 @@ import UI from '../ui';
import Actions from '../actions'; import Actions from '../actions';
import Q from '../q-module'; import Q from '../q-module';
export default function createChannel(document: Document, q: Q, ui: UI, server: ClientController, channel: Channel) { export default function createChannel(document: Document, q: Q, ui: UI, guild: CombinedGuild, channel: Channel) {
let element = q.create({ class: 'channel text', 'meta-id': channel.id, 'meta-server-id': server.id, content: [ let element = q.create({ class: 'channel text', 'meta-id': channel.id, 'meta-server-id': server.id, content: [
// Scraped directly from discord (#) // Scraped directly from discord (#)
{ class: 'icon', content: BaseElements.TEXT_CHANNEL_ICON }, { class: 'icon', content: BaseElements.TEXT_CHANNEL_ICON },

View File

@ -6,7 +6,7 @@ import createPersonalizeOverlay from './overlay-personalize.js';
import Q from '../q-module.js'; import Q from '../q-module.js';
import UI from '../ui.js'; import UI from '../ui.js';
export default function createConnectionContextMenu(document: Document, q: Q, ui: UI, server: ClientController) { export default function createConnectionContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild) {
let statuses = [ 'online', 'away', 'busy', 'invisible' ]; let statuses = [ 'online', 'away', 'busy', 'invisible' ];
let content: any[] = [ let content: any[] = [
{ class: 'item personalize', content: [ { class: 'item personalize', content: [

View File

@ -12,7 +12,7 @@ import Q from '../q-module';
export default function createImageContextMenu( export default function createImageContextMenu(
document: Document, document: Document,
q: Q, q: Q,
server: ClientController, guild: CombinedGuild,
resourceName: string, resourceName: string,
buffer: Buffer, buffer: Buffer,
mime: string, mime: string,

View File

@ -16,7 +16,7 @@ import createCreateInviteTokenOverlay from './overlay-create-invite-token';
import createCreateChannelOverlay from './overlay-create-channel'; import createCreateChannelOverlay from './overlay-create-channel';
import createTokenLogOverlay from './overlay-token-log'; import createTokenLogOverlay from './overlay-token-log';
export default function createServerTitleContextMenu(document: Document, q: Q, ui: UI, server: ClientController): HTMLElement { export default function createServerTitleContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild): HTMLElement {
if (ui.activeConnection === null) { if (ui.activeConnection === null) {
LOG.warn('no active connection when creating server title context menu'); LOG.warn('no active connection when creating server title context menu');
return q.create({}) as HTMLElement; return q.create({}) as HTMLElement;

View File

@ -10,7 +10,7 @@ import Q from '../q-module';
import UI from '../ui'; import UI from '../ui';
import Controller from '../controller'; import Controller from '../controller';
export default function createServerContextMenu(document: Document, q: Q, ui: UI, controller: Controller, server: ClientController) { export default function createServerContextMenu(document: Document, q: Q, ui: UI, controller: Controller, guild: CombinedGuild) {
let element = BaseElements.createContextMenu(document, { let element = BaseElements.createContextMenu(document, {
class: 'server-context', content: [ class: 'server-context', content: [
{ class: 'item red leave-server', content: 'Leave Server' } { class: 'item red leave-server', content: 'Leave Server' }

View File

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

View File

@ -11,18 +11,18 @@ export default function bindInfiniteScrollEvents(q: Q, ui: UI): void {
let scrollHeight = q.$('#channel-feed-content-wrapper').scrollHeight; let scrollHeight = q.$('#channel-feed-content-wrapper').scrollHeight;
let clientHeight = q.$('#channel-feed-content-wrapper').clientHeight; let clientHeight = q.$('#channel-feed-content-wrapper').clientHeight;
if (ui.activeServer === null) return; if (ui.activeGuild === null) return;
if (ui.activeChannel === null) return; if (ui.activeChannel === null) return;
if (!loadingBefore && !ui.messagesAtTop && scrollTop < 600) { // Approaching the unloaded top of the page if (!loadingBefore && !ui.messagesAtTop && scrollTop < 600) { // Approaching the unloaded top of the page
// Fetch more messages to add above // Fetch more messages to add above
loadingBefore = true; loadingBefore = true;
await Actions.fetchAndUpdateMessagesBefore(q, ui, ui.activeServer, ui.activeChannel); await Actions.fetchAndUpdateMessagesBefore(q, ui, ui.activeGuild, ui.activeChannel);
loadingBefore = false; loadingBefore = false;
} else if (!loadingAfter && !ui.messagesAtBottom && scrollHeight - clientHeight - scrollTop < 600) { // Approaching the unloaded bottom of the page } else if (!loadingAfter && !ui.messagesAtBottom && scrollHeight - clientHeight - scrollTop < 600) { // Approaching the unloaded bottom of the page
// Fetch more messages to add below // Fetch more messages to add below
loadingAfter = true; loadingAfter = true;
await Actions.fetchAndUpdateMessagesAfter(q, ui, ui.activeServer, ui.activeChannel); await Actions.fetchAndUpdateMessagesAfter(q, ui, ui.activeGuild, ui.activeChannel);
loadingAfter = false; loadingAfter = false;
} }
} }

View File

@ -6,14 +6,14 @@ import ElementsUtil from './require/elements-util';
export default function bindAddServerTitleEvents(document: Document, q: Q, ui: UI) { export default function bindAddServerTitleEvents(document: Document, q: Q, ui: UI) {
q.$('#server-name-container').addEventListener('click', () => { q.$('#server-name-container').addEventListener('click', () => {
if (ui.activeConnection === null) return; if (ui.activeConnection === null) return;
if (ui.activeServer === null) return; if (ui.activeGuild === null) return;
if (!ui.activeServer.isVerified) return; if (!ui.activeGuild.isVerified) return;
if ( if (
!ui.activeConnection.privileges.includes('modify_profile') && !ui.activeConnection.privileges.includes('modify_profile') &&
!ui.activeConnection.privileges.includes('modify_members') !ui.activeConnection.privileges.includes('modify_members')
) return; ) return;
let contextMenu = createServerTitleContextMenu(document, q, ui, ui.activeServer); let contextMenu = createServerTitleContextMenu(document, q, ui, ui.activeGuild);
document.body.appendChild(contextMenu); document.body.appendChild(contextMenu);
ElementsUtil.alignContextElement(contextMenu, q.$('#server-name-container'), { top: 'bottom', centerX: 'centerX' }); ElementsUtil.alignContextElement(contextMenu, q.$('#server-name-container'), { top: 'bottom', centerX: 'centerX' });
}); });

View File

@ -19,7 +19,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v
let sendingMessage = false; let sendingMessage = false;
async function sendCurrentTextMessage() { async function sendCurrentTextMessage() {
if (sendingMessage) return; if (sendingMessage) return;
if (ui.activeServer === null) return; if (ui.activeGuild === null) return;
if (ui.activeChannel === null) return; if (ui.activeChannel === null) return;
let text = q.$('#text-input').innerText.trim(); // trimming is not done server-side, just a client-side 'feature' let text = q.$('#text-input').innerText.trim(); // trimming is not done server-side, just a client-side 'feature'
@ -27,7 +27,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v
sendingMessage = true; sendingMessage = true;
let server = ui.activeServer as ClientController; let server = ui.activeGuild as ClientController;
let channel = ui.activeChannel as Channel; let channel = ui.activeChannel as Channel;
if (!server.isVerified) { if (!server.isVerified) {
@ -92,7 +92,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v
// Open resource select dialog when resource-input-button is clicked // Open resource select dialog when resource-input-button is clicked
let selectingResources = false; let selectingResources = false;
q.$('#resource-input-button').addEventListener('click', async () => { q.$('#resource-input-button').addEventListener('click', async () => {
if (ui.activeServer === null) return; if (ui.activeGuild === null) return;
if (ui.activeChannel === null) return; if (ui.activeChannel === null) return;
if (selectingResources) { if (selectingResources) {
@ -106,7 +106,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v
}); });
// TODO: multiple files do consecutive overlays? // TODO: multiple files do consecutive overlays?
if (!result.canceled) { if (!result.canceled) {
let element = createUploadOverlayFromPath(document, ui.activeServer, ui.activeChannel, result.filePaths[0]); let element = createUploadOverlayFromPath(document, ui.activeGuild, ui.activeChannel, result.filePaths[0]);
document.body.appendChild(element); document.body.appendChild(element);
q.$$$(element, '.text-input').focus(); q.$$$(element, '.text-input').focus();
} }
@ -115,7 +115,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v
// Open upload resource dialog when an image is pasted // Open upload resource dialog when an image is pasted
window.addEventListener('paste', (e) => { window.addEventListener('paste', (e) => {
if (ui.activeServer === null) return; if (ui.activeGuild === null) return;
if (ui.activeChannel === null) return; if (ui.activeChannel === null) return;
let fileTransferItem: DataTransferItem | null = null; let fileTransferItem: DataTransferItem | null = null;
@ -127,7 +127,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v
} }
} }
if (fileTransferItem) { if (fileTransferItem) {
let element = createUploadOverlayFromDataTransferItem(document, ui.activeServer, ui.activeChannel, fileTransferItem); let element = createUploadOverlayFromDataTransferItem(document, ui.activeGuild, ui.activeChannel, fileTransferItem);
document.body.appendChild(element); document.body.appendChild(element);
q.$$$(element, '.text-input').focus(); q.$$$(element, '.text-input').focus();
} }
@ -135,11 +135,11 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v
// TODO: drag+drop new server files? // TODO: drag+drop new server files?
document.addEventListener('dragenter', () => { document.addEventListener('dragenter', () => {
if (ui.activeServer === null) return; if (ui.activeGuild === null) return;
if (ui.activeChannel === null) return; if (ui.activeChannel === null) return;
if (q.$('.overlay .drop-target')) return; if (q.$('.overlay .drop-target')) return;
let element = createUploadDropTarget(document, q, ui.activeServer, ui.activeChannel); let element = createUploadDropTarget(document, q, ui.activeGuild, ui.activeChannel);
if (!element) return; if (!element) return;
document.body.appendChild(element); document.body.appendChild(element);
}); });

View File

@ -6,41 +6,41 @@ const LOG = Logger.create(__filename, electronConsole);
import BaseElements from './require/base-elements'; import BaseElements from './require/base-elements';
import ElementsUtil from './require/elements-util'; import ElementsUtil from './require/elements-util';
import ClientController from '../client-controller'; import { GuildMetadata } from '../data-types';
import { CacheServerData, ServerMetaData } from '../data-types';
import Q from '../q-module'; import Q from '../q-module';
import UI from '../ui'; import UI from '../ui';
import Actions from '../actions'; import Actions from '../actions';
import createServerContextMenu from './context-menu-srv'; import createGuildContextMenu from './context-menu-srv';
import Controller from '../controller'; import Controller from '../controller';
import CombinedGuild from '../guild-combined';
export default function createServerListServer(document: Document, q: Q, ui: UI, controller: Controller, server: ClientController) { export default function createGuildListGuild(document: Document, q: Q, ui: UI, controller: Controller, guild: CombinedGuild) {
let element = q.create({ class: 'server', 'meta-id': server.id, 'meta-name': server.id, content: [ let element = q.create({ class: 'guild', 'meta-id': guild.id, 'meta-name': guild.id, content: [
{ class: 'pill' }, { class: 'pill' },
{ tag: 'img', src: './img/loading.svg', alt: 'server' }, // src is set later by script.js { tag: 'img', src: './img/loading.svg', alt: 'guild' }, // src is set later by script.js
] }) as HTMLElement; ] }) as HTMLElement;
// Hover over for name + connection info // Hover over for name + connection info
(async () => { (async () => {
let serverData: ServerMetaData | CacheServerData; let guildData: GuildMetadata;
try { try {
serverData = await server.grabMetadata(); guildData = await guild.grabMetadata();
if (!serverData.iconResourceId) throw new Error('server icon not identified yet'); if (!guildData.iconResourceId) throw new Error('guild icon not identified yet');
let serverIcon = await server.fetchResource(serverData.iconResourceId); let guildIcon = await guild.fetchResource(guildData.iconResourceId);
let serverIconSrc = await ElementsUtil.getImageBufferSrc(serverIcon); let guildIconSrc = await ElementsUtil.getImageBufferSrc(guildIcon);
(q.$$$(element, 'img') as HTMLImageElement).src = serverIconSrc; (q.$$$(element, 'img') as HTMLImageElement).src = guildIconSrc;
} catch (e) { } catch (e) {
LOG.error('Error fetching server icon', e); LOG.error('Error fetching guild icon', e);
(q.$$$(element, 'img') as HTMLImageElement).src = './img/error.png'; (q.$$$(element, 'img') as HTMLImageElement).src = './img/error.png';
return; return;
} }
element.setAttribute('meta-name', serverData.name); element.setAttribute('meta-name', guildData.name);
let contextElement = q.create({ class: 'context', content: { let contextElement = q.create({ class: 'context', content: {
class: 'info', content: [ class: 'info', content: [
BaseElements.TAB_LEFT, BaseElements.TAB_LEFT,
{ class: 'content server' } // populated later { class: 'content guild' } // populated later
] ]
} }) as HTMLElement; } }) as HTMLElement;
@ -52,7 +52,7 @@ export default function createServerListServer(document: Document, q: Q, ui: UI,
document.body.appendChild(contextElement); document.body.appendChild(contextElement);
ElementsUtil.alignContextElement(contextElement, element, { left: 'right', centerY: 'centerY' }); ElementsUtil.alignContextElement(contextElement, element, { left: 'right', centerY: 'centerY' });
(async () => { (async () => {
let connection = await server.fetchConnectionInfo(); let connection = await guild.fetchConnectionInfo();
let connectionElement = q.create({ class: 'connection ' + connection.status, content: [ let connectionElement = q.create({ class: 'connection ' + connection.status, content: [
{ class: 'status-circle' }, { class: 'status-circle' },
{ class: 'display-name', content: connection.displayName } { class: 'display-name', content: connection.displayName }
@ -72,38 +72,38 @@ export default function createServerListServer(document: Document, q: Q, ui: UI,
element.addEventListener('click', async () => { element.addEventListener('click', async () => {
if (element.classList.contains('active')) return; if (element.classList.contains('active')) return;
ui.setActiveServer(server); ui.setActiveGuild(guild);
// Connection information // Connection information
(async () => { (async () => {
await Actions.fetchAndUpdateConnection(ui, server); await Actions.fetchAndUpdateConnection(ui, guild);
})(); })();
// Server Channel Name // Guild Channel Name
(async () => { (async () => {
// Explicitly not using a withPotentialError to make this simpler // Explicitly not using a withPotentialError to make this simpler
try { try {
let serverData = await server.grabMetadata(); let guildData = await guild.grabMetadata();
ui.updateServerName(server, serverData.name); ui.updateGuildName(guild, guildData.name);
} catch (e) { } catch (e) {
LOG.error('Error fetching server name', e); LOG.error('Error fetching guild name', e);
ui.updateServerName(server, 'ERROR'); ui.updateGuildName(guild, 'ERROR');
} }
})(); })();
// Server Channel List // Guild Channel List
(async () => { (async () => {
await Actions.fetchAndUpdateChannels(q, ui, server); await Actions.fetchAndUpdateChannels(q, ui, guild);
})(); })();
// Server Members // Guild Members
(async () => { (async () => {
await Actions.fetchAndUpdateMembers(q, ui, server); await Actions.fetchAndUpdateMembers(q, ui, guild);
})(); })();
}); });
element.addEventListener('contextmenu', (e) => { element.addEventListener('contextmenu', (e) => {
let contextMenu = createServerContextMenu(document, q, ui, controller, server); let contextMenu = createGuildContextMenu(document, q, ui, controller, guild);
document.body.appendChild(contextMenu); document.body.appendChild(contextMenu);
let relativeTo = { x: e.pageX, y: e.pageY }; let relativeTo = { x: e.pageX, y: e.pageY };
ElementsUtil.alignContextElement(contextMenu, relativeTo, { top: 'centerY', left: 'centerX' }); ElementsUtil.alignContextElement(contextMenu, relativeTo, { top: 'centerY', left: 'centerX' });

View File

@ -4,7 +4,7 @@ import Q from "../q-module";
import ElementsUtil from "./require/elements-util"; import ElementsUtil from "./require/elements-util";
export default function createMember(q: Q, server: ClientController, member: Member): HTMLElement { export default function createMember(q: Q, guild: CombinedGuild, member: Member): HTMLElement {
let nameStyle = member.roleColor ? 'color: ' + member.roleColor : ''; let nameStyle = member.roleColor ? 'color: ' + member.roleColor : '';
let element = q.create({ class: 'member ' + member.status, 'meta-id': member.id, content: [ let element = q.create({ class: 'member ' + member.status, 'meta-id': member.id, content: [
{ class: 'icon', content: [ { class: 'icon', content: [

View File

@ -8,7 +8,7 @@ import createResourceMessageContinued from './msg-res-cont';
import createTextMessage from './msg-txt'; import createTextMessage from './msg-txt';
import createTextMessageContinued from './msg-txt-cont'; import createTextMessageContinued from './msg-txt-cont';
export default function createMessage(document: Document, q: Q, server: ClientController, message: Message, lastMessage: Message | null): HTMLElement { export default function createMessage(document: Document, q: Q, guild: CombinedGuild, message: Message, lastMessage: Message | null): HTMLElement {
let element: HTMLElement; let element: HTMLElement;
if (message.hasResource()) { if (message.hasResource()) {
if (message.isImageResource()) { if (message.isImageResource()) {

View File

@ -14,7 +14,7 @@ import Q from '../q-module';
import createImageOverlay from './overlay-image'; import createImageOverlay from './overlay-image';
import createImageContextMenu from './context-menu-img'; import createImageContextMenu from './context-menu-img';
export default function createImageResourceMessageContinued(document: Document, q: Q, server: ClientController, message: Message): HTMLElement { export default function createImageResourceMessageContinued(document: Document, q: Q, guild: CombinedGuild, message: Message): HTMLElement {
if (!message.resourceId || !message.resourcePreviewId || !message.resourceName) { if (!message.resourceId || !message.resourcePreviewId || !message.resourceName) {
throw new ShouldNeverHappenError('Message is not a resource message'); throw new ShouldNeverHappenError('Message is not a resource message');
} }

View File

@ -14,7 +14,7 @@ import Q from '../q-module';
import createImageOverlay from './overlay-image'; import createImageOverlay from './overlay-image';
import createImageContextMenu from './context-menu-img'; import createImageContextMenu from './context-menu-img';
export default function createImageResourceMessage(document: Document, q: Q, server: ClientController, message: Message) { export default function createImageResourceMessage(document: Document, q: Q, guild: CombinedGuild, message: Message) {
if (!message.resourceId || !message.resourcePreviewId || !message.resourceName) { if (!message.resourceId || !message.resourcePreviewId || !message.resourceName) {
throw new ShouldNeverHappenError('Message is not a resource message'); throw new ShouldNeverHappenError('Message is not a resource message');
} }

View File

@ -12,7 +12,7 @@ class ShouldNeverHappenError extends Error {
} }
} }
export default function createResourceMessageContinued(q: Q, server: ClientController, message: Message): HTMLElement { export default function createResourceMessageContinued(q: Q, guild: CombinedGuild, message: Message): HTMLElement {
if (!message.resourceId || !message.resourceName) { if (!message.resourceId || !message.resourceName) {
throw new ShouldNeverHappenError('Message is not a resource message'); throw new ShouldNeverHappenError('Message is not a resource message');
} }

View File

@ -12,7 +12,7 @@ class ShouldNeverHappenError extends Error {
} }
} }
export default function createResourceMessage(q: Q, server: ClientController, message: Message): HTMLElement { export default function createResourceMessage(q: Q, guild: CombinedGuild, message: Message): HTMLElement {
if (!message.resourceId || !message.resourceName) { if (!message.resourceId || !message.resourceName) {
throw new ShouldNeverHappenError('Message is not a resource message'); throw new ShouldNeverHappenError('Message is not a resource message');
} }

View File

@ -5,7 +5,7 @@ import Q from '../q-module.js';
import ElementsUtil from './require/elements-util.js'; import ElementsUtil from './require/elements-util.js';
export default function createTextMessageContinued(q: Q, server: ClientController, message: Message): HTMLElement { export default function createTextMessageContinued(q: Q, guild: CombinedGuild, message: Message): HTMLElement {
return q.create({ class: 'message continued', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-server-id': server.id, content: [ return q.create({ class: 'message continued', 'meta-id': message.id, 'meta-member-id': message.member.id, 'meta-server-id': server.id, content: [
{ class: 'timestamp', content: moment(message.sent).format('HH:mm') }, { class: 'timestamp', content: moment(message.sent).format('HH:mm') },
{ class: 'right', content: [ { class: 'right', content: [

View File

@ -6,7 +6,7 @@ import { Message, Member, IDummyTextMessage } from '../data-types';
import ClientController from '../client-controller'; import ClientController from '../client-controller';
import Q from '../q-module'; import Q from '../q-module';
export default function createTextMessage(q: Q, server: ClientController, message: Message | IDummyTextMessage): HTMLElement { export default function createTextMessage(q: Q, guild: CombinedGuild, message: Message | IDummyTextMessage): HTMLElement {
let memberInfo: { let memberInfo: {
roleColor: string | null, roleColor: string | null,
displayName: string, displayName: string,

View File

@ -177,7 +177,7 @@ export default function createAddServerOverlay(document: Document, q: Q, ui: UI,
q.$$$(element, '.display-name-input').removeAttribute('contenteditable'); q.$$$(element, '.display-name-input').removeAttribute('contenteditable');
let newServer: ClientController | null = null; let newguild: CombinedGuild | null = null;
if (addServerData == null) { if (addServerData == null) {
q.$$$(element, '.error').innerText = 'Very bad server file'; q.$$$(element, '.error').innerText = 'Very bad server file';
q.$$$(element, '.submit').innerText = 'Try Again'; q.$$$(element, '.submit').innerText = 'Try Again';
@ -201,7 +201,7 @@ export default function createAddServerOverlay(document: Document, q: Q, ui: UI,
} else { // NOTE: Avatar size is checked above } else { // NOTE: Avatar size is checked above
q.$$$(element, '.submit').innerText = 'Registering...'; q.$$$(element, '.submit').innerText = 'Registering...';
try { try {
newServer = await controller.addNewServer(addServerData, displayName, avatarBuff); newServer = await controller.addNewGuild(addServerData, displayName, avatarBuff);
} catch (e) { } catch (e) {
LOG.warn('error adding new server: ' + e.message, e); // explicitly not printing stack trace here LOG.warn('error adding new server: ' + e.message, e); // explicitly not printing stack trace here
q.$$$(element, '.error').innerText = e.message; q.$$$(element, '.error').innerText = e.message;

View File

@ -10,7 +10,7 @@ import ElementsUtil from "./require/elements-util";
import BaseElements from "./require/base-elements"; import BaseElements from "./require/base-elements";
import Q from '../q-module'; import Q from '../q-module';
export default function createCreateChannelOverlay(document: Document, q: Q, server: ClientController): HTMLElement { export default function createCreateChannelOverlay(document: Document, q: Q, guild: CombinedGuild): HTMLElement {
// See also overlay-modify-channel // See also overlay-modify-channel

View File

@ -2,7 +2,7 @@ import ClientController from "../client-controller";
import BaseElements from "./require/base-elements"; import BaseElements from "./require/base-elements";
export default function createCreateInviteTokenOverlay(document: Document, server: ClientController): HTMLElement { export default function createCreateInviteTokenOverlay(document: Document, guild: CombinedGuild): HTMLElement {
let element = BaseElements.createOverlay(document, { class: 'content submit-dialog', content: [ let element = BaseElements.createOverlay(document, { class: 'content submit-dialog', content: [
{ class: 'role-select category-select', content: [ { class: 'role-select category-select', content: [
{ class: 'label', content: 'Select Starting Roles' }, { class: 'label', content: 'Select Starting Roles' },

View File

@ -12,7 +12,7 @@ import ClientController from '../client-controller';
import Q from '../q-module'; import Q from '../q-module';
import createImageContextMenu from './context-menu-img'; import createImageContextMenu from './context-menu-img';
export default function createImageOverlay(document: Document, q: Q, server: ClientController, resourceId: string, resourceName: string): HTMLElement { export default function createImageOverlay(document: Document, q: Q, guild: CombinedGuild, resourceId: string, resourceName: string): HTMLElement {
let element = BaseElements.createOverlay(document, { class: 'content popup-image', content: [ let element = BaseElements.createOverlay(document, { class: 'content popup-image', content: [
{ tag: 'img', src: './img/loading.svg', alt: resourceName, title: resourceName }, { tag: 'img', src: './img/loading.svg', alt: resourceName, title: resourceName },
{ class: 'download', content: [ { class: 'download', content: [

View File

@ -11,7 +11,7 @@ import BaseElements from './require/base-elements.js';
import ElementsUtil from './require/elements-util.js'; import ElementsUtil from './require/elements-util.js';
import Q from '../q-module'; import Q from '../q-module';
export default function createModifyChannelOverlay(document: Document, q: Q, server: ClientController, channel: Channel): HTMLElement { export default function createModifyChannelOverlay(document: Document, q: Q, guild: CombinedGuild, channel: Channel): HTMLElement {
// See also overlay-create-channel // See also overlay-create-channel
let element = BaseElements.createOverlay(document, { class: 'content submit-dialog modify-channel', content: [ let element = BaseElements.createOverlay(document, { class: 'content submit-dialog modify-channel', content: [

View File

@ -12,7 +12,7 @@ import ClientController from '../client-controller';
import Q from '../q-module'; import Q from '../q-module';
import createTextMessage from './msg-txt'; import createTextMessage from './msg-txt';
export default function createPersonalizeOverlay(document: Document, q: Q, server: ClientController, connection: any): HTMLElement { export default function createPersonalizeOverlay(document: Document, q: Q, guild: CombinedGuild, connection: any): HTMLElement {
let element = BaseElements.createOverlay(document, { let element = BaseElements.createOverlay(document, {
class: 'content submit-dialog personalize', content: [ class: 'content submit-dialog personalize', content: [
createTextMessage(q, server, { id: 'test-message', member: connection, sent: new Date(), text: 'Example Message' }), createTextMessage(q, server, { id: 'test-message', member: connection, sent: new Date(), text: 'Example Message' }),

View File

@ -12,7 +12,7 @@ import ClientController from '../client-controller';
import { CacheServerData, ServerMetaData } from '../data-types'; import { CacheServerData, ServerMetaData } from '../data-types';
import Q from '../q-module'; import Q from '../q-module';
export default function createServerSettingsOverlay(document: Document, q: Q, server: ClientController, serverMeta: ServerMetaData | CacheServerData): HTMLElement { export default function createServerSettingsOverlay(document: Document, q: Q, guild: CombinedGuild, serverMeta: ServerMetaData | CacheServerData): HTMLElement {
let element = BaseElements.createOverlay(document, { let element = BaseElements.createOverlay(document, {
class: 'content submit-dialog server-settings', content: [ class: 'content submit-dialog server-settings', content: [
{ class: 'server preview', content: [ { class: 'server preview', content: [

View File

@ -13,7 +13,7 @@ import ClientController from '../client-controller';
import { Member } from '../data-types'; import { Member } from '../data-types';
import Q from '../q-module'; import Q from '../q-module';
export default function createTokenLogOverlay(document: Document, q: Q, server: ClientController): HTMLElement { export default function createTokenLogOverlay(document: Document, q: Q, guild: CombinedGuild): HTMLElement {
let element = BaseElements.createOverlay(document, { let element = BaseElements.createOverlay(document, {
class: 'content token-log', content: [ class: 'content token-log', content: [
{ class: 'tokens', content: { tag: 'img', src: './img/loading.svg', alt: 'loading...' } }, { class: 'tokens', content: { tag: 'img', src: './img/loading.svg', alt: 'loading...' } },

View File

@ -3,7 +3,7 @@ import BaseElements from './require/base-elements.js';
import { Channel, ShouldNeverHappenError } from '../data-types'; import { Channel, ShouldNeverHappenError } from '../data-types';
import ClientController from '../client-controller.js'; import ClientController from '../client-controller.js';
export default function createUploadOverlayFromDataTransferItem(document: Document, server: ClientController, channel: Channel, dataTransferItem: DataTransferItem): HTMLElement { export default function createUploadOverlayFromDataTransferItem(document: Document, guild: CombinedGuild, channel: Channel, dataTransferItem: DataTransferItem): HTMLElement {
let file = dataTransferItem.getAsFile(); let file = dataTransferItem.getAsFile();
if (file === null) throw new ShouldNeverHappenError('no file in the data transfer item'); if (file === null) throw new ShouldNeverHappenError('no file in the data transfer item');
let element = BaseElements.createUploadOverlay(document, { let element = BaseElements.createUploadOverlay(document, {

View File

@ -9,7 +9,7 @@ import ClientController from '../client-controller';
import Q from '../q-module'; import Q from '../q-module';
import createUploadOverlayFromDataTransferItem from './overlay-upload-datatransfer'; import createUploadOverlayFromDataTransferItem from './overlay-upload-datatransfer';
export default function createUploadDropTarget(document: Document, q: Q, server: ClientController, channel: Channel): HTMLElement { export default function createUploadDropTarget(document: Document, q: Q, guild: CombinedGuild, channel: Channel): HTMLElement {
let element = BaseElements.createOverlay(document, { class: 'content drop-target', content: [ let element = BaseElements.createOverlay(document, { class: 'content drop-target', content: [
// TODO: icon? // TODO: icon?
{ class: 'message', content: 'Upload to #' + channel.name } { class: 'message', content: 'Upload to #' + channel.name }

View File

@ -6,7 +6,7 @@ import BaseElements from './require/base-elements';
import { Channel } from '../data-types'; import { Channel } from '../data-types';
import ClientController from '../client-controller'; import ClientController from '../client-controller';
export default function createUploadOverlayFromPath(document: Document, server: ClientController, channel: Channel, resourcePath: string): HTMLElement { export default function createUploadOverlayFromPath(document: Document, guild: CombinedGuild, channel: Channel, resourcePath: string): HTMLElement {
let resourceName = path.basename(resourcePath); let resourceName = path.basename(resourcePath);
let element = BaseElements.createUploadOverlay(document, { let element = BaseElements.createUploadOverlay(document, {
server: server, channel: channel, resourceName: resourceName, server: server, channel: channel, resourceName: resourceName,

View File

@ -18,7 +18,7 @@ interface HTMLElementWithRemoveSelf extends HTMLElement {
} }
interface CreateUploadOverlayProps { interface CreateUploadOverlayProps {
server: ClientController; guild: CombinedGuild;
channel: Channel; channel: Channel;
resourceName: string; resourceName: string;
resourceBuffFunc: (() => Promise<Buffer>); resourceBuffFunc: (() => Promise<Buffer>);

View File

@ -99,7 +99,7 @@ export default class ElementsUtil {
} }
} }
static async getImageBufferFromResourceFailSoftly(server: ClientController, resourceId: string | null): Promise<string> { static async getImageBufferFromResourceFailSoftly(guild: CombinedGuild, resourceId: string | null): Promise<string> {
if (!resourceId) { if (!resourceId) {
LOG.warn('no server resource specified, showing error instead', new Error()); LOG.warn('no server resource specified, showing error instead', new Error());
return './img/error.png'; return './img/error.png';

View File

@ -4,7 +4,7 @@ import { AutoVerifier, AutoVerifierChangesType } from './auto-verifier';
import { AutoVerifierWithArg, PartialMessageListQuery, IDQuery } from './auto-verifier-with-args'; import { AutoVerifierWithArg, PartialMessageListQuery, IDQuery } from './auto-verifier-with-args';
import { EventEmitter } from 'tsee'; import { EventEmitter } from 'tsee';
export default class GuildPairVerifierGuild extends EventEmitter<Conflictable> implements AsyncFetchable { export default class PairVerifierFetchable extends EventEmitter<Conflictable> implements AsyncFetchable {
private readonly fetchMetadataVerifier: AutoVerifier<GuildMetadata>; private readonly fetchMetadataVerifier: AutoVerifier<GuildMetadata>;
private readonly fetchMembersVerifier: AutoVerifier<Member[]>; private readonly fetchMembersVerifier: AutoVerifier<Member[]>;

View File

@ -7,7 +7,7 @@ import * as socketio from 'socket.io-client';
import PersonalDBGuild from './guild-personal-db'; import PersonalDBGuild from './guild-personal-db';
import RAMGuild from './guild-ram'; import RAMGuild from './guild-ram';
import SocketGuild from './guild-socket'; import SocketGuild from './guild-socket';
import { Changes, Channel, GuildMetadata, Member, Message, Resource, ServerMetaData, SocketConfig, Token } from './data-types'; import { Changes, Channel, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, ServerMetaData, SocketConfig, Token } from './data-types';
import MessageRAMCache from "./message-ram-cache"; import MessageRAMCache from "./message-ram-cache";
import PersonalDB from "./personal-db"; import PersonalDB from "./personal-db";
@ -27,7 +27,7 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
private readonly fetchable: AsyncGuaranteedFetchable; private readonly fetchable: AsyncGuaranteedFetchable;
constructor( constructor(
private readonly id: number, public readonly id: number,
private readonly memberId: string, private readonly memberId: string,
socket: socketio.Socket, socket: socketio.Socket,
socketVerifier: SocketVerifier, socketVerifier: SocketVerifier,
@ -149,7 +149,7 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
} }
static async create( static async create(
personalGuildId: number, guildMetadata: GuildMetadataWithIds,
socketConfig: SocketConfig, socketConfig: SocketConfig,
messageRAMCache: MessageRAMCache, messageRAMCache: MessageRAMCache,
resourceRAMCache: ResourceRAMCache, resourceRAMCache: ResourceRAMCache,
@ -161,8 +161,11 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
}); });
let socketVerifier = new SocketVerifier(socket, socketConfig.publicKey, socketConfig.privateKey); let socketVerifier = new SocketVerifier(socket, socketConfig.publicKey, socketConfig.privateKey);
let memberId = await socketVerifier.verify(); let memberId = await socketVerifier.verify();
if (guildMetadata.memberId && memberId !== guildMetadata.memberId) {
throw new Error('Verified member differs from original member');
}
return new CombinedGuild( return new CombinedGuild(
personalGuildId, guildMetadata.id,
memberId, memberId,
socket, socket,
socketVerifier, socketVerifier,
@ -172,25 +175,35 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
); );
} }
private unverify() { private unverify(): void {
for (let pairVerifier of this.pairVerifiers) { for (let pairVerifier of this.pairVerifiers) {
pairVerifier.unverify(); pairVerifier.unverify();
} }
} }
async ensureRAMMembers() { private async ensureRAMMembers(): Promise<void> {
if (this.ramGuild.getMembers().size === 0) { if (this.ramGuild.getMembers().size === 0) {
await this.fetchMembers(); await this.fetchMembers();
} }
if (this.ramGuild.getMembers().size === 0) throw new Error('RAM Members was not updated through fetchMembers'); if (this.ramGuild.getMembers().size === 0) throw new Error('RAM Members was not updated through fetchMembers');
} }
async ensureRAMChannels() { private async ensureRAMChannels(): Promise<void> {
if (this.ramGuild.getChannels().size === 0) { if (this.ramGuild.getChannels().size === 0) {
await this.fetchChannels(); await this.fetchChannels();
} }
if (this.ramGuild.getChannels().size === 0) throw new Error('RAM Channels was not updated through fetchChannels'); if (this.ramGuild.getChannels().size === 0) throw new Error('RAM Channels was not updated through fetchChannels');
} }
public async grabRAMMembersMap(): Promise<Map<string, Member>> {
await this.ensureRAMMembers();
return this.ramGuild.getMembers();
}
public async grabRAMChannelsMap(): Promise<Map<string, Channel>> {
await this.ensureRAMChannels();
return this.ramGuild.getChannels();
}
// Fetched through the triple-cache system (RAM -> Disk -> Server) // Fetched through the triple-cache system (RAM -> Disk -> Server)
async fetchMetadata(): Promise<GuildMetadata> { async fetchMetadata(): Promise<GuildMetadata> {

View File

@ -28,7 +28,7 @@ export default class MessageRAMCache {
} }
// Removes the oldest messages from the channel until the channel is under the max cached character limit // Removes the oldest messages from the channel until the channel is under the max cached character limit
private trimOldChannelMessagesIfNeeded(guildId: string, channelId: string) { private trimOldChannelMessagesIfNeeded(guildId: number, channelId: string) {
let id = `g#${guildId}/c#${channelId}`; let id = `g#${guildId}/c#${channelId}`;
let value = this.data.get(id); let value = this.data.get(id);
if (!value) return; if (!value) return;

View File

@ -1,3 +1,5 @@
import * as crypto from 'crypto';
import ConcurrentQueue from "../../concurrent-queue/concurrent-queue"; import ConcurrentQueue from "../../concurrent-queue/concurrent-queue";
import * as sqlite from 'sqlite'; import * as sqlite from 'sqlite';
@ -150,10 +152,10 @@ export default class PersonalDB {
// Guilds // Guilds
async addGuild(name: string | null, icon: Resource | null): Promise<number> { async addGuild(name: string, iconResourceId: string, memberId: string): Promise<number> {
let result = await this.db.run( let result = await this.db.run(
`INSERT INTO guilds (name, icon_resource_id) VALUES (:name, :icon_resource_id)`, `INSERT INTO guilds (name, icon_resource_id, member_id) VALUES (:name, :icon_resource_id, :member_id)`,
{ ':name': name, ':icon_resource_id': icon?.id ?? null } { ':name': name, ':icon_resource_id': iconResourceId, ':member_id': memberId }
); );
if (result.changes !== 1) throw new Error('unable to add guild'); if (result.changes !== 1) throw new Error('unable to add guild');
if (result.lastID === undefined) throw new Error('unable to get guild last id'); if (result.lastID === undefined) throw new Error('unable to get guild last id');
@ -200,24 +202,33 @@ export default class PersonalDB {
// Guild Sockets // Guild Sockets
async addGuildSocket(guildId: number, socketConfig: SocketConfig): Promise<void> { async addGuildSocket(guildId: number, url: string, cert: string, publicKey: crypto.KeyObject, privateKey: crypto.KeyObject): Promise<number> {
let publicKeyPem = socketConfig.publicKey.export({ type: 'spki', format: 'pem' }); let publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' });
let privateKeyPem = socketConfig.privateKey.export({ type: 'pkcs8', format: 'pem' }); let privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
let result = await this.db.all( let result = await this.db.run(
`INSERT INTO guild_sockets (guild_id, url, cert, public_key, private_key) VALUES (:guild_id, :url, :cert, :public_key, :private_key)`, `INSERT INTO guild_sockets (guild_id, url, cert, public_key, private_key) VALUES (:guild_id, :url, :cert, :public_key, :private_key)`,
{ ':guild_id': guildId, ':url': socketConfig.url, ':cert': socketConfig.cert, ':public_key': publicKeyPem, ':private_key': privateKeyPem } { ':guild_id': guildId, ':url': url, ':cert': cert, ':public_key': publicKeyPem, ':private_key': privateKeyPem }
); );
if (result.length !== 1) throw new Error('unable to add guild'); if (result.changes !== 1) throw new Error('unable to add guild');
return result.lastID as number;
} }
async removeGuildSocket(guildId: number, guildSocketId: number): Promise<void> { async removeGuildSocket(guildId: number, guildSocketId: number): Promise<void> {
let result = await this.db.run( let result = await this.db.run(
`DELETE FROM guilds WHERE id=:guild_socket_id AND guild_id=:guild_id`, `DELETE FROM guild_sockets WHERE id=:guild_socket_id AND guild_id=:guild_id`,
{ ':guild_id': guildId, ':guild_socket_id': guildSocketId } { ':guild_id': guildId, ':guild_socket_id': guildSocketId }
); );
if (result?.changes !== 1) throw new Error('unable to remove guild'); if (result?.changes !== 1) throw new Error('unable to remove guild');
} }
async removeGuildSockets(guildId: number): Promise<void> {
let result = await this.db.run(
`DELETE FROM guild_sockets WHERE guild_id=:guild_id`,
{ ':guild_id': guildId }
);
if (result?.changes !== 1) throw new Error('unable to remove guild');
}
async fetchGuildSockets(guildId: number): Promise<SocketConfig[]> { async fetchGuildSockets(guildId: number): Promise<SocketConfig[]> {
let result = await this.db.all( let result = await this.db.all(
`SELECT * FROM guild_sockets WHERE guild_id=:guild_id`, `SELECT * FROM guild_sockets WHERE guild_id=:guild_id`,
@ -226,6 +237,15 @@ export default class PersonalDB {
return result.map(dataGuildSocket => SocketConfig.fromDBData(dataGuildSocket)); return result.map(dataGuildSocket => SocketConfig.fromDBData(dataGuildSocket));
} }
async fetchGuildSocket(guildId: number, guildSocketId: number): Promise<SocketConfig> {
let result = await this.db.get(
`SELECT * FROM guild_sockets WHERE id=:guild_socket_id AND guild_id=:guild_id`,
{ ':guild_socket_id': guildSocketId, ':guild_id': guildId }
);
if (!result) throw new Error('unable to fetch specific guild socket');
return SocketConfig.fromDBData(result);
}
// Members // Members
async addMembers(guildId: number, members: Member[]): Promise<void> { async addMembers(guildId: number, members: Member[]): Promise<void> {
@ -285,6 +305,10 @@ export default class PersonalDB {
return result.map(dataMember => Member.fromDBData(dataMember)); return result.map(dataMember => Member.fromDBData(dataMember));
} }
async clearAllMembersStatus(guildId: number): Promise<void> {
await this.db.run(`UPDATE members SET status='unknown' WHERE guild_id=:guild_id`, { ':guild_id': guildId });
}
// Channels // Channels
async addChannels(guildId: number, channels: Channel[]) { async addChannels(guildId: number, channels: Channel[]) {

View File

@ -74,8 +74,8 @@ window.addEventListener('DOMContentLoaded', () => {
} }
// Receive Current Channel Messages // Receive Current Channel Messages
controller.on('new-message', async (server: ClientController, message: Message) => { controller.on('new-message', async (guild: CombinedGuild, message: Message) => {
if (ui.activeServer === null || ui.activeServer.id !== server.id) return; if (ui.activeGuild === null || ui.activeGuild.id !== server.id) return;
if (ui.activeChannel === null || ui.activeChannel.id !== message.channel.id) return; if (ui.activeChannel === null || ui.activeChannel.id !== message.channel.id) return;
if (ui.messagesAtBottom) { if (ui.messagesAtBottom) {
// add the message to the bottom of the message feed // add the message to the bottom of the message feed
@ -88,7 +88,7 @@ window.addEventListener('DOMContentLoaded', () => {
} }
}); });
controller.on('verified', async (server: ClientController) => { controller.on('verified', async (guild: CombinedGuild) => {
(async () => { // update connection info (async () => { // update connection info
await Actions.fetchAndUpdateConnection(ui, server); await Actions.fetchAndUpdateConnection(ui, server);
})(); })();
@ -120,7 +120,7 @@ window.addEventListener('DOMContentLoaded', () => {
})(); })();
}); });
controller.on('disconnected', (server: ClientController) => { controller.on('disconnected', (guild: CombinedGuild) => {
(async () => { (async () => {
await Actions.fetchAndUpdateConnection(ui, server); await Actions.fetchAndUpdateConnection(ui, server);
})(); })();
@ -129,7 +129,7 @@ window.addEventListener('DOMContentLoaded', () => {
})(); })();
}); });
controller.on('update-server', async (server: ClientController, serverData: ServerMetaData | CacheServerData) => { controller.on('update-server', async (guild: CombinedGuild, serverData: ServerMetaData | CacheServerData) => {
LOG.debug(`s#${server.id} metadata updated`) LOG.debug(`s#${server.id} metadata updated`)
await ui.updateServerName(server, serverData.name); await ui.updateServerName(server, serverData.name);
@ -146,12 +146,12 @@ window.addEventListener('DOMContentLoaded', () => {
} }
}); });
controller.on('deleted-members', async (server: ClientController, members: Member[]) => { controller.on('deleted-members', async (guild: CombinedGuild, members: Member[]) => {
LOG.debug(members.length + ' deleted members'); LOG.debug(members.length + ' deleted members');
await ui.deleteMembers(server, members); await ui.deleteMembers(server, members);
}); });
controller.on('updated-members', async (server: ClientController, data: { oldMember: Member, newMember: Member }[]) => { controller.on('updated-members', async (guild: CombinedGuild, data: { oldMember: Member, newMember: Member }[]) => {
LOG.debug(data.length + ' updated members s#' + server.id); LOG.debug(data.length + ' updated members s#' + server.id);
await ui.updateMembers(server, data); await ui.updateMembers(server, data);
if ( if (
@ -162,40 +162,40 @@ window.addEventListener('DOMContentLoaded', () => {
} }
}); });
controller.on('added-members', async (server: ClientController, members: Member[]) => { controller.on('added-members', async (guild: CombinedGuild, members: Member[]) => {
LOG.debug(members.length + ' added members'); LOG.debug(members.length + ' added members');
await ui.addMembers(server, members); await ui.addMembers(server, members);
}); });
controller.on('deleted-channels', async (server: ClientController, channels: Channel[]) => { controller.on('deleted-channels', async (guild: CombinedGuild, channels: Channel[]) => {
LOG.debug(channels.length + ' deleted channels'); LOG.debug(channels.length + ' deleted channels');
await ui.deleteChannels(server, channels); await ui.deleteChannels(server, channels);
}); });
controller.on('updated-channels', async (server: ClientController, data: { oldChannel: Channel, newChannel: Channel }[]) => { controller.on('updated-channels', async (guild: CombinedGuild, data: { oldChannel: Channel, newChannel: Channel }[]) => {
LOG.debug(data.length + ' updated channels'); LOG.debug(data.length + ' updated channels');
await ui.updateChannels(server, data); await ui.updateChannels(server, data);
}); });
controller.on('added-channels', async (server: ClientController, channels: Channel[]) => { controller.on('added-channels', async (guild: CombinedGuild, channels: Channel[]) => {
LOG.debug(channels.length + ' added channels'); LOG.debug(channels.length + ' added channels');
await ui.addChannels(server, channels); await ui.addChannels(server, channels);
}); });
controller.on('deleted-messages', async (server: ClientController, channel: Channel, messages: Message[]) => { controller.on('deleted-messages', async (guild: CombinedGuild, channel: Channel, messages: Message[]) => {
LOG.debug(messages.length + ' deleted messages'); LOG.debug(messages.length + ' deleted messages');
//LOG.debug('deleted messages:', { messages: deletedMessages.map(message => message.text) }); //LOG.debug('deleted messages:', { messages: deletedMessages.map(message => message.text) });
// messages were deleted but the cache still had them // messages were deleted but the cache still had them
await ui.deleteMessages(server, channel, messages); await ui.deleteMessages(server, channel, messages);
}); });
controller.on('updated-messages', async (server: ClientController, channel: Channel, data: { oldMessage: Message, newMessage: Message }[]) => { controller.on('updated-messages', async (guild: CombinedGuild, channel: Channel, data: { oldMessage: Message, newMessage: Message }[]) => {
LOG.debug(data.length + ' updated messages'); LOG.debug(data.length + ' updated messages');
// messages were updated on the server-side // messages were updated on the server-side
await ui.updateMessages(server, channel, data); await ui.updateMessages(server, channel, data);
}); });
controller.on('added-messages', async (server: ClientController, channel: Channel, addedAfter: Map<string, Message>, addedBefore: Map<string, Message>) => { 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(addedAfter.size + ' added messages'); // addedBefore.size should equal addedAfter.size
//LOG.debug('added messages', { messages: Array.from(addedAfter.values()).map(message => message.text) }); //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 // messages were added in a place that the cache did not have them

View File

@ -4,6 +4,7 @@ import * as socketio from 'socket.io-client';
import DedupAwaiter from "./dedup-awaiter"; import DedupAwaiter from "./dedup-awaiter";
import Util from './util'; import Util from './util';
// Automatically re-verifies the socket when connected
export default class SocketVerifier { export default class SocketVerifier {
public isVerified = false; public isVerified = false;
private memberId: string | null = null; private memberId: string | null = null;

View File

@ -9,13 +9,12 @@ import ElementsUtil from './elements/require/elements-util';
import Globals from './globals'; import Globals from './globals';
import Util from './util'; import Util from './util';
import ClientController from './client-controller'; import CombinedGuild from './guild-combined';
import { Message, Member, Channel, ConnectionInfo, ShouldNeverHappenError } from './data-types'; import { Message, Member, Channel, ConnectionInfo, ShouldNeverHappenError } from './data-types';
import Q from './q-module'; import Q from './q-module';
import createServerListServer from './elements/server-list-server'; import createGuildListGuild from './elements/guild-list-guild';
import createChannel from './elements/channel'; import createChannel from './elements/channel';
import createMember from './elements/member'; import createMember from './elements/member';
import Actions from './actions';
import Controller from './controller'; import Controller from './controller';
import createMessage from './elements/message'; import createMessage from './elements/message';
@ -25,14 +24,14 @@ interface SetMessageProps {
} }
export default class UI { export default class UI {
public activeServer: ClientController | null = null; public activeGuild: CombinedGuild | null = null;
public activeChannel: Channel | null = null; public activeChannel: Channel | null = null;
public activeConnection: ConnectionInfo | null = null; public activeConnection: ConnectionInfo | null = null;
public messagesAtTop = false; public messagesAtTop = false;
public messagesAtBottom = false; public messagesAtBottom = false;
public messagePairsServer: ClientController | null = null; public messagePairsGuild: CombinedGuild | null = null;
public messagePairsChannel: Channel | { id: string } | null = null; public messagePairsChannel: Channel | { id: string } | null = null;
public messagePairs = new Map<string | null, { message: Message, element: HTMLElement }>(); // messageId -> { message: Message, element: HTMLElement } public messagePairs = new Map<string | null, { message: Message, element: HTMLElement }>(); // messageId -> { message: Message, element: HTMLElement }
@ -44,8 +43,8 @@ export default class UI {
this.q = q; this.q = q;
} }
public isMessagePairsServer(server: ClientController): boolean { public isMessagePairsGuild(guild: CombinedGuild): boolean {
return this.messagePairsServer !== null && server.id === this.messagePairsServer.id; return this.messagePairsGuild !== null && guild.id === this.messagePairsGuild.id;
} }
public isMessagePairsChannel(channel: Channel): boolean { public isMessagePairsChannel(channel: Channel): boolean {
@ -55,66 +54,66 @@ export default class UI {
// Use non-concurrent queues to prevent concurrent updates to parts of the view // Use non-concurrent queues to prevent concurrent updates to parts of the view
// This is effectively a javascript version of a 'lock' // This is effectively a javascript version of a 'lock'
// These 'locks' should be called from working code rather than the updating functions themselves to work properly // These 'locks' should be called from working code rather than the updating functions themselves to work properly
private _serversLock = new ConcurrentQueue<void>(1); private _guildsLock = new ConcurrentQueue<void>(1);
private _serverNameLock = new ConcurrentQueue<void>(1); private _guildNameLock = new ConcurrentQueue<void>(1);
private _connectionLock = new ConcurrentQueue<void>(1); private _connectionLock = new ConcurrentQueue<void>(1);
private _channelsLock = new ConcurrentQueue<void>(1); private _channelsLock = new ConcurrentQueue<void>(1);
private _membersLock = new ConcurrentQueue<void>(1); private _membersLock = new ConcurrentQueue<void>(1);
private _messagesLock = new ConcurrentQueue<void>(1); private _messagesLock = new ConcurrentQueue<void>(1);
private async _lockWithServer(server: ClientController, task: (() => Promise<void> | void), lock: ConcurrentQueue<void>): Promise<void> { private async _lockWithGuild(guild: CombinedGuild, task: (() => Promise<void> | void), lock: ConcurrentQueue<void>): Promise<void> {
if (this.activeServer === null || this.activeServer.id !== server.id) return; if (this.activeGuild === null || this.activeGuild.id !== guild.id) return;
await lock.push(async () => { await lock.push(async () => {
if (this.activeServer === null || this.activeServer.id !== server.id) return; if (this.activeGuild === null || this.activeGuild.id !== guild.id) return;
await task(); await task();
}); });
} }
private async _lockWithServerChannel(server: ClientController, channel: Channel | { id: string }, task: (() => Promise<void> | void), lock: ConcurrentQueue<void>): Promise<void> { private async _lockWithGuildChannel(guild: CombinedGuild, channel: Channel | { id: string }, task: (() => Promise<void> | void), lock: ConcurrentQueue<void>): Promise<void> {
if (this.activeServer === null || this.activeServer.id !== server.id) return; if (this.activeGuild === null || this.activeGuild.id !== guild.id) return;
if (this.activeChannel === null || this.activeChannel.id !== channel.id) return; if (this.activeChannel === null || this.activeChannel.id !== channel.id) return;
await lock.push(async () => { await lock.push(async () => {
if (this.activeServer === null || this.activeServer.id !== server.id) return; if (this.activeGuild === null || this.activeGuild.id !== guild.id) return;
if (this.activeChannel === null || this.activeChannel.id !== channel.id) return; if (this.activeChannel === null || this.activeChannel.id !== channel.id) return;
await task(); await task();
}); });
} }
public async lockServerName(server: ClientController, task: (() => Promise<void> | void)): Promise<void> { public async lockGuildName(guild: CombinedGuild, task: (() => Promise<void> | void)): Promise<void> {
await this._lockWithServer(server, task, this._serverNameLock); await this._lockWithGuild(guild, task, this._guildNameLock);
} }
public async lockConnection(server: ClientController, task: (() => Promise<void> | void)): Promise<void> { public async lockConnection(guild: CombinedGuild, task: (() => Promise<void> | void)): Promise<void> {
await this._lockWithServer(server, task, this._connectionLock); await this._lockWithGuild(guild, task, this._connectionLock);
} }
public async lockChannels(server: ClientController, task: (() => Promise<void> | void)): Promise<void> { public async lockChannels(guild: CombinedGuild, task: (() => Promise<void> | void)): Promise<void> {
await this._lockWithServer(server, task, this._channelsLock); await this._lockWithGuild(guild, task, this._channelsLock);
} }
public async lockMembers(server: ClientController, task: (() => Promise<void> | void)): Promise<void> { public async lockMembers(guild: CombinedGuild, task: (() => Promise<void> | void)): Promise<void> {
await this._lockWithServer(server, task, this._membersLock); await this._lockWithGuild(guild, task, this._membersLock);
} }
public async lockMessages(server: ClientController, channel: Channel | { id: string }, task: (() => Promise<void> | void)): Promise<void> { public async lockMessages(guild: CombinedGuild, channel: Channel | { id: string }, task: (() => Promise<void> | void)): Promise<void> {
await this._lockWithServerChannel(server, channel, task, this._messagesLock); await this._lockWithGuildChannel(guild, channel, task, this._messagesLock);
} }
public setActiveServer(server: ClientController): void { public setActiveGuild(guild: CombinedGuild): void {
if (this.activeServer !== null) { if (this.activeGuild !== null) {
let prev = this.q.$_('#server-list .server[meta-id="' + this.activeServer.id + '"]'); let prev = this.q.$_('#guild-list .guild[meta-id="' + this.activeGuild.id + '"]');
if (prev) { if (prev) {
prev.classList.remove('active'); prev.classList.remove('active');
} }
} }
let next = this.q.$('#server-list .server[meta-id="' + server.id + '"]'); let next = this.q.$('#guild-list .guild[meta-id="' + guild.id + '"]');
next.classList.add('active'); next.classList.add('active');
this.q.$('#server').setAttribute('meta-id', server.id); this.q.$('#guild').setAttribute('meta-id', guild.id + '');
this.activeServer = server; this.activeGuild = guild;
} }
public async setActiveChannel(server: ClientController, channel: Channel): Promise<void> { public async setActiveChannel(guild: CombinedGuild, channel: Channel): Promise<void> {
await this.lockChannels(server, () => { await this.lockChannels(guild, () => {
// Channel List Highlight // Channel List Highlight
if (this.activeChannel !== null) { if (this.activeChannel !== null) {
let prev = this.q.$_('#channel-list .channel[meta-id="' + this.activeChannel.id + '"]'); let prev = this.q.$_('#channel-list .channel[meta-id="' + this.activeChannel.id + '"]');
@ -135,25 +134,25 @@ export default class UI {
}); });
} }
public async setActiveConnection(server: ClientController, connection: ConnectionInfo): Promise<void> { public async setActiveConnection(guild: CombinedGuild, connection: ConnectionInfo): Promise<void> {
await this.lockConnection(server, () => { await this.lockConnection(guild, () => {
this.activeConnection = connection; this.activeConnection = connection;
this.q.$('#connection').className = 'member ' + connection.status; this.q.$('#connection').className = 'member ' + connection.status;
this.q.$('#member-name').innerText = connection.displayName; this.q.$('#member-name').innerText = connection.displayName;
this.q.$('#member-status-text').innerText = connection.status; this.q.$('#member-status-text').innerText = connection.status;
this.q.$('#server').className = ''; this.q.$('#guild').className = '';
for (let privilege of connection.privileges) { for (let privilege of connection.privileges) {
this.q.$('#server').classList.add('privilege-' + privilege); this.q.$('#guild').classList.add('privilege-' + privilege);
} }
if (connection.avatarResourceId) { if (connection.avatarResourceId) {
(async () => { (async () => {
// Update avatar // Update avatar
if (this.activeServer === null || this.activeServer.id !== server.id) return; if (this.activeGuild === null || this.activeGuild.id !== guild.id) return;
let src = await ElementsUtil.getImageBufferFromResourceFailSoftly(server, connection.avatarResourceId); let src = await ElementsUtil.getImageBufferFromResourceFailSoftly(guild, connection.avatarResourceId);
if (this.activeServer === null || this.activeServer.id !== server.id) return; if (this.activeGuild === null || this.activeGuild.id !== guild.id) return;
(this.q.$('#member-avatar') as HTMLImageElement).src = src; (this.q.$('#member-avatar') as HTMLImageElement).src = src;
})(); })();
} else { } else {
@ -162,55 +161,55 @@ export default class UI {
}); });
} }
public async setServers(controller: Controller, servers: ClientController[]): Promise<void> { public async setGuilds(controller: Controller, guilds: CombinedGuild[]): Promise<void> {
await this._serversLock.push(() => { await this._guildsLock.push(() => {
Q.clearChildren(this.q.$('#server-list')); Q.clearChildren(this.q.$('#guild-list'));
for (let server of servers) { for (let guild of guilds) {
let element = createServerListServer(this.document, this.q, this, controller, server); let element = createGuildListGuild(this.document, this.q, this, controller, guild);
this.q.$('#server-list').appendChild(element); this.q.$('#guild-list').appendChild(element);
} }
}); });
} }
public async addServer(controller: Controller, server: ClientController): Promise<HTMLElement> { public async addGuild(controller: Controller, guild: CombinedGuild): Promise<HTMLElement> {
let element: HTMLElement | null = null; let element: HTMLElement | null = null;
await this._serversLock.push(() => { await this._guildsLock.push(() => {
element = createServerListServer(this.document, this.q, this, controller, server) as HTMLElement; element = createGuildListGuild(this.document, this.q, this, controller, guild) as HTMLElement;
this.q.$('#server-list').appendChild(element); this.q.$('#guild-list').appendChild(element);
}); });
if (element == null) throw new ShouldNeverHappenError('element was not set'); if (element == null) throw new ShouldNeverHappenError('element was not set');
return element; return element;
} }
public async removeServer(server: ClientController): Promise<void> { public async removeGuild(guild: CombinedGuild): Promise<void> {
await this._serversLock.push(() => { await this._guildsLock.push(() => {
let element = this.q.$_('#server-list .server[meta-id="' + server.id + '"]'); let element = this.q.$_('#guild-list .guild[meta-id="' + guild.id + '"]');
element?.parentElement?.removeChild(element); element?.parentElement?.removeChild(element);
}); });
} }
public async updateServerIcon(server: ClientController, iconBuff: Buffer): Promise<void> { public async updateGuildIcon(guild: CombinedGuild, iconBuff: Buffer): Promise<void> {
await this._serversLock.push(async () => { await this._guildsLock.push(async () => {
let iconElement = this.q.$('#server-list .server[meta-id="' + server.id + '"] img') as HTMLImageElement; let iconElement = this.q.$('#guild-list .guild[meta-id="' + guild.id + '"] img') as HTMLImageElement;
iconElement.src = await ElementsUtil.getImageBufferSrc(iconBuff); iconElement.src = await ElementsUtil.getImageBufferSrc(iconBuff);
}); });
} }
public async updateServerName(server: ClientController, name: string): Promise<void>{ public async updateGuildName(guild: CombinedGuild, name: string): Promise<void>{
await this.lockServerName(server, () => { await this.lockGuildName(guild, () => {
this.q.$('#server-name').innerText = name; this.q.$('#guild-name').innerText = name;
let baseElement = this.q.$('#server-list .server[meta-id="' + server.id + '"]'); let baseElement = this.q.$('#guild-list .guild[meta-id="' + guild.id + '"]');
baseElement.setAttribute('meta-name', name); baseElement.setAttribute('meta-name', name);
}); });
} }
private _updatePosition<T>(element: HTMLElement, serverCacheMap: Map<string | null, T>, getDirection: ((prevData: T, data: T) => number)) { private _updatePosition<T>(element: HTMLElement, guildCacheMap: Map<string | null, T>, getDirection: ((prevData: T, data: T) => number)) {
let data = serverCacheMap.get(element.getAttribute('meta-id')); let data = guildCacheMap.get(element.getAttribute('meta-id'));
if (!data) throw new ShouldNeverHappenError('unable to get data from cache map'); if (!data) throw new ShouldNeverHappenError('unable to get data from cache map');
// TODO: do-while may be a bit cleaner? // TODO: do-while may be a bit cleaner?
let prev = Q.previousElement(element); let prev = Q.previousElement(element);
while (prev != null) { while (prev != null) {
let prevData = serverCacheMap.get(prev.getAttribute('meta-id')); let prevData = guildCacheMap.get(prev.getAttribute('meta-id'));
if (!prevData) throw new ShouldNeverHappenError('unable to get prevData from cache map'); if (!prevData) throw new ShouldNeverHappenError('unable to get prevData from cache map');
if (getDirection(prevData, data) > 0) { // this element comes before previous element if (getDirection(prevData, data) > 0) { // this element comes before previous element
prev.parentElement?.insertBefore(element, prev); prev.parentElement?.insertBefore(element, prev);
@ -221,7 +220,7 @@ export default class UI {
} }
let next = Q.nextElement(element); let next = Q.nextElement(element);
while (next != null) { while (next != null) {
let nextData = serverCacheMap.get(next.getAttribute('meta-id')); let nextData = guildCacheMap.get(next.getAttribute('meta-id'));
if (!nextData) throw new ShouldNeverHappenError('unable to get nextData from cache map'); if (!nextData) throw new ShouldNeverHappenError('unable to get nextData from cache map');
if (getDirection(data, nextData) > 0) { // this element comes after next element if (getDirection(data, nextData) > 0) { // this element comes after next element
next.parentElement?.insertBefore(next, element); next.parentElement?.insertBefore(next, element);
@ -232,27 +231,27 @@ export default class UI {
} }
} }
public updateChannelPosition(server: ClientController, channelElement: HTMLElement): void { public async updateChannelPosition(guild: CombinedGuild, channelElement: HTMLElement): Promise<void> {
this._updatePosition(channelElement, server.channels, (a, b) => { this._updatePosition(channelElement, await guild.grabRAMChannelsMap(), (a, b) => {
return a.index - b.index; return a.index - b.index;
}); });
} }
public async addChannels(server: ClientController, channels: Channel[], options?: { clear: boolean }): Promise<void> { public async addChannels(guild: CombinedGuild, channels: Channel[], options?: { clear: boolean }): Promise<void> {
await this.lockChannels(server, () => { await this.lockChannels(guild, async () => {
if (options?.clear) { if (options?.clear) {
Q.clearChildren(this.q.$('#channel-list')); Q.clearChildren(this.q.$('#channel-list'));
} }
for (let channel of channels) { for (let channel of channels) {
let element = createChannel(this.document, this.q, this, server, channel); let element = createChannel(this.document, this.q, this, guild, channel);
this.q.$('#channel-list').appendChild(element); this.q.$('#channel-list').appendChild(element);
this.updateChannelPosition(server, element); await this.updateChannelPosition(guild, element);
} }
}); });
} }
public async deleteChannels(server: ClientController, channels: Channel[]): Promise<void> { public async deleteChannels(guild: CombinedGuild, channels: Channel[]): Promise<void> {
await this.lockChannels(server, () => { await this.lockChannels(guild, () => {
for (let channel of channels) { for (let channel of channels) {
let element = this.q.$_('#channel-list .channel[meta-id="' + channel.id + '"]'); let element = this.q.$_('#channel-list .channel[meta-id="' + channel.id + '"]');
element?.parentElement?.removeChild(element); element?.parentElement?.removeChild(element);
@ -263,13 +262,13 @@ export default class UI {
}); });
} }
public async updateChannels(server: ClientController, data: { oldChannel: Channel, newChannel: Channel }[]): Promise<void> { public async updateChannels(guild: CombinedGuild, data: { oldChannel: Channel, newChannel: Channel }[]): Promise<void> {
await this.lockChannels(server, () => { await this.lockChannels(guild, async () => {
for (const { oldChannel, newChannel } of data) { for (const { oldChannel, newChannel } of data) {
let oldElement = this.q.$('#channel-list .channel[meta-id="' + newChannel.id + '"]'); let oldElement = this.q.$('#channel-list .channel[meta-id="' + newChannel.id + '"]');
let newElement = createChannel(this.document, this.q, this, server, newChannel); let newElement = createChannel(this.document, this.q, this, guild, newChannel);
oldElement.parentElement?.replaceChild(newElement, oldElement); oldElement.parentElement?.replaceChild(newElement, oldElement);
this.updateChannelPosition(server, newElement); await this.updateChannelPosition(guild, newElement);
if (this.activeChannel !== null && this.activeChannel.id === newChannel.id) { if (this.activeChannel !== null && this.activeChannel.id === newChannel.id) {
newElement.classList.add('active'); newElement.classList.add('active');
@ -284,38 +283,39 @@ export default class UI {
}); });
} }
public async setChannels(server: ClientController, channels: Channel[]): Promise<void> { public async setChannels(guild: CombinedGuild, channels: Channel[]): Promise<void> {
// check if an element with the same channel and server exists before adding the new channels // check if an element with the same channel and guild exists before adding the new channels
// this is nescessary to make sure that if two servers have channels with the same id, the channel list is still // this is nescessary to make sure that if two guilds have channels with the same id, the channel list is still
// properly refreshed and the active channel is not improperly set. // properly refreshed and the active channel is not improperly set.
let oldMatchingElement: HTMLElement | null = null; let oldMatchingElement: HTMLElement | null = null;
if (this.activeServer !== null && this.activeChannel !== null) { if (this.activeGuild !== null && this.activeChannel !== null) {
oldMatchingElement = this.q.$_('#channel-list .channel[meta-id="' + this.activeChannel.id + '"][meta-server-id="' + this.activeServer.id + '"]'); oldMatchingElement = this.q.$_('#channel-list .channel[meta-id="' + this.activeChannel.id + '"][meta-guild-id="' + this.activeGuild.id + '"]');
} }
await this.addChannels(server, channels, { clear: true }); await this.addChannels(guild, channels, { clear: true });
if (this.activeServer !== null && this.activeServer.id === server.id && this.activeChannel !== null) { if (this.activeGuild !== null && this.activeGuild.id === guild.id && this.activeChannel !== null) {
let newActiveElement = this.q.$_('#channel-list .channel[meta-id="' + this.activeChannel.id + '"][meta-server-id="' + this.activeServer.id + '"]'); let newActiveElement = this.q.$_('#channel-list .channel[meta-id="' + this.activeChannel.id + '"][meta-guild-id="' + this.activeGuild.id + '"]');
if (newActiveElement && oldMatchingElement) { if (newActiveElement && oldMatchingElement) {
let activeChannelId = this.activeChannel.id; let activeChannelId = this.activeChannel.id;
let channel = channels.find(channel => channel.id === activeChannelId); let channel = channels.find(channel => channel.id === activeChannelId);
if (channel === undefined) throw new ShouldNeverHappenError('current channel does not exist in channels list') if (channel === undefined) throw new ShouldNeverHappenError('current channel does not exist in channels list')
this.setActiveChannel(server, channel); this.setActiveChannel(guild, channel);
} else { } else {
this.activeChannel = null; // the active channel was removed this.activeChannel = null; // the active channel was removed
} }
} }
} }
public async setChannelsErrorIndicator(server: ClientController, errorIndicatorElement: HTMLElement): Promise<void> { public async setChannelsErrorIndicator(guild: CombinedGuild, errorIndicatorElement: HTMLElement): Promise<void> {
await this.lockChannels(server, () => { await this.lockChannels(guild, () => {
Q.clearChildren(this.q.$('#channel-list')); Q.clearChildren(this.q.$('#channel-list'));
this.q.$('#channel-list').appendChild(errorIndicatorElement); this.q.$('#channel-list').appendChild(errorIndicatorElement);
}); });
} }
public updateMemberPosition(server: ClientController, memberElement: HTMLElement): void { public async updateMemberPosition(guild: CombinedGuild, memberElement: HTMLElement): Promise<void> {
// TODO: Change 100 to a constant?
let statusOrder = { let statusOrder = {
'online': 0, 'online': 0,
'away': 1, 'away': 1,
@ -324,7 +324,7 @@ export default class UI {
'invisible': 3, // this would only be shown in the case of the current member. 'invisible': 3, // this would only be shown in the case of the current member.
'unknown': 100, 'unknown': 100,
}; };
this._updatePosition(memberElement, server.members, (a, b) => { this._updatePosition(memberElement, await guild.grabRAMMembersMap(), (a, b) => {
let onlineCmp = (a.status == 'offline' ? 1 : 0) - (b.status == 'offline' ? 1 : 0); let onlineCmp = (a.status == 'offline' ? 1 : 0) - (b.status == 'offline' ? 1 : 0);
if (onlineCmp != 0) return onlineCmp; if (onlineCmp != 0) return onlineCmp;
let rolePriorityCmp = (a.rolePriority == null ? 100 : a.rolePriority) - (b.rolePriority == null ? 100 : b.rolePriority); let rolePriorityCmp = (a.rolePriority == null ? 100 : a.rolePriority) - (b.rolePriority == null ? 100 : b.rolePriority);
@ -336,41 +336,41 @@ export default class UI {
}); });
} }
public async addMembers(server: ClientController, members: Member[], options?: { clear: boolean }): Promise<void> { public async addMembers(guild: CombinedGuild, members: Member[], options?: { clear: boolean }): Promise<void> {
await this.lockMembers(server, () => { await this.lockMembers(guild, async () => {
if (options?.clear) { if (options?.clear) {
Q.clearChildren(this.q.$('#server-members')); Q.clearChildren(this.q.$('#guild-members'));
} }
for (let member of members) { for (let member of members) {
let element = createMember(this.q, server, member); let element = createMember(this.q, guild, member);
this.q.$('#server-members').appendChild(element); this.q.$('#guild-members').appendChild(element);
this.updateMemberPosition(server, element); await this.updateMemberPosition(guild, element);
} }
}); });
} }
public async deleteMembers(server: ClientController, members: Member[]): Promise<void> { public async deleteMembers(guild: CombinedGuild, members: Member[]): Promise<void> {
await this.lockMembers(server, () => { await this.lockMembers(guild, () => {
for (let member of members) { for (let member of members) {
let element = this.q.$_('#server-members .member[meta-id="' + member.id + '"]'); let element = this.q.$_('#guild-members .member[meta-id="' + member.id + '"]');
element?.parentElement?.removeChild(element); element?.parentElement?.removeChild(element);
} }
}); });
} }
public async updateMembers(server: ClientController, data: { oldMember: Member, newMember: Member }[]): Promise<void> { public async updateMembers(guild: CombinedGuild, data: { oldMember: Member, newMember: Member }[]): Promise<void> {
await this.lockMembers(server, () => { await this.lockMembers(guild, async () => {
for (const { oldMember, newMember } of data) { for (const { oldMember, newMember } of data) {
let oldElement = this.q.$_('#server-members .member[meta-id="' + newMember.id + '"]'); let oldElement = this.q.$_('#guild-members .member[meta-id="' + newMember.id + '"]');
if (oldElement) { if (oldElement) {
let newElement = createMember(this.q, server, newMember); let newElement = createMember(this.q, guild, newMember);
oldElement.parentElement?.replaceChild(newElement, oldElement); oldElement.parentElement?.replaceChild(newElement, oldElement);
this.updateMemberPosition(server, newElement); await this.updateMemberPosition(guild, newElement);
} }
} }
}); });
if (this.activeChannel === null) return; if (this.activeChannel === null) return;
await this.lockMessages(server, this.activeChannel, () => { await this.lockMessages(guild, this.activeChannel, () => {
for (const { oldMember, newMember } of data) { for (const { oldMember, newMember } of data) {
let newStyle = newMember.roleColor ? 'color: ' + newMember.roleColor : null; let newStyle = newMember.roleColor ? 'color: ' + newMember.roleColor : null;
let newName = newMember.displayName; let newName = newMember.displayName;
@ -386,14 +386,14 @@ export default class UI {
}); });
} }
public async setMembers(server: ClientController, members: Member[]): Promise<void> { public async setMembers(guild: CombinedGuild, members: Member[]): Promise<void> {
await this.addMembers(server, members, { clear: true }); await this.addMembers(guild, members, { clear: true });
} }
public async setMembersErrorIndicator(server: ClientController, errorIndicatorElement: HTMLElement): Promise<void> { public async setMembersErrorIndicator(guild: CombinedGuild, errorIndicatorElement: HTMLElement): Promise<void> {
await this.lockMembers(server, () => { await this.lockMembers(guild, () => {
Q.clearChildren(this.q.$('#server-members')); Q.clearChildren(this.q.$('#guild-members'));
this.q.$('#server-members').appendChild(errorIndicatorElement); this.q.$('#guild-members').appendChild(errorIndicatorElement);
}); });
} }
@ -408,8 +408,8 @@ export default class UI {
return element && this.messagePairs.get(element.getAttribute('meta-id')) || null; return element && this.messagePairs.get(element.getAttribute('meta-id')) || null;
} }
public async addMessagesBefore(server: ClientController, channel: Channel, messages: Message[], prevTopMessage: Message | null): Promise<void> { public async addMessagesBefore(guild: CombinedGuild, channel: Channel, messages: Message[], prevTopMessage: Message | null): Promise<void> {
this.lockMessages(server, channel, () => { this.lockMessages(guild, channel, () => {
if (prevTopMessage && this.getTopMessagePair()?.message.id !== prevTopMessage.id) return; if (prevTopMessage && this.getTopMessagePair()?.message.id !== prevTopMessage.id) return;
this.messagesAtTop = false; this.messagesAtTop = false;
@ -441,22 +441,22 @@ export default class UI {
for (let i = messages.length - 1; i >= 0; --i) { for (let i = messages.length - 1; i >= 0; --i) {
let message = messages[i]; let message = messages[i];
let priorMessage = messages[i - 1] || null; let priorMessage = messages[i - 1] || null;
let element = createMessage(this.document, this.q, server, message, priorMessage); let element = createMessage(this.document, this.q, guild, message, priorMessage);
this.messagePairs.set(message.id, { message: message, element: element }); this.messagePairs.set(message.id, { message: message, element: element });
this.q.$('#channel-feed').prepend(element); this.q.$('#channel-feed').prepend(element);
} }
if (messages.length > 0 && prevTopPair) { if (messages.length > 0 && prevTopPair) {
// Update the previous top message since it may have changed format // Update the previous top message since it may have changed format
let newPrevTopElement = createMessage(this.document, this.q, server, prevTopPair.message, messages[messages.length - 1]); let newPrevTopElement = createMessage(this.document, this.q, guild, prevTopPair.message, messages[messages.length - 1]);
prevTopPair.element.parentElement?.replaceChild(newPrevTopElement, prevTopPair.element); prevTopPair.element.parentElement?.replaceChild(newPrevTopElement, prevTopPair.element);
this.messagePairs.set(prevTopPair.message.id, { message: prevTopPair.message, element: newPrevTopElement }); this.messagePairs.set(prevTopPair.message.id, { message: prevTopPair.message, element: newPrevTopElement });
} }
}); });
} }
public async addMessagesAfter(server: ClientController, channel: Channel | { id: string }, messages: Message[], prevBottomMessage: Message | null): Promise<void> { public async addMessagesAfter(guild: CombinedGuild, channel: Channel | { id: string }, messages: Message[], prevBottomMessage: Message | null): Promise<void> {
await this.lockMessages(server, channel, () => { await this.lockMessages(guild, channel, () => {
if (prevBottomMessage && this.getBottomMessagePair()?.message.id !== prevBottomMessage.id) return; if (prevBottomMessage && this.getBottomMessagePair()?.message.id !== prevBottomMessage.id) return;
this.messagesAtBottom = false; this.messagesAtBottom = false;
@ -487,7 +487,7 @@ export default class UI {
for (let i = 0; i < messages.length; ++i) { // add in-order since we will be scrolling from oldest to newest for (let i = 0; i < messages.length; ++i) { // add in-order since we will be scrolling from oldest to newest
let message = messages[i]; let message = messages[i];
let priorMessage = messages[i - 1] || (prevBottomPair && prevBottomPair.message); let priorMessage = messages[i - 1] || (prevBottomPair && prevBottomPair.message);
let element = createMessage(this.document, this.q, server, message, priorMessage); let element = createMessage(this.document, this.q, guild, message, priorMessage);
this.messagePairs.set(message.id, { message: message, element: element }); this.messagePairs.set(message.id, { message: message, element: element });
this.q.$('#channel-feed').appendChild(element); this.q.$('#channel-feed').appendChild(element);
} }
@ -495,8 +495,8 @@ export default class UI {
} }
// TODO: use topMessage, bottomMessage / topMessageId, bottomMessageId instead? // TODO: use topMessage, bottomMessage / topMessageId, bottomMessageId instead?
public async addMessagesBetween(server: ClientController, channel: Channel, messages: Message[], topElement: HTMLElement, bottomElement: HTMLElement): Promise<void> { public async addMessagesBetween(guild: CombinedGuild, channel: Channel, messages: Message[], topElement: HTMLElement, bottomElement: HTMLElement): Promise<void> {
await this.lockMessages(server, channel, () => { await this.lockMessages(guild, channel, () => {
if (!(messages.length > 0 && topElement != null && bottomElement != null && bottomElement == Q.nextElement(topElement))) { 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 }); LOG.error('invalid messages between', { messages, top: topElement.innerText, bottom: bottomElement.innerText, afterTop: Q.nextElement(topElement)?.innerText });
throw new Error('invalid messages between'); throw new Error('invalid messages between');
@ -566,7 +566,7 @@ export default class UI {
for (let i = 0; i < messages.length; ++i) { for (let i = 0; i < messages.length; ++i) {
let message = messages[i]; let message = messages[i];
let priorMessage = messages[i - 1] || topMessage; let priorMessage = messages[i - 1] || topMessage;
let element = createMessage(this.document, this.q, server, message, priorMessage); let element = createMessage(this.document, this.q, guild, message, priorMessage);
this.messagePairs.set(message.id, { message: message, element: element }); this.messagePairs.set(message.id, { message: message, element: element });
this.q.$('#channel-feed').insertBefore(element, bottomElement); this.q.$('#channel-feed').insertBefore(element, bottomElement);
} }
@ -575,16 +575,16 @@ export default class UI {
// update the bottom element since the element above it changed // update the bottom element since the element above it changed
let bottomMessage = this.messagePairs.get(bottomElement.getAttribute('meta-id'))?.message; let bottomMessage = this.messagePairs.get(bottomElement.getAttribute('meta-id'))?.message;
if (!bottomMessage) throw new ShouldNeverHappenError('could not find bottom message'); if (!bottomMessage) throw new ShouldNeverHappenError('could not find bottom message');
let newBottomElement = createMessage(this.document, this.q, server, bottomMessage, messages[messages.length - 1]); let newBottomElement = createMessage(this.document, this.q, guild, bottomMessage, messages[messages.length - 1]);
bottomElement.parentElement?.replaceChild(newBottomElement, bottomElement); bottomElement.parentElement?.replaceChild(newBottomElement, bottomElement);
this.messagePairs.set(bottomMessage.id, { element: newBottomElement, message: bottomMessage }); this.messagePairs.set(bottomMessage.id, { element: newBottomElement, message: bottomMessage });
} }
}); });
} }
public async setMessages(server: ClientController, channel: Channel | { id: string }, messages: Message[], props: SetMessageProps): Promise<void> { public async setMessages(guild: CombinedGuild, channel: Channel | { id: string }, messages: Message[], props: SetMessageProps): Promise<void> {
const { atTop, atBottom } = props; const { atTop, atBottom } = props;
await this.lockMessages(server, channel, () => { await this.lockMessages(guild, channel, () => {
this.messagesAtTop = atTop; this.messagesAtTop = atTop;
this.messagesAtBottom = atBottom; this.messagesAtBottom = atBottom;
@ -592,7 +592,7 @@ export default class UI {
Util.removeErrorIndicators(this.q, this.q.$('#channel-feed'), [ 'before' ]); Util.removeErrorIndicators(this.q, this.q.$('#channel-feed'), [ 'before' ]);
Util.removeErrorIndicators(this.q, this.q.$('#channel-feed'), [ 'after' ]); Util.removeErrorIndicators(this.q, this.q.$('#channel-feed'), [ 'after' ]);
this.messagePairsServer = server; this.messagePairsGuild = guild;
this.messagePairsChannel = channel; this.messagePairsChannel = channel;
this.messagePairs.clear(); this.messagePairs.clear();
@ -604,7 +604,7 @@ export default class UI {
for (let i = messages.length - 1; i >= 0; --i) { for (let i = messages.length - 1; i >= 0; --i) {
let message = messages[i]; let message = messages[i];
let priorMessage = messages[i - 1] || null; let priorMessage = messages[i - 1] || null;
let element = createMessage(this.document, this.q, server, message, priorMessage); let element = createMessage(this.document, this.q, guild, message, priorMessage);
this.messagePairs.set(message.id, { message: message, element: element }); this.messagePairs.set(message.id, { message: message, element: element });
this.q.$('#channel-feed').prepend(element); this.q.$('#channel-feed').prepend(element);
} }
@ -618,8 +618,8 @@ export default class UI {
this.messagesAtBottom = true; this.messagesAtBottom = true;
} }
public async deleteMessages(server: ClientController, channel: Channel, messages: Message[]) { public async deleteMessages(guild: CombinedGuild, channel: Channel, messages: Message[]) {
await this.lockMessages(server, channel, () => { await this.lockMessages(guild, channel, () => {
for (let message of messages) { for (let message of messages) {
if (this.messagePairs.has(message.id)) { if (this.messagePairs.has(message.id)) {
let messagePair = this.messagePairs.get(message.id) as { message: Message, element: HTMLElement }; let messagePair = this.messagePairs.get(message.id) as { message: Message, element: HTMLElement };
@ -632,14 +632,14 @@ export default class UI {
}); });
} }
public async updateMessages(server: ClientController, channel: Channel, data: { oldMessage: Message, newMessage: Message }[]): Promise<void> { public async updateMessages(guild: CombinedGuild, channel: Channel, data: { oldMessage: Message, newMessage: Message }[]): Promise<void> {
await this.lockMessages(server, channel, () => { await this.lockMessages(guild, channel, () => {
for (const { oldMessage, newMessage } of data) { for (const { oldMessage, newMessage } of data) {
if (this.messagePairs.has(oldMessage.id)) { if (this.messagePairs.has(oldMessage.id)) {
let oldElement = (this.messagePairs.get(oldMessage.id) as { message: Message, element: HTMLElement }).element; let oldElement = (this.messagePairs.get(oldMessage.id) as { message: Message, element: HTMLElement }).element;
let prevElement = Q.previousElement(oldElement); let prevElement = Q.previousElement(oldElement);
let prevMessage = prevElement && (this.messagePairs.get(prevElement.getAttribute('meta-id')) as { message: Message, element: HTMLElement }).message; let prevMessage = prevElement && (this.messagePairs.get(prevElement.getAttribute('meta-id')) as { message: Message, element: HTMLElement }).message;
let newElement = createMessage(this.document, this.q, server, newMessage, prevMessage); let newElement = createMessage(this.document, this.q, guild, newMessage, prevMessage);
oldElement.parentElement?.replaceChild(newElement, oldElement); oldElement.parentElement?.replaceChild(newElement, oldElement);
// TODO: we should be updating messages sent below this message // TODO: we should be updating messages sent below this message
// however, these events should be relatively rare so that's for the future // however, these events should be relatively rare so that's for the future
@ -649,20 +649,20 @@ export default class UI {
}); });
} }
public async addMessagesErrorIndicatorBefore(server: ClientController, channel: Channel, errorIndicatorElement: HTMLElement): Promise<void> { public async addMessagesErrorIndicatorBefore(guild: CombinedGuild, channel: Channel, errorIndicatorElement: HTMLElement): Promise<void> {
await this.lockMessages(server, channel, () => { await this.lockMessages(guild, channel, () => {
this.q.$('#channel-feed').prepend(errorIndicatorElement); this.q.$('#channel-feed').prepend(errorIndicatorElement);
}); });
} }
public async addMessagesErrorIndicatorAfter(server: ClientController, channel: Channel, errorIndicatorElement: HTMLElement): Promise<void> { public async addMessagesErrorIndicatorAfter(guild: CombinedGuild, channel: Channel, errorIndicatorElement: HTMLElement): Promise<void> {
await this.lockMessages(server, channel, () => { await this.lockMessages(guild, channel, () => {
this.q.$('#channel-feed').appendChild(errorIndicatorElement); this.q.$('#channel-feed').appendChild(errorIndicatorElement);
}); });
} }
public async setMessagesErrorIndicator(server: ClientController, channel: Channel | { id: string }, errorIndicatorElement: HTMLElement): Promise<void> { public async setMessagesErrorIndicator(guild: CombinedGuild, channel: Channel | { id: string }, errorIndicatorElement: HTMLElement): Promise<void> {
await this.lockMessages(server, channel, () => { await this.lockMessages(guild, channel, () => {
Q.clearChildren(this.q.$('#channel-feed')); Q.clearChildren(this.q.$('#channel-feed'));
this.q.$('#channel-feed').appendChild(errorIndicatorElement); this.q.$('#channel-feed').appendChild(errorIndicatorElement);
}); });

View File

@ -178,10 +178,11 @@ function bindRegistrationEvents(io: socketio.Server, client: socketio.Socket): v
const { guildId, memberId } = await DB.registerWithToken(token, publicKeyBuff, displayName, avatarBuff); const { guildId, memberId } = await DB.registerWithToken(token, publicKeyBuff, displayName, avatarBuff);
const member = await DB.getMember(guildId, memberId); const member = await DB.getMember(guildId, memberId);
const meta = await DB.getGuild(guildId);
LOG.info(`c#${client.id}: registered with t#${token} as u#${member.id} / ${member.display_name}`); LOG.info(`c#${client.id}: registered with t#${token} as u#${member.id} / ${member.display_name}`);
respond(null, member); respond(null, member, meta);
io.to(guildId).emit('new-member', member); io.to(guildId).emit('new-member', member);
} }