remove old message handling code
This commit is contained in:
parent
49f3a7096a
commit
16054af1f8
@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user