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);
|
const LOG = Logger.create(__filename, electronConsole);
|
||||||
|
|
||||||
import Util from './util';
|
import Util from './util';
|
||||||
import Globals from './globals';
|
|
||||||
|
|
||||||
import UI from './ui';
|
import UI from './ui';
|
||||||
import CombinedGuild from './guild-combined';
|
import CombinedGuild from './guild-combined';
|
||||||
import { Channel } from './data-types';
|
|
||||||
import Q from './q-module';
|
import Q from './q-module';
|
||||||
|
|
||||||
export default class Actions {
|
export default class Actions {
|
||||||
@ -45,66 +43,4 @@ export default class Actions {
|
|||||||
errorMessage: 'Error fetching channels'
|
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 BaseElements from './require/base-elements';
|
||||||
import { Channel } from '../data-types';
|
import { Channel } from '../data-types';
|
||||||
import UI from '../ui';
|
import UI from '../ui';
|
||||||
import Actions from '../actions';
|
|
||||||
import Q from '../q-module';
|
import Q from '../q-module';
|
||||||
import CombinedGuild from '../guild-combined';
|
import CombinedGuild from '../guild-combined';
|
||||||
import ChannelOverlay from './overlays/overlay-channel';
|
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 () => {
|
element.addEventListener('click', async () => {
|
||||||
if (element.classList.contains('active')) return;
|
if (element.classList.contains('active')) return;
|
||||||
await ui.setActiveChannel(guild, channel);
|
await ui.setActiveChannel(guild, channel);
|
||||||
await Actions.fetchAndUpdateMessagesRecent(q, ui, guild, channel);
|
|
||||||
q.$('#text-input').focus();
|
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 UI from './ui';
|
||||||
import Actions from './actions';
|
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 Q from './q-module';
|
||||||
import bindWindowButtonEvents from './elements/events-window-buttons';
|
import bindWindowButtonEvents from './elements/events-window-buttons';
|
||||||
import bindTextInputEvents from './elements/events-text-input';
|
import bindTextInputEvents from './elements/events-text-input';
|
||||||
import bindInfiniteScrollEvents from './elements/events-infinite-scroll';
|
|
||||||
import bindConnectionEvents from './elements/events-connection';
|
import bindConnectionEvents from './elements/events-connection';
|
||||||
import bindAddGuildTitleEvents from './elements/events-guild-title';
|
import bindAddGuildTitleEvents from './elements/events-guild-title';
|
||||||
import bindAddGuildEvents from './elements/events-add-guild';
|
import bindAddGuildEvents from './elements/events-add-guild';
|
||||||
@ -27,7 +26,7 @@ import MessageRAMCache from './message-ram-cache';
|
|||||||
import ResourceRAMCache from './resource-ram-cache';
|
import ResourceRAMCache from './resource-ram-cache';
|
||||||
import CombinedGuild from './guild-combined';
|
import CombinedGuild from './guild-combined';
|
||||||
import { AutoVerifierChangesType } from './auto-verifier';
|
import { AutoVerifierChangesType } from './auto-verifier';
|
||||||
import { IDQuery, PartialMessageListQuery } from './auto-verifier-with-args';
|
import { IDQuery } from './auto-verifier-with-args';
|
||||||
|
|
||||||
LOG.silly('modules loaded');
|
LOG.silly('modules loaded');
|
||||||
|
|
||||||
@ -75,7 +74,6 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
bindWindowButtonEvents(q);
|
bindWindowButtonEvents(q);
|
||||||
bindTextInputEvents(document, q, ui);
|
bindTextInputEvents(document, q, ui);
|
||||||
bindInfiniteScrollEvents(q, ui);
|
|
||||||
bindConnectionEvents(document, q, ui);
|
bindConnectionEvents(document, q, ui);
|
||||||
bindAddGuildTitleEvents(document, q, ui);
|
bindAddGuildTitleEvents(document, q, ui);
|
||||||
bindAddGuildEvents(document, q, ui, guildsManager);
|
bindAddGuildEvents(document, q, ui, guildsManager);
|
||||||
@ -99,20 +97,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
(async () => { // refresh channels list
|
(async () => { // refresh channels list
|
||||||
await Actions.fetchAndUpdateChannels(q, ui, guild);
|
await Actions.fetchAndUpdateChannels(q, ui, guild);
|
||||||
})();
|
})();
|
||||||
(async () => { // refresh current channel messages
|
// TODO: React set hasMessagesAbove and hasMessagesBelow when re-verified
|
||||||
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();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
guildsManager.on('disconnect', (guild: CombinedGuild) => {
|
guildsManager.on('disconnect', (guild: CombinedGuild) => {
|
||||||
@ -166,31 +151,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
await ui.addChannels(guild, channels);
|
await ui.addChannels(guild, channels);
|
||||||
});
|
});
|
||||||
|
|
||||||
guildsManager.on('remove-messages', async (guild: CombinedGuild, messages: Message[]) => {
|
// TODO: React jump messages to bottom when the current user sent a 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Conflict Events
|
// 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>) => {
|
guildsManager.on('conflict-tokens', async (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes<Token>) => {
|
||||||
LOG.debug('tokens conflict', { changes });
|
LOG.debug('tokens conflict', { changes });
|
||||||
// TODO
|
// 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 "error-indicator.scss";
|
||||||
@import "general.scss";
|
@import "general.scss";
|
||||||
@import "members.scss";
|
@import "members.scss";
|
||||||
@import "messages.scss";
|
|
||||||
@import "overlays.scss";
|
@import "overlays.scss";
|
||||||
@import "scrollbars.scss";
|
@import "scrollbars.scss";
|
||||||
@import "guild-list.scss";
|
@import "guild-list.scss";
|
||||||
|
@ -7,15 +7,12 @@ import ConcurrentQueue from '../../concurrent-queue/concurrent-queue';
|
|||||||
|
|
||||||
import ElementsUtil from './elements/require/elements-util';
|
import ElementsUtil from './elements/require/elements-util';
|
||||||
|
|
||||||
import Globals from './globals';
|
|
||||||
import Util from './util';
|
|
||||||
import CombinedGuild from './guild-combined';
|
import CombinedGuild from './guild-combined';
|
||||||
import { Message, Channel, ConnectionInfo, ShouldNeverHappenError } from './data-types';
|
import { Message, Channel, ConnectionInfo, ShouldNeverHappenError } from './data-types';
|
||||||
import Q from './q-module';
|
import Q from './q-module';
|
||||||
import createGuildListGuild from './elements/guild-list-guild';
|
import createGuildListGuild from './elements/guild-list-guild';
|
||||||
import createChannel from './elements/channel';
|
import createChannel from './elements/channel';
|
||||||
import GuildsManager from './guilds-manager';
|
import GuildsManager from './guilds-manager';
|
||||||
import createMessage from './elements/message';
|
|
||||||
import { mountGuildChannelComponents, mountGuildComponents } from './elements/mounts';
|
import { mountGuildChannelComponents, mountGuildComponents } from './elements/mounts';
|
||||||
|
|
||||||
interface SetMessageProps {
|
interface SetMessageProps {
|
||||||
@ -321,308 +318,4 @@ export default class UI {
|
|||||||
this.q.$('#channel-list').appendChild(errorIndicatorElement);
|
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