109 lines
2.9 KiB
JavaScript
109 lines
2.9 KiB
JavaScript
import { Controller } from "@hotwired/stimulus"
|
|
import { nextFrame } from "helpers/timing_helpers"
|
|
|
|
export default class extends Controller {
|
|
static SCROLL_TRESHOLD = 100
|
|
static EMOJI_MATCHER = /^(\p{Emoji_Presentation}|\p{Extended_Pictographic}|\uFE0F)+$/gu
|
|
|
|
static targets = [ "messages", "messageTemplate" ]
|
|
static classes = [ "failedMessage", "emojiMessage" ]
|
|
|
|
connect() {
|
|
this.scrollToBottom()
|
|
}
|
|
|
|
failPendingMessage(clientMessageId) {
|
|
const message = this.element.querySelector(`#message_${clientMessageId}`)
|
|
|
|
if (message) {
|
|
message.classList.add(this.failedMessageClass)
|
|
}
|
|
}
|
|
|
|
async insertPendingMessage(clientMessageId, content) {
|
|
const html = this.#pendingMessageHtml({
|
|
clientMessageId: clientMessageId,
|
|
content: content,
|
|
extraClasses: this.#allEmoji(content) ? this.emojiMessageClass : ""
|
|
})
|
|
|
|
this.messagesTarget.insertAdjacentHTML("beforeend", html)
|
|
|
|
await nextFrame()
|
|
this.scrollToBottom()
|
|
}
|
|
|
|
scrollToBottom() {
|
|
this.element.scrollTop = this.element.scrollHeight
|
|
}
|
|
|
|
beforeStreamRender(event) {
|
|
const target = event.detail.newStream.getAttribute("target")
|
|
|
|
if (target === this.messagesTarget.id) {
|
|
this.#handleScrollPositionOnTurboStreamRender(event)
|
|
}
|
|
}
|
|
|
|
#pendingMessageHtml(data) {
|
|
let html = this.messageTemplateTarget.innerHTML
|
|
|
|
for (const key in data) {
|
|
html = html.replaceAll(`$${key}$`, data[key])
|
|
}
|
|
|
|
return html
|
|
}
|
|
|
|
#allEmoji(content) {
|
|
return content.match(this.constructor.EMOJI_MATCHER)
|
|
}
|
|
|
|
#handleScrollPositionOnTurboStreamRender(event) {
|
|
if (event.detail.newStream.action === "append") {
|
|
this.#scrollToBottomOnTurboStreamRender(event)
|
|
} else {
|
|
this.#preserveScrollPositionOnTurboStreamRender(event)
|
|
}
|
|
}
|
|
|
|
#scrollToBottomOnTurboStreamRender(event) {
|
|
const render = event.detail.render
|
|
|
|
event.detail.render = async (streamElement) => {
|
|
await render(streamElement)
|
|
await nextFrame()
|
|
this.scrollToBottom()
|
|
}
|
|
}
|
|
|
|
#preserveScrollPositionOnTurboStreamRender(event) {
|
|
const render = event.detail.render
|
|
|
|
event.detail.render = async (streamElement) => {
|
|
const previousScrollHeight = this.element.scrollHeight
|
|
const previousScrollTop = this.element.scrollTop
|
|
const previouslyWasAtBottom = this.#scrolledToBottom
|
|
|
|
await render(streamElement)
|
|
|
|
if (previouslyWasAtBottom) {
|
|
this.messagesTarget.scrollTop = this.messagesTarget.scrollHeight
|
|
} else {
|
|
const newScrollHeight = this.element.scrollHeight
|
|
const scrollDelta = newScrollHeight - previousScrollHeight
|
|
this.element.scrollTop = previousScrollTop + scrollDelta
|
|
}
|
|
|
|
await nextFrame()
|
|
}
|
|
}
|
|
|
|
get #scrolledToBottom() {
|
|
const scrollBottom = this.element.scrollTop + this.element.clientHeight
|
|
const contentBottom = this.element.scrollHeight - this.constructor.SCROLL_THRESHOLD
|
|
|
|
return scrollBottom >= contentBottom
|
|
}
|
|
}
|