diff --git a/src/client/webapp/actions.ts b/src/client/webapp/actions.ts index 21d81eb..d157933 100644 --- a/src/client/webapp/actions.ts +++ b/src/client/webapp/actions.ts @@ -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' - }); - } } diff --git a/src/client/webapp/elements/channel.tsx b/src/client/webapp/elements/channel.tsx index fee8b47..c009622 100644 --- a/src/client/webapp/elements/channel.tsx +++ b/src/client/webapp/elements/channel.tsx @@ -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(); }); diff --git a/src/client/webapp/elements/events-infinite-scroll.ts b/src/client/webapp/elements/events-infinite-scroll.ts deleted file mode 100644 index bb686a1..0000000 --- a/src/client/webapp/elements/events-infinite-scroll.ts +++ /dev/null @@ -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 -} diff --git a/src/client/webapp/elements/message.tsx b/src/client/webapp/elements/message.tsx deleted file mode 100644 index ce9051d..0000000 --- a/src/client/webapp/elements/message.tsx +++ /dev/null @@ -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; -} diff --git a/src/client/webapp/elements/msg-img-res-cont.tsx b/src/client/webapp/elements/msg-img-res-cont.tsx deleted file mode 100644 index 5a0ddd2..0000000 --- a/src/client/webapp/elements/msg-img-res-cont.tsx +++ /dev/null @@ -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( -
-
{moment(message.sent).format('HH:mm')}
-
-
- {message.resourceName} -
-
{ElementsUtil.parseMessageText(message.text ?? '')}
-
-
- ); - - q.$$$(element, '.content.image').addEventListener('click', () => { - ElementsUtil.presentReactOverlay(document, - - ); - }); - (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; -} diff --git a/src/client/webapp/elements/msg-img-res.tsx b/src/client/webapp/elements/msg-img-res.tsx deleted file mode 100644 index 03d95bf..0000000 --- a/src/client/webapp/elements/msg-img-res.tsx +++ /dev/null @@ -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( -
-
- {memberInfo.displayName} -
-
-
-
{memberInfo.displayName}
-
{moment(message.sent).calendar(ElementsUtil.calendarFormats)}
-
-
- {message.resourceName} -
-
{ElementsUtil.parseMessageText(message.text ?? '')}
-
-
- ); - - q.$$$(element, '.content.image').addEventListener('click', (e) => { - ElementsUtil.presentReactOverlay(document, - - ); - //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; -} diff --git a/src/client/webapp/elements/msg-res-cont.tsx b/src/client/webapp/elements/msg-res-cont.tsx deleted file mode 100644 index 72eb150..0000000 --- a/src/client/webapp/elements/msg-res-cont.tsx +++ /dev/null @@ -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( -
-
{moment(message.sent).format('HH:mm')}
-
-
- file -
-
{message.resourceName}
-
Click to Download
-
-
-
{ElementsUtil.parseMessageText(message.text ?? '')}
-
-
- ); - - 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; -} diff --git a/src/client/webapp/elements/msg-res.tsx b/src/client/webapp/elements/msg-res.tsx deleted file mode 100644 index 4582de6..0000000 --- a/src/client/webapp/elements/msg-res.tsx +++ /dev/null @@ -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( -
-
- {memberInfo.displayName} -
-
-
-
{memberInfo.displayName}
-
{moment(message.sent).calendar(ElementsUtil.calendarFormats)}
-
-
- file -
-
{message.resourceName}
-
Click to Download
-
-
-
{ElementsUtil.parseMessageText(message.text ?? '')}
-
-
- ); - - 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; -} diff --git a/src/client/webapp/elements/msg-txt-cont.tsx b/src/client/webapp/elements/msg-txt-cont.tsx deleted file mode 100644 index 0d01840..0000000 --- a/src/client/webapp/elements/msg-txt-cont.tsx +++ /dev/null @@ -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( -
-
{moment(message.sent).format('HH:mm')}
-
-
{ElementsUtil.parseMessageText(message.text ?? '')}
-
-
- ); -} diff --git a/src/client/webapp/elements/msg-txt.tsx b/src/client/webapp/elements/msg-txt.tsx deleted file mode 100644 index 57a9594..0000000 --- a/src/client/webapp/elements/msg-txt.tsx +++ /dev/null @@ -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( -
-
- {memberInfo.displayName} -
-
-
-
{memberInfo.displayName}
-
{moment(message.sent).calendar(ElementsUtil.calendarFormats)}
-
-
{ElementsUtil.parseMessageText(message.text ?? '')}
-
-
- ); - (async () => { - (q.$$$(element, '.member-avatar img') as HTMLImageElement).src = - await ElementsUtil.getImageSrcFromResourceFailSoftly(guild, memberInfo.avatarResourceId); - })(); - return element; -} diff --git a/src/client/webapp/preload.ts b/src/client/webapp/preload.ts index ebbacb9..6c5b920 100644 --- a/src/client/webapp/preload.ts +++ b/src/client/webapp/preload.ts @@ -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) => { - //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) => { LOG.debug('tokens conflict', { changes }); // TODO diff --git a/src/client/webapp/styles/messages.scss b/src/client/webapp/styles/messages.scss deleted file mode 100644 index 1b46c5c..0000000 --- a/src/client/webapp/styles/messages.scss +++ /dev/null @@ -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; - } - } - } -} diff --git a/src/client/webapp/styles/styles.scss b/src/client/webapp/styles/styles.scss index d89685b..5674025 100644 --- a/src/client/webapp/styles/styles.scss +++ b/src/client/webapp/styles/styles.scss @@ -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"; diff --git a/src/client/webapp/ui.ts b/src/client/webapp/ui.ts index f7cf706..e119a1a 100644 --- a/src/client/webapp/ui.ts +++ b/src/client/webapp/ui.ts @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - await this.lockMessages(guild, channel, () => { - this.q.$('#channel-feed').prepend(errorIndicatorElement); - }); - } - - public async addMessagesErrorIndicatorAfter(guild: CombinedGuild, channel: Channel, errorIndicatorElement: Element): Promise { - await this.lockMessages(guild, channel, () => { - this.q.$('#channel-feed').appendChild(errorIndicatorElement); - }); - } - - public async setMessagesErrorIndicator(guild: CombinedGuild, channel: Channel | { id: string }, errorIndicatorElement: Element): Promise { - await this.lockMessages(guild, channel, () => { - Q.clearChildren(this.q.$('#channel-feed')); - this.q.$('#channel-feed').appendChild(errorIndicatorElement); - }); - } }