import { marked } from 'marked'; import DOMPurify from 'dompurify'; const RUN_COALESCE_MAX_INTERVAL = 10 /* min */ * 60 /* sec */ * 1000; /* ms */ export class Messages { channels = $state({}); inChannel(channel) { return this.channels[channel]; } addMessage(channel, id, { at, sender, body }) { let parsedAt = new Date(at); let renderedBody = DOMPurify.sanitize(marked.parse(body, { breaks: true })); const message = { id, at: parsedAt, body, renderedBody }; // You might be thinking, can't this be // // let runs = (this.channels[channel] ||= []); // // Let me tell you, I thought that too. Javascript's semantics allow it. It // didn't work - the first message in each channel was getting lost as the // update to `this.channels` wasn't actually happening. I suspect this is // due to the implementation of Svelte's `$state` rune, but I don't know it // for sure. // // In any case, splitting the read and write up like this has the same // semantics, and _works_. (This time, for sure!) let runs = this.channels[channel] || []; let currentRun = runs.slice(-1)[0]; if (currentRun === undefined) { currentRun = { sender, messages: [message] }; runs.push(currentRun); } else { let lastMessage = currentRun.messages.slice(-1)[0]; let newRun = currentRun.sender !== sender || parsedAt - lastMessage.at > RUN_COALESCE_MAX_INTERVAL; if (newRun) { currentRun = { sender, messages: [message] }; runs.push(currentRun); } else { currentRun.messages.push(message); } } this.channels[channel] = runs; return this; } setMessages(messages) { this.channels = {}; for (let { channel, id, at, sender, body } of messages) { this.addMessage(channel, id, { at, sender, body }); } return this; } deleteMessage(messageId) { for (let channel in this.channels) { this.channels[channel] = this.channels[channel] .map(({ sender, messages }) => ({ sender, messages: messages.filter(({ id }) => id != messageId) })) .filter(({ messages }) => messages.length > 0); } return this; } deleteChannel(id) { delete this.channels[id]; return this; } }