blob: ae108813a3a929054416e113536b6c4ac453fe51 [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 });
234 readToggle.parentNode.insertBefore(
235 clone, (readToggle.nextSibling || readToggle));
236}
237
avm999633eae4522021-04-22 01:14:27 +0200238// TODO(avm99963): This is a prototype. DON'T FORGET TO ADD ERROR HANDLING.
239function injectAvatars(node) {
240 var header = node.querySelector(
241 'ec-thread-summary .main-header .panel-description a.header');
242 if (header === null) return;
243
244 var link = parseUrl(header.href);
245 if (link === false) return;
246
247 var APIRequestUrl = 'https://support.google.com/s/community/api/ViewThread' +
248 (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
249
250 fetch(APIRequestUrl, {
251 'headers': {
252 'content-type': 'text/plain; charset=utf-8',
253 },
254 'body': JSON.stringify({
255 1: link.forum,
256 2: link.thread,
257 3: {
258 1: {2: 15},
259 3: true,
260 5: true,
261 10: true,
262 16: true,
263 18: true,
264 }
265 }),
266 'method': 'POST',
267 'mode': 'cors',
268 'credentials': 'omit',
269 })
270 .then(res => {
271 if (res.status == 200 || res.status == 400) {
272 return res.json().then(data => ({
273 status: res.status,
274 body: data,
275 }));
276 } else {
277 throw new Error('Status code ' + res.status + ' was not expected.');
278 }
279 })
280 .then(res => {
281 if (res.status == 400) {
282 throw new Error(
283 res.body[4] ||
284 ('Response status: 400. Error code: ' + res.body[2]));
285 }
286
287 return res.body;
288 })
289 .then(data => {
290 if (!('1' in data) || !('8' in data['1'])) return false;
291
292 var messages = data['1']['8'];
293 if (messages == 0) return;
294
295 var avatarUrls = [];
296
297 if (!('3' in data['1'])) return false;
298 for (var m of data['1']['3']) {
299 if (!('3' in m) || !('1' in m['3']) || !('2' in m['3']['1']))
300 continue;
301
302 var url = m['3']['1']['2'];
303
304 if (!avatarUrls.includes(url)) avatarUrls.push(url);
305
306 if (avatarUrls.length == 3) break;
307 }
308
309 var avatarsContainer = document.createElement('div');
310 avatarsContainer.classList.add('TWPT-avatars');
311
312 var count = Math.floor(Math.random() * 4);
313
314 for (var i = 0; i < avatarUrls.length; ++i) {
315 var avatar = document.createElement('div');
316 avatar.classList.add('TWPT-avatar');
317 avatar.style.backgroundImage = 'url(\''+avatarUrls[i]+'\')';
318 avatarsContainer.appendChild(avatar);
319 }
320
321 header.appendChild(avatarsContainer);
322 });
323}
324
avm9996390cc2e32021-02-05 18:14:16 +0100325function injectPreviousPostsLinks(nameElement) {
326 var mainCardContent = getNParent(nameElement, 3);
327 if (mainCardContent === null) {
328 console.error(
329 '[previousposts] Couldn\'t find |.main-card-content| element.');
330 return;
avm99963490114d2021-02-05 16:12:20 +0100331 }
avm9996390cc2e32021-02-05 18:14:16 +0100332
333 var forumId = location.href.split('/forum/')[1].split('/')[0] || '0';
334
335 var name = escapeUsername(nameElement.textContent);
336 var query1 = encodeURIComponent(
337 '(creator:"' + name + '" | replier:"' + name + '") forum:' + forumId);
338 var query2 = encodeURIComponent(
339 '(creator:"' + name + '" | replier:"' + name + '") forum:any');
340
341 var container = document.createElement('div');
342 container.classList.add('TWPT-previous-posts');
343
344 var badge = createExtBadge();
345 container.appendChild(badge);
346
347 var linkContainer = document.createElement('div');
348 linkContainer.classList.add('TWPT-previous-posts--links');
349
350 addProfileHistoryLink(linkContainer, 'forum', query1);
351 addProfileHistoryLink(linkContainer, 'all', query2);
352
353 container.appendChild(linkContainer);
354
355 mainCardContent.appendChild(container);
avm99963490114d2021-02-05 16:12:20 +0100356}
357
358const watchedNodesSelectors = [
avm9996328fddc62021-02-05 20:33:48 +0100359 // App container (used to set up the intersection observer and inject the dark
360 // mode button)
avm9996311707032021-02-05 19:11:25 +0100361 'ec-app',
362
avm99963490114d2021-02-05 16:12:20 +0100363 // Load more bar (for the "load more"/"load all" buttons)
364 '.load-more-bar',
365
avm9996390cc2e32021-02-05 18:14:16 +0100366 // Username span inside ec-user (user profile view)
367 'ec-user .main-card .header > .name > span',
avm99963490114d2021-02-05 16:12:20 +0100368
369 // Rich text editor
370 'ec-movable-dialog',
371 'ec-rich-text-editor',
372
373 // Read/unread bulk action in the list of thread, for the batch lock feature
374 'ec-bulk-actions material-button[debugid="mark-read-button"]',
375 'ec-bulk-actions material-button[debugid="mark-unread-button"]',
avm999633eae4522021-04-22 01:14:27 +0200376
377 // Thread list items (used to inject the avatars)
378 'li',
avm99963490114d2021-02-05 16:12:20 +0100379];
380
381function handleCandidateNode(node) {
382 if (typeof node.classList !== 'undefined') {
avm9996328fddc62021-02-05 20:33:48 +0100383 if (('tagName' in node) && node.tagName == 'EC-APP') {
384 // Set up the intersectionObserver
385 if (typeof intersectionObserver === 'undefined') {
386 var scrollableContent = node.querySelector('.scrollable-content');
387 if (scrollableContent !== null) {
388 intersectionOptions = {
389 root: scrollableContent,
390 rootMargin: '0px',
391 threshold: 1.0,
392 };
avm9996311707032021-02-05 19:11:25 +0100393
avm9996328fddc62021-02-05 20:33:48 +0100394 intersectionObserver = new IntersectionObserver(
395 intersectionCallback, intersectionOptions);
396 }
397 }
398
399 // Inject the dark mode button
400 if (options.ccdarktheme && options.ccdarktheme_mode == 'switch') {
401 var rightControl = node.querySelector('header .right-control');
402 if (rightControl !== null) injectDarkModeButton(rightControl);
avm99963d24e2db2021-02-05 20:03:55 +0100403 }
avm99963490114d2021-02-05 16:12:20 +0100404 }
405
avm9996311707032021-02-05 19:11:25 +0100406 // Start the intersectionObserver for the "load more"/"load all" buttons
407 // inside a thread
408 if ((options.thread || options.threadall) &&
409 node.classList.contains('load-more-bar')) {
410 if (typeof intersectionObserver !== 'undefined') {
411 if (options.thread)
412 intersectionObserver.observe(node.querySelector('.load-more-button'));
413 if (options.threadall)
414 intersectionObserver.observe(node.querySelector('.load-all-button'));
415 } else {
416 console.warn(
417 '[infinitescroll] ' +
418 'The intersectionObserver is not ready yet.');
419 }
avm99963490114d2021-02-05 16:12:20 +0100420 }
421
422 // Show the "previous posts" links
423 // Here we're selecting the 'ec-user > div' element (unique child)
avm9996390cc2e32021-02-05 18:14:16 +0100424 if (options.history &&
425 node.matches('ec-user .main-card .header > .name > span')) {
avm99963490114d2021-02-05 16:12:20 +0100426 injectPreviousPostsLinks(node);
427 }
428
429 // Fix the drag&drop issue with the rich text editor
430 //
431 // We target both tags because in different contexts different
432 // elements containing the text editor get added to the DOM structure.
433 // Sometimes it's a EC-MOVABLE-DIALOG which already contains the
434 // EC-RICH-TEXT-EDITOR, and sometimes it's the EC-RICH-TEXT-EDITOR
435 // directly.
436 if (options.ccdragndropfix && ('tagName' in node) &&
437 (node.tagName == 'EC-MOVABLE-DIALOG' ||
438 node.tagName == 'EC-RICH-TEXT-EDITOR')) {
439 applyDragAndDropFix(node);
440 }
441
442 // Inject the batch lock button in the thread list
443 if (options.batchlock && nodeIsReadToggleBtn(node)) {
444 addBatchLockBtn(node);
445 }
avm999633eae4522021-04-22 01:14:27 +0200446
447 // Inject avatar links to threads in the thread list
448 if (options.threadlistavatars && ('tagName' in node) &&
449 (node.tagName == 'LI') &&
450 node.querySelector('ec-thread-summary') !== null) {
451 injectAvatars(node);
452 }
avm99963490114d2021-02-05 16:12:20 +0100453 }
454}
455
avm99963847ee632019-03-27 00:57:44 +0100456function mutationCallback(mutationList, observer) {
457 mutationList.forEach((mutation) => {
avm99963b69eb3d2020-08-20 02:03:44 +0200458 if (mutation.type == 'childList') {
459 mutation.addedNodes.forEach(function(node) {
avm99963490114d2021-02-05 16:12:20 +0100460 handleCandidateNode(node);
avm99963847ee632019-03-27 00:57:44 +0100461 });
462 }
463 });
464}
465
avm99963adf90862020-04-12 13:27:45 +0200466function intersectionCallback(entries, observer) {
avm99963847ee632019-03-27 00:57:44 +0100467 entries.forEach(entry => {
468 if (entry.isIntersecting) {
469 entry.target.click();
470 }
471 });
472};
473
474var observerOptions = {
475 childList: true,
avm99963b69eb3d2020-08-20 02:03:44 +0200476 subtree: true,
avm99963129fb502020-08-28 05:18:53 +0200477};
avm99963847ee632019-03-27 00:57:44 +0100478
avm99963129fb502020-08-28 05:18:53 +0200479chrome.storage.sync.get(null, function(items) {
480 options = items;
avm99963cbea3142019-03-28 00:48:15 +0100481
avm99963a2945b62020-11-27 00:32:02 +0100482 var startup =
483 JSON.parse(document.querySelector('html').getAttribute('data-startup'));
484 authuser = startup[2][1] || '0';
485
avm99963490114d2021-02-05 16:12:20 +0100486 // Before starting the mutation Observer, check whether we missed any
avm9996311707032021-02-05 19:11:25 +0100487 // mutations by manually checking whether some watched nodes already
488 // exist.
avm99963490114d2021-02-05 16:12:20 +0100489 var cssSelectors = watchedNodesSelectors.join(',');
490 document.querySelectorAll(cssSelectors)
491 .forEach(node => handleCandidateNode(node));
492
avm99963129fb502020-08-28 05:18:53 +0200493 mutationObserver = new MutationObserver(mutationCallback);
avm99963e4cac402020-12-03 16:10:58 +0100494 mutationObserver.observe(document.body, observerOptions);
avm99963cbea3142019-03-28 00:48:15 +0100495
avm99963129fb502020-08-28 05:18:53 +0200496 if (options.fixedtoolbar) {
497 injectStyles(
avm999630bc113a2020-09-07 13:02:11 +0200498 'ec-bulk-actions{position: sticky; top: 0; background: var(--TWPT-primary-background, #fff); z-index: 96;}');
avm99963129fb502020-08-28 05:18:53 +0200499 }
avm99963ae6a26d2020-04-12 14:03:51 +0200500
avm99963129fb502020-08-28 05:18:53 +0200501 if (options.increasecontrast) {
avm999630bc113a2020-09-07 13:02:11 +0200502 injectStyles(
avm99963a2a06442020-11-25 21:11:10 +0100503 '.thread-summary.read:not(.checked){background: var(--TWPT-thread-read-background, #ecedee)!important;}');
avm99963129fb502020-08-28 05:18:53 +0200504 }
avm999630f9503f2020-07-27 13:56:52 +0200505
avm99963129fb502020-08-28 05:18:53 +0200506 if (options.stickysidebarheaders) {
507 injectStyles(
avm999630bc113a2020-09-07 13:02:11 +0200508 'material-drawer .main-header{background: var(--TWPT-drawer-background, #fff)!important; position: sticky; top: 0; z-index: 1;}');
509 }
510
avm99963698d3762021-02-16 01:19:54 +0100511 if (options.enhancedannouncementsdot) {
512 injectStylesheet(
513 chrome.runtime.getURL('injections/enhanced_announcements_dot.css'));
514 }
515
avm99963d98126f2021-02-17 10:44:36 +0100516 if (options.repositionexpandthread) {
517 injectStylesheet(
518 chrome.runtime.getURL('injections/reposition_expand_thread.css'));
519 }
520
avm99963129942f2020-09-08 02:07:18 +0200521 if (options.ccforcehidedrawer) {
522 var drawer = document.querySelector('material-drawer');
523 if (drawer !== null && drawer.classList.contains('mat-drawer-expanded')) {
524 document.querySelector('.material-drawer-button').click();
525 }
526 }
avm99963f5923962020-12-07 16:44:37 +0100527
528 if (options.batchlock) {
529 injectScript(chrome.runtime.getURL('injections/batchlock_inject.js'));
530 }
avm999633eae4522021-04-22 01:14:27 +0200531
532 if (options.threadlistavatars) {
533 injectStylesheet(
534 chrome.runtime.getURL('injections/thread_list_avatars.css'));
535 }
avm99963129fb502020-08-28 05:18:53 +0200536});