Thread viewer for Bluesky
1class PrivateSearchPage {
2
3 /** @type {number | undefined} */
4 fetchStartTime;
5
6 constructor() {
7 this.pageElement = $id('private_search_page');
8
9 this.rangeInput = $(this.pageElement.querySelector('input[type="range"]'), HTMLInputElement);
10 this.submitButton = $(this.pageElement.querySelector('input[type="submit"]'), HTMLInputElement);
11 this.progressBar = $(this.pageElement.querySelector('input[type="submit"] + progress'), HTMLProgressElement);
12 this.archiveStatus = $(this.pageElement.querySelector('.archive-status'));
13
14 this.searchLine = $(this.pageElement.querySelector('.search'));
15 this.searchField = $(this.pageElement.querySelector('.search-query'), HTMLInputElement);
16 this.results = $(this.pageElement.querySelector('.results'));
17
18 this.timelinePosts = [];
19
20 this.setupEvents();
21
22 let params = new URLSearchParams(location.search);
23 this.mode = params.get('mode');
24 this.lycanMode = params.get('lycan');
25
26 if (this.lycanMode == 'local') {
27 this.lycan = new BlueskyAPI('http://localhost:3000', false);
28 }
29 }
30
31 setupEvents() {
32 $(this.pageElement.querySelector('form')).addEventListener('submit', (e) => {
33 e.preventDefault();
34
35 if (!this.fetchStartTime) {
36 this.fetchTimeline();
37 } else {
38 this.stopFetch();
39 }
40 });
41
42 this.rangeInput.addEventListener('input', (e) => {
43 let days = parseInt(this.rangeInput.value, 10);
44 let label = $(this.pageElement.querySelector('input[type=range] + label'));
45 label.innerText = (days == 1) ? '1 day' : `${days} days`;
46 });
47
48 this.searchField.addEventListener('keydown', (e) => {
49 if (e.key == 'Enter') {
50 e.preventDefault();
51
52 let query = this.searchField.value.trim().toLowerCase();
53
54 if (this.mode == 'likes') {
55 this.searchInLycan(query);
56 } else {
57 this.searchInTimeline(query);
58 }
59 }
60 });
61 }
62
63 /** @returns {number} */
64
65 selectedDaysRange() {
66 return parseInt(this.rangeInput.value, 10);
67 }
68
69 show() {
70 this.pageElement.style.display = 'block';
71
72 if (this.mode == 'likes') {
73 this.pageElement.querySelector('.timeline-search').style.display = 'none';
74 this.searchLine.style.display = 'block';
75 } else {
76 this.pageElement.querySelector('.timeline-search').style.display = 'block';
77 }
78 }
79
80 /** @returns {Promise<void>} */
81
82 async fetchTimeline() {
83 this.submitButton.value = 'Cancel';
84
85 let requestedDays = this.selectedDaysRange();
86
87 this.progressBar.max = requestedDays;
88 this.progressBar.value = 0;
89 this.progressBar.style.display = 'inline';
90
91 let startTime = new Date().getTime();
92 this.fetchStartTime = startTime;
93
94 let timeline = await accountAPI.loadHomeTimeline(requestedDays, {
95 onPageLoad: (data) => {
96 if (this.fetchStartTime != startTime) {
97 return { cancel: true };
98 }
99
100 this.updateProgress(data, startTime);
101 }
102 });
103
104 if (this.fetchStartTime != startTime) {
105 return;
106 }
107
108 let last = timeline.at(-1);
109 let daysBack;
110
111 if (last) {
112 let lastDate = feedPostTime(last);
113 daysBack = Math.round((startTime - lastDate) / 86400 / 1000);
114 } else {
115 daysBack = 0;
116 }
117
118 this.timelinePosts = timeline.map(x => Post.parseFeedPost(x));
119
120 this.archiveStatus.innerText = "Timeline archive fetched: " + ((daysBack == 1) ? '1 day' : `${daysBack} days`);
121 this.searchLine.style.display = 'block';
122
123 this.submitButton.value = 'Fetch timeline';
124 this.progressBar.style.display = 'none';
125 this.fetchStartTime = undefined;
126 }
127
128 /** @param {string} query */
129
130 searchInTimeline(query) {
131 this.results.innerHTML = '';
132
133 if (query.length == 0) {
134 return;
135 }
136
137 let matching = this.timelinePosts.filter(x => x.lowercaseText.includes(query));
138
139 for (let post of matching) {
140 let postView = new PostComponent(post, 'feed').buildElement();
141 this.results.appendChild(postView);
142 }
143 }
144
145 /** @param {string} query */
146
147 searchInLycan(query) {
148 this.results.innerHTML = '';
149
150 if (query.length == 0) {
151 return;
152 }
153
154 let loading = $tag('p', { text: "..." });
155 this.results.append(loading);
156
157 let isLoading = false;
158 let firstPageLoaded = false;
159 let cursor;
160 let finished = false;
161
162 Paginator.loadInPages(async () => {
163 if (isLoading || finished) { return; }
164 isLoading = true;
165
166 let response;
167
168 if (this.lycanMode == 'local') {
169 let params = { query: query, user: window.accountAPI.user.did };
170 if (cursor) params.cursor = cursor;
171
172 response = await this.lycan.getRequest('blue.feeds.lycan.searchPosts', params);
173 } else {
174 let params = { query: query };
175 if (cursor) params.cursor = cursor;
176
177 response = await accountAPI.getRequest('blue.feeds.lycan.searchPosts', params, {
178 headers: { 'atproto-proxy': 'did:web:lycan.feeds.blue#lycan' }
179 });
180 }
181
182 if (response.posts.length == 0) {
183 let p = $tag('p.results-end', { text: firstPageLoaded ? "No more results." : "No results." });
184 loading.remove();
185 this.results.append(p);
186
187 isLoading = false;
188 finished = true;
189 return;
190 }
191
192 let records = await window.accountAPI.loadPosts(response.posts);
193 let posts = records.map(x => new Post(x));
194
195 if (!firstPageLoaded) {
196 loading.remove();
197 firstPageLoaded = true;
198 }
199
200 for (let post of posts) {
201 let postView = new PostComponent(post, 'feed').buildElement();
202 this.results.appendChild(postView);
203 }
204
205 isLoading = false;
206 cursor = response.cursor;
207
208 if (!cursor) {
209 finished = true;
210 this.results.append("No more results.");
211 }
212 });
213 }
214
215 /** @param {json[]} dataPage, @param {number} startTime */
216
217 updateProgress(dataPage, startTime) {
218 let last = dataPage.at(-1);
219
220 if (!last) { return }
221
222 let lastDate = feedPostTime(last);
223 let daysBack = (startTime - lastDate) / 86400 / 1000;
224
225 this.progressBar.value = daysBack;
226 }
227
228 stopFetch() {
229 this.submitButton.value = 'Fetch timeline';
230 this.progressBar.style.display = 'none';
231 this.fetchStartTime = undefined;
232 }
233}