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 UI from './ui';
import ClientController from './client-controller';
import CombinedGuild from './guild-combined';
import { Channel } from './data-types';
import Q from './q-module';
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
try {
let connection = await server.fetchConnectionInfo();
ui.setActiveConnection(server, connection);
let connection = await guild.fetchConnectionInfo();
ui.setActiveConnection(guild, connection);
} catch (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, {
taskFunc: async () => {
if (ui.activeServer === null || ui.activeServer.id !== server.id) return;
let members = await server.grabMembers();
await ui.setMembers(server, members);
if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
let members = await guild.fetchMembers();
await ui.setMembers(guild, members);
},
errorIndicatorAddFunc: async (errorIndicatorElement) => {
await ui.setMembersErrorIndicator(server, errorIndicatorElement);
await ui.setMembersErrorIndicator(guild, errorIndicatorElement);
},
errorContainer: q.$('#server-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, {
taskFunc: async () => {
if (ui.activeServer === null || ui.activeServer.id !== server.id) return;
let channels = await server.grabChannels();
await ui.setChannels(server, channels);
if (ui.activeServer === null || ui.activeServer.id !== server.id) return;
if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
let channels = await guild.fetchChannels();
await ui.setChannels(guild, channels);
if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
if (ui.activeChannel === null) {
// click on the first channel in the list if no channel is active yet
let element = q.$_('#channel-list .channel');
@ -54,45 +54,45 @@ export default class Actions {
}
},
errorIndicatorAddFunc: async (errorIndicatorElement) => {
await ui.setChannelsErrorIndicator(server, errorIndicatorElement);
await ui.setChannelsErrorIndicator(guild, errorIndicatorElement);
},
errorContainer: q.$('#channel-list'),
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, {
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;
let messages = await server.grabRecentMessages(channel.id, Globals.MESSAGES_PER_REQUEST);
await ui.setMessages(server, channel, messages, { atTop: messages.length < Globals.MESSAGES_PER_REQUEST, atBottom: true });
let messages = await guild.fetchMessagesRecent(channel.id, Globals.MESSAGES_PER_REQUEST);
await ui.setMessages(guild, channel, messages, { atTop: messages.length < Globals.MESSAGES_PER_REQUEST, atBottom: true });
},
errorIndicatorAddFunc: async (errorIndicatorElement) => {
await ui.setMessagesErrorIndicator(server, channel, errorIndicatorElement);
await ui.setMessagesErrorIndicator(guild, channel, errorIndicatorElement);
},
errorContainer: q.$('#channel-feed'),
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, {
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;
let topPair = ui.getTopMessagePair();
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) {
await ui.addMessagesBefore(server, channel, messages, topPair.message);
await ui.addMessagesBefore(guild, channel, messages, topPair.message);
} else {
ui.messagesAtTop = true;
}
},
errorIndicatorAddFunc: async (errorIndicatorElement) => {
await ui.addMessagesErrorIndicatorBefore(server, channel, errorIndicatorElement);
await ui.addMessagesErrorIndicatorBefore(guild, channel, errorIndicatorElement);
},
errorContainer: q.$('#channel-feed'),
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, {
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;
let bottomPair = ui.getBottomMessagePair();
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) {
await ui.addMessagesAfter(server, channel, messages, bottomPair.message);
await ui.addMessagesAfter(guild, channel, messages, bottomPair.message);
} else {
ui.messagesAtBottom = true;
}
},
errorIndicatorAddFunc: async (errorIndicatorElement) => {
await ui.addMessagesErrorIndicatorAfter(server, channel, errorIndicatorElement);
await ui.addMessagesErrorIndicatorAfter(guild, channel, errorIndicatorElement);
},
errorContainer: q.$('#channel-feed'),
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) => {
let resolved = false;
try {
let origTrustedStatus = this.trustedStatus;
let origTrustedPromise = this.trustedPromise;
let primaryPromise = this.primaryFunc();
if (this.trustedStatus === 'none') {
@ -154,6 +157,9 @@ export class AutoVerifier<T> {
if (primaryResult) {
resolve(primaryResult);
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)

View File

@ -5,73 +5,87 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import { EventEmitter } from "stream";
import ClientController from "./client-controller";
import * as socketio from 'socket.io-client';
import * as crypto from 'crypto';
import DBCache from './db-cache';
import { ServerConfig } from './data-types';
import { Changes, Channel, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, ServerConfig, SocketConfig, Token } from './data-types';
import { IAddServerData } from './elements/overlay-add-server';
import { DefaultEventMap, EventEmitter } from 'tsee';
import 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 {
public servers: ClientController[] = [];
export default class Controller extends EventEmitter<{
'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(
private dbCache: DBCache
private messageRAMCache: MessageRAMCache,
private resourceRAMCache: ResourceRAMCache,
private personalDB: PersonalDB
) {
super();
}
async _connectFromConfig(serverConfig: ServerConfig): Promise<ClientController> {
LOG.debug(`connecting to server#${serverConfig.guildId} at ${serverConfig.url}`);
async _connectFromConfig(guildMetadata: GuildMetadataWithIds, socketConfig: SocketConfig): Promise<CombinedGuild> {
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
let serverEvents = [
'connected',
'disconnected',
'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);
// Forward guild events through this event emitter
for (let eventName of guild.eventNames()) {
guild.on(eventName as any, (...args: any) => {
this.emit(eventName as any, guild, ...args);
});
}
return server;
return guild;
}
async init(): Promise<void> {
this.servers = [];
this.guilds = [];
let serverConfigs = await this.dbCache.getServerConfigs();
// TODO: HTML prompt if no server configs
if (serverConfigs.length == 0) {
LOG.warn('no server configs found in client-side db');
// TODO: connect concurrently
for (let guildMeta of await this.personalDB.fetchGuilds()) {
for (let guildSocket of await this.personalDB.fetchGuildSockets(guildMeta.id)) {
await this._connectFromConfig(guildMeta, guildSocket);
}
}
for (let serverConfig of serverConfigs) {
await this._connectFromConfig(serverConfig);
if (this.guilds.length === 0) {
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;
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) => {
let clientPublicKeyDerBuff = publicKey.export({ type: 'spki', format: 'der' });
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) {
reject(new Error(errStr));
} else {
try {
let serverConfig: ServerConfig | null = null;
await this.dbCache.queueTransaction(async () => {
let publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' });
let privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
let identityId = await this.dbCache.addIdentity(publicKeyPem, privateKeyPem);
let guildId = await this.dbCache.addServer(url, cert, name);
await this.dbCache.addServerIdentity(guildId, identityId);
serverConfig = await this.dbCache.getServerConfig(guildId, identityId);
const member = Member.fromDBData(dataMember);
const meta = GuildMetadata.fromDBData(dataMetadata);
let guildMeta: GuildMetadataWithIds | null = null;
let socketConfig: SocketConfig | null = null;
await this.personalDB.queueTransaction(async () => {
let guildId = await this.personalDB.addGuild(meta.name, meta.iconResourceId, member.id);
let guildSocketId = await this.personalDB.addGuildSocket(guildId, url, cert, publicKey, privateKey);
guildMeta = await this.personalDB.fetchGuild(guildId);
socketConfig = await this.personalDB.fetchGuildSocket(guildId, guildSocketId);
});
if (serverConfig == null) {
throw new Error('unable to get server config');
if (!guildMeta || !socketConfig) {
throw new Error('unable to properly add guild');
}
let server = await this._connectFromConfig(serverConfig);
let server = await this._connectFromConfig(guildMeta, socketConfig);
resolve(server);
} catch (e) {
reject(e);
@ -159,8 +174,11 @@ export default class Controller extends EventEmitter {
}
}
async removeServer(server: ClientController): Promise<void> {
await this.dbCache.removeServer(server.id);
this.servers = this.servers.filter(s => s != server);
async removeServer(guild: CombinedGuild): Promise<void> {
await this.personalDB.queueTransaction(async () => {
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 {
private constructor(
public readonly id: number,
public readonly id: number | null,
public readonly guildId: number,
public readonly url: string,
public readonly cert: string,
public readonly publicKey: crypto.KeyObject,
@ -225,6 +226,7 @@ export class SocketConfig {
static fromDBData(data: any): SocketConfig {
return new SocketConfig(
data.id,
data.guild_id,
data.url,
data.cert,
crypto.createPublicKey(data.public_key),

View File

@ -7,7 +7,7 @@ import UI from '../ui';
import Actions from '../actions';
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: [
// Scraped directly from discord (#)
{ 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 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 content: any[] = [
{ class: 'item personalize', content: [

View File

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

View File

@ -16,7 +16,7 @@ import createCreateInviteTokenOverlay from './overlay-create-invite-token';
import createCreateChannelOverlay from './overlay-create-channel';
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) {
LOG.warn('no active connection when creating server title context menu');
return q.create({}) as HTMLElement;

View File

@ -10,7 +10,7 @@ import Q from '../q-module';
import UI from '../ui';
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, {
class: 'server-context', content: [
{ 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 {
q.$('#connection').addEventListener('click', () => {
if (ui.activeServer === null) return;
if (!ui.activeServer.isVerified) return;
if (ui.activeGuild === null) 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);
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 clientHeight = q.$('#channel-feed-content-wrapper').clientHeight;
if (ui.activeServer === null) return;
if (ui.activeGuild === null) return;
if (ui.activeChannel === null) return;
if (!loadingBefore && !ui.messagesAtTop && scrollTop < 600) { // Approaching the unloaded top of the page
// Fetch more messages to add above
loadingBefore = true;
await Actions.fetchAndUpdateMessagesBefore(q, ui, ui.activeServer, ui.activeChannel);
await Actions.fetchAndUpdateMessagesBefore(q, ui, ui.activeGuild, ui.activeChannel);
loadingBefore = false;
} else if (!loadingAfter && !ui.messagesAtBottom && scrollHeight - clientHeight - scrollTop < 600) { // Approaching the unloaded bottom of the page
// Fetch more messages to add below
loadingAfter = true;
await Actions.fetchAndUpdateMessagesAfter(q, ui, ui.activeServer, ui.activeChannel);
await Actions.fetchAndUpdateMessagesAfter(q, ui, ui.activeGuild, ui.activeChannel);
loadingAfter = false;
}
}

View File

@ -6,14 +6,14 @@ import ElementsUtil from './require/elements-util';
export default function bindAddServerTitleEvents(document: Document, q: Q, ui: UI) {
q.$('#server-name-container').addEventListener('click', () => {
if (ui.activeConnection === null) return;
if (ui.activeServer === null) return;
if (!ui.activeServer.isVerified) return;
if (ui.activeGuild === null) return;
if (!ui.activeGuild.isVerified) return;
if (
!ui.activeConnection.privileges.includes('modify_profile') &&
!ui.activeConnection.privileges.includes('modify_members')
) return;
let contextMenu = createServerTitleContextMenu(document, q, ui, ui.activeServer);
let contextMenu = createServerTitleContextMenu(document, q, ui, ui.activeGuild);
document.body.appendChild(contextMenu);
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;
async function sendCurrentTextMessage() {
if (sendingMessage) return;
if (ui.activeServer === null) return;
if (ui.activeGuild === 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'
@ -27,7 +27,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v
sendingMessage = true;
let server = ui.activeServer as ClientController;
let server = ui.activeGuild as ClientController;
let channel = ui.activeChannel as Channel;
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
let selectingResources = false;
q.$('#resource-input-button').addEventListener('click', async () => {
if (ui.activeServer === null) return;
if (ui.activeGuild === null) return;
if (ui.activeChannel === null) return;
if (selectingResources) {
@ -106,7 +106,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v
});
// TODO: multiple files do consecutive overlays?
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);
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
window.addEventListener('paste', (e) => {
if (ui.activeServer === null) return;
if (ui.activeGuild === null) return;
if (ui.activeChannel === null) return;
let fileTransferItem: DataTransferItem | null = null;
@ -127,7 +127,7 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v
}
}
if (fileTransferItem) {
let element = createUploadOverlayFromDataTransferItem(document, ui.activeServer, ui.activeChannel, fileTransferItem);
let element = createUploadOverlayFromDataTransferItem(document, ui.activeGuild, ui.activeChannel, fileTransferItem);
document.body.appendChild(element);
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?
document.addEventListener('dragenter', () => {
if (ui.activeServer === null) return;
if (ui.activeGuild === null) return;
if (ui.activeChannel === null) 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;
document.body.appendChild(element);
});

View File

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

View File

@ -8,7 +8,7 @@ import createResourceMessageContinued from './msg-res-cont';
import createTextMessage from './msg-txt';
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;
if (message.hasResource()) {
if (message.isImageResource()) {

View File

@ -14,7 +14,7 @@ import Q from '../q-module';
import createImageOverlay from './overlay-image';
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) {
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 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) {
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) {
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) {
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';
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: [
{ class: 'timestamp', content: moment(message.sent).format('HH:mm') },
{ class: 'right', content: [

View File

@ -6,7 +6,7 @@ import { Message, Member, IDummyTextMessage } from '../data-types';
import ClientController from '../client-controller';
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: {
roleColor: string | null,
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');
let newServer: ClientController | null = null;
let newguild: CombinedGuild | null = null;
if (addServerData == null) {
q.$$$(element, '.error').innerText = 'Very bad server file';
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
q.$$$(element, '.submit').innerText = 'Registering...';
try {
newServer = await controller.addNewServer(addServerData, displayName, avatarBuff);
newServer = await controller.addNewGuild(addServerData, displayName, avatarBuff);
} catch (e) {
LOG.warn('error adding new server: ' + e.message, e); // explicitly not printing stack trace here
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 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

View File

@ -2,7 +2,7 @@ import ClientController from "../client-controller";
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: [
{ class: 'role-select category-select', content: [
{ class: 'label', content: 'Select Starting Roles' },

View File

@ -12,7 +12,7 @@ import ClientController from '../client-controller';
import Q from '../q-module';
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: [
{ tag: 'img', src: './img/loading.svg', alt: resourceName, title: resourceName },
{ class: 'download', content: [

View File

@ -11,7 +11,7 @@ import BaseElements from './require/base-elements.js';
import ElementsUtil from './require/elements-util.js';
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
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 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, {
class: 'content submit-dialog personalize', content: [
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 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, {
class: 'content submit-dialog server-settings', content: [
{ class: 'server preview', content: [

View File

@ -13,7 +13,7 @@ import ClientController from '../client-controller';
import { Member } from '../data-types';
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, {
class: 'content token-log', content: [
{ 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 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();
if (file === null) throw new ShouldNeverHappenError('no file in the data transfer item');
let element = BaseElements.createUploadOverlay(document, {

View File

@ -9,7 +9,7 @@ import ClientController from '../client-controller';
import Q from '../q-module';
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: [
// TODO: icon?
{ 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 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 element = BaseElements.createUploadOverlay(document, {
server: server, channel: channel, resourceName: resourceName,

View File

@ -18,7 +18,7 @@ interface HTMLElementWithRemoveSelf extends HTMLElement {
}
interface CreateUploadOverlayProps {
server: ClientController;
guild: CombinedGuild;
channel: Channel;
resourceName: string;
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) {
LOG.warn('no server resource specified, showing error instead', new Error());
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 { 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 fetchMembersVerifier: AutoVerifier<Member[]>;

View File

@ -7,7 +7,7 @@ import * as socketio from 'socket.io-client';
import PersonalDBGuild from './guild-personal-db';
import RAMGuild from './guild-ram';
import SocketGuild from './guild-socket';
import { Changes, Channel, GuildMetadata, 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 PersonalDB from "./personal-db";
@ -27,7 +27,7 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
private readonly fetchable: AsyncGuaranteedFetchable;
constructor(
private readonly id: number,
public readonly id: number,
private readonly memberId: string,
socket: socketio.Socket,
socketVerifier: SocketVerifier,
@ -149,7 +149,7 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
}
static async create(
personalGuildId: number,
guildMetadata: GuildMetadataWithIds,
socketConfig: SocketConfig,
messageRAMCache: MessageRAMCache,
resourceRAMCache: ResourceRAMCache,
@ -161,8 +161,11 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
});
let socketVerifier = new SocketVerifier(socket, socketConfig.publicKey, socketConfig.privateKey);
let memberId = await socketVerifier.verify();
if (guildMetadata.memberId && memberId !== guildMetadata.memberId) {
throw new Error('Verified member differs from original member');
}
return new CombinedGuild(
personalGuildId,
guildMetadata.id,
memberId,
socket,
socketVerifier,
@ -172,25 +175,35 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
);
}
private unverify() {
private unverify(): void {
for (let pairVerifier of this.pairVerifiers) {
pairVerifier.unverify();
}
}
async ensureRAMMembers() {
private async ensureRAMMembers(): Promise<void> {
if (this.ramGuild.getMembers().size === 0) {
await this.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) {
await this.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)
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
private trimOldChannelMessagesIfNeeded(guildId: string, channelId: string) {
private trimOldChannelMessagesIfNeeded(guildId: number, channelId: string) {
let id = `g#${guildId}/c#${channelId}`;
let value = this.data.get(id);
if (!value) return;

View File

@ -1,3 +1,5 @@
import * as crypto from 'crypto';
import ConcurrentQueue from "../../concurrent-queue/concurrent-queue";
import * as sqlite from 'sqlite';
@ -150,10 +152,10 @@ export default class PersonalDB {
// 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(
`INSERT INTO guilds (name, icon_resource_id) VALUES (:name, :icon_resource_id)`,
{ ':name': name, ':icon_resource_id': icon?.id ?? null }
`INSERT INTO guilds (name, icon_resource_id, member_id) VALUES (:name, :icon_resource_id, :member_id)`,
{ ':name': name, ':icon_resource_id': iconResourceId, ':member_id': memberId }
);
if (result.changes !== 1) throw new Error('unable to add guild');
if (result.lastID === undefined) throw new Error('unable to get guild last id');
@ -200,24 +202,33 @@ export default class PersonalDB {
// Guild Sockets
async addGuildSocket(guildId: number, socketConfig: SocketConfig): Promise<void> {
let publicKeyPem = socketConfig.publicKey.export({ type: 'spki', format: 'pem' });
let privateKeyPem = socketConfig.privateKey.export({ type: 'pkcs8', format: 'pem' });
let result = await this.db.all(
async addGuildSocket(guildId: number, url: string, cert: string, publicKey: crypto.KeyObject, privateKey: crypto.KeyObject): Promise<number> {
let publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' });
let privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
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)`,
{ ':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> {
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 }
);
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[]> {
let result = await this.db.all(
`SELECT * FROM guild_sockets WHERE guild_id=:guild_id`,
@ -226,6 +237,15 @@ export default class PersonalDB {
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
async addMembers(guildId: number, members: Member[]): Promise<void> {
@ -285,6 +305,10 @@ export default class PersonalDB {
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
async addChannels(guildId: number, channels: Channel[]) {

View File

@ -74,8 +74,8 @@ window.addEventListener('DOMContentLoaded', () => {
}
// Receive Current Channel Messages
controller.on('new-message', async (server: ClientController, message: Message) => {
if (ui.activeServer === null || ui.activeServer.id !== server.id) return;
controller.on('new-message', async (guild: CombinedGuild, message: Message) => {
if (ui.activeGuild === null || ui.activeGuild.id !== server.id) return;
if (ui.activeChannel === null || ui.activeChannel.id !== message.channel.id) return;
if (ui.messagesAtBottom) {
// add the message to the bottom of the message feed
@ -88,7 +88,7 @@ window.addEventListener('DOMContentLoaded', () => {
}
});
controller.on('verified', async (server: ClientController) => {
controller.on('verified', async (guild: CombinedGuild) => {
(async () => { // update connection info
await Actions.fetchAndUpdateConnection(ui, server);
})();
@ -120,7 +120,7 @@ window.addEventListener('DOMContentLoaded', () => {
})();
});
controller.on('disconnected', (server: ClientController) => {
controller.on('disconnected', (guild: CombinedGuild) => {
(async () => {
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`)
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');
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);
await ui.updateMembers(server, data);
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');
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');
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');
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');
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('deleted messages:', { messages: deletedMessages.map(message => message.text) });
// messages were deleted but the cache still had them
await ui.deleteMessages(server, channel, messages);
});
controller.on('updated-messages', async (server: ClientController, channel: Channel, data: { oldMessage: Message, newMessage: Message }[]) => {
controller.on('updated-messages', async (guild: CombinedGuild, channel: Channel, data: { oldMessage: Message, newMessage: Message }[]) => {
LOG.debug(data.length + ' updated messages');
// messages were updated on the server-side
await ui.updateMessages(server, channel, data);
});
controller.on('added-messages', async (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('added messages', { messages: Array.from(addedAfter.values()).map(message => message.text) });
// 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 Util from './util';
// Automatically re-verifies the socket when connected
export default class SocketVerifier {
public isVerified = false;
private memberId: string | null = null;

View File

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