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 lastDate = feedPostTime(last);
94 daysBack = Math.round((startTime - lastDate) / 86400 / 1000);
95 } else {
96 daysBack = 0;
97 }
98
99 this.timelinePosts = timeline.map(x => Post.parseFeedPost(x));
100
101 this.archiveStatus.innerText = "Timeline archive fetched: " + ((daysBack == 1) ? '1 day' : `${daysBack} days`);
102 this.searchLine.style.display = 'block';
103
104 this.submitButton.value = 'Fetch timeline';
105 this.progressBar.style.display = 'none';
106 this.fetchStartTime = undefined;
107 }
108
109 searchInTimeline(query) {
110 this.results.innerHTML = '';
111
112 if (query.length == 0) {
113 return;
114 }
115
116 let matching = this.timelinePosts.filter(x => x.lowercaseText.includes(query));
117
118 for (let post of matching) {
119 let postView = new PostComponent(post, 'feed').buildElement();
120 this.results.appendChild(postView);
121 }
122 }
123
124 /** @param {json[]} dataPage, @param {number} startTime */
125
126 updateProgress(dataPage, startTime) {
127 let last = dataPage.at(-1);
128
129 if (!last) { return }
130
131 let lastDate = feedPostTime(last);
132 let daysBack = (startTime - lastDate) / 86400 / 1000;
133
134 this.progressBar.value = daysBack;
135 }
136
137 stopFetch() {
138 this.submitButton.value = 'Fetch timeline';
139 this.progressBar.style.display = 'none';
140 this.fetchStartTime = undefined;
141 }
142}