main
js 127 lines 4.55 KB
Raw
1 import { createStore } from "/js/AlpineStore.js";
2 import {
3 getMessageWindowState,
4 loadAdjacentMessageWindow,
5 scrollMessageWindowToEdge,
6 } from "/js/messages.js";
7
8 const model = {
9 // Configuration
10 scrollMargin: 60,
11 prevTolerance: 35,
12 nextTolerance: 5,
13
14 init() {
15 // Any initialization if needed
16 },
17
18 async scrollToTop() {
19 await scrollMessageWindowToEdge("start");
20 },
21
22 async scrollToBottom() {
23 await scrollMessageWindowToEdge("end");
24 },
25
26 async scrollToPrevUserMessage() {
27 const scroller = this._getChatHistoryEl();
28 if (!scroller) return;
29
30 const positions = this._getUserMessagePositions(scroller);
31 const scrollerRect = scroller.getBoundingClientRect();
32
33 const prevThreshold = this.scrollMargin - this.prevTolerance; // 25px
34
35 const currentIndex = positions.findIndex((p) => {
36 const relativeTop = p.el.getBoundingClientRect().top - scrollerRect.top;
37 return relativeTop >= prevThreshold;
38 });
39
40 if (currentIndex > 0) {
41 // Go to previous message
42 positions[currentIndex - 1].el.scrollIntoView({ block: "start", behavior: "smooth" });
43 } else if (currentIndex === 0) {
44 if (getMessageWindowState().hasOlder) {
45 await loadAdjacentMessageWindow("older");
46 this._scrollToLastUserAboveThreshold(scroller, prevThreshold);
47 } else {
48 scroller.scrollTo({ top: 0, behavior: "instant" });
49 }
50 } else if (currentIndex === -1 && positions.length > 0) {
51 // All messages are above the threshold (scrolled past), scroll to bottom
52 positions[positions.length - 1].el.scrollIntoView({ block: "start", behavior: "smooth" });
53 } else if (positions.length === 0 && getMessageWindowState().hasOlder) {
54 await loadAdjacentMessageWindow("older");
55 this._scrollToLastUserAboveThreshold(scroller, prevThreshold);
56 }
57 },
58
59 async scrollToNextUserMessage() {
60 const scroller = this._getChatHistoryEl();
61 if (!scroller) return;
62
63 const positions = this._getUserMessagePositions(scroller);
64 const scrollerRect = scroller.getBoundingClientRect();
65
66 const nextThreshold = this.scrollMargin + this.nextTolerance; // 65px
67
68 // Find first message below the threshold
69 const targetIndex = positions.findIndex((p) => {
70 const relativeTop = p.el.getBoundingClientRect().top - scrollerRect.top;
71 return relativeTop > nextThreshold;
72 });
73
74 if (targetIndex !== -1) {
75 // Go to that message
76 positions[targetIndex].el.scrollIntoView({ block: "start", behavior: "smooth" });
77 } else {
78 if (getMessageWindowState().hasNewer) {
79 await loadAdjacentMessageWindow("newer");
80 this._scrollToFirstUserBelowThreshold(scroller, nextThreshold);
81 } else {
82 await this.scrollToBottom();
83 }
84 }
85 },
86
87 _scrollToLastUserAboveThreshold(scroller, threshold) {
88 const scrollerRect = scroller.getBoundingClientRect();
89 const positions = this._getUserMessagePositions(scroller);
90 const candidates = positions.filter(
91 ({ el }) => el.getBoundingClientRect().top - scrollerRect.top < threshold
92 );
93 candidates.at(-1)?.el.scrollIntoView({ block: "start", behavior: "smooth" });
94 },
95
96 _scrollToFirstUserBelowThreshold(scroller, threshold) {
97 const scrollerRect = scroller.getBoundingClientRect();
98 const target = this._getUserMessagePositions(scroller).find(
99 ({ el }) => el.getBoundingClientRect().top - scrollerRect.top > threshold
100 );
101 target?.el.scrollIntoView({ block: "start", behavior: "smooth" });
102 },
103
104 // Helpers
105 _getChatHistoryEl() {
106 return document.getElementById("chat-history");
107 },
108
109 _getUserMessagePositions(scroller) {
110 const userMessageEls = Array.from(
111 scroller.querySelectorAll(".message-container.user-container")
112 );
113 // Helper for getElementTopInScroller
114 const getTop = (el) => {
115 const elRect = el.getBoundingClientRect();
116 const scrollerRect = scroller.getBoundingClientRect();
117 return elRect.top - scrollerRect.top + scroller.scrollTop;
118 };
119
120 return userMessageEls
121 .map((el) => ({ el, top: getTop(el) }))
122 .sort((a, b) => a.top - b.top);
123 }
124 };
125
126 const store = createStore("chatNavigation", model);
127 export { store };