greatly improve performance

This commit is contained in:
hippoz 2022-09-06 20:52:44 +03:00
parent 10c851c679
commit e3e1b9ddad
Signed by: hippoz
GPG key ID: 7C52899193467641
7 changed files with 64 additions and 29 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
node_modules/ node_modules/
dist/ dist/
frontend-new/
.env .env

View file

@ -86,7 +86,7 @@ body {
line-height: 1.75; line-height: 1.75;
display: flex; display: flex;
flex-direction: column; flex-direction: row;
width: var(--viewportWidth); width: var(--viewportWidth);
height: var(--viewportHeight); height: var(--viewportHeight);

View file

@ -1,10 +1,8 @@
<script> <script>
import { CornerUpLeftIcon, MoreVerticalIcon } from "svelte-feather-icons"; import { CornerUpLeftIcon, MoreVerticalIcon } from "svelte-feather-icons";
import { getItem } from "../storage";
import { overlayStore, OverlayType, setMessageInputEvent, userInfoStore } from "../stores"; import { overlayStore, OverlayType, setMessageInputEvent, userInfoStore } from "../stores";
export let message; export let message;
export let clumped = false;
const reply = () => { const reply = () => {
let replyString = ""; let replyString = "";
@ -61,10 +59,6 @@
margin-right: var(--space-xs); margin-right: var(--space-xs);
} }
.author-hidden {
visibility: hidden;
}
.edit-message { .edit-message {
flex-shrink: 0; flex-shrink: 0;
float: right; float: right;
@ -85,8 +79,12 @@
margin-left: auto; margin-left: auto;
} }
.message.clumped .author {
visibility: hidden;
}
.message:hover .date, .message:hover .date,
.message:hover .author-hidden { .message.clumped:hover .author {
visibility: visible; visibility: visible;
} }
@ -101,15 +99,15 @@
} }
</style> </style>
<div class="message" class:clumped class:pinged={ userInfoStore.value && message.content.includes(`@${userInfoStore.value.username}`) }> <div class="message" class:clumped={ message._clumped } class:pinged={ message._mentions }>
<span class="author" class:author-hidden={ clumped }>{ message.author_username }</span> <span class="author">{ message.author_username }</span>
<span class="message-content" class:pending={ message._isPending }>{ message.content }</span> <span class="message-content" class:pending={ message._isPending }>{ message.content }</span>
<span class="date">{ new Intl.DateTimeFormat(getItem("ui:locale"), { hour: "numeric", minute: "numeric" }).format(new Date(parseInt(message.created_at))) }</span> <span class="date">{ message._createdAtTimeString }</span>
<button class="icon-button edit-message" on:click="{ reply }" aria-label="Reply to Message"> <button class="icon-button edit-message" on:click="{ reply }" aria-label="Reply to Message">
<CornerUpLeftIcon /> <CornerUpLeftIcon />
</button> </button>
{#if userInfoStore.value && (message.author_id === userInfoStore.value.id || userInfoStore.value.is_superuser)} {#if message._editable}
<button class="icon-button edit-message" on:click="{ () => overlayStore.push(OverlayType.EditMessage, { message }) }" aria-label="Edit Message"> <button class="icon-button edit-message" on:click="{ () => overlayStore.push(OverlayType.EditMessage, { message }) }" aria-label="Edit Message">
<MoreVerticalIcon /> <MoreVerticalIcon />
</button> </button>

View file

@ -83,17 +83,13 @@
</style> </style>
<div class="messages-container" on:scroll={ onScroll } bind:this={ scrollTarget }> <div class="messages-container" on:scroll={ onScroll } bind:this={ scrollTarget }>
{#each $messages as message, i (message.id)} {#each $messages as message (message.id)}
{@const previousMessage = $messages[i - 1]} {#if message._showDateMarkerAbove}
{@const previousDate = previousMessage ? new Date(parseInt(previousMessage.created_at)) : null}
{@const currentDate = new Date(parseInt(message.created_at))}
{#if previousDate && previousDate.toLocaleDateString() !== currentDate.toLocaleDateString()}
<div class="time-separator"> <div class="time-separator">
<span>{ new Intl.DateTimeFormat(getItem("ui:locale"), { month: "long", day: "numeric", year: "numeric" }).format(currentDate) }</span> <span>{ new Intl.DateTimeFormat(getItem("ui:locale"), { month: "long", day: "numeric", year: "numeric" }).format(message._createdAtDate) }</span>
</div> </div>
{/if} {/if}
<Message message={message} clumped={ previousDate && (currentDate.getTime() - previousDate.getTime()) <= 100 * 1000 } /> <Message message={message} />
{/each} {/each}
<div bind:this={ scrollAnchor } /> <div bind:this={ scrollAnchor } />
</div> </div>

View file

@ -141,12 +141,39 @@ class MessageStore extends Store {
this.didDoInitialLoad = false; this.didDoInitialLoad = false;
} }
_recomputeMessages() {
this.value = this.value.map((e, i) => this._processMessage(e, this.value[i - 1]));
}
_processMessage(message, previous=null) {
message._createdAtDate = new Date(parseInt(message.created_at));
message._createdAtTimeString = new Intl.DateTimeFormat(getItem("ui:locale"), { hour: "numeric", minute: "numeric" }).format(message._createdAtDate);
message._createdAtDateString = message._createdAtDate.toLocaleDateString();
message._mentions = false;
message._editable = false;
message._clumped = false;
message._showDateMarkerAbove = false;
if (userInfoStore.value && message.content.includes("@" + userInfoStore.value.username)) {
message._mentions = true;
}
if (userInfoStore.value && (message.author_id === userInfoStore.value.id || userInfoStore.value.is_superuser)) {
message._editable = true;
}
if (previous && (message._createdAtDate.getTime() - previous._createdAtDate.getTime()) <= 100 * 1000) {
message._clumped = true;
}
if (previous && (previous._createdAtDateString !== message._createdAtDateString)) {
message._showDateMarkerAbove = true;
}
return message;
}
setMessage(id, message) { setMessage(id, message) {
const index = this.value.findIndex(e => e.id === id); const index = this.value.findIndex(e => e.id === id);
if (index === -1) if (index === -1)
return; return;
this.value[index] = message; this.value[index] = this._processMessage(message, this.value[index - 1]);
this.updated(); this.updated();
} }
@ -155,13 +182,13 @@ class MessageStore extends Store {
if (message.optimistic_id) { if (message.optimistic_id) {
const index = this.value.findIndex(e => e.id === message.optimistic_id); const index = this.value.findIndex(e => e.id === message.optimistic_id);
if (index !== -1) { if (index !== -1) {
this.value[index] = message; this.value[index] = this._processMessage(message, this.value[index - 1]);
this.updated(); this.updated();
return; return;
} }
} }
this.value.push(message); this.value.push(this._processMessage(message, this.value[this.value.length - 1]));
// only dispatch update if collectOldMessages didn't // only dispatch update if collectOldMessages didn't
if (!this.collectOldMessages()) { if (!this.collectOldMessages()) {
this.updated(); this.updated();
@ -183,7 +210,7 @@ class MessageStore extends Store {
if (index === -1) if (index === -1)
return; return;
this.value[index] = message; this.value[index] = this._processMessage(message, this.value[index - 1]);
this.updated(); this.updated();
} }
@ -194,6 +221,7 @@ class MessageStore extends Store {
return; return;
this.value.splice(index, 1); this.value.splice(index, 1);
this._recomputeMessages();
this.updated(); this.updated();
} }
@ -231,6 +259,11 @@ class MessageStore extends Store {
if (beforeCommitToStore) if (beforeCommitToStore)
beforeCommitToStore(res.json); beforeCommitToStore(res.json);
res.json.reverse(); res.json.reverse();
for (let i = 0; i < res.json.length; i++) {
const message = res.json[i];
const previous = res.json[i - 1];
res.json[i] = this._processMessage(message, previous);
}
this.value = res.json.concat(this.value); this.value = res.json.concat(this.value);
this.updated(); this.updated();
} else { } else {

View file

@ -1,3 +1,3 @@
export const getMessageById = "SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id = $1"; export const getMessageById = "SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id = $1";
export const getMessagesByChannelFirstPage = "SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.channel_id = $1 ORDER BY id DESC LIMIT 50"; export const getMessagesByChannelFirstPage = (limit: number) => `SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.channel_id = $1 ORDER BY id DESC LIMIT ${limit}`;
export const getMessagesByChannelPage = "SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id < $1 AND messages.channel_id = $2 ORDER BY id DESC LIMIT 50"; export const getMessagesByChannelPage = (limit: number) => `SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id < $1 AND messages.channel_id = $2 ORDER BY id DESC LIMIT ${limit}`;

View file

@ -228,22 +228,29 @@ router.get(
"/:id/messages", "/:id/messages",
authenticateRoute(), authenticateRoute(),
param("id").isNumeric(), param("id").isNumeric(),
param("count").optional().isInt({ min: 10, max: 50 }),
async (req, res) => { async (req, res) => {
const validationErrors = validationResult(req); const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) { if (!validationErrors.isEmpty()) {
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() }); return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
} }
const { before } = req.query; const { before, count } = req.query;
let limit = typeof count === "string" ? parseInt(count || "25") : 25;
if (limit === NaN) {
return res.status(400).json({ ...errors.INVALID_DATA });
}
const channelId = parseInt(req.params.id); const channelId = parseInt(req.params.id);
let finalRows = []; let finalRows = [];
if (before) { if (before) {
const result = await query(getMessagesByChannelPage, [before, channelId]); const result = await query(getMessagesByChannelPage(limit), [before, channelId]);
finalRows = result ? result.rows : []; finalRows = result ? result.rows : [];
} else { } else {
const result = await query(getMessagesByChannelFirstPage, [channelId]); const result = await query(getMessagesByChannelFirstPage(limit), [channelId]);
finalRows = result ? result.rows : []; finalRows = result ? result.rows : [];
} }