blob: 29ff70dcffd1de9b0c652937de449e5fc5d4a504 [file] [log] [blame]
avm99963129fb502020-08-28 05:18:53 +02001var profileRegex =
2 /^(?:https:\/\/support\.google\.com)?\/s\/community\/forum\/[0-9]*\/user\/(?:[0-9]+)$/;
3
4const OP_FIRST_POST = 0;
5const OP_OTHER_POSTS_READ = 1;
6const OP_OTHER_POSTS_UNREAD = 2;
7
8const OPClasses = {
9 0: 'profile-indicator--first-post',
10 1: 'profile-indicator--other-posts-read',
11 2: 'profile-indicator--other-posts-unread',
12};
13
14const OPi18n = {
15 0: 'first_post',
16 1: 'other_posts_read',
17 2: 'other_posts_unread',
18};
19
20// Filter used as a workaround to speed up the ViewForum request.
21const FILTER_ALL_LANGUAGES =
22 'lang:(ar | bg | ca | "zh-hk" | "zh-cn" | "zh-tw" | hr | cs | da | nl | en | "en-au" | "en-gb" | et | fil | fi | fr | de | el | iw | hi | hu | id | it | ja | ko | lv | lt | ms | no | pl | "pt-br" | "pt-pt" | ro | ru | sr | sk | sl | es | "es-419" | sv | th | tr | uk | vi)';
23
24function isElementInside(element, outerTag) {
25 while (element !== null && ('tagName' in element)) {
26 if (element.tagName == outerTag) return true;
27 element = element.parentNode;
28 }
29
30 return false;
31}
32
33function escapeUsername(username) {
34 var quoteRegex = /"/g;
35 var commentRegex = /<!---->/g;
36 return username.replace(quoteRegex, '\\"').replace(commentRegex, '');
37}
38
39function getPosts(query, forumId) {
40 return fetch('https://support.google.com/s/community/api/ViewForum', {
41 'credentials': 'include',
42 'headers': {'content-type': 'text/plain; charset=utf-8'},
43 'body': JSON.stringify({
44 '1': forumId,
45 '2': {
46 '1': {
47 '2': 5,
48 },
49 '2': {
50 '1': 1,
51 '2': true,
52 },
53 '12': query,
54 },
55 }),
56 'method': 'POST',
57 'mode': 'cors',
58 })
59 .then(res => res.json());
60}
61
62// Source:
63// https://stackoverflow.com/questions/33063774/communication-from-an-injected-script-to-the-content-script-with-a-response
64var i18nRequest = (function() {
65 var requestId = 0;
66
67 function getMessage(msg) {
68 var id = requestId++;
69
70 return new Promise(function(resolve, reject) {
71 var listener = function(evt) {
72 if (evt.detail.requestId == id) {
73 // Deregister self
74 window.removeEventListener('sendChromeData', listener);
75 resolve(evt.detail.string);
76 }
77 };
78
79 window.addEventListener('sendi18nString', listener);
80
81 var payload = {msg: msg, id: id};
82
83 window.dispatchEvent(new CustomEvent('geti18nString', {detail: payload}));
84 });
85 }
86
87 return {getMessage: getMessage};
88})();
89
90function mutationCallback(mutationList, observer) {
91 mutationList.forEach((mutation) => {
92 if (mutation.type == 'childList') {
93 mutation.addedNodes.forEach(function(node) {
94 if (node.tagName == 'A' && ('href' in node) &&
95 profileRegex.test(node.href) &&
96 isElementInside(node, 'EC-QUESTION') && ('children' in node) &&
97 node.children.length == 0) {
98 var escapedUsername = escapeUsername(node.innerHTML);
99
100 // Create profile indicator dot with a loading state
101 var dotContainer = document.createElement('span');
102 dotContainer.classList.add('profile-indicator');
103 dotContainer.classList.add('profile-indicator--loading');
104 i18nRequest.getMessage('inject_profileindicator_loading')
105 .then(string => dotContainer.setAttribute('title', string));
106
107 var forumUrlSplit = document.location.href.split('/forum/');
108 if (forumUrlSplit.length < 2) throw new Error('Can\'t get forum id.');
109
110 var forumId = forumUrlSplit[1].split('/')[0];
111 var query = '(replier:"' + escapedUsername + '" | creator:"' +
112 escapedUsername + '") ' + FILTER_ALL_LANGUAGES;
113 var encodedQuery = encodeURIComponent(query);
114 var urlpart = encodeURIComponent('query=' + encodedQuery);
115 var dotLink = document.createElement('a');
116 dotLink.href =
117 'https://support.google.com/s/community/search/' + urlpart;
118 dotLink.innerText = '●';
119
120 dotContainer.appendChild(dotLink);
121 node.parentNode.appendChild(dotContainer);
122
123 // Query threads in order to see what state the indicator should be in
124 getPosts(query, forumId)
125 .then(res => {
126 if (!('1' in res) || !('2' in res['1'])) {
127 throw new Error('Unexpected response.');
128 return;
129 }
130
131 // Current thread ID
132 var threadUrlSplit = document.location.href.split('/thread/');
133 if (threadUrlSplit.length < 2)
134 throw new Error('Can\'t get thread id.');
135
136 var currId = threadUrlSplit[1].split('/')[0];
137
138 var OPStatus = OP_FIRST_POST;
139
140 for (const thread of res['1']['2']) {
141 var id = thread['2']['1']['1'] || undefined;
142 if (id === undefined || id == currId) continue;
143
144 var isRead = thread['6'] || false;
145 if (isRead)
146 OPStatus = Math.max(OP_OTHER_POSTS_READ, OPStatus);
147 else
148 OPStatus = Math.max(OP_OTHER_POSTS_UNREAD, OPStatus);
149 }
150
151 dotContainer.classList.remove('profile-indicator--loading');
152 dotContainer.classList.add(OPClasses[OPStatus]);
153 i18nRequest
154 .getMessage('inject_profileindicator_' + OPi18n[OPStatus])
155 .then(string => dotContainer.setAttribute('title', string));
156 })
157 .catch(
158 err => console.error(
159 'Unexpected error. Couldn\'t load recent posts.', err));
160 }
161 });
162 }
163 });
164};
165
166var observerOptions = {
167 childList: true,
168 subtree: true,
169}
170
171mutationObserver = new MutationObserver(mutationCallback);
172mutationObserver.observe(
173 document.querySelector('.scrollable-content'), observerOptions);