blob: a35a6f0c09037cbaf8baac8242472c871f2f65aa [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';
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +02003import {isOptionEnabled} from '../../common/optionsUtils.js';
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +02004
avm99963d3f4ac02021-08-12 18:36:58 +02005import {createExtBadge} from './utils/common.js';
6
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +02007var authuser = getAuthUser();
8
avm99963b6f68b62021-08-12 23:13:06 +02009const threadListRequestEvent = 'TWPT_ViewForumRequest';
avm99963dca87222021-08-16 19:10:25 +020010const threadListLoadEvent = 'TWPT_ViewForumResponse';
11const intervalMs = 3 * 60 * 1000; // 3 minutes
avm99963d3f4ac02021-08-12 18:36:58 +020012
13export default class AutoRefresh {
14 constructor() {
15 this.isLookingForUpdates = false;
16 this.isUpdatePromptShown = false;
17 this.lastTimestamp = null;
avm99963dca87222021-08-16 19:10:25 +020018 this.forumId = null;
avm99963d3f4ac02021-08-12 18:36:58 +020019 this.filter = null;
20 this.path = null;
avm99963b6f68b62021-08-12 23:13:06 +020021 this.requestId = null;
22 this.requestOrderOptions = null;
avm99963d3f4ac02021-08-12 18:36:58 +020023 this.snackbar = null;
avm9996358697fe2021-08-17 11:20:51 +020024 this.statusIndicator = null;
avm99963d3f4ac02021-08-12 18:36:58 +020025 this.interval = null;
avm99963b6f68b62021-08-12 23:13:06 +020026
27 this.setUpHandlers();
avm99963d3f4ac02021-08-12 18:36:58 +020028 }
29
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020030 isOrderedByTimestampDescending() {
avm99963b6f68b62021-08-12 23:13:06 +020031 // This means we didn't intercept the request.
Adrià Vilanova Martínez8ef13d42021-08-16 10:07:26 +020032 if (!this.requestOrderOptions) return false;
avm99963b6f68b62021-08-12 23:13:06 +020033
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020034 // Returns orderOptions.by == TIMESTAMP && orderOptions.desc == true
35 return (
avm99963b6f68b62021-08-12 23:13:06 +020036 this.requestOrderOptions?.[1] == 1 &&
37 this.requestOrderOptions?.[2] == true);
avm99963d3f4ac02021-08-12 18:36:58 +020038 }
39
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020040 getLastTimestamp() {
avm99963d3f4ac02021-08-12 18:36:58 +020041 return CCApi(
42 'ViewForum', {
avm99963dca87222021-08-16 19:10:25 +020043 1: this.forumId,
avm99963d3f4ac02021-08-12 18:36:58 +020044 // options
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020045 2: {
avm99963d3f4ac02021-08-12 18:36:58 +020046 // pagination
47 1: {
48 2: 2, // maxNum
49 },
50 // order
51 2: {
52 1: 1, // by
53 2: true, // desc
54 },
55 12: this.filter, // forumViewFilters
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020056 },
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020057 },
avm99963d3f4ac02021-08-12 18:36:58 +020058 /* authenticated = */ true, authuser)
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020059 .then(body => {
60 var timestamp = body?.[1]?.[2]?.[0]?.[2]?.[17];
61 if (timestamp === undefined)
62 throw new Error(
63 'Unexpected body of response (' +
64 (body?.[1]?.[2]?.[0] === undefined ?
65 'no threads were returned' :
66 'the timestamp value is not present in the first thread') +
67 ').');
68
69 return timestamp;
70 });
71 // TODO(avm99963): Add retry mechanism (sometimes thread lists are empty,
72 // but when loading the next page the thread appears).
73 //
74 // NOTE(avm99963): It seems like loading the first 2 threads instead of only
75 // the first one fixes this (empty lists are now rarely returned).
avm99963d3f4ac02021-08-12 18:36:58 +020076 }
77
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020078 unregister() {
79 console.debug('autorefresh_list: unregistering');
80
81 if (!this.isLookingForUpdates) return;
82
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020083 window.clearInterval(this.interval);
84 this.isUpdatePromptShown = false;
85 this.isLookingForUpdates = false;
avm99963d3f4ac02021-08-12 18:36:58 +020086 }
87
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020088 showUpdatePrompt() {
89 this.snackbar.classList.remove('TWPT-hidden');
90 document.title = '[!!!] ' + document.title.replace('[!!!] ', '');
91 this.isUpdatePromptShown = true;
avm99963d3f4ac02021-08-12 18:36:58 +020092 }
93
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +020094 // This function can be called even if the update prompt is not shown.
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020095 hideUpdatePrompt() {
avm999635203fa62021-08-16 18:36:02 +020096 if (this.snackbar) this.snackbar.classList.add('TWPT-hidden');
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020097 document.title = document.title.replace('[!!!] ', '');
98 this.isUpdatePromptShown = false;
avm99963d3f4ac02021-08-12 18:36:58 +020099 }
100
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200101 injectUpdatePrompt() {
102 var pane = document.createElement('div');
103 pane.classList.add('TWPT-pane-for-snackbar');
104
105 var snackbar = document.createElement('material-snackbar-panel');
106 snackbar.classList.add('TWPT-snackbar');
107 snackbar.classList.add('TWPT-hidden');
108
109 var ac = document.createElement('div');
110 ac.classList.add('TWPT-animation-container');
111
112 var nb = document.createElement('div');
113 nb.classList.add('TWPT-notification-bar');
114
115 var ft = document.createElement('focus-trap');
116
117 var content = document.createElement('div');
118 content.classList.add('TWPT-focus-content-wrapper');
119
120 var badge = createExtBadge();
121
122 var message = document.createElement('div');
123 message.classList.add('TWPT-message');
124 message.textContent =
125 chrome.i18n.getMessage('inject_autorefresh_list_snackbar_message');
126
127 var action = document.createElement('div');
128 action.classList.add('TWPT-action');
129 action.textContent =
130 chrome.i18n.getMessage('inject_autorefresh_list_snackbar_action');
131
132 action.addEventListener('click', e => {
133 this.hideUpdatePrompt();
134 document.querySelector('.app-title-button').click();
135 });
136
137 content.append(badge, message, action);
138 ft.append(content);
139 nb.append(ft);
140 ac.append(nb);
141 snackbar.append(ac);
142 pane.append(snackbar);
143 document.getElementById('default-acx-overlay-container').append(pane);
144 this.snackbar = snackbar;
avm99963d3f4ac02021-08-12 18:36:58 +0200145 }
146
avm9996358697fe2021-08-17 11:20:51 +0200147 // Create an indicator element.
148 createStatusIndicator(isSetUp) {
149 var container = document.createElement('div');
150 container.classList.add('TWPT-autorefresh-status-indicator-container');
151 var title = chrome.i18n.getMessage(
152 isSetUp ? 'inject_autorefresh_list_status_indicator_label_active' :
153 'inject_autorefresh_list_status_indicator_label_disabled');
154 container.setAttribute('title', title);
155
156 var indicator = document.createElement('div');
157 indicator.classList.add(
158 'TWPT-autorefresh-status-indicator',
159 isSetUp ? 'TWPT-autorefresh-status-indicator--active' :
160 'TWPT-autorefresh-status-indicator--disabled');
161 indicator.textContent =
162 isSetUp ? 'notifications_active' : 'notifications_off';
163
164 var badge = createExtBadge();
165
166 container.append(indicator, badge);
167 return container;
168 }
169
170 injectStatusIndicator(isSetUp) {
171 this.statusIndicator = this.createStatusIndicator(isSetUp);
172
173 var sortOptionsDiv = document.querySelector('ec-thread-list .sort-options');
174 if (sortOptionsDiv) {
175 sortOptionsDiv.prepend(this.statusIndicator);
176 return;
177 }
178
179 console.error('threadListAvatars: Couldn\'t inject status indicator.');
180 }
181
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200182 checkUpdate() {
183 if (location.pathname != this.path) {
184 this.unregister();
185 return;
186 }
187
avm99963dca87222021-08-16 19:10:25 +0200188 if (!this.lastTimestamp) {
189 console.error('autorefresh_list: this.lastTimestamp is not set.');
190 this.unregister();
191 return;
192 }
193
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200194 if (this.isUpdatePromptShown) return;
195
196 console.debug('Checking for update at: ', new Date());
197
198 this.getLastTimestamp()
199 .then(timestamp => {
200 if (timestamp != this.lastTimestamp) this.showUpdatePrompt();
201 })
202 .catch(
203 err => console.error(
204 'Coudln\'t get last timestamp (while updating): ', err));
avm99963d3f4ac02021-08-12 18:36:58 +0200205 }
206
avm99963b6f68b62021-08-12 23:13:06 +0200207 setUpHandlers() {
208 window.addEventListener(
209 threadListRequestEvent, e => this.handleListRequest(e));
avm99963dca87222021-08-16 19:10:25 +0200210 window.addEventListener(threadListLoadEvent, e => this.handleListLoad(e));
avm99963b6f68b62021-08-12 23:13:06 +0200211 }
212
avm99963dca87222021-08-16 19:10:25 +0200213 // This will set the forum ID and filter which is going to be used to check
214 // for new updates in the thread list.
avm99963b6f68b62021-08-12 23:13:06 +0200215 handleListRequest(e) {
Adrià Vilanova Martínez8ef13d42021-08-16 10:07:26 +0200216 // If the request was made before the last known one, return.
217 if (this.requestId !== null && e.detail.id < this.requestId) return;
218
219 // Ignore ViewForum requests made by the chat feature and the "Mark as
220 // duplicate" dialog.
221 //
222 // All those requests have |maxNum| set to 10 and 20 respectively, while the
223 // request that we want to handle is the initial request to load the thread
224 // list which currently requests 100 threads.
225 var maxNum = e.detail.body?.['2']?.['1']?.['2'];
226 if (maxNum == 10 || maxNum == 20) return;
227
228 // Ignore requests to load more threads in the current thread list. All
229 // those requests include a PaginationToken, and also have |maxNum| set
230 // to 50.
231 var token = e.detail.body?.['2']?.['1']?.['3'];
232 if (token) return;
233
Adrià Vilanova Martínez8ef13d42021-08-16 10:07:26 +0200234 this.requestId = e.detail.id;
235 this.requestOrderOptions = e.detail.body?.['2']?.['2'];
avm99963dca87222021-08-16 19:10:25 +0200236 this.forumId = e.detail.body?.['1'] ?? '0';
237 this.filter = e.detail.body?.['2']?.['12'] ?? '';
238
239 console.debug(
240 'autorefresh_list: handled valid ViewForum request (forumId: ' +
241 this.forumId + ', filter: [' + this.filter + '])');
avm99963b6f68b62021-08-12 23:13:06 +0200242 }
243
avm99963dca87222021-08-16 19:10:25 +0200244 // This will set the timestamp of the first thread in the list, so we can
245 // decide in the future whether there is an update or not.
246 handleListLoad(e) {
247 // We ignore past requests and only consider the most recent one.
248 if (this.requestId !== e.detail.id) return;
249
250 console.debug(
251 'autorefresh_list: handling corresponding ViewForum response');
252
253 this.lastTimestamp = e.detail.body?.['1']?.['2']?.[0]?.['2']?.['17'];
254 if (this.lastTimestamp === undefined)
255 console.error(
256 'autorefresh_list: Unexpected body of response (' +
Adrià Vilanova Martínez60b155d2021-08-28 01:56:38 +0200257 (e.detail.body?.['1']?.['2']?.[0] === undefined ?
avm99963dca87222021-08-16 19:10:25 +0200258 'no threads were returned' :
259 'the timestamp value is not present in the first thread') +
260 ').');
261 }
262
263 // This is called when a thread list node is detected in the page. This
264 // initializes the interval to check for updates, and several other things.
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200265 setUp() {
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200266 isOptionEnabled('autorefreshlist').then(isEnabled => {
267 if (!isEnabled) return;
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200268
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200269 if (!this.isOrderedByTimestampDescending()) {
270 this.injectStatusIndicator(false);
271 console.debug(
272 'autorefresh_list: refused to start up because the order is not by timestamp descending.');
273 return;
274 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200275
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200276 this.unregister();
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200277
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200278 console.debug('autorefresh_list: starting set up...');
avm9996358697fe2021-08-17 11:20:51 +0200279
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200280 if (this.snackbar === null) this.injectUpdatePrompt();
281 this.injectStatusIndicator(true);
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200282
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200283 this.isLookingForUpdates = true;
284 this.path = location.pathname;
285
286 var checkUpdateCallback = this.checkUpdate.bind(this);
287 this.interval = window.setInterval(checkUpdateCallback, intervalMs);
288 });
avm99963d3f4ac02021-08-12 18:36:58 +0200289 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200290};