blob: 64813e4ee8d3468ae417921eb673325e92dfc816 [file] [log] [blame]
avm99963d3f4ac02021-08-12 18:36:58 +02001import {CCApi} from '../../common/api.js';
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +02002import {getAuthUser} from '../../common/communityConsoleUtils.js';
3
avm99963d3f4ac02021-08-12 18:36:58 +02004import {createExtBadge} from './utils/common.js';
5
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +02006var authuser = getAuthUser();
7
avm99963b6f68b62021-08-12 23:13:06 +02008const threadListRequestEvent = 'TWPT_ViewForumRequest';
Adrià Vilanova Martínez8ef13d42021-08-16 10:07:26 +02009const intervalMs = 3 * 60 * 1000; // 3 minutes
avm99963d3f4ac02021-08-12 18:36:58 +020010const firstCallDelayMs = 3 * 1000; // 3 seconds
11
12export default class AutoRefresh {
13 constructor() {
14 this.isLookingForUpdates = false;
15 this.isUpdatePromptShown = false;
16 this.lastTimestamp = null;
17 this.filter = null;
18 this.path = null;
avm99963b6f68b62021-08-12 23:13:06 +020019 this.requestId = null;
20 this.requestOrderOptions = null;
avm99963d3f4ac02021-08-12 18:36:58 +020021 this.snackbar = null;
22 this.interval = null;
23 this.firstCallTimeout = null;
avm99963b6f68b62021-08-12 23:13:06 +020024
25 this.setUpHandlers();
Adrià Vilanova Martínez8f723682021-08-16 11:48:02 +020026 this.injectUpdatePrompt();
avm99963d3f4ac02021-08-12 18:36:58 +020027 }
28
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020029 getStartupData() {
30 return JSON.parse(
31 document.querySelector('html').getAttribute('data-startup'));
avm99963d3f4ac02021-08-12 18:36:58 +020032 }
33
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020034 isOrderedByTimestampDescending() {
avm99963b6f68b62021-08-12 23:13:06 +020035 // This means we didn't intercept the request.
Adrià Vilanova Martínez8ef13d42021-08-16 10:07:26 +020036 if (!this.requestOrderOptions) return false;
avm99963b6f68b62021-08-12 23:13:06 +020037
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020038 // Returns orderOptions.by == TIMESTAMP && orderOptions.desc == true
39 return (
avm99963b6f68b62021-08-12 23:13:06 +020040 this.requestOrderOptions?.[1] == 1 &&
41 this.requestOrderOptions?.[2] == true);
avm99963d3f4ac02021-08-12 18:36:58 +020042 }
43
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020044 getCustomFilter(path) {
45 var searchRegex = /^\/s\/community\/search\/([^\/]*)/;
46 var matches = path.match(searchRegex);
47 if (matches !== null && matches.length > 1) {
48 var search = decodeURIComponent(matches[1]);
49 var params = new URLSearchParams(search);
50 return params.get('query') || '';
51 }
52
53 return '';
avm99963d3f4ac02021-08-12 18:36:58 +020054 }
55
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020056 filterHasOverride(filter, override) {
57 var escapedOverride = override.replace(/([^\w\d\s])/gi, '\\$1');
58 var regex = new RegExp('[^a-zA-Z0-9]?' + escapedOverride + ':');
59 return regex.test(filter);
avm99963d3f4ac02021-08-12 18:36:58 +020060 }
61
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020062 getFilter(path) {
63 var query = this.getCustomFilter(path);
64
65 // Note: This logic has been copied and adapted from the
66 // _buildQuery$1$threadId function in the Community Console
67 var conditions = '';
68 var startup = this.getStartupData();
69
70 // TODO(avm99963): if the selected forums are changed without reloading the
71 // page, this will get the old selected forums. Fix this.
72 var forums = startup?.[1]?.[1]?.[3]?.[8] ?? [];
73 if (!this.filterHasOverride(query, 'forum') && forums !== null &&
74 forums.length > 0)
75 conditions += ' forum:(' + forums.join(' | ') + ')';
76
77 var langs = startup?.[1]?.[1]?.[3]?.[5] ?? [];
78 if (!this.filterHasOverride(query, 'lang') && langs !== null &&
79 langs.length > 0)
80 conditions += ' lang:(' + langs.map(l => '"' + l + '"').join(' | ') + ')';
81
82 if (query.length !== 0 && conditions.length !== 0)
83 return '(' + query + ')' + conditions;
84 return query + conditions;
avm99963d3f4ac02021-08-12 18:36:58 +020085 }
86
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020087 getLastTimestamp() {
avm99963d3f4ac02021-08-12 18:36:58 +020088 return CCApi(
89 'ViewForum', {
90 1: '0', // TODO: Change, when only a forum is selected, it
91 // should be set here
92 // options
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020093 2: {
avm99963d3f4ac02021-08-12 18:36:58 +020094 // pagination
95 1: {
96 2: 2, // maxNum
97 },
98 // order
99 2: {
100 1: 1, // by
101 2: true, // desc
102 },
103 12: this.filter, // forumViewFilters
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200104 },
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200105 },
avm99963d3f4ac02021-08-12 18:36:58 +0200106 /* authenticated = */ true, authuser)
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200107 .then(body => {
108 var timestamp = body?.[1]?.[2]?.[0]?.[2]?.[17];
109 if (timestamp === undefined)
110 throw new Error(
111 'Unexpected body of response (' +
112 (body?.[1]?.[2]?.[0] === undefined ?
113 'no threads were returned' :
114 'the timestamp value is not present in the first thread') +
115 ').');
116
117 return timestamp;
118 });
119 // TODO(avm99963): Add retry mechanism (sometimes thread lists are empty,
120 // but when loading the next page the thread appears).
121 //
122 // NOTE(avm99963): It seems like loading the first 2 threads instead of only
123 // the first one fixes this (empty lists are now rarely returned).
avm99963d3f4ac02021-08-12 18:36:58 +0200124 }
125
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200126 unregister() {
127 console.debug('autorefresh_list: unregistering');
128
129 if (!this.isLookingForUpdates) return;
130
131 window.clearTimeout(this.firstCallTimeout);
132 window.clearInterval(this.interval);
133 this.isUpdatePromptShown = false;
134 this.isLookingForUpdates = false;
avm99963d3f4ac02021-08-12 18:36:58 +0200135 }
136
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200137 showUpdatePrompt() {
138 this.snackbar.classList.remove('TWPT-hidden');
139 document.title = '[!!!] ' + document.title.replace('[!!!] ', '');
140 this.isUpdatePromptShown = true;
avm99963d3f4ac02021-08-12 18:36:58 +0200141 }
142
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200143 hideUpdatePrompt() {
144 this.snackbar.classList.add('TWPT-hidden');
145 document.title = document.title.replace('[!!!] ', '');
146 this.isUpdatePromptShown = false;
avm99963d3f4ac02021-08-12 18:36:58 +0200147 }
148
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200149 injectUpdatePrompt() {
150 var pane = document.createElement('div');
151 pane.classList.add('TWPT-pane-for-snackbar');
152
153 var snackbar = document.createElement('material-snackbar-panel');
154 snackbar.classList.add('TWPT-snackbar');
155 snackbar.classList.add('TWPT-hidden');
156
157 var ac = document.createElement('div');
158 ac.classList.add('TWPT-animation-container');
159
160 var nb = document.createElement('div');
161 nb.classList.add('TWPT-notification-bar');
162
163 var ft = document.createElement('focus-trap');
164
165 var content = document.createElement('div');
166 content.classList.add('TWPT-focus-content-wrapper');
167
168 var badge = createExtBadge();
169
170 var message = document.createElement('div');
171 message.classList.add('TWPT-message');
172 message.textContent =
173 chrome.i18n.getMessage('inject_autorefresh_list_snackbar_message');
174
175 var action = document.createElement('div');
176 action.classList.add('TWPT-action');
177 action.textContent =
178 chrome.i18n.getMessage('inject_autorefresh_list_snackbar_action');
179
180 action.addEventListener('click', e => {
181 this.hideUpdatePrompt();
182 document.querySelector('.app-title-button').click();
183 });
184
185 content.append(badge, message, action);
186 ft.append(content);
187 nb.append(ft);
188 ac.append(nb);
189 snackbar.append(ac);
190 pane.append(snackbar);
191 document.getElementById('default-acx-overlay-container').append(pane);
192 this.snackbar = snackbar;
avm99963d3f4ac02021-08-12 18:36:58 +0200193 }
194
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200195 checkUpdate() {
196 if (location.pathname != this.path) {
197 this.unregister();
198 return;
199 }
200
201 if (this.isUpdatePromptShown) return;
202
203 console.debug('Checking for update at: ', new Date());
204
205 this.getLastTimestamp()
206 .then(timestamp => {
207 if (timestamp != this.lastTimestamp) this.showUpdatePrompt();
208 })
209 .catch(
210 err => console.error(
211 'Coudln\'t get last timestamp (while updating): ', err));
avm99963d3f4ac02021-08-12 18:36:58 +0200212 }
213
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200214 firstCall() {
215 console.debug(
216 'autorefresh_list: now performing first call to finish setup (filter: [' +
217 this.filter + '])');
218
219 if (location.pathname != this.path) {
220 this.unregister();
221 return;
222 }
223
224 this.getLastTimestamp()
225 .then(timestamp => {
226 this.lastTimestamp = timestamp;
227 var checkUpdateCallback = this.checkUpdate.bind(this);
avm99963d3f4ac02021-08-12 18:36:58 +0200228 this.interval = window.setInterval(checkUpdateCallback, intervalMs);
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200229 })
230 .catch(
231 err => console.error(
232 'Couldn\'t get last timestamp (while setting up): ', err));
avm99963d3f4ac02021-08-12 18:36:58 +0200233 }
234
avm99963b6f68b62021-08-12 23:13:06 +0200235 setUpHandlers() {
236 window.addEventListener(
237 threadListRequestEvent, e => this.handleListRequest(e));
238 }
239
240 handleListRequest(e) {
Adrià Vilanova Martínez8ef13d42021-08-16 10:07:26 +0200241 // If the request was made before the last known one, return.
242 if (this.requestId !== null && e.detail.id < this.requestId) return;
243
244 // Ignore ViewForum requests made by the chat feature and the "Mark as
245 // duplicate" dialog.
246 //
247 // All those requests have |maxNum| set to 10 and 20 respectively, while the
248 // request that we want to handle is the initial request to load the thread
249 // list which currently requests 100 threads.
250 var maxNum = e.detail.body?.['2']?.['1']?.['2'];
251 if (maxNum == 10 || maxNum == 20) return;
252
253 // Ignore requests to load more threads in the current thread list. All
254 // those requests include a PaginationToken, and also have |maxNum| set
255 // to 50.
256 var token = e.detail.body?.['2']?.['1']?.['3'];
257 if (token) return;
258
259 console.debug('autorefresh_list: handling valid ViewForum request');
260
261 this.requestId = e.detail.id;
262 this.requestOrderOptions = e.detail.body?.['2']?.['2'];
avm99963b6f68b62021-08-12 23:13:06 +0200263 }
264
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200265 setUp() {
avm99963b6f68b62021-08-12 23:13:06 +0200266 if (!this.isOrderedByTimestampDescending()) {
267 console.debug(
268 'autorefresh_list: refused to start up because the order is not by timestamp descending.');
269 return;
270 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200271
272 this.unregister();
273
274 console.debug('autorefresh_list: starting set up...');
275
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200276 this.isLookingForUpdates = true;
277 this.path = location.pathname;
278 this.filter = this.getFilter(this.path);
279
280 var firstCall = this.firstCall.bind(this);
avm99963d3f4ac02021-08-12 18:36:58 +0200281 this.firstCallTimeout = window.setTimeout(firstCall, firstCallDelayMs);
282 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200283};