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
23 setupEvents() {
24 $(this.pageElement.querySelector('form')).addEventListener('submit', (e) => {
25 e.preventDefault();
26
27 if (!this.fetchStartTime) {
28 this.fetchTimeline();
29 } else {
30 this.stopFetch();
31 }
32 });
33
34 this.rangeInput.addEventListener('input', (e) => {
35 let days = parseInt(this.rangeInput.value, 10);
36 let label = $(this.pageElement.querySelector('input[type=range] + label'));
37 label.innerText = (days == 1) ? '1 day' : `${days} days`;
38 });
39
40 this.searchField.addEventListener('input', (e) => {
41 let query = this.searchField.value.trim().toLowerCase();
42
43 if (this.searchTimer) {
44 clearTimeout(this.searchTimer);
45 }
46
47 this.searchTimer = setTimeout(() => this.searchInTimeline(query), 100);
48 });
49 }
50
51 /** @returns {number} */
52
53 selectedDaysRange() {
54 return parseInt(this.rangeInput.value, 10);
55 }
56
57 show() {
58 this.pageElement.style.display = 'block';
59 }
60
61 /** @returns {Promise<void>} */
62
63 async fetchTimeline() {
64 this.submitButton.value = 'Cancel';
65
66 let requestedDays = this.selectedDaysRange();
67
68 this.progressBar.max = requestedDays;
69 this.progressBar.value = 0;
70 this.progressBar.style.display = 'inline';
71
72 let startTime = new Date().getTime();
73 this.fetchStartTime = startTime;
74
75 let timeline = await accountAPI.loadHomeTimeline(requestedDays, {
76 onPageLoad: (data) => {
77 if (this.fetchStartTime != startTime) {
78 return { cancel: true };
79 }
80
81 this.updateProgress(data, startTime);
82 }
83 });
84
85 if (this.fetchStartTime != startTime) {
86 return;
87 }
88
89 let last = timeline.at(-1);
90 let daysBack;
91
92 if (last) {
93 let lastTimestamp = last.reason ? last.reason.indexedAt : last.post.record.createdAt;
94 let lastDate = Date.parse(lastTimestamp);
95 daysBack = Math.round((startTime - lastDate) / 86400 / 1000);
96 } else {
97 daysBack = 0;
98 }
99
100 this.timelinePosts = timeline.map(x => Post.parseFeedPost(x));
101
102 this.archiveStatus.innerText = "Timeline archive fetched: " + ((daysBack == 1) ? '1 day' : `${daysBack} days`);
103 this.searchLine.style.display = 'block';
104
105 this.submitButton.value = 'Fetch timeline';
106 this.progressBar.style.display = 'none';
107 this.fetchStartTime = undefined;
108 }
109
110 searchInTimeline(query) {
111 this.results.innerHTML = '';
112
113 if (query.length == 0) {
114 return;
115 }
116
117 let matching = this.timelinePosts.filter(x => x.lowercaseText.includes(query));
118
119 for (let post of matching) {
120 let postView = new PostComponent(post, 'feed').buildElement();
121 this.results.appendChild(postView);
122 }
123 }
124
125 /** @param {json[]} dataPage, @param {number} startTime */
126
127 updateProgress(dataPage, startTime) {
128 if (dataPage.length == 0) { return }
129
130 let last = dataPage.at(-1);
131 let lastTimestamp = last.reason ? last.reason.indexedAt : last.post.record.createdAt;
132 let lastDate = Date.parse(lastTimestamp);
133
134 let daysBack = (startTime - lastDate) / 86400 / 1000;
135 this.progressBar.value = daysBack;
136 }
137
138 stopFetch() {
139 this.submitButton.value = 'Fetch timeline';
140 this.progressBar.style.display = 'none';
141 this.fetchStartTime = undefined;
142 }
143}