remove old message handling code

This commit is contained in:
Michael Peters 2021-12-24 18:29:11 -06:00
parent 49f3a7096a
commit 16054af1f8
14 changed files with 4 additions and 1034 deletions

View File

@ -4,11 +4,9 @@ import Logger from '../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import Util from './util';
import Globals from './globals';
import UI from './ui';
import CombinedGuild from './guild-combined';
import { Channel } from './data-types';
import Q from './q-module';
export default class Actions {
@ -45,66 +43,4 @@ export default class Actions {
errorMessage: 'Error fetching channels'
});
}
static async fetchAndUpdateMessagesRecent(q: Q, ui: UI, guild: CombinedGuild, channel: Channel | { id: string }) {
await Util.withPotentialErrorWarnOnCancel(q, {
taskFunc: async () => {
if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
if (ui.activeChannel === null || ui.activeChannel.id !== channel.id) return;
const 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(guild, channel, errorIndicatorElement);
},
errorContainer: q.$('#channel-feed'),
errorMessage: 'Error fetching messages'
});
}
static async fetchAndUpdateMessagesBefore(q: Q, ui: UI, guild: CombinedGuild, channel: Channel) {
await Util.withPotentialErrorWarnOnCancel(q, {
taskFunc: async () => {
if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
if (ui.activeChannel === null || ui.activeChannel.id !== channel.id) return;
const topPair = ui.getTopMessagePair();
if (topPair == null) return;
const messages = await guild.fetchMessagesBefore(channel.id, topPair.message.id, Globals.MESSAGES_PER_REQUEST);
if (messages && messages.length > 0) {
await ui.addMessagesBefore(guild, channel, messages, topPair.message);
} else {
ui.messagesAtTop = true;
}
},
errorIndicatorAddFunc: async (errorIndicatorElement) => {
await ui.addMessagesErrorIndicatorBefore(guild, channel, errorIndicatorElement);
},
errorContainer: q.$('#channel-feed'),
errorClasses: [ 'before' ],
errorMessage: 'Error loading older messages'
});
}
static async fetchAndUpdateMessagesAfter(q: Q, ui: UI, guild: CombinedGuild, channel: Channel) {
await Util.withPotentialErrorWarnOnCancel(q, {
taskFunc: async () => {
if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
if (ui.activeChannel === null || ui.activeChannel.id !== channel.id) return;
const bottomPair = ui.getBottomMessagePair();
if (bottomPair == null) return;
const messages = await guild.fetchMessagesAfter(channel.id, bottomPair.message.id, Globals.MESSAGES_PER_REQUEST);
if (messages && messages.length > 0) {
await ui.addMessagesAfter(guild, channel, messages, bottomPair.message);
} else {
ui.messagesAtBottom = true;
}
},
errorIndicatorAddFunc: async (errorIndicatorElement) => {
await ui.addMessagesErrorIndicatorAfter(guild, channel, errorIndicatorElement);
},
errorContainer: q.$('#channel-feed'),
errorClasses: [ 'after' ],
errorMessage: 'Error loading newer messages'
});
}
}

View File

@ -5,7 +5,6 @@ import ElementsUtil from './require/elements-util';
import BaseElements from './require/base-elements';
import { Channel } from '../data-types';
import UI from '../ui';
import Actions from '../actions';
import Q from '../q-module';
import CombinedGuild from '../guild-combined';
import ChannelOverlay from './overlays/overlay-channel';
@ -22,7 +21,6 @@ export default function createChannel(document: Document, q: Q, ui: UI, guild: C
element.addEventListener('click', async () => {
if (element.classList.contains('active')) return;
await ui.setActiveChannel(guild, channel);
await Actions.fetchAndUpdateMessagesRecent(q, ui, guild, channel);
q.$('#text-input').focus();
});

View File

@ -1,53 +0,0 @@
import * as electronRemote from '@electron/remote';
const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import Actions from "../actions";
import Q from "../q-module";
import UI from "../ui";
export default function bindInfiniteScrollEvents(q: Q, ui: UI): void {
// Update current channel messages as the pane is scrolled
let loadingBefore = false;
let loadingAfter = false;
async function updateInfiniteScroll() {
const scrollTop = q.$('#channel-feed-content-wrapper').scrollTop;
const scrollHeight = q.$('#channel-feed-content-wrapper').scrollHeight;
const clientHeight = q.$('#channel-feed-content-wrapper').clientHeight;
// WARNING
// There's likely an inconsistency between browsers on this so have fun when you're working
// on the cross-platform implementation of this
// scrollTop apparantly is negative for column-reverse divs (this actually kindof makes sense if you flip your head upside down)
// have to reverse this
// I expect this was a change with some version of chromium.
// MDN documentation issue: https://github.com/mdn/content/issues/10968
const distToTop = -(clientHeight - scrollHeight - scrollTop); // keep in mind scrollTop is negative >:]
const distToBottom = -scrollTop;
//LOG.debug('update infinite scroll', { scrollTop, scrollHeight, clientHeight, distToTop, distToBottom, loadingBefore, loadingAfter });
if (ui.activeGuild === null) return;
if (ui.activeChannel === null) return;
if (!loadingBefore && !ui.messagesAtTop && distToTop < 600) { // Approaching the unloaded top of the page
// Fetch more messages to add above
LOG.debug('fetching messages before', { loadingBefore, messagesAtTop: ui.messagesAtTop, distToTop });
loadingBefore = true;
await Actions.fetchAndUpdateMessagesBefore(q, ui, ui.activeGuild, ui.activeChannel);
loadingBefore = false;
} else if (!loadingAfter && !ui.messagesAtBottom && distToBottom < 600) { // Approaching the unloaded bottom of the page
// Fetch more messages to add below
LOG.debug('fetching messages after', { loadingAfter, messagesAtBottom: ui.messagesAtBottom, distToBottom: distToBottom });
loadingAfter = true;
await Actions.fetchAndUpdateMessagesAfter(q, ui, ui.activeGuild, ui.activeChannel);
loadingAfter = false;
}
}
q.$('#channel-feed-content-wrapper').addEventListener('scroll', updateInfiniteScroll);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(q.$('#channel-feed-content-wrapper') as any).updateInfiniteScroll = updateInfiniteScroll; // custom element function
}

View File

@ -1,36 +0,0 @@
import { Message } from '../data-types';
import CombinedGuild from '../guild-combined';
import Q from '../q-module';
import createImageResourceMessage from './msg-img-res';
import createImageResourceMessageContinued from './msg-img-res-cont';
import createResourceMessage from './msg-res';
import createResourceMessageContinued from './msg-res-cont';
import createTextMessage from './msg-txt';
import createTextMessageContinued from './msg-txt-cont';
// TODO: This is probably best as a react class
export default function createMessage(document: Document, q: Q, guild: CombinedGuild, message: Message, lastMessage: Message | null): Element {
let element: Element;
if (message.hasResource()) {
if (message.isImageResource()) {
if (message.isContinued(lastMessage)) {
element = createImageResourceMessageContinued(document, q, guild, message);
} else {
element = createImageResourceMessage(document, q, guild, message);
}
} else {
if (message.isContinued(lastMessage)) {
element = createResourceMessageContinued(q, guild, message);
} else {
element = createResourceMessage(q, guild, message);
}
}
} else {
if (message.isContinued(lastMessage)) {
element = createTextMessageContinued(q, guild, message);
} else {
element = createTextMessage(q, guild, message);
}
}
return element;
}

View File

@ -1,66 +0,0 @@
import * as electronRemote from '@electron/remote';
const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import moment from 'moment';
import * as FileType from 'file-type';
import ElementsUtil from './require/elements-util.js';
import { Message, ShouldNeverHappenError } from '../data-types';
import Q from '../q-module';
import createImageContextMenu from './context-menu-img';
import CombinedGuild from '../guild-combined';
import React from 'react';
import ReactHelper from './require/react-helper';
import ImageOverlay from './overlays/overlay-image';
export default function createImageResourceMessageContinued(document: Document, q: Q, guild: CombinedGuild, message: Message): Element {
if (!message.resourceId || !message.resourcePreviewId || !message.resourceName) {
throw new ShouldNeverHappenError('Message is not a resource message');
}
const element = ReactHelper.createElementFromJSX(
<div className="message continued" data-id={message.id} data-member-id={message.member.id} data-guild-id={guild.id}>
<div className="timestamp">{moment(message.sent).format('HH:mm')}</div>
<div className="right">
<div className="content image" style={{width: message.previewWidth + 'px', height: message.previewHeight + 'px'}}>
<img src="./img/loading.svg" alt={message.resourceName}></img>
</div>
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
</div>
</div>
);
q.$$$(element, '.content.image').addEventListener('click', () => {
ElementsUtil.presentReactOverlay(document,
<ImageOverlay guild={guild}
resourceId={message.resourceId as string} resourceName={message.resourceName as string} />
);
});
(async () => {
try {
const resource = await guild.fetchResource(message.resourcePreviewId as string);
const src = await ElementsUtil.getImageBufferSrc(resource.data);
(q.$$$(element, '.content.image img') as HTMLImageElement).src = src;
const { mime, ext } = (await FileType.fromBuffer(resource.data)) ?? { mime: null, ext: null };
if (mime === null || ext === null) throw new Error('unable to get mime/ext');
q.$$$(element, '.content.image').addEventListener('contextmenu', (e) => {
const contextMenu = createImageContextMenu(document, q, guild, message.resourceName as string, resource.data, mime as string, ext as string, true);
document.body.appendChild(contextMenu);
const relativeTo = { x: e.pageX, y: e.pageY };
ElementsUtil.alignContextElement(contextMenu, relativeTo, { top: 'centerY', left: 'right' });
});
} catch (e) {
LOG.error('error loading preview image', e);
(q.$$$(element, '.content.image img') as HTMLImageElement).src = './img/error.png';
}
})();
return element;
}

View File

@ -1,95 +0,0 @@
import * as electronRemote from '@electron/remote';
const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import moment from 'moment';
import * as FileType from 'file-type';
import ElementsUtil from './require/elements-util.js';
import { Message, Member, ShouldNeverHappenError } from '../data-types';
import Q from '../q-module';
import createImageContextMenu from './context-menu-img';
import CombinedGuild from '../guild-combined';
import React from 'react';
import ReactHelper from './require/react-helper';
import ImageOverlay from './overlays/overlay-image';
export default function createImageResourceMessage(document: Document, q: Q, guild: CombinedGuild, message: Message): Element {
if (!message.resourceId || !message.resourcePreviewId || !message.resourceName) {
throw new ShouldNeverHappenError('Message is not a resource message');
}
let memberInfo: {
roleColor: string | null,
displayName: string,
avatarResourceId: string | null
};
if (message.member instanceof Member) {
memberInfo = {
roleColor: message.member.roleColor,
displayName: message.member.displayName,
avatarResourceId: message.member.avatarResourceId
};
} else {
memberInfo = {
roleColor: null,
displayName: 'Unknown Member',
avatarResourceId: null
};
}
const nameStyle = memberInfo.roleColor != null ? { color: memberInfo.roleColor } : {};
const element = ReactHelper.createElementFromJSX(
<div className="message" data-id={message.id} data-member-id={message.member.id} data-guild-id={guild.id}>
<div className="member-avatar">
<img src="./img/loading.svg" alt={memberInfo.displayName}></img>
</div>
<div className="right">
<div className="header">
<div className="member-name" style={nameStyle}>{memberInfo.displayName}</div>
<div className="timestamp">{moment(message.sent).calendar(ElementsUtil.calendarFormats)}</div>
</div>
<div className="content image" style={{width: message.previewWidth + 'px', height: message.previewHeight + 'px'}}>
<img src="./img/loading.svg" alt={message.resourceName}></img>
</div>
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
</div>
</div>
);
q.$$$(element, '.content.image').addEventListener('click', (e) => {
ElementsUtil.presentReactOverlay(document,
<ImageOverlay guild={guild}
resourceId={message.resourceId as string} resourceName={message.resourceName as string} />
);
//document.body.appendChild(createImageOverlay(document, q, guild, message.resourceId as string, message.resourceName as string));
});
(async () => {
(q.$$$(element, '.member-avatar img') as HTMLImageElement).src =
await ElementsUtil.getImageSrcFromResourceFailSoftly(guild, memberInfo.avatarResourceId);
})();
(async () => {
try {
const resource = await guild.fetchResource(message.resourcePreviewId as string);
const src = await ElementsUtil.getImageBufferSrc(resource.data);
(q.$$$(element, '.content.image img') as HTMLImageElement).src = src;
const { mime, ext } = (await FileType.fromBuffer(resource.data)) ?? { mime: null, ext: null };
if (mime === null || ext === null) throw new Error('unable to get mime/ext');
q.$$$(element, '.content.image').addEventListener('contextmenu', (e) => {
const contextMenu = createImageContextMenu(document, q, guild, message.resourceName as string, resource.data, mime as string, ext as string, true);
document.body.appendChild(contextMenu);
const relativeTo = { x: e.pageX, y: e.pageY };
ElementsUtil.alignContextElement(contextMenu, relativeTo, { top: 'centerY', left: 'right' });
});
} catch (e) {
LOG.error('error loading preview image', e);
(q.$$$(element, '.content.image img') as HTMLImageElement).src = './img/error.png';
}
})();
return element;
}

View File

@ -1,53 +0,0 @@
import moment from 'moment';
import { Message, ShouldNeverHappenError } from '../data-types';
import CombinedGuild from "../guild-combined";
import Q from "../q-module";
import ElementsUtil from "./require/elements-util";
import React from 'react';
import ReactHelper from './require/react-helper';
export default function createResourceMessageContinued(q: Q, guild: CombinedGuild, message: Message): Element {
if (!message.resourceId || !message.resourceName) {
throw new ShouldNeverHappenError('Message is not a resource message');
}
const element = ReactHelper.createElementFromJSX(
<div className="message continued" data-id={message.id} data-member-id={message.member.id} data-guild-id={guild.id}>
<div className="timestamp">{moment(message.sent).format('HH:mm')}</div>
<div className="right">
<div className="content resource">
<img className="icon" src="./img/file-icon.png" alt="file"></img>
<div className="text">
<div className="filename">{message.resourceName}</div>
<div className="download-status">Click to Download</div>
</div>
</div>
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
</div>
</div>
);
q.$$$(element, '.resource').addEventListener('click', ElementsUtil.createDownloadListener({
guild: guild, resourceId: message.resourceId, resourceName: message.resourceName,
downloadStartFunc: () => {
q.$$$(element, '.resource .download-status').innerText = 'Downloading...';
},
downloadFailFunc: async () => {
q.$$$(element, '.resource .download-status').innerText = 'Error Downloading. Click to Try Again';
await ElementsUtil.shakeElement(q.$$$(element, '.resource .download-status'), 400);
},
writeStartFunc: () => {
q.$$$(element, '.resource .download-status').innerText = 'Writing...';
},
writeFailFunc: async () => {
q.$$$(element, '.resource .download-status').innerText = 'Error Writing. Click to Try Again';
await ElementsUtil.shakeElement(q.$$$(element, '.resource .download-status'), 400);
},
successFunc: (downloadPath) => {
q.$$$(element, '.resource .download-status').innerText = 'Click to Open in Explorer';
}
}));
return element;
}

View File

@ -1,83 +0,0 @@
import moment from 'moment';
import { Message, Member, ShouldNeverHappenError } from '../data-types';
import CombinedGuild from '../guild-combined';
import Q from '../q-module';
import ElementsUtil from './require/elements-util';
import React from 'react';
import ReactHelper from './require/react-helper';
export default function createResourceMessage(q: Q, guild: CombinedGuild, message: Message): Element {
if (!message.resourceId || !message.resourceName) {
throw new ShouldNeverHappenError('Message is not a resource message');
}
let memberInfo: {
roleColor: string | null,
displayName: string,
avatarResourceId: string | null
};
if (message.member instanceof Member) {
memberInfo = {
roleColor: message.member.roleColor,
displayName: message.member.displayName,
avatarResourceId: message.member.avatarResourceId
};
} else {
memberInfo = {
roleColor: null,
displayName: 'Unknown Member',
avatarResourceId: null
};
}
const nameStyle = memberInfo.roleColor != null ? { color: memberInfo.roleColor } : {};
const element = ReactHelper.createElementFromJSX(
<div className="message" data-id={message.id} data-member-id={message.member.id} data-guild-id={guild.id}>
<div className="member-avatar">
<img src="./img/loading.svg" alt={memberInfo.displayName}></img>
</div>
<div className="right">
<div className="header">
<div className="member-name" style={nameStyle}>{memberInfo.displayName}</div>
<div className="timestamp">{moment(message.sent).calendar(ElementsUtil.calendarFormats)}</div>
</div>
<div className="content resource">
<img className="icon" src="./img/file-icon.png" alt="file"></img>
<div className="text">
<div className="filename">{message.resourceName}</div>
<div className="download-status">Click to Download</div>
</div>
</div>
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
</div>
</div>
);
q.$$$(element, '.resource').addEventListener('click', ElementsUtil.createDownloadListener({
guild: guild, resourceId: message.resourceId, resourceName: message.resourceName,
downloadStartFunc: () => {
q.$$$(element, '.resource .download-status').innerText = 'Downloading...';
},
downloadFailFunc: async () => {
q.$$$(element, '.resource .download-status').innerText = 'Error Downloading. Click to Try Again';
await ElementsUtil.shakeElement(q.$$$(element, '.resource .download-status'), 400);
},
writeStartFunc: () => {
q.$$$(element, '.resource .download-status').innerText = 'Writing...';
},
writeFailFunc: async () => {
q.$$$(element, '.resource .download-status').innerText = 'Error Writing. Click to Try Again';
await ElementsUtil.shakeElement(q.$$$(element, '.resource .download-status'), 400);
},
successFunc: (_downloadPath: string) => {
q.$$$(element, '.resource .download-status').innerText = 'Click to Open in Explorer';
}
}));
(async () => {
(q.$$$(element, '.member-avatar img') as HTMLImageElement).src =
await ElementsUtil.getImageSrcFromResourceFailSoftly(guild, memberInfo.avatarResourceId);
})();
return element;
}

View File

@ -1,20 +0,0 @@
import moment from 'moment';
import { Message } from '../data-types';
import CombinedGuild from '../guild-combined';
import Q from '../q-module.js';
import ElementsUtil from './require/elements-util.js';
import React from 'react';
import ReactHelper from './require/react-helper';
export default function createTextMessageContinued(q: Q, guild: CombinedGuild, message: Message): Element {
return ReactHelper.createElementFromJSX(
<div className="message continued" data-id={message.id} data-member-id={message.member.id} data-guild-id={guild.id}>
<div className="timestamp">{moment(message.sent).format('HH:mm')}</div>
<div className="right">
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
</div>
</div>
);
}

View File

@ -1,60 +0,0 @@
import moment from 'moment';
import ElementsUtil from './require/elements-util';
import { Message, Member, IDummyTextMessage } from '../data-types';
import Q from '../q-module';
import CombinedGuild from '../guild-combined';
import React from 'react';
import ReactHelper from './require/react-helper';
export default function createTextMessage(q: Q, guild: CombinedGuild, message: Message | IDummyTextMessage): Element {
let memberInfo: {
roleColor: string | null,
displayName: string,
avatarResourceId: string | null
};
if (message instanceof Message) {
if (message.member instanceof Member) {
memberInfo = {
roleColor: message.member.roleColor,
displayName: message.member.displayName,
avatarResourceId: message.member.avatarResourceId
};
} else {
memberInfo = {
roleColor: null,
displayName: 'Unknown Member',
avatarResourceId: null
};
}
} else {
memberInfo = {
roleColor: null,
displayName: message.member.displayName,
avatarResourceId: message.member.avatarResourceId
};
}
const nameStyle = memberInfo.roleColor != null ? { color: memberInfo.roleColor } : {};
const element = ReactHelper.createElementFromJSX(
<div className="message" data-id={message.id} data-member-id={message.member.id} data-guild-id={guild.id}>
<div className="member-avatar">
<img src="./img/loading.svg" alt={memberInfo.displayName}></img>
</div>
<div className="right">
<div className="header">
<div className="member-name" style={nameStyle}>{memberInfo.displayName}</div>
<div className="timestamp">{moment(message.sent).calendar(ElementsUtil.calendarFormats)}</div>
</div>
<div className="content text">{ElementsUtil.parseMessageText(message.text ?? '')}</div>
</div>
</div>
);
(async () => {
(q.$$$(element, '.member-avatar img') as HTMLImageElement).src =
await ElementsUtil.getImageSrcFromResourceFailSoftly(guild, memberInfo.avatarResourceId);
})();
return element;
}

View File

@ -14,11 +14,10 @@ import Globals from './globals';
import UI from './ui';
import Actions from './actions';
import { Changes, Channel, ConnectionInfo, GuildMetadata, Member, Message, Resource, Token } from './data-types';
import { Changes, Channel, ConnectionInfo, GuildMetadata, Member, Resource, Token } from './data-types';
import Q from './q-module';
import bindWindowButtonEvents from './elements/events-window-buttons';
import bindTextInputEvents from './elements/events-text-input';
import bindInfiniteScrollEvents from './elements/events-infinite-scroll';
import bindConnectionEvents from './elements/events-connection';
import bindAddGuildTitleEvents from './elements/events-guild-title';
import bindAddGuildEvents from './elements/events-add-guild';
@ -27,7 +26,7 @@ import MessageRAMCache from './message-ram-cache';
import ResourceRAMCache from './resource-ram-cache';
import CombinedGuild from './guild-combined';
import { AutoVerifierChangesType } from './auto-verifier';
import { IDQuery, PartialMessageListQuery } from './auto-verifier-with-args';
import { IDQuery } from './auto-verifier-with-args';
LOG.silly('modules loaded');
@ -75,7 +74,6 @@ window.addEventListener('DOMContentLoaded', () => {
bindWindowButtonEvents(q);
bindTextInputEvents(document, q, ui);
bindInfiniteScrollEvents(q, ui);
bindConnectionEvents(document, q, ui);
bindAddGuildTitleEvents(document, q, ui);
bindAddGuildEvents(document, q, ui, guildsManager);
@ -99,20 +97,7 @@ window.addEventListener('DOMContentLoaded', () => {
(async () => { // refresh channels list
await Actions.fetchAndUpdateChannels(q, ui, guild);
})();
(async () => { // refresh current channel messages
if (ui.activeChannel === null) return;
if (ui.messagePairs.size == 0) {
// fetch messages again since there are no messages yet
await Actions.fetchAndUpdateMessagesRecent(q, ui, guild, ui.activeChannel);
} else {
// If we already have messages, just update the infinite scroll.
// NOTE: this will not add/remove new/deleted messages
ui.messagesAtTop = false;
ui.messagesAtBottom = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(q.$('#channel-feed-content-wrapper') as any).updateInfiniteScroll();
}
})();
// TODO: React set hasMessagesAbove and hasMessagesBelow when re-verified
});
guildsManager.on('disconnect', (guild: CombinedGuild) => {
@ -166,31 +151,7 @@ window.addEventListener('DOMContentLoaded', () => {
await ui.addChannels(guild, channels);
});
guildsManager.on('remove-messages', async (guild: CombinedGuild, messages: Message[]) => {
LOG.debug(messages.length + ' deleted messages');
await ui.deleteMessages(guild, messages);
});
guildsManager.on('update-messages', async (guild: CombinedGuild, updatedMessages: Message[]) => {
LOG.debug(updatedMessages.length + ' updated messages');
await ui.updateMessages(guild, updatedMessages);
});
guildsManager.on('new-messages', async (guild: CombinedGuild, messages: Message[]) => {
if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
for (const message of messages) {
if (ui.activeChannel === null || ui.activeChannel.id !== message.channel.id) return;
if (ui.messagesAtBottom) {
// add the message to the bottom of the message feed
await ui.addMessages(guild, [ message ]);
ui.jumpMessagesToBottom();
} else if (message.member.id == guild.memberId) {
// this set of messages will include the new messageguildId
LOG.debug('not at bottom, jumping down since message was sent by the current user');
await Actions.fetchAndUpdateMessagesRecent(q, ui, guild, message.channel);
}
}
});
// TODO: React jump messages to bottom when the current user sent a message
// Conflict Events
@ -221,13 +182,6 @@ window.addEventListener('DOMContentLoaded', () => {
}
});
guildsManager.on('conflict-messages', async (guild: CombinedGuild, query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes<Message>) => {
//LOG.debug('messages conflict', { changes });
if (changes.deleted.length > 0) await ui.deleteMessages(guild, changes.deleted);
if (changes.added.length > 0) await ui.addMessages(guild, changes.added);
if (changes.updated.length > 0) await ui.updateMessages(guild, changes.updated.map(pair => pair.newDataPoint));
});
guildsManager.on('conflict-tokens', async (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes<Token>) => {
LOG.debug('tokens conflict', { changes });
// TODO

View File

@ -1,144 +0,0 @@
@import "theme.scss";
.message {
display: flex;
padding: 4px 16px;
&.continued {
margin-top: -4px;
.timestamp {
flex: none; /* >:| NOT GONNA SHRINK BOI */
margin: 0;
width: 40px;
visibility: hidden;
}
&:hover .timestamp {
visibility: visible;
}
}
.member-avatar img {
width: 40px;
height: 40px;
border-radius: 20px;
cursor: pointer;
user-select: none;
}
.right {
flex: 1;
display: flex;
flex-flow: column;
align-items: flex-start;
margin-left: 16px; /* putting the margin here rather than on the avatar makes selecting better */
user-select: text;
}
.header {
display: flex;
align-items: baseline;
}
.member-name {
font-size: 16px;
line-height: 22px;
font-weight: 500;
color: $header-primary;
cursor: pointer;
white-space: nowrap;
}
.member-name:hover {
text-decoration: underline;
}
.timestamp {
font-size: 12px;
line-height: 22px;
font-weight: 500;
color: $text-muted;
margin-left: 4px;
white-space: nowrap;
}
.content {
font-size: 16px;
color: $text-normal;
overflow-wrap: anywhere;
&.text {
white-space: pre-wrap;
.bold {
font-weight: 600;
color: $header-primary; /* a bit brighter for some more emphasis */
}
.underline {
text-decoration: underline;
}
.italic {
font-style: italic;
}
}
&.image {
/* Center the loading icon */
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
background-color: $background-secondary-alt;
cursor: pointer;
overflow: hidden;
img {
max-width: 400px;
max-height: 300px;
border-radius: 8px;
}
}
/* TODO: ellipse the overflow */
&.resource {
min-width: 300px;
max-width: 400px;
display: flex;
align-items: center;
background-color: $background-secondary-alt;
padding: 8px;
border-radius: 8px;
cursor: pointer;
&:hover .download-status {
text-decoration: underline;
}
> :not(:last-child) {
margin-right: 8px;
}
.icon {
width: 26px;
height: 26px;
-webkit-user-select: none;
user-select: none;
}
.filename {
color: $text-link;
}
.download-status {
font-size: 12px;
font-weight: 500;
color: $text-muted;
-webkit-user-select: none;
user-select: none;
}
}
}
}

View File

@ -16,7 +16,6 @@
@import "error-indicator.scss";
@import "general.scss";
@import "members.scss";
@import "messages.scss";
@import "overlays.scss";
@import "scrollbars.scss";
@import "guild-list.scss";

View File

@ -7,15 +7,12 @@ import ConcurrentQueue from '../../concurrent-queue/concurrent-queue';
import ElementsUtil from './elements/require/elements-util';
import Globals from './globals';
import Util from './util';
import CombinedGuild from './guild-combined';
import { Message, Channel, ConnectionInfo, ShouldNeverHappenError } from './data-types';
import Q from './q-module';
import createGuildListGuild from './elements/guild-list-guild';
import createChannel from './elements/channel';
import GuildsManager from './guilds-manager';
import createMessage from './elements/message';
import { mountGuildChannelComponents, mountGuildComponents } from './elements/mounts';
interface SetMessageProps {
@ -321,308 +318,4 @@ export default class UI {
this.q.$('#channel-list').appendChild(errorIndicatorElement);
});
}
public getTopMessagePair(): { message: Message, element: Element } | null {
const element = this.q.$$('#channel-feed .message')[0];
return element && this.messagePairs.get(element.getAttribute('data-id')) || null;
}
public getBottomMessagePair(): { message: Message, element: Element } | null {
const messageElements = this.q.$$('#channel-feed .message');
const element = messageElements[messageElements.length - 1];
return element && this.messagePairs.get(element.getAttribute('data-id')) || null;
}
public async addMessages(guild: CombinedGuild, messages: Message[]) {
const channelIds = new Set(messages.map(message => message.channel.id));
for (const channelId of channelIds) {
let channelMessages = messages.filter(message => message.channel.id === channelId);
channelMessages = channelMessages.sort(Message.sortOrder);
// No Previous Messages is an easy case
if (this.messagePairs.size === 0) {
await this.addMessagesBefore(guild, { id: channelId }, channelMessages, null);
continue;
}
const topMessagePair = this.getTopMessagePair() as { message: Message, element: HTMLElement };
const bottomMessagePair = this.getBottomMessagePair() as { message: Message, element: HTMLElement };
const aboveMessages = messages.filter(message => message.sortsBefore(topMessagePair.message));
const belowMessages = messages.filter(message => message.sortsAfter(bottomMessagePair.message));
const betweenMessages = messages.filter(message => !message.sortsBefore(topMessagePair.message) && !message.sortsAfter(bottomMessagePair.message));
if (aboveMessages.length > 0) await this.addMessagesBefore(guild, { id: channelId }, aboveMessages, topMessagePair.message);
if (belowMessages.length > 0) await this.addMessagesAfter(guild, { id: channelId }, belowMessages, bottomMessagePair.message);
if (betweenMessages.length > 0) await this.addMessagesBetween(guild, { id: channelId }, betweenMessages, topMessagePair.element, bottomMessagePair.element);
}
}
public async addMessagesBefore(guild: CombinedGuild, channel: Channel | { id: string }, messages: Message[], prevTopMessage: Message | null): Promise<void> {
this.lockMessages(guild, channel, () => {
if (prevTopMessage && this.getTopMessagePair()?.message.id !== prevTopMessage.id) return;
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) {
const currentMessageElements = this.q.$$('#channel-feed .message');
if (this.messagePairs.size !== currentMessageElements.length) throw new Error(`message lengths disjointed, ${this.messagePairs.size} != ${currentMessageElements.length}`); // sanity check
const toRemove = currentMessageElements.slice(-(this.messagePairs.size + messages.length - Globals.MAX_CURRENT_MESSAGES));
for (const element of toRemove) {
const id = element.getAttribute('data-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(this.q, this.q.$('#channel-feed'), [ 'recent' ]);
Util.removeErrorIndicators(this.q, this.q.$('#channel-feed'), [ 'before' ]);
// Keep track of the top messages before we add new messages
const 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) {
const message = messages[i] as Message;
const priorMessage = messages[i - 1] || null;
const 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
const newPrevTopElement = createMessage(this.document, this.q, guild, prevTopPair.message, messages[messages.length - 1] as Message);
prevTopPair.element.parentElement?.replaceChild(newPrevTopElement, prevTopPair.element);
this.messagePairs.set(prevTopPair.message.id, { message: prevTopPair.message, element: newPrevTopElement });
}
});
}
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;
// 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) {
const currentMessageElements = this.q.$$('#channel-feed .message');
if (this.messagePairs.size !== currentMessageElements.length) throw new Error('message lengths disjointed'); // sanity check
const toRemove = currentMessageElements.slice(0, this.messagePairs.size + messages.length - Globals.MAX_CURRENT_MESSAGES);
for (const element of toRemove) {
const id = element.getAttribute('data-id');
this.messagePairs.delete(id);
element.parentElement?.removeChild(element);
}
this.messagesAtTop = false;
}
Util.removeErrorIndicators(this.q, this.q.$('#channel-feed'), [ 'recent' ]);
Util.removeErrorIndicators(this.q, this.q.$('#channel-feed'), [ 'after' ]);
// Get the bottom message to use as the prior message to the first new message
const 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
const message = messages[i] as Message;
const priorMessage = messages[i - 1] || (prevBottomPair && prevBottomPair.message);
const element = createMessage(this.document, this.q, guild, message, priorMessage);
this.messagePairs.set(message.id, { message: message, element: element });
this.q.$('#channel-feed').appendChild(element);
}
});
}
// TODO: use topMessage, bottomMessage / topMessageId, bottomMessageId instead?
private async addMessagesBetween(guild: CombinedGuild, channel: Channel | { id: string }, messages: Message[], topElement: HTMLElement, bottomElement: HTMLElement): Promise<void> {
await this.lockMessages(guild, channel, () => {
if (!(messages.length > 0 && topElement != null && bottomElement != null && bottomElement == Q.nextElement(topElement))) {
LOG.error('invalid messages between', { messages, top: topElement.innerText, bottom: bottomElement.innerText, afterTop: (Q.nextElement(topElement) as HTMLElement | null)?.innerText });
throw new Error('invalid messages between');
}
if (this.messagePairs.size + messages.length > Globals.MAX_CURRENT_MESSAGES) {
const currentMessageElements = this.q.$$('#channel-feed .message');
if (this.messagePairs.size !== currentMessageElements.length) throw new Error('message lengths disjointed'); // sanity check
const totalToRemove = this.messagePairs.size + messages.length - Globals.MAX_CURRENT_MESSAGES
const 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
const above = bottomElement.offsetTop > this.q.$('#channel-feed-wrapper').scrollTop;
if (above) {
// remove elements at the top first
for (const messageElement of currentMessageElements) {
if (toRemove.length == totalToRemove) {
break;
}
if (messageElement.getAttribute('data-id') == topElement.getAttribute('data-id')) {
break;
}
toRemove.push(messageElement);
}
// remove elements at the bottom if still needed
for (const messageElement of currentMessageElements.reverse()) {
if (toRemove.length == totalToRemove) {
break;
}
if (messageElement.getAttribute('data-id') == bottomElement.getAttribute('data-id')) {
break;
}
toRemove.push(messageElement);
}
} else {
// remove elements at the bottom first
for (const messageElement of currentMessageElements.reverse()) {
if (toRemove.length == totalToRemove) {
break;
}
if (messageElement.getAttribute('data-id') == topElement.getAttribute('data-id')) {
break;
}
toRemove.push(messageElement);
}
// remove elements at the top if still needed
for (const messageElement of currentMessageElements.reverse()) {
if (toRemove.length == totalToRemove) {
break;
}
if (messageElement.getAttribute('data-id') == bottomElement.getAttribute('data-id')) {
break;
}
toRemove.push(messageElement);
}
}
for (const element of toRemove) {
const id = element.getAttribute('data-id');
if (!id) continue;
this.messagePairs.delete(id);
element.parentElement?.removeChild(element);
}
}
const topElementId = topElement.getAttribute('data-id');
const topMessage = this.messagePairs.get(topElementId)?.message;
for (let i = 0; i < messages.length; ++i) {
const message = messages[i] as Message;
const priorMessage = messages[i - 1] || topMessage || null;
const 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);
}
if (messages.length > 0) {
// update the bottom element since the element above it changed
const bottomMessage = this.messagePairs.get(bottomElement.getAttribute('data-id'))?.message;
if (!bottomMessage) throw new ShouldNeverHappenError('could not find bottom message');
const newBottomElement = createMessage(this.document, this.q, guild, bottomMessage, messages[messages.length - 1] as Message);
bottomElement.parentElement?.replaceChild(newBottomElement, bottomElement);
this.messagePairs.set(bottomMessage.id, { element: newBottomElement, message: bottomMessage });
}
});
}
public async setMessages(guild: CombinedGuild, channel: Channel | { id: string }, messages: Message[], props: SetMessageProps): Promise<void> {
const { atTop, atBottom } = props;
await this.lockMessages(guild, channel, () => {
this.messagesAtTop = atTop;
this.messagesAtBottom = atBottom;
Util.removeErrorIndicators(this.q, this.q.$('#channel-feed'), [ 'recent' ]);
Util.removeErrorIndicators(this.q, this.q.$('#channel-feed'), [ 'before' ]);
Util.removeErrorIndicators(this.q, this.q.$('#channel-feed'), [ 'after' ]);
this.messagePairsGuild = guild;
this.messagePairsChannel = channel;
this.messagePairs.clear();
Q.clearChildren(this.q.$('#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) {
const message = messages[i] as Message;
const priorMessage = messages[i - 1] || null;
const element = createMessage(this.document, this.q, guild, message, priorMessage);
this.messagePairs.set(message.id, { message: message, element: element });
this.q.$('#channel-feed').prepend(element);
}
this.jumpMessagesToBottom();
});
}
public jumpMessagesToBottom(): void {
this.q.$('#channel-feed-content-wrapper').scrollTop = this.q.$('#channel-feed-content-wrapper').scrollHeight;
this.messagesAtBottom = true;
}
public async deleteMessages(guild: CombinedGuild, messages: Message[]) {
const channelIds = new Set(messages.map(message => message.channel.id));
for (const channelId of channelIds) {
const channelMessages = messages.filter(message => message.channel.id === channelId);
await this.lockMessages(guild, { id: channelId }, () => {
for (const message of channelMessages) {
if (this.messagePairs.has(message.id)) {
const messagePair = this.messagePairs.get(message.id) as { message: Message, element: HTMLElement };
messagePair.element.parentElement?.removeChild(messagePair.element);
// TODO: we should be updating messages sent below this message
// however, these events should be relatively rare so that's for the future
this.messagePairs.delete(message.id);
}
}
});
}
}
public async updateMessages(guild: CombinedGuild, updatedMessages: Message[]): Promise<void> {
const channelIds = new Set(updatedMessages.map(message => message.channel.id));
for (const channelId of channelIds) {
const channelMessages = updatedMessages.filter(message => message.channel.id === channelId);
await this.lockMessages(guild, { id: channelId }, () => {
for (const message of channelMessages) {
if (this.messagePairs.has(message.id)) {
const oldElement = (this.messagePairs.get(message.id) as { message: Message, element: HTMLElement }).element;
const prevElement = Q.previousElement(oldElement);
const prevMessage = prevElement && (this.messagePairs.get(prevElement.getAttribute('data-id')) as { message: Message, element: HTMLElement }).message;
const newElement = createMessage(this.document, this.q, guild, message, prevMessage);
oldElement.parentElement?.replaceChild(newElement, oldElement);
// TODO: we should be updating messages sent below this message
// however, these events should be relatively rare so that's for the future
this.messagePairs.set(message.id, { message: message, element: newElement });
}
}
});
}
}
public async addMessagesErrorIndicatorBefore(guild: CombinedGuild, channel: Channel, errorIndicatorElement: Element): Promise<void> {
await this.lockMessages(guild, channel, () => {
this.q.$('#channel-feed').prepend(errorIndicatorElement);
});
}
public async addMessagesErrorIndicatorAfter(guild: CombinedGuild, channel: Channel, errorIndicatorElement: Element): Promise<void> {
await this.lockMessages(guild, channel, () => {
this.q.$('#channel-feed').appendChild(errorIndicatorElement);
});
}
public async setMessagesErrorIndicator(guild: CombinedGuild, channel: Channel | { id: string }, errorIndicatorElement: Element): Promise<void> {
await this.lockMessages(guild, channel, () => {
Q.clearChildren(this.q.$('#channel-feed'));
this.q.$('#channel-feed').appendChild(errorIndicatorElement);
});
}
}