1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static values = { 5 url: String, 6 batchSize: { type: Number, default: 50 }, 7 signedIn: { type: Boolean, default: false } 8 } 9 10 static targets = ["skeleton"] 11 12 connect() { 13 this.loadAllBatches() 14 } 15 16 async loadAllBatches() { 17 const skeletons = this.skeletonTargets 18 if (skeletons.length === 0) return 19 20 const allIds = skeletons.map(el => { 21 return el.closest(".message-card").dataset.messageId 22 }) 23 24 for (let i = 0; i < allIds.length; i += this.batchSizeValue) { 25 const batchIds = allIds.slice(i, i + this.batchSizeValue) 26 await this.fetchAndInjectBatch(batchIds) 27 } 28 } 29 30 async fetchAndInjectBatch(ids) { 31 const url = `${this.urlValue}?ids=${ids.join(",")}` 32 try { 33 const response = await fetch(url) 34 if (!response.ok) return 35 const html = await response.text() 36 this.injectBatch(html) 37 } catch (e) { 38 console.warn("batch load failed", e) 39 } 40 } 41 42 findScrollAnchor() { 43 if (window.location.hash) { 44 const el = document.getElementById(window.location.hash.slice(1)) 45 if (el) return el 46 } 47 for (const card of this.element.querySelectorAll('.message-card')) { 48 const rect = card.getBoundingClientRect() 49 if (rect.bottom > 0 && rect.top < window.innerHeight) return card 50 } 51 return null 52 } 53 54 injectBatch(html) { 55 const template = document.createElement("template") 56 template.innerHTML = html 57 58 const anchor = this.findScrollAnchor() 59 const anchorTop = anchor ? anchor.getBoundingClientRect().top : null 60 61 template.content.querySelectorAll("[data-message-id]").forEach(batchItem => { 62 const messageId = batchItem.dataset.messageId 63 const card = this.element.querySelector( 64 `.message-card[data-message-id="${messageId}"]` 65 ) 66 if (!card) return 67 68 const skeleton = card.querySelector(".message-batch-skeleton") 69 if (!skeleton) return // already rendered inline, skip 70 71 const isRead = card.dataset.read === "true" 72 73 // The batchItem contains a div[data-message-id] wrapping .message-content 74 // Extract the .message-content element 75 const content = batchItem.firstElementChild 76 if (!content) return 77 78 if (isRead) { 79 content.classList.add("is-read") 80 } 81 82 skeleton.replaceWith(content) 83 84 if (this.signedInValue) { 85 const messageContent = card.querySelector(".message-content") 86 if (messageContent) { 87 messageContent.dataset.controller = "read-status" 88 messageContent.dataset.readStatusMessageIdValue = messageId 89 messageContent.dataset.readStatusReadUrlValue = `/messages/${messageId}/read.json` 90 messageContent.dataset.readStatusDelaySecondsValue = "5" 91 } 92 } 93 }) 94 95 if (anchor && anchorTop !== null) { 96 const drift = anchor.getBoundingClientRect().top - anchorTop 97 if (Math.abs(drift) > 1) { 98 window.scrollBy(0, drift) 99 } 100 } 101 } 102}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.