blob: e5dc8c8f094fd67b113ea896c44750942df04152 [file] [log] [blame]
avm999632485a3e2021-09-08 22:18:38 +02001import {MDCTooltip} from '@material/tooltip';
2
avm99963d3f4ac02021-08-12 18:36:58 +02003import {CCApi} from '../../common/api.js';
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +02004import {getAuthUser} from '../../common/communityConsoleUtils.js';
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +02005import {isOptionEnabled} from '../../common/optionsUtils.js';
avm999632485a3e2021-09-08 22:18:38 +02006import {createPlainTooltip} from '../../common/tooltip.js';
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +02007
avm99963d3f4ac02021-08-12 18:36:58 +02008import {createExtBadge} from './utils/common.js';
9
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020010var authuser = getAuthUser();
11
avm99963b6f68b62021-08-12 23:13:06 +020012const threadListRequestEvent = 'TWPT_ViewForumRequest';
avm99963dca87222021-08-16 19:10:25 +020013const threadListLoadEvent = 'TWPT_ViewForumResponse';
14const intervalMs = 3 * 60 * 1000; // 3 minutes
avm99963d3f4ac02021-08-12 18:36:58 +020015
16export default class AutoRefresh {
17 constructor() {
18 this.isLookingForUpdates = false;
19 this.isUpdatePromptShown = false;
20 this.lastTimestamp = null;
avm99963dca87222021-08-16 19:10:25 +020021 this.forumId = null;
avm99963d3f4ac02021-08-12 18:36:58 +020022 this.filter = null;
23 this.path = null;
avm99963b6f68b62021-08-12 23:13:06 +020024 this.requestId = null;
25 this.requestOrderOptions = null;
avm99963d3f4ac02021-08-12 18:36:58 +020026 this.snackbar = null;
avm9996358697fe2021-08-17 11:20:51 +020027 this.statusIndicator = null;
avm99963d3f4ac02021-08-12 18:36:58 +020028 this.interval = null;
avm99963b6f68b62021-08-12 23:13:06 +020029
30 this.setUpHandlers();
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 getLastTimestamp() {
avm99963d3f4ac02021-08-12 18:36:58 +020044 return CCApi(
45 'ViewForum', {
avm99963dca87222021-08-16 19:10:25 +020046 1: this.forumId,
avm99963d3f4ac02021-08-12 18:36:58 +020047 // options
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020048 2: {
avm99963d3f4ac02021-08-12 18:36:58 +020049 // pagination
50 1: {
51 2: 2, // maxNum
52 },
53 // order
54 2: {
55 1: 1, // by
56 2: true, // desc
57 },
58 12: this.filter, // forumViewFilters
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020059 },
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020060 },
avm99963d3f4ac02021-08-12 18:36:58 +020061 /* authenticated = */ true, authuser)
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020062 .then(body => {
63 var timestamp = body?.[1]?.[2]?.[0]?.[2]?.[17];
64 if (timestamp === undefined)
65 throw new Error(
66 'Unexpected body of response (' +
67 (body?.[1]?.[2]?.[0] === undefined ?
68 'no threads were returned' :
69 'the timestamp value is not present in the first thread') +
70 ').');
71
72 return timestamp;
73 });
74 // TODO(avm99963): Add retry mechanism (sometimes thread lists are empty,
75 // but when loading the next page the thread appears).
76 //
77 // NOTE(avm99963): It seems like loading the first 2 threads instead of only
78 // the first one fixes this (empty lists are now rarely returned).
avm99963d3f4ac02021-08-12 18:36:58 +020079 }
80
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020081 unregister() {
82 console.debug('autorefresh_list: unregistering');
83
84 if (!this.isLookingForUpdates) return;
85
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020086 window.clearInterval(this.interval);
87 this.isUpdatePromptShown = false;
88 this.isLookingForUpdates = false;
avm99963d3f4ac02021-08-12 18:36:58 +020089 }
90
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020091 showUpdatePrompt() {
92 this.snackbar.classList.remove('TWPT-hidden');
93 document.title = '[!!!] ' + document.title.replace('[!!!] ', '');
94 this.isUpdatePromptShown = true;
avm99963d3f4ac02021-08-12 18:36:58 +020095 }
96
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +020097 // This function can be called even if the update prompt is not shown.
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020098 hideUpdatePrompt() {
avm999635203fa62021-08-16 18:36:02 +020099 if (this.snackbar) this.snackbar.classList.add('TWPT-hidden');
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200100 document.title = document.title.replace('[!!!] ', '');
101 this.isUpdatePromptShown = false;
avm99963d3f4ac02021-08-12 18:36:58 +0200102 }
103
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200104 injectUpdatePrompt() {
105 var pane = document.createElement('div');
106 pane.classList.add('TWPT-pane-for-snackbar');
107
108 var snackbar = document.createElement('material-snackbar-panel');
109 snackbar.classList.add('TWPT-snackbar');
110 snackbar.classList.add('TWPT-hidden');
111
112 var ac = document.createElement('div');
113 ac.classList.add('TWPT-animation-container');
114
115 var nb = document.createElement('div');
116 nb.classList.add('TWPT-notification-bar');
117
118 var ft = document.createElement('focus-trap');
119
120 var content = document.createElement('div');
121 content.classList.add('TWPT-focus-content-wrapper');
122
avm999632485a3e2021-09-08 22:18:38 +0200123 let badge, badgeTooltip;
124 [badge, badgeTooltip] = createExtBadge();
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200125
126 var message = document.createElement('div');
127 message.classList.add('TWPT-message');
128 message.textContent =
129 chrome.i18n.getMessage('inject_autorefresh_list_snackbar_message');
130
131 var action = document.createElement('div');
132 action.classList.add('TWPT-action');
133 action.textContent =
134 chrome.i18n.getMessage('inject_autorefresh_list_snackbar_action');
135
136 action.addEventListener('click', e => {
137 this.hideUpdatePrompt();
138 document.querySelector('.app-title-button').click();
139 });
140
141 content.append(badge, message, action);
142 ft.append(content);
143 nb.append(ft);
144 ac.append(nb);
145 snackbar.append(ac);
146 pane.append(snackbar);
147 document.getElementById('default-acx-overlay-container').append(pane);
avm999632485a3e2021-09-08 22:18:38 +0200148 new MDCTooltip(badgeTooltip);
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200149 this.snackbar = snackbar;
avm99963d3f4ac02021-08-12 18:36:58 +0200150 }
151
avm9996358697fe2021-08-17 11:20:51 +0200152 // Create an indicator element.
153 createStatusIndicator(isSetUp) {
154 var container = document.createElement('div');
155 container.classList.add('TWPT-autorefresh-status-indicator-container');
avm9996358697fe2021-08-17 11:20:51 +0200156
157 var indicator = document.createElement('div');
158 indicator.classList.add(
159 'TWPT-autorefresh-status-indicator',
160 isSetUp ? 'TWPT-autorefresh-status-indicator--active' :
161 'TWPT-autorefresh-status-indicator--disabled');
162 indicator.textContent =
163 isSetUp ? 'notifications_active' : 'notifications_off';
avm999632485a3e2021-09-08 22:18:38 +0200164 let label = chrome.i18n.getMessage(
165 isSetUp ? 'inject_autorefresh_list_status_indicator_label_active' :
166 'inject_autorefresh_list_status_indicator_label_disabled');
167 let statusTooltip = createPlainTooltip(indicator, label, false);
avm9996358697fe2021-08-17 11:20:51 +0200168
avm999632485a3e2021-09-08 22:18:38 +0200169 let badge, badgeTooltip;
170 [badge, badgeTooltip] = createExtBadge();
avm9996358697fe2021-08-17 11:20:51 +0200171
172 container.append(indicator, badge);
avm999632485a3e2021-09-08 22:18:38 +0200173 return [container, badgeTooltip, statusTooltip];
avm9996358697fe2021-08-17 11:20:51 +0200174 }
175
176 injectStatusIndicator(isSetUp) {
avm999632485a3e2021-09-08 22:18:38 +0200177 let badgeTooltip, statusTooltip;
178 [this.statusIndicator, badgeTooltip, statusTooltip] = this.createStatusIndicator(isSetUp);
avm9996358697fe2021-08-17 11:20:51 +0200179
180 var sortOptionsDiv = document.querySelector('ec-thread-list .sort-options');
181 if (sortOptionsDiv) {
182 sortOptionsDiv.prepend(this.statusIndicator);
avm999632485a3e2021-09-08 22:18:38 +0200183 new MDCTooltip(badgeTooltip);
184 new MDCTooltip(statusTooltip);
avm9996358697fe2021-08-17 11:20:51 +0200185 return;
186 }
187
188 console.error('threadListAvatars: Couldn\'t inject status indicator.');
189 }
190
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200191 checkUpdate() {
192 if (location.pathname != this.path) {
193 this.unregister();
194 return;
195 }
196
avm99963dca87222021-08-16 19:10:25 +0200197 if (!this.lastTimestamp) {
198 console.error('autorefresh_list: this.lastTimestamp is not set.');
199 this.unregister();
200 return;
201 }
202
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200203 if (this.isUpdatePromptShown) return;
204
205 console.debug('Checking for update at: ', new Date());
206
207 this.getLastTimestamp()
208 .then(timestamp => {
209 if (timestamp != this.lastTimestamp) this.showUpdatePrompt();
210 })
211 .catch(
212 err => console.error(
213 'Coudln\'t get last timestamp (while updating): ', err));
avm99963d3f4ac02021-08-12 18:36:58 +0200214 }
215
avm99963b6f68b62021-08-12 23:13:06 +0200216 setUpHandlers() {
217 window.addEventListener(
218 threadListRequestEvent, e => this.handleListRequest(e));
avm99963dca87222021-08-16 19:10:25 +0200219 window.addEventListener(threadListLoadEvent, e => this.handleListLoad(e));
avm99963b6f68b62021-08-12 23:13:06 +0200220 }
221
avm99963dca87222021-08-16 19:10:25 +0200222 // This will set the forum ID and filter which is going to be used to check
223 // for new updates in the thread list.
avm99963b6f68b62021-08-12 23:13:06 +0200224 handleListRequest(e) {
Adrià Vilanova Martínez8ef13d42021-08-16 10:07:26 +0200225 // If the request was made before the last known one, return.
226 if (this.requestId !== null && e.detail.id < this.requestId) return;
227
228 // Ignore ViewForum requests made by the chat feature and the "Mark as
229 // duplicate" dialog.
230 //
231 // All those requests have |maxNum| set to 10 and 20 respectively, while the
232 // request that we want to handle is the initial request to load the thread
233 // list which currently requests 100 threads.
234 var maxNum = e.detail.body?.['2']?.['1']?.['2'];
235 if (maxNum == 10 || maxNum == 20) return;
236
237 // Ignore requests to load more threads in the current thread list. All
238 // those requests include a PaginationToken, and also have |maxNum| set
239 // to 50.
240 var token = e.detail.body?.['2']?.['1']?.['3'];
241 if (token) return;
242
Adrià Vilanova Martínez8ef13d42021-08-16 10:07:26 +0200243 this.requestId = e.detail.id;
244 this.requestOrderOptions = e.detail.body?.['2']?.['2'];
avm99963dca87222021-08-16 19:10:25 +0200245 this.forumId = e.detail.body?.['1'] ?? '0';
246 this.filter = e.detail.body?.['2']?.['12'] ?? '';
247
248 console.debug(
249 'autorefresh_list: handled valid ViewForum request (forumId: ' +
250 this.forumId + ', filter: [' + this.filter + '])');
avm99963b6f68b62021-08-12 23:13:06 +0200251 }
252
avm99963dca87222021-08-16 19:10:25 +0200253 // This will set the timestamp of the first thread in the list, so we can
254 // decide in the future whether there is an update or not.
255 handleListLoad(e) {
256 // We ignore past requests and only consider the most recent one.
257 if (this.requestId !== e.detail.id) return;
258
259 console.debug(
260 'autorefresh_list: handling corresponding ViewForum response');
261
262 this.lastTimestamp = e.detail.body?.['1']?.['2']?.[0]?.['2']?.['17'];
263 if (this.lastTimestamp === undefined)
264 console.error(
265 'autorefresh_list: Unexpected body of response (' +
Adrià Vilanova Martínez60b155d2021-08-28 01:56:38 +0200266 (e.detail.body?.['1']?.['2']?.[0] === undefined ?
avm99963dca87222021-08-16 19:10:25 +0200267 'no threads were returned' :
268 'the timestamp value is not present in the first thread') +
269 ').');
270 }
271
272 // This is called when a thread list node is detected in the page. This
273 // initializes the interval to check for updates, and several other things.
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200274 setUp() {
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200275 isOptionEnabled('autorefreshlist').then(isEnabled => {
276 if (!isEnabled) return;
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200277
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200278 if (!this.isOrderedByTimestampDescending()) {
279 this.injectStatusIndicator(false);
280 console.debug(
281 'autorefresh_list: refused to start up because the order is not by timestamp descending.');
282 return;
283 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200284
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200285 this.unregister();
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200286
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200287 console.debug('autorefresh_list: starting set up...');
avm9996358697fe2021-08-17 11:20:51 +0200288
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200289 if (this.snackbar === null) this.injectUpdatePrompt();
290 this.injectStatusIndicator(true);
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200291
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200292 this.isLookingForUpdates = true;
293 this.path = location.pathname;
294
295 var checkUpdateCallback = this.checkUpdate.bind(this);
296 this.interval = window.setInterval(checkUpdateCallback, intervalMs);
297 });
avm99963d3f4ac02021-08-12 18:36:58 +0200298 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200299};