blob: 7503df5bf0437d1724300b16bf018f3d78ddfc2e [file] [log] [blame]
avm9996311707032021-02-05 19:11:25 +01001var mutationObserver, intersectionObserver, intersectionOptions, options,
2 authuser;
avm99963cbea3142019-03-28 00:48:15 +01003
avm99963f5923962020-12-07 16:44:37 +01004function removeChildNodes(node) {
5 while (node.firstChild) {
6 node.removeChild(node.firstChild);
avm99963af7860e2019-06-04 03:33:26 +02007 }
avm99963f5923962020-12-07 16:44:37 +01008}
avm99963af7860e2019-06-04 03:33:26 +02009
avm9996390cc2e32021-02-05 18:14:16 +010010function getNParent(node, n) {
11 if (n <= 0) return node;
12 if (!('parentNode' in node)) return null;
13 return getNParent(node.parentNode, n - 1);
14}
15
avm999633eae4522021-04-22 01:14:27 +020016function parseUrl(url) {
17 var forum_a = url.match(/forum\/([0-9]+)/i);
18 var thread_a = url.match(/thread\/([0-9]+)/i);
19
20 if (forum_a === null || thread_a === null) {
21 return false;
22 }
23
24 return {
25 'forum': forum_a[1],
26 'thread': thread_a[1],
27 };
28}
29
avm99963f5923962020-12-07 16:44:37 +010030function createExtBadge() {
31 var badge = document.createElement('div');
32 badge.classList.add('TWPT-badge');
33 badge.setAttribute(
34 'title', chrome.i18n.getMessage('inject_extension_badge_helper', [
35 chrome.i18n.getMessage('appName')
36 ]));
37
38 var badgeI = document.createElement('i');
39 badgeI.classList.add('material-icon-i', 'material-icons-extended');
40 badgeI.textContent = 'repeat';
41
42 badge.append(badgeI);
43 return badge;
avm99963af7860e2019-06-04 03:33:26 +020044}
45
avm99963943b8492020-08-31 23:40:43 +020046function addProfileHistoryLink(node, type, query) {
47 var urlpart = encodeURIComponent('query=' + query);
avm99963a2945b62020-11-27 00:32:02 +010048 var authuserpart =
49 (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
avm99963943b8492020-08-31 23:40:43 +020050 var container = document.createElement('div');
51 container.style.margin = '3px 0';
52
53 var link = document.createElement('a');
54 link.setAttribute(
avm99963a2945b62020-11-27 00:32:02 +010055 'href',
56 'https://support.google.com/s/community/search/' + urlpart +
57 authuserpart);
avm99963943b8492020-08-31 23:40:43 +020058 link.innerText = chrome.i18n.getMessage('inject_previousposts_' + type);
59
60 container.appendChild(link);
avm9996306167752020-09-08 00:50:36 +020061 node.appendChild(container);
avm99963943b8492020-08-31 23:40:43 +020062}
63
avm999638e0c1002020-12-03 16:54:20 +010064function applyDragAndDropFix(node) {
65 console.debug('Adding link drag&drop fix to ', node);
66 node.addEventListener('drop', e => {
67 if (e.dataTransfer.types.includes('text/uri-list')) {
68 e.stopImmediatePropagation();
69 console.debug('Stopping link drop event propagation.');
70 }
71 }, true);
72}
73
avm99963f5923962020-12-07 16:44:37 +010074function nodeIsReadToggleBtn(node) {
75 return ('tagName' in node) && node.tagName == 'MATERIAL-BUTTON' &&
76 node.getAttribute('debugid') !== null &&
77 (node.getAttribute('debugid') == 'mark-read-button' ||
78 node.getAttribute('debugid') == 'mark-unread-button') &&
79 ('parentNode' in node) && node.parentNode !== null &&
80 ('parentNode' in node.parentNode) &&
81 node.parentNode.querySelector('[debugid="batchlock"]') === null &&
82 node.parentNode.parentNode !== null &&
83 ('tagName' in node.parentNode.parentNode) &&
84 node.parentNode.parentNode.tagName == 'EC-BULK-ACTIONS';
85}
86
avm9996328fddc62021-02-05 20:33:48 +010087function injectDarkModeButton(rightControl) {
88 var darkThemeSwitch = document.createElement('material-button');
89 darkThemeSwitch.classList.add('TWPT-dark-theme', 'TWPT-btn--with-badge');
90 darkThemeSwitch.setAttribute('button', '');
91 darkThemeSwitch.setAttribute(
92 'title', chrome.i18n.getMessage('inject_ccdarktheme_helper'));
93
94 darkThemeSwitch.addEventListener('click', e => {
95 chrome.storage.sync.get(null, currentOptions => {
96 currentOptions.ccdarktheme_switch_status =
97 !options.ccdarktheme_switch_status;
98 chrome.storage.sync.set(currentOptions, _ => {
99 location.reload();
100 });
101 });
102 });
103
104 var switchContent = document.createElement('div');
105 switchContent.classList.add('content');
106
107 var icon = document.createElement('material-icon');
108
109 var i = document.createElement('i');
110 i.classList.add('material-icon-i', 'material-icons-extended');
111 i.textContent = 'brightness_4';
112
113 icon.appendChild(i);
114 switchContent.appendChild(icon);
115 darkThemeSwitch.appendChild(switchContent);
116
117 var badgeContent = createExtBadge();
118
119 darkThemeSwitch.appendChild(badgeContent);
120
121 rightControl.style.width =
122 (parseInt(window.getComputedStyle(rightControl).width) + 58) + 'px';
123 rightControl.insertAdjacentElement('afterbegin', darkThemeSwitch);
124}
125
avm99963f5923962020-12-07 16:44:37 +0100126function addBatchLockBtn(readToggle) {
127 var clone = readToggle.cloneNode(true);
128 clone.setAttribute('debugid', 'batchlock');
129 clone.classList.add('TWPT-btn--with-badge');
130 clone.setAttribute('title', chrome.i18n.getMessage('inject_lockbtn'));
131 clone.querySelector('material-icon').setAttribute('icon', 'lock');
132 clone.querySelector('i.material-icon-i').textContent = 'lock';
133
134 var badge = createExtBadge();
135 clone.append(badge);
136
137 clone.addEventListener('click', function() {
138 var modal = document.querySelector('.pane[pane-id="default-1"]');
139
140 var dialog = document.createElement('material-dialog');
141 dialog.setAttribute('role', 'dialog');
142 dialog.setAttribute('aria-modal', 'true');
143 dialog.classList.add('TWPT-dialog');
144
145 var header = document.createElement('header');
146 header.setAttribute('role', 'presentation');
147 header.classList.add('TWPT-dialog-header');
148
149 var title = document.createElement('div');
150 title.classList.add('TWPT-dialog-header--title', 'title');
151 title.textContent = chrome.i18n.getMessage('inject_lockbtn');
152
153 header.append(title);
154
155 var main = document.createElement('main');
156 main.setAttribute('role', 'presentation');
157 main.classList.add('TWPT-dialog-main');
158
159 var p = document.createElement('p');
160 p.textContent = chrome.i18n.getMessage('inject_lockdialog_desc');
161
162 main.append(p);
163
164 dialog.append(header, main);
165
166 var footers = [['lock', 'unlock', 'cancel'], ['reload', 'close']];
167
168 for (var i = 0; i < footers.length; ++i) {
169 var footer = document.createElement('footer');
170 footer.setAttribute('role', 'presentation');
171 footer.classList.add('TWPT-dialog-footer');
172 footer.setAttribute('data-footer-id', i);
173
174 if (i > 0) footer.classList.add('is-hidden');
175
176 footers[i].forEach(action => {
177 var btn = document.createElement('material-button');
178 btn.setAttribute('role', 'button');
179 btn.classList.add('TWPT-dialog-footer-btn');
180 if (i == 1) btn.classList.add('is-disabled');
181
182 switch (action) {
183 case 'lock':
184 case 'unlock':
185 btn.addEventListener('click', _ => {
186 if (btn.classList.contains('is-disabled')) return;
187 var message = {
188 action,
189 prefix: 'TWPT-batchlock',
190 };
191 window.postMessage(message, '*');
192 });
193 break;
194
195 case 'cancel':
196 case 'close':
197 btn.addEventListener('click', _ => {
198 if (btn.classList.contains('is-disabled')) return;
199 modal.classList.remove('visible');
200 modal.style.display = 'none';
201 removeChildNodes(modal);
202 });
203 break;
204
205 case 'reload':
206 btn.addEventListener('click', _ => {
207 if (btn.classList.contains('is-disabled')) return;
208 window.location.reload()
209 });
210 break;
211 }
212
213 var content = document.createElement('div');
214 content.classList.add('content', 'TWPT-dialog-footer-btn--content');
215 content.textContent =
216 chrome.i18n.getMessage('inject_lockdialog_btn_' + action);
217
218 btn.append(content);
219 footer.append(btn);
220 });
221
222 var clear = document.createElement('div');
223 clear.style.clear = 'both';
224
225 footer.append(clear);
226 dialog.append(footer);
227 }
228
229 removeChildNodes(modal);
230 modal.append(dialog);
231 modal.classList.add('visible', 'modal');
232 modal.style.display = 'flex';
233 });
Adrià Vilanova Martínez74273ee2021-06-25 19:23:27 +0200234
235 var duplicateBtn =
236 readToggle.parentNode.querySelector('[debugid="mark-duplicate-button"]');
237 if (duplicateBtn)
238 duplicateBtn.parentNode.insertBefore(
239 clone, (duplicateBtn.nextSibling || duplicateBtn));
240 else
241 readToggle.parentNode.insertBefore(
242 clone, (readToggle.nextSibling || readToggle));
avm99963f5923962020-12-07 16:44:37 +0100243}
244
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200245var avatars = {
246 isFilterSetUp: false,
247 privateForums: [],
avm999633eae4522021-04-22 01:14:27 +0200248
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200249 // Gets a list of private forums. If it is already cached, the cached list is
250 // returned; otherwise it is also computed and cached.
251 getPrivateForums() {
252 return new Promise((resolve, reject) => {
253 if (this.isFilterSetUp) return resolve(this.privateForums);
avm999633eae4522021-04-22 01:14:27 +0200254
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200255 if (!document.documentElement.hasAttribute('data-startup'))
256 return reject('[threadListAvatars] Couldn\'t get startup data.');
avm999633eae4522021-04-22 01:14:27 +0200257
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200258 var startupData =
259 JSON.parse(document.documentElement.getAttribute('data-startup'));
260 var forums = startupData?.['1']?.['2'];
261 if (forums === undefined)
262 return reject(
263 '[threadListAvatars] Couldn\'t retrieve forums from startup data.');
264
265 for (var f of forums) {
266 var forumId = f?.['2']?.['1']?.['1'];
267 var forumVisibility = f?.['2']?.['18'];
268 if (forumId === undefined || forumVisibility === undefined) {
269 console.warn(
270 '[threadListAvatars] Coudln\'t retrieve forum id and/or forum visibility for the following forum:',
271 f);
272 continue;
273 }
274
275 // forumVisibility's value 1 means "PUBLIC".
276 if (forumVisibility != 1) this.privateForums.push(forumId);
avm999633eae4522021-04-22 01:14:27 +0200277 }
avm999633eae4522021-04-22 01:14:27 +0200278
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200279 // Forum 51488989 is marked as public but it is in fact private.
280 this.privateForums.push('51488989');
avm999633eae4522021-04-22 01:14:27 +0200281
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200282 this.isFilterSetUp = true;
283 return resolve(this.privateForums);
284 });
285 },
avm999633eae4522021-04-22 01:14:27 +0200286
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200287 // Some threads belong to private forums, and this feature will not be able to
288 // get its avatars since it makes an anonymomus call to get the contents of
289 // the thread.
290 //
291 // This function returns whether avatars should be retrieved depending on if
292 // the thread belongs to a known private forum.
293 shouldRetrieveAvatars(thread) {
294 return this.getPrivateForums().then(privateForums => {
295 return !privateForums.includes(thread.forum);
296 });
297 },
298
Adrià Vilanova Martínezd6cdfa72021-07-10 21:13:20 +0200299 // Get an object with the author of the thread and an array of the first |num|
300 // replies from the thread |thread|.
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200301 getFirstMessages(thread, num = 15) {
302 return CCApi(
303 'ViewThread', {
304 1: thread.forum,
305 2: thread.thread,
306 // options
307 3: {
308 // pagination
309 1: {
310 2: num, // maxNum
311 },
312 3: true, // withMessages
313 5: true, // withUserProfile
314 10: false, // withPromotedMessages
315 16: false, // withThreadNotes
316 18: true, // sendNewThreadIfMoved
317 }
318 },
319 // |authentication| is false because otherwise this would mark
320 // the thread as read as a side effect, and that would mark all
321 // threads in the list as read.
322 //
323 // Due to the fact that we have to call this endpoint
324 // anonymously, this means we can't retrieve information about
325 // threads in private forums.
326 /* authentication = */ false)
327 .then(data => {
328 var numMessages = data?.['1']?.['8'];
Adrià Vilanova Martínezd6cdfa72021-07-10 21:13:20 +0200329 if (numMessages === undefined)
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200330 throw new Error(
331 'Request to view thread doesn\'t include the number of messages');
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200332
Adrià Vilanova Martínezd6cdfa72021-07-10 21:13:20 +0200333 var messages = numMessages == 0 ? [] : data?.['1']['3'];
334 if (messages === undefined)
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200335 throw new Error(
336 'numMessages was ' + numMessages +
337 ' but the response didn\'t include any message.');
Adrià Vilanova Martínezd6cdfa72021-07-10 21:13:20 +0200338
339 var author = data?.['1']?.['4'];
340 if (author === undefined)
341 throw new Error(
342 'Author isn\'t included in the ViewThread response.');
343
344 return {
345 messages,
346 author,
347 };
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200348 });
349 },
350
351 // Get a list of at most |num| avatars for thread |thread|
352 getVisibleAvatars(thread, num = 3) {
353 return this.shouldRetrieveAvatars(thread).then(shouldRetrieve => {
354 if (!shouldRetrieve) {
355 console.debug('[threadListAvatars] Skipping thread', thread);
356 return [];
357 }
358
Adrià Vilanova Martínezd6cdfa72021-07-10 21:13:20 +0200359 return this.getFirstMessages(thread).then(result => {
360 var messages = result.messages;
361 var author = result.author;
362
avm999633eae4522021-04-22 01:14:27 +0200363 var avatarUrls = [];
Adrià Vilanova Martínezd6cdfa72021-07-10 21:13:20 +0200364
365 var authorUrl = author?.['1']?.['2'];
366 if (authorUrl !== undefined) avatarUrls.push(authorUrl);
367
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200368 for (var m of messages) {
369 var url = m?.['3']?.['1']?.['2'];
avm999633eae4522021-04-22 01:14:27 +0200370
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200371 if (url === undefined) continue;
avm999633eae4522021-04-22 01:14:27 +0200372 if (!avatarUrls.includes(url)) avatarUrls.push(url);
avm999633eae4522021-04-22 01:14:27 +0200373 if (avatarUrls.length == 3) break;
374 }
375
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200376 return avatarUrls;
avm999633eae4522021-04-22 01:14:27 +0200377 });
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200378 });
379 },
380
381 // Inject avatars for thread summary (thread item) |node| in a thread list.
382 inject(node) {
383 var header = node.querySelector(
384 'ec-thread-summary .main-header .panel-description a.header');
385 if (header === null) {
386 console.error(
387 '[threadListAvatars] Header is not present in the thread item\'s DOM.');
388 return;
389 }
390
391 var thread = parseUrl(header.href);
392 if (thread === false) {
393 console.error('[threadListAvatars] Thread\'s link cannot be parsed.');
394 return;
395 }
396
397 this.getVisibleAvatars(thread)
398 .then(avatarUrls => {
399 var avatarsContainer = document.createElement('div');
400 avatarsContainer.classList.add('TWPT-avatars');
401
402 var count = Math.floor(Math.random() * 4);
403
404 for (var i = 0; i < avatarUrls.length; ++i) {
405 var avatar = document.createElement('div');
406 avatar.classList.add('TWPT-avatar');
407 avatar.style.backgroundImage = 'url(\'' + avatarUrls[i] + '\')';
408 avatarsContainer.appendChild(avatar);
409 }
410
411 header.appendChild(avatarsContainer);
412 })
413 .catch(err => {
414 console.error(
415 '[threadListAvatars] Could not retrieve avatars for thread',
416 thread, err);
417 });
418 },
419};
avm999633eae4522021-04-22 01:14:27 +0200420
avm99963a007d492021-05-02 12:32:03 +0200421var autoRefresh = {
422 isLookingForUpdates: false,
423 isUpdatePromptShown: false,
424 lastTimestamp: null,
425 filter: null,
426 path: null,
427 snackbar: null,
428 interval: null,
429 firstCallTimeout: null,
430 intervalMs: 3 * 60 * 1000, // 3 minutes
431 firstCallDelayMs: 3 * 1000, // 3 seconds
432 getStartupData() {
433 return JSON.parse(
434 document.querySelector('html').getAttribute('data-startup'));
435 },
436 isOrderedByTimestampDescending() {
437 var startup = this.getStartupData();
438 // Returns orderOptions.by == TIMESTAMP && orderOptions.desc == true
439 return (
440 startup?.[1]?.[1]?.[3]?.[14]?.[1] == 1 &&
441 startup?.[1]?.[1]?.[3]?.[14]?.[2] == true);
442 },
443 getCustomFilter(path) {
444 var searchRegex = /^\/s\/community\/search\/([^\/]*)/;
445 var matches = path.match(searchRegex);
446 if (matches !== null && matches.length > 1) {
447 var search = decodeURIComponent(matches[1]);
448 var params = new URLSearchParams(search);
449 return params.get('query') || '';
450 }
451
452 return '';
453 },
454 filterHasOverride(filter, override) {
455 var escapedOverride = override.replace(/([^\w\d\s])/gi, '\\$1');
456 var regex = new RegExp('[^a-zA-Z0-9]?' + escapedOverride + ':');
457 return regex.test(filter);
458 },
459 getFilter(path) {
460 var query = this.getCustomFilter(path);
461
462 // Note: This logic has been copied and adapted from the
463 // _buildQuery$1$threadId function in the Community Console
464 var conditions = '';
465 var startup = this.getStartupData();
466
467 // TODO(avm99963): if the selected forums are changed without reloading the
468 // page, this will get the old selected forums. Fix this.
469 var forums = startup?.[1]?.[1]?.[3]?.[8] ?? [];
470 if (!this.filterHasOverride(query, 'forum') && forums !== null &&
471 forums.length > 0)
472 conditions += ' forum:(' + forums.join(' | ') + ')';
473
474 var langs = startup?.[1]?.[1]?.[3]?.[5] ?? [];
475 if (!this.filterHasOverride(query, 'lang') && langs !== null &&
476 langs.length > 0)
477 conditions += ' lang:(' + langs.map(l => '"' + l + '"').join(' | ') + ')';
478
479 if (query.length !== 0 && conditions.length !== 0)
480 return '(' + query + ')' + conditions;
481 return query + conditions;
482 },
483 getLastTimestamp() {
484 var APIRequestUrl = 'https://support.google.com/s/community/api/ViewForum' +
485 (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
486
487 return fetch(APIRequestUrl, {
488 'headers': {
489 'content-type': 'text/plain; charset=utf-8',
490 },
491 'body': JSON.stringify({
492 1: '0', // TODO: Change, when only a forum is selected, it
493 // should be set here
494 2: {
495 1: {
496 2: 2,
497 },
498 2: {
499 1: 1,
500 2: true,
501 },
502 12: this.filter,
503 },
504 }),
505 'method': 'POST',
506 'mode': 'cors',
507 'credentials': 'include',
508 })
509 .then(res => {
510 if (res.status == 200 || res.status == 400) {
511 return res.json().then(data => ({
512 status: res.status,
513 body: data,
514 }));
515 } else {
516 throw new Error('Status code ' + res.status + ' was not expected.');
517 }
518 })
519 .then(res => {
520 if (res.status == 400) {
521 throw new Error(
522 res.body[4] ||
523 ('Response status: 400. Error code: ' + res.body[2]));
524 }
525
526 return res.body;
527 })
528 .then(body => {
529 var timestamp = body?.[1]?.[2]?.[0]?.[2]?.[17];
530 if (timestamp === undefined)
531 throw new Error(
532 'Unexpected body of response (' +
533 (body?.[1]?.[2]?.[0] === undefined ?
534 'no threads were returned' :
535 'the timestamp value is not present in the first thread') +
536 ').');
537
538 return timestamp;
539 });
540 // TODO(avm99963): Add retry mechanism (sometimes thread lists are empty,
541 // but when loading the next page the thread appears).
542 //
543 // NOTE(avm99963): It seems like loading the first 2 threads instead of only
544 // the first one fixes this (empty lists are now rarely returned).
545 },
546 unregister() {
547 console.debug('autorefresh_list: unregistering');
548
549 if (!this.isLookingForUpdates) return;
550
551 window.clearTimeout(this.firstCallTimeout);
552 window.clearInterval(this.interval);
553 this.isUpdatePromptShown = false;
554 this.isLookingForUpdates = false;
555 },
556 showUpdatePrompt() {
557 this.snackbar.classList.remove('TWPT-hidden');
558 document.title = '[!!!] ' + document.title.replace('[!!!] ', '');
559 this.isUpdatePromptShown = true;
560 },
561 hideUpdatePrompt() {
562 this.snackbar.classList.add('TWPT-hidden');
563 document.title = document.title.replace('[!!!] ', '');
564 this.isUpdatePromptShown = false;
565 },
566 injectUpdatePrompt() {
567 var pane = document.createElement('div');
568 pane.classList.add('TWPT-pane-for-snackbar');
569
570 var snackbar = document.createElement('material-snackbar-panel');
571 snackbar.classList.add('TWPT-snackbar');
572 snackbar.classList.add('TWPT-hidden');
573
574 var ac = document.createElement('div');
575 ac.classList.add('TWPT-animation-container');
576
577 var nb = document.createElement('div');
578 nb.classList.add('TWPT-notification-bar');
579
580 var ft = document.createElement('focus-trap');
581
582 var content = document.createElement('div');
583 content.classList.add('TWPT-focus-content-wrapper');
584
585 var badge = createExtBadge();
586
587 var message = document.createElement('div');
588 message.classList.add('TWPT-message');
589 message.textContent =
590 chrome.i18n.getMessage('inject_autorefresh_list_snackbar_message');
591
592 var action = document.createElement('div');
593 action.classList.add('TWPT-action');
594 action.textContent =
595 chrome.i18n.getMessage('inject_autorefresh_list_snackbar_action');
596
597 action.addEventListener('click', e => {
598 this.hideUpdatePrompt();
599 document.querySelector('.app-title-button').click();
600 });
601
602 content.append(badge, message, action);
603 ft.append(content);
604 nb.append(ft);
605 ac.append(nb);
606 snackbar.append(ac);
607 pane.append(snackbar);
608 document.getElementById('default-acx-overlay-container').append(pane);
609 this.snackbar = snackbar;
610 },
611 checkUpdate() {
612 if (location.pathname != this.path) {
613 this.unregister();
614 return;
615 }
616
617 if (this.isUpdatePromptShown) return;
618
619 console.debug('Checking for update at: ', new Date());
620
621 this.getLastTimestamp()
622 .then(timestamp => {
623 if (timestamp != this.lastTimestamp) this.showUpdatePrompt();
624 })
625 .catch(
626 err => console.error(
627 'Coudln\'t get last timestamp (while updating): ', err));
628 },
629 firstCall() {
630 console.debug(
631 'autorefresh_list: now performing first call to finish setup (filter: [' +
632 this.filter + '])');
633
634 if (location.pathname != this.path) {
635 this.unregister();
636 return;
637 }
638
639 this.getLastTimestamp()
640 .then(timestamp => {
641 this.lastTimestamp = timestamp;
642 var checkUpdateCallback = this.checkUpdate.bind(this);
643 this.interval =
644 window.setInterval(checkUpdateCallback, this.intervalMs);
645 })
646 .catch(
647 err => console.error(
648 'Couldn\'t get last timestamp (while setting up): ', err));
649 },
650 setUp() {
651 if (!this.isOrderedByTimestampDescending()) return;
652
653 this.unregister();
654
655 console.debug('autorefresh_list: starting set up...');
656
657 if (this.snackbar === null) this.injectUpdatePrompt();
658 this.isLookingForUpdates = true;
659 this.path = location.pathname;
660 this.filter = this.getFilter(this.path);
661
662 var firstCall = this.firstCall.bind(this);
663 this.firstCallTimeout = window.setTimeout(firstCall, this.firstCallDelayMs);
664 },
665};
666
Adrià Vilanova Martínezc6aacfa2021-06-09 14:16:11 +0200667function isDarkThemeOn() {
668 if (!options.ccdarktheme) return false;
669
670 if (options.ccdarktheme_mode == 'switch')
671 return options.ccdarktheme_switch_status;
672
673 return window.matchMedia &&
674 window.matchMedia('(prefers-color-scheme: dark)').matches;
675}
676
677var unifiedProfilesFix = {
678 checkIframe(iframe) {
679 var srcRegex = /support.*\.google\.com\/profile\//;
Adrià Vilanova Martínezc6aacfa2021-06-09 14:16:11 +0200680 return srcRegex.test(iframe.src ?? '');
681 },
682 fixIframe(iframe) {
683 console.info('[unifiedProfilesFix] Fixing unified profiles iframe');
684 var url = new URL(iframe.src);
685 url.searchParams.set('dark', 1);
686 iframe.src = url.href;
687 },
688};
689
avm9996390cc2e32021-02-05 18:14:16 +0100690function injectPreviousPostsLinks(nameElement) {
691 var mainCardContent = getNParent(nameElement, 3);
692 if (mainCardContent === null) {
693 console.error(
694 '[previousposts] Couldn\'t find |.main-card-content| element.');
695 return;
avm99963490114d2021-02-05 16:12:20 +0100696 }
avm9996390cc2e32021-02-05 18:14:16 +0100697
698 var forumId = location.href.split('/forum/')[1].split('/')[0] || '0';
699
avm99963193233a2021-05-29 15:49:29 +0200700 var nameTag =
701 (nameElement.tagName == 'EC-DISPLAY-NAME-EDITOR' ?
702 nameElement.querySelector('.top-section > span') ?? nameElement :
703 nameElement);
704 var name = escapeUsername(nameTag.textContent);
avm9996390cc2e32021-02-05 18:14:16 +0100705 var query1 = encodeURIComponent(
706 '(creator:"' + name + '" | replier:"' + name + '") forum:' + forumId);
707 var query2 = encodeURIComponent(
708 '(creator:"' + name + '" | replier:"' + name + '") forum:any');
709
710 var container = document.createElement('div');
711 container.classList.add('TWPT-previous-posts');
712
713 var badge = createExtBadge();
714 container.appendChild(badge);
715
716 var linkContainer = document.createElement('div');
717 linkContainer.classList.add('TWPT-previous-posts--links');
718
719 addProfileHistoryLink(linkContainer, 'forum', query1);
720 addProfileHistoryLink(linkContainer, 'all', query2);
721
722 container.appendChild(linkContainer);
723
724 mainCardContent.appendChild(container);
avm99963490114d2021-02-05 16:12:20 +0100725}
726
Adrià Vilanova Martíneza10fff22021-06-29 21:07:40 +0200727// Send a request to mark the current thread as read
728function markCurrentThreadAsRead() {
Adrià Vilanova Martínezcb6fdba2021-07-07 14:42:38 +0200729 console.debug(
730 '[forceMarkAsRead] %cTrying to mark a thread as read.',
731 'color: #1a73e8;');
732
Adrià Vilanova Martíneza10fff22021-06-29 21:07:40 +0200733 var threadRegex =
734 /\/s\/community\/?.*\/forum\/([0-9]+)\/?.*\/thread\/([0-9]+)/;
735
736 var url = location.href;
737 var matches = url.match(threadRegex);
738 if (matches !== null && matches.length > 2) {
739 var forumId = matches[1];
740 var threadId = matches[2];
741
Adrià Vilanova Martínezcb6fdba2021-07-07 14:42:38 +0200742 console.debug('[forceMarkAsRead] Thread details:', {forumId, threadId});
743
Adrià Vilanova Martíneza10fff22021-06-29 21:07:40 +0200744 return CCApi(
745 'ViewThread', {
746 1: forumId,
747 2: threadId,
748 // options
749 3: {
750 // pagination
751 1: {
752 2: 0, // maxNum
753 },
754 3: false, // withMessages
755 5: false, // withUserProfile
756 6: true, // withUserReadState
757 9: false, // withRequestorProfile
758 10: false, // withPromotedMessages
759 11: false, // withExpertResponder
760 },
761 },
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200762 true, authuser)
Adrià Vilanova Martíneza10fff22021-06-29 21:07:40 +0200763 .then(thread => {
764 if (thread?.[1]?.[6] === true) {
765 console.debug(
Adrià Vilanova Martínezcb6fdba2021-07-07 14:42:38 +0200766 '[forceMarkAsRead] This thread is already marked as read, but marking it as read anyways.');
Adrià Vilanova Martíneza10fff22021-06-29 21:07:40 +0200767 }
768
769 var lastMessageId = thread?.[1]?.[2]?.[10];
Adrià Vilanova Martínezcb6fdba2021-07-07 14:42:38 +0200770
Adrià Vilanova Martínezfe8acef2021-07-07 17:27:23 +0200771 console.debug('[forceMarkAsRead] lastMessageId is:', lastMessageId);
Adrià Vilanova Martínezcb6fdba2021-07-07 14:42:38 +0200772
Adrià Vilanova Martíneza10fff22021-06-29 21:07:40 +0200773 if (lastMessageId === undefined)
774 throw new Error(
775 'Couldn\'t find lastMessageId in the ViewThread response.');
776
777 return CCApi(
778 'SetUserReadStateBulk', {
779 1: [{
780 1: forumId,
781 2: threadId,
782 3: lastMessageId,
783 }],
784 },
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200785 true, authuser);
Adrià Vilanova Martíneza10fff22021-06-29 21:07:40 +0200786 })
Adrià Vilanova Martínezcb6fdba2021-07-07 14:42:38 +0200787 .then(_ => {
788 console.debug(
789 '[forceMarkAsRead] %cSuccessfully set as read!',
790 'color: #1e8e3e;');
791 })
Adrià Vilanova Martíneza10fff22021-06-29 21:07:40 +0200792 .catch(err => {
793 console.error(
794 '[forceMarkAsRead] Error while marking current thread as read: ',
795 err);
796 });
Adrià Vilanova Martínezcb6fdba2021-07-07 14:42:38 +0200797 } else {
798 console.error(
799 '[forceMarkAsRead] Couldn\'t retrieve forumId and threadId from the current URL.',
800 url);
Adrià Vilanova Martíneza10fff22021-06-29 21:07:40 +0200801 }
802}
803
avm99963490114d2021-02-05 16:12:20 +0100804const watchedNodesSelectors = [
avm9996328fddc62021-02-05 20:33:48 +0100805 // App container (used to set up the intersection observer and inject the dark
806 // mode button)
avm9996311707032021-02-05 19:11:25 +0100807 'ec-app',
808
avm99963490114d2021-02-05 16:12:20 +0100809 // Load more bar (for the "load more"/"load all" buttons)
810 '.load-more-bar',
811
avm99963b3e89862021-05-15 19:58:21 +0200812 // Username span/editor inside ec-user (user profile view)
avm9996390cc2e32021-02-05 18:14:16 +0100813 'ec-user .main-card .header > .name > span',
avm99963b3e89862021-05-15 19:58:21 +0200814 'ec-user .main-card .header > .name > ec-display-name-editor',
avm99963490114d2021-02-05 16:12:20 +0100815
816 // Rich text editor
817 'ec-movable-dialog',
818 'ec-rich-text-editor',
819
820 // Read/unread bulk action in the list of thread, for the batch lock feature
821 'ec-bulk-actions material-button[debugid="mark-read-button"]',
822 'ec-bulk-actions material-button[debugid="mark-unread-button"]',
avm999633eae4522021-04-22 01:14:27 +0200823
824 // Thread list items (used to inject the avatars)
825 'li',
avm99963a007d492021-05-02 12:32:03 +0200826
827 // Thread list (used for the autorefresh feature)
828 'ec-thread-list',
Adrià Vilanova Martínezc6aacfa2021-06-09 14:16:11 +0200829
830 // Unified profile iframe
831 'iframe',
Adrià Vilanova Martíneza10fff22021-06-29 21:07:40 +0200832
833 // Thread component
834 'ec-thread',
avm99963490114d2021-02-05 16:12:20 +0100835];
836
837function handleCandidateNode(node) {
838 if (typeof node.classList !== 'undefined') {
avm9996328fddc62021-02-05 20:33:48 +0100839 if (('tagName' in node) && node.tagName == 'EC-APP') {
840 // Set up the intersectionObserver
841 if (typeof intersectionObserver === 'undefined') {
842 var scrollableContent = node.querySelector('.scrollable-content');
843 if (scrollableContent !== null) {
844 intersectionOptions = {
845 root: scrollableContent,
846 rootMargin: '0px',
847 threshold: 1.0,
848 };
avm9996311707032021-02-05 19:11:25 +0100849
avm9996328fddc62021-02-05 20:33:48 +0100850 intersectionObserver = new IntersectionObserver(
851 intersectionCallback, intersectionOptions);
852 }
853 }
854
855 // Inject the dark mode button
856 if (options.ccdarktheme && options.ccdarktheme_mode == 'switch') {
857 var rightControl = node.querySelector('header .right-control');
858 if (rightControl !== null) injectDarkModeButton(rightControl);
avm99963d24e2db2021-02-05 20:03:55 +0100859 }
avm99963490114d2021-02-05 16:12:20 +0100860 }
861
avm9996311707032021-02-05 19:11:25 +0100862 // Start the intersectionObserver for the "load more"/"load all" buttons
863 // inside a thread
864 if ((options.thread || options.threadall) &&
865 node.classList.contains('load-more-bar')) {
866 if (typeof intersectionObserver !== 'undefined') {
867 if (options.thread)
868 intersectionObserver.observe(node.querySelector('.load-more-button'));
869 if (options.threadall)
870 intersectionObserver.observe(node.querySelector('.load-all-button'));
871 } else {
872 console.warn(
873 '[infinitescroll] ' +
874 'The intersectionObserver is not ready yet.');
875 }
avm99963490114d2021-02-05 16:12:20 +0100876 }
877
878 // Show the "previous posts" links
879 // Here we're selecting the 'ec-user > div' element (unique child)
avm9996390cc2e32021-02-05 18:14:16 +0100880 if (options.history &&
avm99963b3e89862021-05-15 19:58:21 +0200881 (node.matches('ec-user .main-card .header > .name > span') ||
882 node.matches(
883 'ec-user .main-card .header > .name > ec-display-name-editor'))) {
avm99963490114d2021-02-05 16:12:20 +0100884 injectPreviousPostsLinks(node);
885 }
886
887 // Fix the drag&drop issue with the rich text editor
888 //
889 // We target both tags because in different contexts different
890 // elements containing the text editor get added to the DOM structure.
891 // Sometimes it's a EC-MOVABLE-DIALOG which already contains the
892 // EC-RICH-TEXT-EDITOR, and sometimes it's the EC-RICH-TEXT-EDITOR
893 // directly.
894 if (options.ccdragndropfix && ('tagName' in node) &&
895 (node.tagName == 'EC-MOVABLE-DIALOG' ||
896 node.tagName == 'EC-RICH-TEXT-EDITOR')) {
897 applyDragAndDropFix(node);
898 }
899
900 // Inject the batch lock button in the thread list
901 if (options.batchlock && nodeIsReadToggleBtn(node)) {
902 addBatchLockBtn(node);
903 }
avm999633eae4522021-04-22 01:14:27 +0200904
905 // Inject avatar links to threads in the thread list
906 if (options.threadlistavatars && ('tagName' in node) &&
907 (node.tagName == 'LI') &&
908 node.querySelector('ec-thread-summary') !== null) {
Adrià Vilanova Martínez3c37e842021-07-10 19:14:47 +0200909 avatars.inject(node);
avm999633eae4522021-04-22 01:14:27 +0200910 }
avm99963a007d492021-05-02 12:32:03 +0200911
912 // Set up the autorefresh list feature
913 if (options.autorefreshlist && ('tagName' in node) &&
914 node.tagName == 'EC-THREAD-LIST') {
915 autoRefresh.setUp();
916 }
Adrià Vilanova Martínezc6aacfa2021-06-09 14:16:11 +0200917
918 // Redirect unified profile iframe to dark version if applicable
919 if (node.tagName == 'IFRAME' && isDarkThemeOn() &&
920 unifiedProfilesFix.checkIframe(node)) {
921 unifiedProfilesFix.fixIframe(node);
922 }
Adrià Vilanova Martíneza10fff22021-06-29 21:07:40 +0200923
924 // Force mark thread as read
925 if (options.forcemarkasread && node.tagName == 'EC-THREAD') {
926 markCurrentThreadAsRead();
927 }
avm99963a007d492021-05-02 12:32:03 +0200928 }
929}
930
931function handleRemovedNode(node) {
932 // Remove snackbar when exiting thread list view
Adrià Vilanova Martínez148f75c2021-07-07 13:47:34 +0200933 if (options.autorefreshlist && 'tagName' in node &&
934 node.tagName == 'EC-THREAD-LIST') {
avm99963a007d492021-05-02 12:32:03 +0200935 autoRefresh.hideUpdatePrompt();
avm99963490114d2021-02-05 16:12:20 +0100936 }
937}
938
avm99963847ee632019-03-27 00:57:44 +0100939function mutationCallback(mutationList, observer) {
940 mutationList.forEach((mutation) => {
avm99963b69eb3d2020-08-20 02:03:44 +0200941 if (mutation.type == 'childList') {
942 mutation.addedNodes.forEach(function(node) {
avm99963490114d2021-02-05 16:12:20 +0100943 handleCandidateNode(node);
avm99963847ee632019-03-27 00:57:44 +0100944 });
avm99963a007d492021-05-02 12:32:03 +0200945
946 mutation.removedNodes.forEach(function(node) {
947 handleRemovedNode(node);
948 });
avm99963847ee632019-03-27 00:57:44 +0100949 }
950 });
951}
952
avm99963adf90862020-04-12 13:27:45 +0200953function intersectionCallback(entries, observer) {
avm99963847ee632019-03-27 00:57:44 +0100954 entries.forEach(entry => {
955 if (entry.isIntersecting) {
956 entry.target.click();
957 }
958 });
959};
960
961var observerOptions = {
962 childList: true,
avm99963b69eb3d2020-08-20 02:03:44 +0200963 subtree: true,
avm99963129fb502020-08-28 05:18:53 +0200964};
avm99963847ee632019-03-27 00:57:44 +0100965
avm99963129fb502020-08-28 05:18:53 +0200966chrome.storage.sync.get(null, function(items) {
967 options = items;
avm99963cbea3142019-03-28 00:48:15 +0100968
avm99963a2945b62020-11-27 00:32:02 +0100969 var startup =
970 JSON.parse(document.querySelector('html').getAttribute('data-startup'));
971 authuser = startup[2][1] || '0';
972
avm99963490114d2021-02-05 16:12:20 +0100973 // Before starting the mutation Observer, check whether we missed any
avm9996311707032021-02-05 19:11:25 +0100974 // mutations by manually checking whether some watched nodes already
975 // exist.
avm99963490114d2021-02-05 16:12:20 +0100976 var cssSelectors = watchedNodesSelectors.join(',');
977 document.querySelectorAll(cssSelectors)
978 .forEach(node => handleCandidateNode(node));
979
avm99963129fb502020-08-28 05:18:53 +0200980 mutationObserver = new MutationObserver(mutationCallback);
avm99963e4cac402020-12-03 16:10:58 +0100981 mutationObserver.observe(document.body, observerOptions);
avm99963cbea3142019-03-28 00:48:15 +0100982
avm99963129fb502020-08-28 05:18:53 +0200983 if (options.fixedtoolbar) {
984 injectStyles(
avm999630bc113a2020-09-07 13:02:11 +0200985 'ec-bulk-actions{position: sticky; top: 0; background: var(--TWPT-primary-background, #fff); z-index: 96;}');
avm99963129fb502020-08-28 05:18:53 +0200986 }
avm99963ae6a26d2020-04-12 14:03:51 +0200987
avm99963129fb502020-08-28 05:18:53 +0200988 if (options.increasecontrast) {
avm999630bc113a2020-09-07 13:02:11 +0200989 injectStyles(
avm99963a2a06442020-11-25 21:11:10 +0100990 '.thread-summary.read:not(.checked){background: var(--TWPT-thread-read-background, #ecedee)!important;}');
avm99963129fb502020-08-28 05:18:53 +0200991 }
avm999630f9503f2020-07-27 13:56:52 +0200992
avm99963129fb502020-08-28 05:18:53 +0200993 if (options.stickysidebarheaders) {
994 injectStyles(
avm999630bc113a2020-09-07 13:02:11 +0200995 'material-drawer .main-header{background: var(--TWPT-drawer-background, #fff)!important; position: sticky; top: 0; z-index: 1;}');
996 }
997
avm99963698d3762021-02-16 01:19:54 +0100998 if (options.enhancedannouncementsdot) {
999 injectStylesheet(
1000 chrome.runtime.getURL('injections/enhanced_announcements_dot.css'));
1001 }
1002
avm99963d98126f2021-02-17 10:44:36 +01001003 if (options.repositionexpandthread) {
1004 injectStylesheet(
1005 chrome.runtime.getURL('injections/reposition_expand_thread.css'));
1006 }
1007
avm99963129942f2020-09-08 02:07:18 +02001008 if (options.ccforcehidedrawer) {
1009 var drawer = document.querySelector('material-drawer');
1010 if (drawer !== null && drawer.classList.contains('mat-drawer-expanded')) {
1011 document.querySelector('.material-drawer-button').click();
1012 }
1013 }
avm99963f5923962020-12-07 16:44:37 +01001014
1015 if (options.batchlock) {
1016 injectScript(chrome.runtime.getURL('injections/batchlock_inject.js'));
Adrià Vilanova Martínez74273ee2021-06-25 19:23:27 +02001017 injectStylesheet(chrome.runtime.getURL('injections/batchlock_inject.css'));
avm99963f5923962020-12-07 16:44:37 +01001018 }
avm999633eae4522021-04-22 01:14:27 +02001019
1020 if (options.threadlistavatars) {
1021 injectStylesheet(
1022 chrome.runtime.getURL('injections/thread_list_avatars.css'));
1023 }
avm99963a007d492021-05-02 12:32:03 +02001024
1025 if (options.autorefreshlist) {
1026 injectStylesheet(chrome.runtime.getURL('injections/autorefresh_list.css'));
1027 }
avm99963129fb502020-08-28 05:18:53 +02001028});