cordis/client/webapp/ui.ts
2021-10-30 12:26:41 -05:00

712 lines
35 KiB
TypeScript

import * as electronRemote from '@electron/remote';
const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../logger/logger';
const LOG = new Logger(__filename, electronConsole);
import ConcurrentQueue from '../../concurrent-queue/concurrent-queue';
import Elements from './elements';
import ElementsUtil from './elements/require/elements-util';
import { $, $$, $$$, $$$$, $_ } from './elements/require/q-module';
import Globals from './globals';
import Util from './util';
import ClientController from './client-controller';
import { Message, Member, Channel, ConnectionInfo } from './data-types';
class ShouldNeverHappenError extends Error {
constructor(...args: any[]) {
super(...args);
this.name = 'ShouldNeverHappenError';
}
}
interface SetMessageProps {
atTop: boolean;
atBottom: boolean;
}
export default class UI {
public activeServer: ClientController | { id: null } = { id: null };
public activeChannel: Channel | { id: null } = { id: null };
public activeConnection: ConnectionInfo | null = null;
public messagesAtTop = false;
public messagesAtBottom = false;
public messagePairsServer: ClientController | { id: null } = { id: null };
public messagePairsChannel: Channel | { id: null } = { id: null };
public messagePairs = new Map<string | null, { message: Message, element: HTMLElement }>(); // messageId -> { message: Message, element: HTMLElement }
private document: Document;
constructor(document: Document) {
this.document = document;
}
public isActiveServer(server: ClientController): boolean {
return server.id === this.activeServer.id;
}
public hasActiveServer(): boolean {
return this.activeServer.id != null;
}
public isActiveChannel(channel: Channel): boolean {
return channel.id === this.activeChannel.id;
}
public hasActiveChannel(): boolean {
return this.activeChannel.id != null;
}
public hasActiveConnection(): boolean {
return this.activeConnection != null;
}
public isMessagePairsServer(server: ClientController): boolean {
return server.id === this.messagePairsServer.id;
}
public isMessagePairsChannel(channel: Channel): boolean {
return channel.id === this.messagePairsChannel.id;
}
// 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 _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.isActiveServer(server)) return;
await lock.push(async () => {
if (!this.isActiveServer(server)) return;
await task();
});
}
private async _lockWithServerChannel(server: ClientController, channel: Channel, task: (() => Promise<void> | void), lock: ConcurrentQueue<void>): Promise<void> {
if (!this.isActiveServer(server)) return;
if (!this.isActiveChannel(channel)) return;
await lock.push(async () => {
if (!this.isActiveServer(server)) return;
if (!this.isActiveChannel(channel)) return;
await task();
});
}
public async lockServerName(server: ClientController, task: (() => Promise<void> | void)): Promise<void> {
await this._lockWithServer(server, task, this._serverNameLock);
}
public async lockConnection(server: ClientController, task: (() => Promise<void> | void)): Promise<void> {
await this._lockWithServer(server, task, this._connectionLock);
}
public async lockChannels(server: ClientController, task: (() => Promise<void> | void)): Promise<void> {
await this._lockWithServer(server, task, this._channelsLock);
}
public async lockMembers(server: ClientController, task: (() => Promise<void> | void)): Promise<void> {
await this._lockWithServer(server, task, this._membersLock);
}
public async lockMessages(server: ClientController, channel: Channel, task: (() => Promise<void> | void)): Promise<void> {
await this._lockWithServerChannel(server, channel, task, this._messagesLock);
}
public setActiveServer(server: ClientController): void {
$.setDocument(this.document);
let prev = $('#server-list .server[meta-id="' + this.activeServer.id + '"]');
if (prev) {
prev.classList.remove('active');
}
let next = $('#server-list .server[meta-id="' + server.id + '"]');
next.classList.add('active');
$('#server').setAttribute('meta-id', server.id);
this.activeServer = server;
}
public async setActiveChannel(server: ClientController, channel: Channel): Promise<void> {
await this.lockChannels(server, () => {
$.setDocument(this.document);
// Channel List Highlight
let prev = $('#channel-list .channel[meta-id="' + this.activeChannel.id + '"]');
if (prev) {
prev.classList.remove('active');
}
let next = $('#channel-list .channel[meta-id="' + channel.id + '"]');
next.classList.add('active');
// Channel Name + Flavor Text Header + Text Input Placeholder
$('#channel-name').innerText = channel.name;
$('#channel-flavor-text').innerText = channel.flavorText || '';
$('#channel-flavor-divider').style.visibility = channel.flavorText ? 'visible' : 'hidden';
$('#text-input').setAttribute('placeholder', 'Message #' + channel.name);
this.activeChannel = channel;
});
}
public async setActiveConnection(server: ClientController, connection: ConnectionInfo): Promise<void> {
await this.lockConnection(server, () => {
$.setDocument(this.document);
this.activeConnection = connection;
$('#connection').className = 'member ' + connection.status;
$('#member-name').innerText = connection.displayName;
$('#member-status-text').innerText = connection.status;
$('#server').className = '';
for (let privilege of connection.privileges) {
$('#server').classList.add('privilege-' + privilege);
}
if (connection.displayName) {
(async () => {
// Update avatar
if (!this.isActiveServer(server)) return;
let src = await ElementsUtil.getImageBufferFromResourceFailSoftly(server, connection.displayName);
if (!this.isActiveServer(server)) return;
($('#member-avatar') as HTMLImageElement).src = src;
})();
} else {
($('#member-avatar') as HTMLImageElement).src = './img/loading.svg';
}
});
}
public async setServers(servers: ClientController[]): Promise<void> {
await this._serversLock.push(() => {
$.setDocument(this.document);
$.clearChildren($('#server-list'));
for (let server of servers) {
let element = Elements.createServerListServer(server);
$('#server-list').appendChild(element);
}
});
}
public async addServer(server: ClientController): Promise<HTMLElement> {
let element: HTMLElement | null = null;
await this._serversLock.push(() => {
$.setDocument(this.document);
element = Elements.createServerListServer(server) as HTMLElement;
$('#server-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(() => {
$.setDocument(this.document);
let element = $_('#server-list .server[meta-id="' + server.id + '"]');
element?.parentElement?.removeChild(element);
});
}
public async updateServerIcon(server: ClientController, iconBuff: Buffer): Promise<void> {
await this._serversLock.push(async () => {
$.setDocument(this.document);
let iconElement = $('#server-list .server[meta-id="' + server.id + '"] img') as HTMLImageElement;
iconElement.src = await ElementsUtil.getImageBufferSrc(iconBuff);
});
}
public async updateServerName(server: ClientController, name: string): Promise<void>{
await this.lockServerName(server, () => {
$.setDocument(this.document);
$('#server-name').innerText = name;
let baseElement = $('#server-list .server[meta-id="' + server.id + '"]');
baseElement.setAttribute('meta-name', name);
});
}
private _updatePosition(element: HTMLElement, serverCacheMap: Map<string | null, any>, getDirection: ((prevData: any, data: any) => number)) {
$.setDocument(this.document);
let data = serverCacheMap.get(element.getAttribute('meta-id'));
$.assert(data != null, 'unable to get data from server cache map');
// TODO: do-while may be a bit cleaner?
let prev = $.previousElement(element);
while (prev != null) {
let prevData = serverCacheMap.get(prev.getAttribute('meta-id'));
if (getDirection(prevData, data) > 0) { // this element comes before previous element
prev.parentElement?.insertBefore(element, prev);
} else {
break;
}
prev = $.previousElement(element);
}
let next = $.nextElement(element);
while (next != null) {
let nextData = serverCacheMap.get(next.getAttribute('meta-id'));
if (getDirection(data, nextData) > 0) { // this element comes after next element
next.parentElement?.insertBefore(next, element);
} else {
break;
}
next = $.nextElement(element);
}
}
public updateChannelPosition(server: ClientController, channelElement: HTMLElement): void {
this._updatePosition(channelElement, server.channels, (a, b) => {
return a.index - b.index;
});
}
public async addChannels(server: ClientController, channels: Channel[], options?: { clear: boolean }): Promise<void> {
await this.lockChannels(server, () => {
$.setDocument(this.document);
if (options?.clear) {
$.clearChildren($('#channel-list'));
}
for (let channel of channels) {
let element = Elements.createChannel(server, channel);
$('#channel-list').appendChild(element);
this.updateChannelPosition(server, element);
}
});
}
public async deleteChannels(server: ClientController, channels: Channel[]): Promise<void> {
await this.lockChannels(server, () => {
$.setDocument(this.document);
for (let channel of channels) {
let element = $_('#channel-list .channel[meta-id="' + channel.id + '"]');
element?.parentElement?.removeChild(element);
if (this.isActiveChannel(channel)) {
this.activeChannel = { id: null };
}
}
});
}
public async updateChannels(server: ClientController, data: { oldDataPoint: Channel, newDataPoint: Channel }[]): Promise<void> {
await this.lockChannels(server, () => {
$.setDocument(this.document);
for (const { oldDataPoint, newDataPoint } of data) {
let newChannel = newDataPoint;
let oldElement = $('#channel-list .channel[meta-id="' + newChannel.id + '"]');
let newElement = Elements.createChannel(server, newChannel);
oldElement.parentElement?.replaceChild(newElement, oldElement);
this.updateChannelPosition(server, newElement);
if (this.isActiveChannel(newChannel)) {
newElement.classList.add('active');
// See also setActiveChannel
$('#channel-name').innerText = newChannel.name;
$('#channel-flavor-text').innerText = newChannel.flavorText ?? '';
$('#channel-flavor-divider').style.visibility = newChannel.flavorText ? 'visible' : 'hidden';
$('#text-input').setAttribute('placeholder', 'Message #' + newChannel.name);
}
}
});
}
public async setChannels(server: ClientController, channels: Channel[]): Promise<void> {
$.setDocument(this.document);
// 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
// properly refreshed and the active channel is not improperly set.
let oldMatchingElement = $_('#channel-list .channel[meta-id="' + this.activeChannel.id + '"][meta-server-id="' + this.activeServer.id + '"]');
await this.addChannels(server, channels, { clear: true });
if (this.isActiveServer(server)) {
let newActiveElement = $_('#channel-list .channel[meta-id="' + this.activeChannel.id + '"][meta-server-id="' + this.activeServer.id + '"]');
if (newActiveElement && oldMatchingElement) {
let channel = channels.find(channel => channel.id === this.activeChannel.id);
if (channel === undefined) throw new ShouldNeverHappenError('current channel does not exist in channels list')
this.setActiveChannel(server, channel);
} else {
this.activeChannel = { id: null }; // the active channel was removed
}
}
}
public async setChannelsErrorIndicator(server: ClientController, errorIndicatorElement: HTMLElement): Promise<void> {
await this.lockChannels(server, () => {
$.setDocument(this.document);
$.clearChildren($('#channel-list'));
$('#channel-list').appendChild(errorIndicatorElement);
});
}
public updateMemberPosition(server: ClientController, memberElement: HTMLElement): void {
let statusOrder = {
'online': 0,
'away': 1,
'busy': 2,
'offline': 3,
'invisible': 3, // this would only be shown in the case of the current member.
'unknown': 100,
};
this._updatePosition(memberElement, server.members, (a, b) => {
let onlineCmp = (a.status == 'offline' ? 1 : 0) - (b.status == 'offline' ? 1 : 0);
if (onlineCmp != 0) return onlineCmp;
let rolePriorityCmp = (a.role_priority == null ? 100 : a.role_priority) - (b.role_priority == null ? 100 : b.role_priority);
if (rolePriorityCmp != 0) return rolePriorityCmp;
let statusCmp = statusOrder[a.status] - statusOrder[b.status];
if (statusCmp != 0) return statusCmp;
let nameCmp = a.display_name.localeCompare(b.display_name);
return nameCmp;
});
}
public async addMembers(server: ClientController, members: Member[], options?: { clear: boolean }): Promise<void> {
await this.lockMembers(server, () => {
$.setDocument(this.document);
if (options?.clear) {
$.clearChildren($('#server-members'));
}
for (let member of members) {
let element = Elements.createMember(server, member);
$('#server-members').appendChild(element);
this.updateMemberPosition(server, element);
}
});
}
public async deleteMembers(server: ClientController, members: Member[]): Promise<void> {
await this.lockMembers(server, () => {
$.setDocument(this.document);
for (let member of members) {
let element = $_('#server-members .member[meta-id="' + member.id + '"]');
element?.parentElement?.removeChild(element);
}
});
}
public async updateMembers(server: ClientController, data: { oldDataPoint: Member, newDataPoint: Member }[]): Promise<void> {
await this.lockMembers(server, () => {
$.setDocument(this.document);
for (const { oldDataPoint, newDataPoint } of data) {
let newMember = newDataPoint;
let oldElement = $_('#server-members .member[meta-id="' + newMember.id + '"]');
if (oldElement) {
let newElement = Elements.createMember(server, newMember);
oldElement.parentElement?.replaceChild(newElement, oldElement);
this.updateMemberPosition(server, newElement);
}
}
});
if (!this.hasActiveChannel()) return;
await this.lockMessages(server, this.activeChannel as Channel, () => {
$.setDocument(this.document);
for (const { oldDataPoint, newDataPoint } of data) {
let newMember = newDataPoint;
let newStyle = newMember.roleColor ? 'color: ' + newMember.roleColor : null;
let newName = newMember.displayName;
// the extra query selectors may be overkill
for (let messageElement of $$(`.message[meta-member-id="${newMember.id}"]`)) {
let nameElement = $$$(messageElement, '.member-name');
if (nameElement) { // continued messages will still show up but need to be skipped
if (newStyle) nameElement.setAttribute('style', newStyle);
nameElement.innerText = newName;
}
}
}
});
}
public async setMembers(server: ClientController, members: Member[]): Promise<void> {
await this.addMembers(server, members, { clear: true });
}
public async setMembersErrorIndicator(server: ClientController, errorIndicatorElement: HTMLElement): Promise<void> {
await this.lockMembers(server, () => {
$.setDocument(this.document);
$.clearChildren($('#server-members'));
$('#server-members').appendChild(errorIndicatorElement);
});
}
public getTopMessagePair(): { message: Message, element: HTMLElement } | null {
$.setDocument(this.document);
let element = $$('#channel-feed .message')[0];
return element && this.messagePairs.get(element.getAttribute('meta-id')) || null;
}
public getBottomMessagePair(): { message: Message, element: HTMLElement } | null {
$.setDocument(this.document);
let messageElements = $$('#channel-feed .message');
let element = messageElements[messageElements.length - 1];
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, () => {
$.setDocument(this.document);
if (prevTopMessage && this.getTopMessagePair()?.message.id !== prevTopMessage.id) return;
this.messagesAtTop = false;
// There are a maximum of MAX_CURRENT_MESSAGES messages in the channel at a time
// Remove messages at the bottom to make space for new messages
if (this.messagePairs.size + messages.length > Globals.MAX_CURRENT_MESSAGES) {
let currentMessageElements = $$('#channel-feed .message');
$.assert(this.messagePairs.size == currentMessageElements.length, `message lengths disjointed, ${this.messagePairs.size} != ${currentMessageElements.length}`); // sanity check
let toRemove = currentMessageElements.slice(-(this.messagePairs.size + messages.length - Globals.MAX_CURRENT_MESSAGES));
for (let element of toRemove) {
let id = element.getAttribute('meta-id');
this.messagePairs.delete(id);
element.parentElement?.removeChild(element);
}
this.messagesAtBottom = false;
}
// Relies on error indicators being in top-to-bottom order in the list
Util.removeErrorIndicators($('#channel-feed'), [ 'recent' ]);
Util.removeErrorIndicators($('#channel-feed'), [ 'before' ]);
// Keep track of the top messages before we add new messages
let prevTopPair = this.getTopMessagePair();
// Add the messages to the channel feed
// Using reverse order so that resources are loaded from bottom to top
// and the client starts at the bottom
for (let i = messages.length - 1; i >= 0; --i) {
let message = messages[i];
let priorMessage = messages[i - 1] || null;
let element = Elements.createMessage(server, message, priorMessage);
this.messagePairs.set(message.id, { message: message, element: element });
$('#channel-feed').prepend(element);
}
if (messages.length > 0 && prevTopPair) {
// Update the previous top message since it may have changed format
let newPrevTopElement = Elements.createMessage(server, 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, messages: Message[], prevBottomMessage: Message | null): Promise<void> {
await this.lockMessages(server, channel, () => {
$.setDocument(this.document);
if (prevBottomMessage && this.getBottomMessagePair()?.message.id !== prevBottomMessage.id) return;
this.messagesAtBottom = false;
// There are a maximum of MAX_CURRENT_MESSAGES messages in the channel at a time
// Remove messages at the top to make space for new messages
if (this.messagePairs.size + messages.length > Globals.MAX_CURRENT_MESSAGES) {
let currentMessageElements = $$('#channel-feed .message');
$.assert(this.messagePairs.size == currentMessageElements.length, 'message lengths disjointed'); // sanity check
let toRemove = currentMessageElements.slice(0, this.messagePairs.size + messages.length - Globals.MAX_CURRENT_MESSAGES);
for (let element of toRemove) {
let id = element.getAttribute('meta-id');
this.messagePairs.delete(id);
element.parentElement?.removeChild(element);
}
this.messagesAtTop = false;
}
Util.removeErrorIndicators($('#channel-feed'), [ 'recent' ]);
Util.removeErrorIndicators($('#channel-feed'), [ 'after' ]);
// Get the bottom message to use as the prior message to the first new message
let prevBottomPair = this.getBottomMessagePair();
// Add new messages to the bottom of the channel feed
// Using forward-order so that resources are loaded from oldest messages to newest messages
// since we are expecting the user to scroll down (to newer messages)
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 = Elements.createMessage(server, message, priorMessage);
this.messagePairs.set(message.id, { message: message, element: element });
$('#channel-feed').appendChild(element);
}
});
}
// 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, () => {
$.setDocument(this.document);
if (!(messages.length > 0 && topElement != null && bottomElement != null && bottomElement == $.nextElement(topElement))) {
LOG.error('invalid messages between', { messages, top: topElement.innerText, bottom: bottomElement.innerText, afterTop: $.nextElement(topElement)?.innerText });
throw new Error('invalid messages between');
}
if (this.messagePairs.size + messages.length > Globals.MAX_CURRENT_MESSAGES) {
let currentMessageElements = $$('#channel-feed .message');
$.assert(this.messagePairs.size == currentMessageElements.length, 'message lengths disjointed'); // sanity check
let totalToRemove = this.messagePairs.size + messages.length - Globals.MAX_CURRENT_MESSAGES
let toRemove: HTMLElement[] = [];
// figure out if the elements are getting added above or below the scroll box.
// NOT TESTED YET: elements added within the scroll box are assumed to make the box resize downward
let above = bottomElement.offsetTop > $('#channel-feed-wrapper').scrollTop;
if (above) {
// remove elements at the top first
for (let messageElement of currentMessageElements) {
if (toRemove.length == totalToRemove) {
break;
}
if (messageElement.getAttribute('meta-id') == topElement.getAttribute('meta-id')) {
break;
}
toRemove.push(messageElement);
}
// remove elements at the bottom if still needed
for (let messageElement of currentMessageElements.reverse()) {
if (toRemove.length == totalToRemove) {
break;
}
if (messageElement.getAttribute('meta-id') == bottomElement.getAttribute('meta-id')) {
break;
}
toRemove.push(messageElement);
}
} else {
// remove elements at the bottom first
for (let messageElement of currentMessageElements.reverse()) {
if (toRemove.length == totalToRemove) {
break;
}
if (messageElement.getAttribute('meta-id') == topElement.getAttribute('meta-id')) {
break;
}
toRemove.push(messageElement);
}
// remove elements at the top if still needed
for (let messageElement of currentMessageElements.reverse()) {
if (toRemove.length == totalToRemove) {
break;
}
if (messageElement.getAttribute('meta-id') == bottomElement.getAttribute('meta-id')) {
break;
}
toRemove.push(messageElement);
}
}
for (let element of toRemove) {
let id = element.getAttribute('meta-id');
this.messagePairs.delete(id);
element.parentElement?.removeChild(element);
}
}
let topMessage = this.messagePairs.get(topElement.getAttribute('meta-id'))?.message;
for (let i = 0; i < messages.length; ++i) {
let message = messages[i];
let priorMessage = messages[i - 1] || topMessage;
let element = Elements.createMessage(server, message, priorMessage);
this.messagePairs.set(message.id, { message: message, element: element });
$('#channel-feed').insertBefore(element, bottomElement);
}
if (messages.length > 0) {
// 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 = Elements.createMessage(server, 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, messages: Message[], props: SetMessageProps): Promise<void> {
const { atTop, atBottom } = props;
await this.lockMessages(server, channel, () => {
$.setDocument(this.document);
this.messagesAtTop = atTop;
this.messagesAtBottom = atBottom;
Util.removeErrorIndicators($('#channel-feed'), [ 'recent' ]);
Util.removeErrorIndicators($('#channel-feed'), [ 'before' ]);
Util.removeErrorIndicators($('#channel-feed'), [ 'after' ]);
this.messagePairsServer = server;
this.messagePairsChannel = channel;
this.messagePairs.clear();
$.clearChildren($('#channel-feed'));
// Add the messages to the channel feed
// Using reverse order so that resources are loaded from bottom to top
// and the client starts at the bottom
for (let i = messages.length - 1; i >= 0; --i) {
let message = messages[i];
let priorMessage = messages[i - 1] || null;
let element = Elements.createMessage(server, message, priorMessage);
this.messagePairs.set(message.id, { message: message, element: element });
$('#channel-feed').prepend(element);
}
this.jumpMessagesToBottom();
});
}
public jumpMessagesToBottom(): void {
$.setDocument(this.document);
$('#channel-feed-content-wrapper').scrollTop = $('#channel-feed-content-wrapper').scrollHeight;
this.messagesAtBottom = true;
}
public async deleteMessages(server: ClientController, channel: Channel, messages: Message[]) {
await this.lockMessages(server, channel, () => {
for (let message of messages) {
if (this.messagePairs.has(message.id)) {
let messagePair = this.messagePairs.get(message.id) as { message: Message, element: HTMLElement };
messagePair.element.parentElement?.removeChild(messagePair.element);
// TODO: we should be updating messages sent below this message
// however, these events should be relatively rare so that's for the future
this.messagePairs.delete(message.id);
}
}
});
}
public async updateMessages(server: ClientController, channel: Channel, data: { oldDataPoint: Message, newDataPoint: Message }[]): Promise<void> {
await this.lockMessages(server, channel, () => {
for (const { oldDataPoint, newDataPoint } of data) {
let oldMessage = oldDataPoint;
let newMessage = newDataPoint;
if (this.messagePairs.has(oldMessage.id)) {
let oldElement = (this.messagePairs.get(oldMessage.id) as { message: Message, element: HTMLElement }).element;
let prevElement = $.previousElement(oldElement);
let prevMessage = prevElement && (this.messagePairs.get(prevElement.getAttribute('meta-id')) as { message: Message, element: HTMLElement }).message;
let newElement = Elements.createMessage(server, newMessage, prevMessage);
oldElement.parentElement?.replaceChild(newElement, oldElement);
// TODO: we should be updating messages sent below this message
// however, these events should be relatively rare so that's for the future
this.messagePairs.set(oldMessage.id, { message: newMessage, element: newElement });
}
}
});
}
public async addMessagesErrorIndicatorBefore(server: ClientController, channel: Channel, errorIndicatorElement: HTMLElement): Promise<void> {
await this.lockMessages(server, channel, () => {
$.setDocument(this.document);
$('#channel-feed').prepend(errorIndicatorElement);
});
}
public async addMessagesErrorIndicatorAfter(server: ClientController, channel: Channel, errorIndicatorElement: HTMLElement): Promise<void> {
await this.lockMessages(server, channel, () => {
$.setDocument(this.document);
$('#channel-feed').appendChild(errorIndicatorElement);
});
}
public async setMessagesErrorIndicator(server: ClientController, channel: Channel, errorIndicatorElement: HTMLElement): Promise<void> {
await this.lockMessages(server, channel, () => {
$.setDocument(this.document);
$.clearChildren($('#channel-feed'));
$('#channel-feed').appendChild(errorIndicatorElement);
});
}
}