PageSourceSearch

https://hackorum.dev/assets/controllers/read_status_controller-148b7a8b.js

js hackorum.dev collected 2026-09-28 07:00:36 UTC 3,636 bytes, 142 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3export default class extends Controller {
4  static values = {
5    messageId: Number,
6    readUrl: String,
7    delaySeconds: Number,
8  }
9
10  connect() {
11    if (this.element.classList.contains("is-read")) {
12      this.marked = true
13      return
14    }
15    this.marked = false
16    this.timer = null
17    this.pollInterval = null
18    this.observe()
19  }
20
21  disconnect() {
22    this.cleanup()
23  }
24
25  observe() {
26    if (!("IntersectionObserver" in window)) return
27
28    this.observer = new IntersectionObserver(entries => {
29      entries.forEach(entry => {
30        if (entry.isIntersecting && this.visibleEnough(entry)) {
31          this.stopPoll()
32          this.startTimer()
33        } else if (entry.isIntersecting && this.needsPollFallback()) {
34          this.startPoll()
35        } else {
36          this.stopPoll()
37          this.clearTimer()
38        }
39      })
40    }, { threshold: [0, 0.25, 0.7] })
41
42    this.observer.observe(this.element)
43  }
44
45  visibleEnough(entry) {
46    const visiblePx = entry.intersectionRect.height
47    const elementHeight = entry.target.getBoundingClientRect().height || 1
48    const ratio = visiblePx / elementHeight
49
50    // Standard case: most of the element is on screen.
51    if (ratio >= 0.7) return true
52
53    // Very tall elements that cannot reach high ratio: accept when a good chunk is visible.
54    const viewportHeight = window.innerHeight || 800
55    const substantialPx = Math.min(viewportHeight * 0.8, 400) // cap to avoid excessive requirement
56    if (visiblePx >= substantialPx) return true
57
58    // Fallback: modest ratio plus reasonable pixels.
59    return ratio >= 0.25 && visiblePx >= 200
60  }
61
62  needsPollFallback() {
63    return this.element.getBoundingClientRect().height > (window.innerHeight || 800)
64  }
65
66  startPoll() {
67    if (this.marked || this.pollInterval || this.timer) return
68    this.pollInterval = setInterval(() => this.checkVisibility(), 1000)
69  }
70
71  stopPoll() {
72    if (this.pollInterval) {
73      clearInterval(this.pollInterval)
74      this.pollInterval = null
75    }
76  }
77
78  checkVisibility() {
79    if (this.marked) { this.stopPoll(); return }
80
81    const rect = this.element.getBoundingClientRect()
82    const viewportHeight = window.innerHeight || 800
83    const visibleTop = Math.max(0, rect.top)
84    const visibleBottom = Math.min(viewportHeight, rect.bottom)
85    const visiblePx = Math.max(0, visibleBottom - visibleTop)
86
87    if (visiblePx <= 0) {
88      this.stopPoll()
89      this.clearTimer()
90      return
91    }
92
93    const substantialPx = Math.min(viewportHeight * 0.8, 400)
94    if (visiblePx >= substantialPx) {
95      this.stopPoll()
96      this.startTimer()
97    }
98  }
99
100  startTimer() {
101    if (this.marked || this.timer) return
102    const delay = (this.delaySecondsValue || 5) * 1000
103    this.timer = setTimeout(() => this.markRead(), delay)
104  }
105
106  clearTimer() {
107    if (this.timer) {
108      clearTimeout(this.timer)
109      this.timer = null
110    }
111  }
112
113  cleanup() {
114    this.clearTimer()
115    this.stopPoll()
116    if (this.observer) {
117      this.observer.disconnect()
118      this.observer = null
119    }
120  }
121
122  async markRead() {
123    if (this.marked) return
124    this.marked = true
125    this.element.classList.add("is-read")
126
127    try {
128      await fetch(this.readUrlValue, {
129        method: "POST",
130        headers: this.csrfHeaders(),
131      })
132    } catch (e) {
133      // If it fails, we leave the optimistic state; we could retry if needed.
134      console.warn("mark read failed", e)
135    }
136  }
137
138  csrfHeaders() {
139    const token = document.querySelector("meta[name=csrf-token]")?.content
140    return token ? { "X-CSRF-Token": token } : {}
141  }
142}

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.