blob: 38e83f3ca8aadfdeb75b3f1f9e676a49550d40ae [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();
avm99963d3f4ac02021-08-12 18:36:58 +020026 }
27
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020028 getStartupData() {
29 return JSON.parse(
30 document.querySelector('html').getAttribute('data-startup'));
avm99963d3f4ac02021-08-12 18:36:58 +020031 }
32
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020033 isOrderedByTimestampDescending() {
avm99963b6f68b62021-08-12 23:13:06 +020034 // This means we didn't intercept the request.
Adrià Vilanova Martínez8ef13d42021-08-16 10:07:26 +020035 if (!this.requestOrderOptions) return false;
avm99963b6f68b62021-08-12 23:13:06 +020036
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020037 // Returns orderOptions.by == TIMESTAMP && orderOptions.desc == true
38 return (
avm99963b6f68b62021-08-12 23:13:06 +020039 this.requestOrderOptions?.[1] == 1 &&
40 this.requestOrderOptions?.[2] == true);
avm99963d3f4ac02021-08-12 18:36:58 +020041 }
42
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020043 getCustomFilter(path) {
44 var searchRegex = /^\/s\/community\/search\/([^\/]*)/;
45 var matches = path.match(searchRegex);
46 if (matches !== null && matches.length > 1) {
47 var search = decodeURIComponent(matches[1]);
48 var params = new URLSearchParams(search);
49 return params.get('query') || '';
50 }
51
52 return '';
avm99963d3f4ac02021-08-12 18:36:58 +020053 }
54
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020055 filterHasOverride(filter, override) {
56 var escapedOverride = override.replace(/([^\w\d\s])/gi, '\\$1');
57 var regex = new RegExp('[^a-zA-Z0-9]?' + escapedOverride + ':');
58 return regex.test(filter);
avm99963d3f4ac02021-08-12 18:36:58 +020059 }
60
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020061 getFilter(path) {
62 var query = this.getCustomFilter(path);
63
64 // Note: This logic has been copied and adapted from the
65 // _buildQuery$1$threadId function in the Community Console
66 var conditions = '';
67 var startup = this.getStartupData();
68
69 // TODO(avm99963): if the selected forums are changed without reloading the
70 // page, this will get the old selected forums. Fix this.
71 var forums = startup?.[1]?.[1]?.[3]?.[8] ?? [];
72 if (!this.filterHasOverride(query, 'forum') && forums !== null &&
73 forums.length > 0)
74 conditions += ' forum:(' + forums.join(' | ') + ')';
75
76 var langs = startup?.[1]?.[1]?.[3]?.[5] ?? [];
77 if (!this.filterHasOverride(query, 'lang') && langs !== null &&
78 langs.length > 0)
79 conditions += ' lang:(' + langs.map(l => '"' + l + '"').join(' | ') + ')';
80
81 if (query.length !== 0 && conditions.length !== 0)
82 return '(' + query + ')' + conditions;
83 return query + conditions;
avm99963d3f4ac02021-08-12 18:36:58 +020084 }
85
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020086 getLastTimestamp() {
avm99963d3f4ac02021-08-12 18:36:58 +020087 return CCApi(
88 'ViewForum', {
89 1: '0', // TODO: Change, when only a forum is selected, it
90 // should be set here
91 // options
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020092 2: {
avm99963d3f4ac02021-08-12 18:36:58 +020093 // pagination
94 1: {
95 2: 2, // maxNum
96 },
97 // order
98 2: {
99 1: 1, // by
100 2: true, // desc
101 },
102 12: this.filter, // forumViewFilters
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200103 },
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200104 },
avm99963d3f4ac02021-08-12 18:36:58 +0200105 /* authenticated = */ true, authuser)
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200106 .then(body => {
107 var timestamp = body?.[1]?.[2]?.[0]?.[2]?.[17];
108 if (timestamp === undefined)
109 throw new Error(
110 'Unexpected body of response (' +
111 (body?.[1]?.[2]?.[0] === undefined ?
112 'no threads were returned' :
113 'the timestamp value is not present in the first thread') +
114 ').');
115
116 return timestamp;
117 });
118 // TODO(avm99963): Add retry mechanism (sometimes thread lists are empty,
119 // but when loading the next page the thread appears).
120 //
121 // NOTE(avm99963): It seems like loading the first 2 threads instead of only
122 // the first one fixes this (empty lists are now rarely returned).
avm99963d3f4ac02021-08-12 18:36:58 +0200123 }
124
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200125 unregister() {
126 console.debug('autorefresh_list: unregistering');
127
128 if (!this.isLookingForUpdates) return;
129
130 window.clearTimeout(this.firstCallTimeout);
131 window.clearInterval(this.interval);
132 this.isUpdatePromptShown = false;
133 this.isLookingForUpdates = false;
avm99963d3f4ac02021-08-12 18:36:58 +0200134 }
135
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200136 showUpdatePrompt() {
137 this.snackbar.classList.remove('TWPT-hidden');
138 document.title = '[!!!] ' + document.title.replace('[!!!] ', '');
139 this.isUpdatePromptShown = true;
avm99963d3f4ac02021-08-12 18:36:58 +0200140 }
141
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200142 hideUpdatePrompt() {
143 this.snackbar.classList.add('TWPT-hidden');
144 document.title = document.title.replace('[!!!] ', '');
145 this.isUpdatePromptShown = false;
avm99963d3f4ac02021-08-12 18:36:58 +0200146 }
147
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200148 injectUpdatePrompt() {
149 var pane = document.createElement('div');
150 pane.classList.add('TWPT-pane-for-snackbar');
151
152 var snackbar = document.createElement('material-snackbar-panel');
153 snackbar.classList.add('TWPT-snackbar');
154 snackbar.classList.add('TWPT-hidden');
155
156 var ac = document.createElement('div');
157 ac.classList.add('TWPT-animation-container');
158
159 var nb = document.createElement('div');
160 nb.classList.add('TWPT-notification-bar');
161
162 var ft = document.createElement('focus-trap');
163
164 var content = document.createElement('div');
165 content.classList.add('TWPT-focus-content-wrapper');
166
167 var badge = createExtBadge();
168
169 var message = document.createElement('div');
170 message.classList.add('TWPT-message');
171 message.textContent =
172 chrome.i18n.getMessage('inject_autorefresh_list_snackbar_message');
173
174 var action = document.createElement('div');
175 action.classList.add('TWPT-action');
176 action.textContent =
177 chrome.i18n.getMessage('inject_autorefresh_list_snackbar_action');
178
179 action.addEventListener('click', e => {
180 this.hideUpdatePrompt();
181 document.querySelector('.app-title-button').click();
182 });
183
184 content.append(badge, message, action);
185 ft.append(content);
186 nb.append(ft);
187 ac.append(nb);
188 snackbar.append(ac);
189 pane.append(snackbar);
190 document.getElementById('default-acx-overlay-container').append(pane);
191 this.snackbar = snackbar;
avm99963d3f4ac02021-08-12 18:36:58 +0200192 }
193
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200194 checkUpdate() {
195 if (location.pathname != this.path) {
196 this.unregister();
197 return;
198 }
199
200 if (this.isUpdatePromptShown) return;
201
202 console.debug('Checking for update at: ', new Date());
203
204 this.getLastTimestamp()
205 .then(timestamp => {
206 if (timestamp != this.lastTimestamp) this.showUpdatePrompt();
207 })
208 .catch(
209 err => console.error(
210 'Coudln\'t get last timestamp (while updating): ', err));
avm99963d3f4ac02021-08-12 18:36:58 +0200211 }
212
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200213 firstCall() {
214 console.debug(
215 'autorefresh_list: now performing first call to finish setup (filter: [' +
216 this.filter + '])');
217
218 if (location.pathname != this.path) {
219 this.unregister();
220 return;
221 }
222
223 this.getLastTimestamp()
224 .then(timestamp => {
225 this.lastTimestamp = timestamp;
226 var checkUpdateCallback = this.checkUpdate.bind(this);
avm99963d3f4ac02021-08-12 18:36:58 +0200227 this.interval = window.setInterval(checkUpdateCallback, intervalMs);
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200228 })
229 .catch(
230 err => console.error(
231 'Couldn\'t get last timestamp (while setting up): ', err));
avm99963d3f4ac02021-08-12 18:36:58 +0200232 }
233
avm99963b6f68b62021-08-12 23:13:06 +0200234 setUpHandlers() {
235 window.addEventListener(
236 threadListRequestEvent, e => this.handleListRequest(e));
237 }
238
239 handleListRequest(e) {
Adrià Vilanova Martínez8ef13d42021-08-16 10:07:26 +0200240 // If the request was made before the last known one, return.
241 if (this.requestId !== null && e.detail.id < this.requestId) return;
242
243 // Ignore ViewForum requests made by the chat feature and the "Mark as
244 // duplicate" dialog.
245 //
246 // All those requests have |maxNum| set to 10 and 20 respectively, while the
247 // request that we want to handle is the initial request to load the thread
248 // list which currently requests 100 threads.
249 var maxNum = e.detail.body?.['2']?.['1']?.['2'];
250 if (maxNum == 10 || maxNum == 20) return;
251
252 // Ignore requests to load more threads in the current thread list. All
253 // those requests include a PaginationToken, and also have |maxNum| set
254 // to 50.
255 var token = e.detail.body?.['2']?.['1']?.['3'];
256 if (token) return;
257
258 console.debug('autorefresh_list: handling valid ViewForum request');
259
260 this.requestId = e.detail.id;
261 this.requestOrderOptions = e.detail.body?.['2']?.['2'];
avm99963b6f68b62021-08-12 23:13:06 +0200262 }
263
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200264 setUp() {
avm99963b6f68b62021-08-12 23:13:06 +0200265 if (!this.isOrderedByTimestampDescending()) {
266 console.debug(
267 'autorefresh_list: refused to start up because the order is not by timestamp descending.');
268 return;
269 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200270
271 this.unregister();
272
273 console.debug('autorefresh_list: starting set up...');
274
275 if (this.snackbar === null) this.injectUpdatePrompt();
276 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};