blob: 94ce9435883a6a5945e2cecebd5aa02ecc0c5c46 [file] [log] [blame]
avm99963e6166ed2021-08-26 10:45:17 +02001import {CCApi} from '../common/api.js';
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +02002import {escapeUsername} from '../common/communityConsoleUtils.js';
avm999632485a3e2021-09-08 22:18:38 +02003import {createPlainTooltip} from '../common/tooltip.js';
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +02004
avm99963e51444e2020-08-31 14:50:06 +02005var CCProfileRegex =
avm99963a945acd2021-02-06 19:23:14 +01006 /^(?:https:\/\/support\.google\.com)?\/s\/community(?:\/forum\/[0-9]*)?\/user\/(?:[0-9]+)$/;
avm99963e51444e2020-08-31 14:50:06 +02007var CCRegex = /^https:\/\/support\.google\.com\/s\/community/;
8
9const OP_FIRST_POST = 0;
10const OP_OTHER_POSTS_READ = 1;
11const OP_OTHER_POSTS_UNREAD = 2;
12
13const OPClasses = {
avm99963ad65e752020-09-01 00:13:59 +020014 0: 'first-post',
15 1: 'other-posts-read',
16 2: 'other-posts-unread',
avm99963e51444e2020-08-31 14:50:06 +020017};
18
19const OPi18n = {
20 0: 'first_post',
21 1: 'other_posts_read',
22 2: 'other_posts_unread',
23};
24
avm99963ad65e752020-09-01 00:13:59 +020025const indicatorTypes = ['numPosts', 'indicatorDot'];
26
avm99963e51444e2020-08-31 14:50:06 +020027// Filter used as a workaround to speed up the ViewForum request.
28const FILTER_ALL_LANGUAGES =
29 '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)';
30
avm99963ad65e752020-09-01 00:13:59 +020031const numPostsForumArraysToSum = [3, 4];
32
avm99963a2945b62020-11-27 00:32:02 +010033var authuser = null;
34
avm99963e51444e2020-08-31 14:50:06 +020035function isElementInside(element, outerTag) {
36 while (element !== null && ('tagName' in element)) {
37 if (element.tagName == outerTag) return true;
38 element = element.parentNode;
39 }
40
41 return false;
42}
43
avm99963a2945b62020-11-27 00:32:02 +010044function getPosts(query, forumId) {
avm99963e6166ed2021-08-26 10:45:17 +020045 return CCApi(
46 'ViewForum', {
47 '1': forumId,
48 '2': {
49 '1': {
50 '2': 5,
51 },
52 '2': {
53 '1': 1,
54 '2': true,
55 },
56 '12': query,
57 },
avm99963a2945b62020-11-27 00:32:02 +010058 },
avm99963e6166ed2021-08-26 10:45:17 +020059 /* authenticated = */ true, authuser);
avm99963a2945b62020-11-27 00:32:02 +010060}
61
avm99963ad65e752020-09-01 00:13:59 +020062function getProfile(userId, forumId) {
avm99963e6166ed2021-08-26 10:45:17 +020063 return CCApi(
64 'ViewUser', {
65 '1': userId,
66 '2': 0,
67 '3': forumId,
68 '4': {
69 '20': true,
70 },
71 },
72 /* authenticated = */ true, authuser);
avm99963ad65e752020-09-01 00:13:59 +020073}
74
avm99963e51444e2020-08-31 14:50:06 +020075// Source:
76// https://stackoverflow.com/questions/33063774/communication-from-an-injected-script-to-the-content-script-with-a-response
avm99963ad65e752020-09-01 00:13:59 +020077var contentScriptRequest = (function() {
avm99963e51444e2020-08-31 14:50:06 +020078 var requestId = 0;
avm999633e238882020-12-07 18:38:54 +010079 var prefix = 'TWPT-profileindicator';
avm99963e51444e2020-08-31 14:50:06 +020080
avm99963ad65e752020-09-01 00:13:59 +020081 function sendRequest(data) {
avm99963e51444e2020-08-31 14:50:06 +020082 var id = requestId++;
83
84 return new Promise(function(resolve, reject) {
85 var listener = function(evt) {
avm999633e238882020-12-07 18:38:54 +010086 if (evt.source === window && evt.data && evt.data.prefix === prefix &&
avm99963ad65e752020-09-01 00:13:59 +020087 evt.data.requestId == id) {
avm99963e51444e2020-08-31 14:50:06 +020088 // Deregister self
avm99963ad65e752020-09-01 00:13:59 +020089 window.removeEventListener('message', listener);
90 resolve(evt.data.data);
avm99963e51444e2020-08-31 14:50:06 +020091 }
92 };
93
avm99963ad65e752020-09-01 00:13:59 +020094 window.addEventListener('message', listener);
avm99963e51444e2020-08-31 14:50:06 +020095
avm999633e238882020-12-07 18:38:54 +010096 var payload = {data, id, prefix};
avm99963e51444e2020-08-31 14:50:06 +020097
avm99963ad65e752020-09-01 00:13:59 +020098 window.dispatchEvent(
99 new CustomEvent('TWPT_sendRequest', {detail: payload}));
avm99963e51444e2020-08-31 14:50:06 +0200100 });
101 }
102
avm99963ad65e752020-09-01 00:13:59 +0200103 return {sendRequest: sendRequest};
avm99963e51444e2020-08-31 14:50:06 +0200104})();
105
avm99963ad65e752020-09-01 00:13:59 +0200106// Create profile indicator dot with a loading state, or return the numPosts
107// badge if it is already created.
108function createIndicatorDot(sourceNode, searchURL, options) {
109 if (options.numPosts) return document.querySelector('.num-posts-indicator');
avm99963a1b23b62020-09-01 14:32:34 +0200110 var dotContainer = document.createElement('div');
avm99963ad65e752020-09-01 00:13:59 +0200111 dotContainer.classList.add('profile-indicator', 'profile-indicator--loading');
avm99963e51444e2020-08-31 14:50:06 +0200112
113 var dotLink = document.createElement('a');
114 dotLink.href = searchURL;
115 dotLink.innerText = '●';
116
117 dotContainer.appendChild(dotLink);
118 sourceNode.parentNode.appendChild(dotContainer);
119
avm999632485a3e2021-09-08 22:18:38 +0200120 contentScriptRequest
121 .sendRequest({
122 'action': 'geti18nMessage',
123 'msg': 'inject_profileindicator_loading'
124 })
125 .then(string => createPlainTooltip(dotContainer, string));
126
avm99963e51444e2020-08-31 14:50:06 +0200127 return dotContainer;
128}
129
avm99963ad65e752020-09-01 00:13:59 +0200130// Create badge indicating the number of posts with a loading state
131function createNumPostsBadge(sourceNode, searchURL) {
132 var link = document.createElement('a');
133 link.href = searchURL;
134
135 var numPostsContainer = document.createElement('div');
136 numPostsContainer.classList.add(
137 'num-posts-indicator', 'num-posts-indicator--loading');
avm99963ad65e752020-09-01 00:13:59 +0200138
139 var numPostsSpan = document.createElement('span');
140 numPostsSpan.classList.add('num-posts-indicator--num');
141
142 numPostsContainer.appendChild(numPostsSpan);
143 link.appendChild(numPostsContainer);
144 sourceNode.parentNode.appendChild(link);
avm999632485a3e2021-09-08 22:18:38 +0200145
146 contentScriptRequest
147 .sendRequest({
148 'action': 'geti18nMessage',
149 'msg': 'inject_profileindicator_loading'
150 })
151 .then(string => createPlainTooltip(numPostsContainer, string));
152
avm99963ad65e752020-09-01 00:13:59 +0200153 return numPostsContainer;
154}
155
avm9996313658b92020-12-02 00:02:53 +0100156// Set the badge text
157function setNumPostsBadge(badge, text) {
158 badge.classList.remove('num-posts-indicator--loading');
159 badge.querySelector('span').classList.remove(
160 'num-posts-indicator--num--loading');
161 badge.querySelector('span').textContent = text;
162}
163
avm99963ad65e752020-09-01 00:13:59 +0200164// Get options and then handle all the indicators
165function getOptionsAndHandleIndicators(sourceNode, isCC) {
166 contentScriptRequest.sendRequest({'action': 'getProfileIndicatorOptions'})
167 .then(options => handleIndicators(sourceNode, isCC, options));
168}
169
170// Handle the profile indicator dot
171function handleIndicators(sourceNode, isCC, options) {
avm99963e51444e2020-08-31 14:50:06 +0200172 var escapedUsername = escapeUsername(
Adrià Vilanova Martínez8cb54432021-09-19 23:05:13 +0200173 (isCC ? sourceNode.querySelector('.name-text').textContent :
174 sourceNode.querySelector('span').textContent));
avm99963e51444e2020-08-31 14:50:06 +0200175
176 if (isCC) {
177 var threadLink = document.location.href;
178 } else {
179 var CCLink = document.getElementById('onebar-community-console');
180 if (CCLink === null) {
181 console.error(
avm99963ad65e752020-09-01 00:13:59 +0200182 '[opindicator] The user is not a PE so the dot indicator cannot be shown in TW.');
avm99963e51444e2020-08-31 14:50:06 +0200183 return;
184 }
185 var threadLink = CCLink.href;
186 }
187
188 var forumUrlSplit = threadLink.split('/forum/');
189 if (forumUrlSplit.length < 2) {
avm99963ad65e752020-09-01 00:13:59 +0200190 console.error('[opindicator] Can\'t get forum id.');
avm99963e51444e2020-08-31 14:50:06 +0200191 return;
192 }
193
194 var forumId = forumUrlSplit[1].split('/')[0];
avm99963ad65e752020-09-01 00:13:59 +0200195
avm99963104bad32021-02-05 22:00:44 +0100196 /*
197 * TODO(avm99963): If the TW filters ever work again, set isCCLink to isCC.
198 * Otherwise, issue #29 should be resolved:
199 * https://github.com/avm99963/infinitegforums/issues/29
200 */
201 var isCCLink = true;
202
avm99963e51444e2020-08-31 14:50:06 +0200203 var query = '(replier:"' + escapedUsername + '" | creator:"' +
204 escapedUsername + '") ' + FILTER_ALL_LANGUAGES;
205 var encodedQuery =
avm99963104bad32021-02-05 22:00:44 +0100206 encodeURIComponent(query + (isCCLink ? ' forum:' + forumId : ''));
avm99963a2945b62020-11-27 00:32:02 +0100207 var authuserPart =
208 (authuser == '0' ?
209 '' :
avm99963104bad32021-02-05 22:00:44 +0100210 (isCCLink ? '?' : '&') + 'authuser=' + encodeURIComponent(authuser));
avm99963e51444e2020-08-31 14:50:06 +0200211 var searchURL =
avm99963104bad32021-02-05 22:00:44 +0100212 (isCCLink ? 'https://support.google.com/s/community/search/' +
avm99963a2945b62020-11-27 00:32:02 +0100213 encodeURIComponent('query=' + encodedQuery) + authuserPart :
avm99963104bad32021-02-05 22:00:44 +0100214 document.location.pathname.split('/thread')[0] +
avm99963a2945b62020-11-27 00:32:02 +0100215 '/threads?thread_filter=' + encodedQuery + authuserPart);
avm99963e51444e2020-08-31 14:50:06 +0200216
avm99963ad65e752020-09-01 00:13:59 +0200217 if (options.numPosts) {
218 var profileURL = new URL(sourceNode.href);
219 var userId =
220 profileURL.pathname.split(isCC ? 'user/' : 'profile/')[1].split('/')[0];
avm99963e51444e2020-08-31 14:50:06 +0200221
avm99963ad65e752020-09-01 00:13:59 +0200222 var numPostsContainer = createNumPostsBadge(sourceNode, searchURL);
avm99963e51444e2020-08-31 14:50:06 +0200223
avm99963ad65e752020-09-01 00:13:59 +0200224 getProfile(userId, forumId)
225 .then(res => {
226 if (!('1' in res) || !('2' in res[1])) {
227 throw new Error('Unexpected profile response.');
228 return;
229 }
avm99963e51444e2020-08-31 14:50:06 +0200230
avm99963ad65e752020-09-01 00:13:59 +0200231 contentScriptRequest.sendRequest({'action': 'getNumPostMonths'})
232 .then(months => {
233 if (!options.indicatorDot)
234 contentScriptRequest
235 .sendRequest({
236 'action': 'geti18nMessage',
237 'msg': 'inject_profileindicatoralt_numposts',
238 'placeholders': [months]
239 })
240 .then(
241 string =>
avm999632485a3e2021-09-08 22:18:38 +0200242 createPlainTooltip(numPostsContainer, string));
avm99963e51444e2020-08-31 14:50:06 +0200243
avm99963ad65e752020-09-01 00:13:59 +0200244 var numPosts = 0;
avm99963e51444e2020-08-31 14:50:06 +0200245
avm99963ad65e752020-09-01 00:13:59 +0200246 for (const index of numPostsForumArraysToSum) {
247 if (!(index in res[1][2])) {
248 throw new Error('Unexpected profile response.');
249 return;
250 }
avm99963e51444e2020-08-31 14:50:06 +0200251
avm99963ad65e752020-09-01 00:13:59 +0200252 var i = 0;
253 for (const month of res[1][2][index].reverse()) {
254 if (i == months) break;
255 numPosts += month[3] || 0;
256 ++i;
257 }
258 }
avm99963e51444e2020-08-31 14:50:06 +0200259
avm9996313658b92020-12-02 00:02:53 +0100260 setNumPostsBadge(numPostsContainer, numPosts);
avm99963ad65e752020-09-01 00:13:59 +0200261 })
262 .catch(
263 err => console.error('[opindicator] Unexpected error.', err));
264 })
avm9996313658b92020-12-02 00:02:53 +0100265 .catch(err => {
266 console.error(
267 '[opindicator] Unexpected error. Couldn\'t load profile.', err);
268 setNumPostsBadge(numPostsContainer, '?');
269 });
avm99963ad65e752020-09-01 00:13:59 +0200270 }
271
272 if (options.indicatorDot) {
273 var dotContainer = createIndicatorDot(sourceNode, searchURL, options);
274
275 // Query threads in order to see what state the indicator should be in
276 getPosts(query, forumId)
277 .then(res => {
avm999639f586f42021-02-05 12:37:51 +0100278 // Throw an error when the replies array is not present in the reply.
avm99963ad65e752020-09-01 00:13:59 +0200279 if (!('1' in res) || !('2' in res['1'])) {
avm999639f586f42021-02-05 12:37:51 +0100280 // Throw a different error when the numThreads field exists and is
281 // equal to 0. This reply can be received, but is enexpected,
282 // because we know that the user has replied in at least 1 thread
283 // (the current one).
284 if (('1' in res) && ('4' in res['1']) && res['1']['4'] == 0)
285 throw new Error(
286 'Thread list is empty ' +
287 '(but the OP has participated in this thread, ' +
288 'so it shouldn\'t be empty).');
289
avm99963ad65e752020-09-01 00:13:59 +0200290 throw new Error('Unexpected thread list response.');
291 return;
292 }
293
294 // Current thread ID
295 var threadUrlSplit = threadLink.split('/thread/');
296 if (threadUrlSplit.length < 2)
297 throw new Error('Can\'t get thread id.');
298
299 var currId = threadUrlSplit[1].split('/')[0];
300
301 var OPStatus = OP_FIRST_POST;
302
303 for (const thread of res['1']['2']) {
304 var id = thread['2']['1']['1'] || undefined;
305 if (id === undefined || id == currId) continue;
306
307 var isRead = thread['6'] || false;
308 if (isRead)
309 OPStatus = Math.max(OP_OTHER_POSTS_READ, OPStatus);
310 else
311 OPStatus = Math.max(OP_OTHER_POSTS_UNREAD, OPStatus);
312 }
313
314 var dotContainerPrefix =
315 (options.numPosts ? 'num-posts-indicator' : 'profile-indicator');
316
317 if (!options.numPosts)
318 dotContainer.classList.remove(dotContainerPrefix + '--loading');
319 dotContainer.classList.add(
320 dotContainerPrefix + '--' + OPClasses[OPStatus]);
321 contentScriptRequest
322 .sendRequest({
323 'action': 'geti18nMessage',
324 'msg': 'inject_profileindicator_' + OPi18n[OPStatus]
325 })
avm999632485a3e2021-09-08 22:18:38 +0200326 .then(string => createPlainTooltip(dotContainer, string));
avm99963ad65e752020-09-01 00:13:59 +0200327 })
328 .catch(
329 err => console.error(
330 '[opindicator] Unexpected error. Couldn\'t load recent posts.',
331 err));
332 }
avm99963e51444e2020-08-31 14:50:06 +0200333}
334
335if (CCRegex.test(location.href)) {
336 // We are in the Community Console
avm99963a2945b62020-11-27 00:32:02 +0100337 var startup =
338 JSON.parse(document.querySelector('html').getAttribute('data-startup'));
339
340 authuser = startup[2][1] || '0';
341
avm9996389812882021-02-02 20:51:25 +0100342 // When the OP's username is found, call getOptionsAndHandleIndicators
avm99963e51444e2020-08-31 14:50:06 +0200343 function mutationCallback(mutationList, observer) {
344 mutationList.forEach((mutation) => {
345 if (mutation.type == 'childList') {
346 mutation.addedNodes.forEach(function(node) {
347 if (node.tagName == 'A' && ('href' in node) &&
348 CCProfileRegex.test(node.href) &&
avm999639f586f42021-02-05 12:37:51 +0100349 node.matches(
350 'ec-question ec-message-header .name-section ec-user-link a')) {
avm9996389812882021-02-02 20:51:25 +0100351 console.info('Handling profile indicator via mutation callback.');
avm99963ad65e752020-09-01 00:13:59 +0200352 getOptionsAndHandleIndicators(node, true);
avm99963e51444e2020-08-31 14:50:06 +0200353 }
354 });
355 }
356 });
357 };
358
359 var observerOptions = {
360 childList: true,
361 subtree: true,
362 }
363
avm9996389812882021-02-02 20:51:25 +0100364 // Before starting the mutation Observer, check if the OP's username link is
365 // already part of the page
366 var node = document.querySelector(
367 'ec-question ec-message-header .name-section ec-user-link a');
368 if (node !== null) {
369 console.info('Handling profile indicator via first check.');
370 getOptionsAndHandleIndicators(node, true);
371 }
372
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200373 var mutationObserver = new MutationObserver(mutationCallback);
avm99963e4cac402020-12-03 16:10:58 +0100374 mutationObserver.observe(document.body, observerOptions);
avm99963e51444e2020-08-31 14:50:06 +0200375} else {
376 // We are in TW
avm99963a2945b62020-11-27 00:32:02 +0100377 authuser = (new URL(location.href)).searchParams.get('authuser') || '0';
378
avm99963ad65e752020-09-01 00:13:59 +0200379 var node =
380 document.querySelector('.thread-question a.user-info-display-name');
avm99963e51444e2020-08-31 14:50:06 +0200381 if (node !== null)
avm99963ad65e752020-09-01 00:13:59 +0200382 getOptionsAndHandleIndicators(node, false);
avm99963e51444e2020-08-31 14:50:06 +0200383 else
avm99963ad65e752020-09-01 00:13:59 +0200384 console.error('[opindicator] Couldn\'t find username.');
avm99963e51444e2020-08-31 14:50:06 +0200385}