blob: 98c609299a18d7ecaaf2f1b792ea60c6af4f88a6 [file] [log] [blame]
avm99963e6166ed2021-08-26 10:45:17 +02001import {CCApi} from '../common/api.js';
Adrià Vilanova Martíneze7770472021-10-17 00:02:37 +02002import {createImmuneLink} from '../common/commonUtils.js';
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +02003import {escapeUsername} from '../common/communityConsoleUtils.js';
avm999632485a3e2021-09-08 22:18:38 +02004import {createPlainTooltip} from '../common/tooltip.js';
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +02005
avm99963e51444e2020-08-31 14:50:06 +02006var CCProfileRegex =
Adrià Vilanova Martínezcb28fd92022-01-28 12:41:52 +01007 /^(?:https:\/\/support\.google\.com)?\/s\/community(?:\/forum\/[0-9]*)?\/user\/(?:[0-9]+)(?:\?.*)?$/;
avm99963e51444e2020-08-31 14:50:06 +02008var CCRegex = /^https:\/\/support\.google\.com\/s\/community/;
9
10const OP_FIRST_POST = 0;
11const OP_OTHER_POSTS_READ = 1;
12const OP_OTHER_POSTS_UNREAD = 2;
13
14const OPClasses = {
avm99963ad65e752020-09-01 00:13:59 +020015 0: 'first-post',
16 1: 'other-posts-read',
17 2: 'other-posts-unread',
avm99963e51444e2020-08-31 14:50:06 +020018};
19
20const OPi18n = {
21 0: 'first_post',
22 1: 'other_posts_read',
23 2: 'other_posts_unread',
24};
25
Adrià Vilanova Martínezad6bedf2022-01-28 10:41:10 +010026const UI_COMMUNITY_CONSOLE = 0;
27const UI_TW_LEGACY = 1;
28const UI_TW_INTEROP = 2;
Adrià Vilanova Martínezcb28fd92022-01-28 12:41:52 +010029const UI_COMMUNITY_CONSOLE_INTEROP = 3;
Adrià Vilanova Martínezad6bedf2022-01-28 10:41:10 +010030
avm99963ad65e752020-09-01 00:13:59 +020031const indicatorTypes = ['numPosts', 'indicatorDot'];
32
avm99963e51444e2020-08-31 14:50:06 +020033// Filter used as a workaround to speed up the ViewForum request.
34const FILTER_ALL_LANGUAGES =
35 '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)';
36
avm99963ad65e752020-09-01 00:13:59 +020037const numPostsForumArraysToSum = [3, 4];
38
avm99963a2945b62020-11-27 00:32:02 +010039var authuser = null;
40
Adrià Vilanova Martínezcb28fd92022-01-28 12:41:52 +010041function isCommunityConsole(ui) {
42 return ui === UI_COMMUNITY_CONSOLE || ui === UI_COMMUNITY_CONSOLE_INTEROP;
43}
44
45function isInterop(ui) {
46 return ui === UI_TW_INTEROP || ui === UI_COMMUNITY_CONSOLE_INTEROP;
47}
48
avm99963e51444e2020-08-31 14:50:06 +020049function isElementInside(element, outerTag) {
50 while (element !== null && ('tagName' in element)) {
51 if (element.tagName == outerTag) return true;
52 element = element.parentNode;
53 }
54
55 return false;
56}
57
avm99963a2945b62020-11-27 00:32:02 +010058function getPosts(query, forumId) {
avm99963e6166ed2021-08-26 10:45:17 +020059 return CCApi(
60 'ViewForum', {
61 '1': forumId,
62 '2': {
63 '1': {
64 '2': 5,
65 },
66 '2': {
67 '1': 1,
68 '2': true,
69 },
70 '12': query,
71 },
avm99963a2945b62020-11-27 00:32:02 +010072 },
avm99963e6166ed2021-08-26 10:45:17 +020073 /* authenticated = */ true, authuser);
avm99963a2945b62020-11-27 00:32:02 +010074}
75
avm99963ad65e752020-09-01 00:13:59 +020076function getProfile(userId, forumId) {
avm99963e6166ed2021-08-26 10:45:17 +020077 return CCApi(
78 'ViewUser', {
79 '1': userId,
80 '2': 0,
81 '3': forumId,
82 '4': {
83 '20': true,
84 },
85 },
86 /* authenticated = */ true, authuser);
avm99963ad65e752020-09-01 00:13:59 +020087}
88
avm99963e51444e2020-08-31 14:50:06 +020089// Source:
90// https://stackoverflow.com/questions/33063774/communication-from-an-injected-script-to-the-content-script-with-a-response
avm99963ad65e752020-09-01 00:13:59 +020091var contentScriptRequest = (function() {
avm99963e51444e2020-08-31 14:50:06 +020092 var requestId = 0;
avm999633e238882020-12-07 18:38:54 +010093 var prefix = 'TWPT-profileindicator';
avm99963e51444e2020-08-31 14:50:06 +020094
avm99963ad65e752020-09-01 00:13:59 +020095 function sendRequest(data) {
avm99963e51444e2020-08-31 14:50:06 +020096 var id = requestId++;
97
98 return new Promise(function(resolve, reject) {
99 var listener = function(evt) {
avm999633e238882020-12-07 18:38:54 +0100100 if (evt.source === window && evt.data && evt.data.prefix === prefix &&
avm99963ad65e752020-09-01 00:13:59 +0200101 evt.data.requestId == id) {
avm99963e51444e2020-08-31 14:50:06 +0200102 // Deregister self
avm99963ad65e752020-09-01 00:13:59 +0200103 window.removeEventListener('message', listener);
104 resolve(evt.data.data);
avm99963e51444e2020-08-31 14:50:06 +0200105 }
106 };
107
avm99963ad65e752020-09-01 00:13:59 +0200108 window.addEventListener('message', listener);
avm99963e51444e2020-08-31 14:50:06 +0200109
avm999633e238882020-12-07 18:38:54 +0100110 var payload = {data, id, prefix};
avm99963e51444e2020-08-31 14:50:06 +0200111
avm99963ad65e752020-09-01 00:13:59 +0200112 window.dispatchEvent(
113 new CustomEvent('TWPT_sendRequest', {detail: payload}));
avm99963e51444e2020-08-31 14:50:06 +0200114 });
115 }
116
avm99963ad65e752020-09-01 00:13:59 +0200117 return {sendRequest: sendRequest};
avm99963e51444e2020-08-31 14:50:06 +0200118})();
119
avm99963ad65e752020-09-01 00:13:59 +0200120// Create profile indicator dot with a loading state, or return the numPosts
121// badge if it is already created.
Adrià Vilanova Martínezad6bedf2022-01-28 10:41:10 +0100122function createIndicatorDot(sourceNode, searchURL, options, ui) {
avm99963ad65e752020-09-01 00:13:59 +0200123 if (options.numPosts) return document.querySelector('.num-posts-indicator');
avm99963a1b23b62020-09-01 14:32:34 +0200124 var dotContainer = document.createElement('div');
avm99963ad65e752020-09-01 00:13:59 +0200125 dotContainer.classList.add('profile-indicator', 'profile-indicator--loading');
avm99963e51444e2020-08-31 14:50:06 +0200126
Adrià Vilanova Martínezcb28fd92022-01-28 12:41:52 +0100127 var dotLink = (isCommunityConsole(ui)) ? createImmuneLink() :
128 document.createElement('a');
avm99963e51444e2020-08-31 14:50:06 +0200129 dotLink.href = searchURL;
130 dotLink.innerText = '●';
131
132 dotContainer.appendChild(dotLink);
133 sourceNode.parentNode.appendChild(dotContainer);
134
avm999632485a3e2021-09-08 22:18:38 +0200135 contentScriptRequest
136 .sendRequest({
137 'action': 'geti18nMessage',
138 'msg': 'inject_profileindicator_loading'
139 })
140 .then(string => createPlainTooltip(dotContainer, string));
141
avm99963e51444e2020-08-31 14:50:06 +0200142 return dotContainer;
143}
144
avm99963ad65e752020-09-01 00:13:59 +0200145// Create badge indicating the number of posts with a loading state
Adrià Vilanova Martínezad6bedf2022-01-28 10:41:10 +0100146function createNumPostsBadge(sourceNode, searchURL, ui) {
Adrià Vilanova Martínezcb28fd92022-01-28 12:41:52 +0100147 var link = (isCommunityConsole(ui)) ? createImmuneLink() :
148 document.createElement('a');
avm99963ad65e752020-09-01 00:13:59 +0200149 link.href = searchURL;
150
151 var numPostsContainer = document.createElement('div');
152 numPostsContainer.classList.add(
153 'num-posts-indicator', 'num-posts-indicator--loading');
avm99963ad65e752020-09-01 00:13:59 +0200154
155 var numPostsSpan = document.createElement('span');
156 numPostsSpan.classList.add('num-posts-indicator--num');
157
158 numPostsContainer.appendChild(numPostsSpan);
159 link.appendChild(numPostsContainer);
160 sourceNode.parentNode.appendChild(link);
avm999632485a3e2021-09-08 22:18:38 +0200161
162 contentScriptRequest
163 .sendRequest({
164 'action': 'geti18nMessage',
165 'msg': 'inject_profileindicator_loading'
166 })
167 .then(string => createPlainTooltip(numPostsContainer, string));
168
avm99963ad65e752020-09-01 00:13:59 +0200169 return numPostsContainer;
170}
171
avm9996313658b92020-12-02 00:02:53 +0100172// Set the badge text
173function setNumPostsBadge(badge, text) {
174 badge.classList.remove('num-posts-indicator--loading');
175 badge.querySelector('span').classList.remove(
176 'num-posts-indicator--num--loading');
177 badge.querySelector('span').textContent = text;
178}
179
avm99963ad65e752020-09-01 00:13:59 +0200180// Get options and then handle all the indicators
Adrià Vilanova Martínezad6bedf2022-01-28 10:41:10 +0100181function getOptionsAndHandleIndicators(sourceNode, ui) {
avm99963ad65e752020-09-01 00:13:59 +0200182 contentScriptRequest.sendRequest({'action': 'getProfileIndicatorOptions'})
Adrià Vilanova Martínezad6bedf2022-01-28 10:41:10 +0100183 .then(options => handleIndicators(sourceNode, ui, options));
avm99963ad65e752020-09-01 00:13:59 +0200184}
185
186// Handle the profile indicator dot
Adrià Vilanova Martínezad6bedf2022-01-28 10:41:10 +0100187function handleIndicators(sourceNode, ui, options) {
188 let nameEl;
189 if (ui === UI_COMMUNITY_CONSOLE)
190 nameEl = sourceNode.querySelector('.name-text');
191 if (ui === UI_TW_LEGACY) nameEl = sourceNode.querySelector('span');
Adrià Vilanova Martínezcb28fd92022-01-28 12:41:52 +0100192 if (isInterop(ui)) nameEl = sourceNode;
Adrià Vilanova Martínezad6bedf2022-01-28 10:41:10 +0100193 var escapedUsername = escapeUsername(nameEl.textContent);
avm99963e51444e2020-08-31 14:50:06 +0200194
Adrià Vilanova Martínezcb28fd92022-01-28 12:41:52 +0100195 if (isCommunityConsole(ui)) {
avm99963e51444e2020-08-31 14:50:06 +0200196 var threadLink = document.location.href;
197 } else {
198 var CCLink = document.getElementById('onebar-community-console');
199 if (CCLink === null) {
200 console.error(
avm99963ad65e752020-09-01 00:13:59 +0200201 '[opindicator] The user is not a PE so the dot indicator cannot be shown in TW.');
avm99963e51444e2020-08-31 14:50:06 +0200202 return;
203 }
204 var threadLink = CCLink.href;
205 }
206
207 var forumUrlSplit = threadLink.split('/forum/');
208 if (forumUrlSplit.length < 2) {
avm99963ad65e752020-09-01 00:13:59 +0200209 console.error('[opindicator] Can\'t get forum id.');
avm99963e51444e2020-08-31 14:50:06 +0200210 return;
211 }
212
213 var forumId = forumUrlSplit[1].split('/')[0];
avm99963ad65e752020-09-01 00:13:59 +0200214
avm99963e51444e2020-08-31 14:50:06 +0200215 var query = '(replier:"' + escapedUsername + '" | creator:"' +
216 escapedUsername + '") ' + FILTER_ALL_LANGUAGES;
Adrià Vilanova Martínezad6bedf2022-01-28 10:41:10 +0100217 var encodedQuery = encodeURIComponent(' forum:' + forumId);
avm99963a2945b62020-11-27 00:32:02 +0100218 var authuserPart =
Adrià Vilanova Martínezad6bedf2022-01-28 10:41:10 +0100219 (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
220 var searchURL = 'https://support.google.com/s/community/search/' +
221 encodeURIComponent('query=' + encodedQuery) + authuserPart;
avm99963e51444e2020-08-31 14:50:06 +0200222
avm99963ad65e752020-09-01 00:13:59 +0200223 if (options.numPosts) {
224 var profileURL = new URL(sourceNode.href);
Adrià Vilanova Martínezcb28fd92022-01-28 12:41:52 +0100225 var userId = profileURL.pathname
226 .split(isCommunityConsole(ui) ? 'user/' : 'profile/')[1]
227 .split('?')[0]
228 .split('/')[0];
avm99963e51444e2020-08-31 14:50:06 +0200229
Adrià Vilanova Martínezad6bedf2022-01-28 10:41:10 +0100230 var numPostsContainer = createNumPostsBadge(sourceNode, searchURL, ui);
avm99963e51444e2020-08-31 14:50:06 +0200231
avm99963ad65e752020-09-01 00:13:59 +0200232 getProfile(userId, forumId)
233 .then(res => {
234 if (!('1' in res) || !('2' in res[1])) {
235 throw new Error('Unexpected profile response.');
236 return;
237 }
avm99963e51444e2020-08-31 14:50:06 +0200238
avm99963ad65e752020-09-01 00:13:59 +0200239 contentScriptRequest.sendRequest({'action': 'getNumPostMonths'})
240 .then(months => {
241 if (!options.indicatorDot)
242 contentScriptRequest
243 .sendRequest({
244 'action': 'geti18nMessage',
245 'msg': 'inject_profileindicatoralt_numposts',
246 'placeholders': [months]
247 })
248 .then(
249 string =>
avm999632485a3e2021-09-08 22:18:38 +0200250 createPlainTooltip(numPostsContainer, string));
avm99963e51444e2020-08-31 14:50:06 +0200251
avm99963ad65e752020-09-01 00:13:59 +0200252 var numPosts = 0;
avm99963e51444e2020-08-31 14:50:06 +0200253
avm99963ad65e752020-09-01 00:13:59 +0200254 for (const index of numPostsForumArraysToSum) {
255 if (!(index in res[1][2])) {
256 throw new Error('Unexpected profile response.');
257 return;
258 }
avm99963e51444e2020-08-31 14:50:06 +0200259
avm99963ad65e752020-09-01 00:13:59 +0200260 var i = 0;
261 for (const month of res[1][2][index].reverse()) {
262 if (i == months) break;
263 numPosts += month[3] || 0;
264 ++i;
265 }
266 }
avm99963e51444e2020-08-31 14:50:06 +0200267
avm9996313658b92020-12-02 00:02:53 +0100268 setNumPostsBadge(numPostsContainer, numPosts);
avm99963ad65e752020-09-01 00:13:59 +0200269 })
270 .catch(
271 err => console.error('[opindicator] Unexpected error.', err));
272 })
avm9996313658b92020-12-02 00:02:53 +0100273 .catch(err => {
274 console.error(
275 '[opindicator] Unexpected error. Couldn\'t load profile.', err);
276 setNumPostsBadge(numPostsContainer, '?');
277 });
avm99963ad65e752020-09-01 00:13:59 +0200278 }
279
280 if (options.indicatorDot) {
Adrià Vilanova Martínezad6bedf2022-01-28 10:41:10 +0100281 var dotContainer = createIndicatorDot(sourceNode, searchURL, options, ui);
avm99963ad65e752020-09-01 00:13:59 +0200282
283 // Query threads in order to see what state the indicator should be in
284 getPosts(query, forumId)
285 .then(res => {
avm999639f586f42021-02-05 12:37:51 +0100286 // Throw an error when the replies array is not present in the reply.
avm99963ad65e752020-09-01 00:13:59 +0200287 if (!('1' in res) || !('2' in res['1'])) {
avm999639f586f42021-02-05 12:37:51 +0100288 // Throw a different error when the numThreads field exists and is
289 // equal to 0. This reply can be received, but is enexpected,
290 // because we know that the user has replied in at least 1 thread
291 // (the current one).
292 if (('1' in res) && ('4' in res['1']) && res['1']['4'] == 0)
293 throw new Error(
294 'Thread list is empty ' +
295 '(but the OP has participated in this thread, ' +
296 'so it shouldn\'t be empty).');
297
avm99963ad65e752020-09-01 00:13:59 +0200298 throw new Error('Unexpected thread list response.');
299 return;
300 }
301
302 // Current thread ID
303 var threadUrlSplit = threadLink.split('/thread/');
304 if (threadUrlSplit.length < 2)
305 throw new Error('Can\'t get thread id.');
306
307 var currId = threadUrlSplit[1].split('/')[0];
308
309 var OPStatus = OP_FIRST_POST;
310
311 for (const thread of res['1']['2']) {
312 var id = thread['2']['1']['1'] || undefined;
313 if (id === undefined || id == currId) continue;
314
315 var isRead = thread['6'] || false;
316 if (isRead)
317 OPStatus = Math.max(OP_OTHER_POSTS_READ, OPStatus);
318 else
319 OPStatus = Math.max(OP_OTHER_POSTS_UNREAD, OPStatus);
320 }
321
322 var dotContainerPrefix =
323 (options.numPosts ? 'num-posts-indicator' : 'profile-indicator');
324
325 if (!options.numPosts)
326 dotContainer.classList.remove(dotContainerPrefix + '--loading');
327 dotContainer.classList.add(
328 dotContainerPrefix + '--' + OPClasses[OPStatus]);
329 contentScriptRequest
330 .sendRequest({
331 'action': 'geti18nMessage',
332 'msg': 'inject_profileindicator_' + OPi18n[OPStatus]
333 })
avm999632485a3e2021-09-08 22:18:38 +0200334 .then(string => createPlainTooltip(dotContainer, string));
avm99963ad65e752020-09-01 00:13:59 +0200335 })
336 .catch(
337 err => console.error(
338 '[opindicator] Unexpected error. Couldn\'t load recent posts.',
339 err));
340 }
avm99963e51444e2020-08-31 14:50:06 +0200341}
342
343if (CCRegex.test(location.href)) {
344 // We are in the Community Console
avm99963a2945b62020-11-27 00:32:02 +0100345 var startup =
346 JSON.parse(document.querySelector('html').getAttribute('data-startup'));
347
348 authuser = startup[2][1] || '0';
349
avm9996389812882021-02-02 20:51:25 +0100350 // When the OP's username is found, call getOptionsAndHandleIndicators
avm99963e51444e2020-08-31 14:50:06 +0200351 function mutationCallback(mutationList, observer) {
352 mutationList.forEach((mutation) => {
353 if (mutation.type == 'childList') {
354 mutation.addedNodes.forEach(function(node) {
355 if (node.tagName == 'A' && ('href' in node) &&
Adrià Vilanova Martínezcb28fd92022-01-28 12:41:52 +0100356 CCProfileRegex.test(node.href)) {
357 if (node.matches(
358 'ec-question ec-message-header .name-section ec-user-link a')) {
359 console.info('Handling profile indicator via mutation callback.');
360 getOptionsAndHandleIndicators(node, UI_COMMUNITY_CONSOLE);
361 } else if (node.matches(
362 'sc-tailwind-thread-question-question-card ' +
363 'sc-tailwind-thread-post_header-user-info ' +
364 '.scTailwindThreadPost_headerUserinfoname a')) {
365 console.info(
366 'Handling interop profile indicator via mutation callback.');
367 getOptionsAndHandleIndicators(node, UI_COMMUNITY_CONSOLE_INTEROP);
368 }
avm99963e51444e2020-08-31 14:50:06 +0200369 }
370 });
371 }
372 });
373 };
374
375 var observerOptions = {
376 childList: true,
377 subtree: true,
378 }
379
avm9996389812882021-02-02 20:51:25 +0100380 // Before starting the mutation Observer, check if the OP's username link is
381 // already part of the page
382 var node = document.querySelector(
383 'ec-question ec-message-header .name-section ec-user-link a');
384 if (node !== null) {
385 console.info('Handling profile indicator via first check.');
Adrià Vilanova Martínezad6bedf2022-01-28 10:41:10 +0100386 getOptionsAndHandleIndicators(node, UI_COMMUNITY_CONSOLE);
Adrià Vilanova Martínezcb28fd92022-01-28 12:41:52 +0100387 } else {
388 var node = document.querySelector(
389 'sc-tailwind-thread-question-question-card ' +
390 'sc-tailwind-thread-post_header-user-info ' +
391 '.scTailwindThreadPost_headerUserinfoname a');
392 if (node !== null) {
393 console.info('Handling interop profile indicator via first check.');
394 getOptionsAndHandleIndicators(node, UI_COMMUNITY_CONSOLE_INTEROP);
395 }
avm9996389812882021-02-02 20:51:25 +0100396 }
397
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200398 var mutationObserver = new MutationObserver(mutationCallback);
avm99963e4cac402020-12-03 16:10:58 +0100399 mutationObserver.observe(document.body, observerOptions);
avm99963e51444e2020-08-31 14:50:06 +0200400} else {
401 // We are in TW
avm99963a2945b62020-11-27 00:32:02 +0100402 authuser = (new URL(location.href)).searchParams.get('authuser') || '0';
403
avm99963ad65e752020-09-01 00:13:59 +0200404 var node =
405 document.querySelector('.thread-question a.user-info-display-name');
avm99963e51444e2020-08-31 14:50:06 +0200406 if (node !== null)
Adrià Vilanova Martínezad6bedf2022-01-28 10:41:10 +0100407 getOptionsAndHandleIndicators(node, UI_TW_LEGACY);
408 else {
409 // The user might be using the redesigned thread page.
410 var node = document.querySelector(
411 'sc-tailwind-thread-question-question-card ' +
412 'sc-tailwind-thread-post_header-user-info ' +
413 '.scTailwindThreadPost_headerUserinfoname a');
414 if (node !== null)
415 getOptionsAndHandleIndicators(node, UI_TW_INTEROP);
416 else
417 console.error('[opindicator] Couldn\'t find username.');
418 }
avm99963e51444e2020-08-31 14:50:06 +0200419}