blob: 2efe63433a088e6ca80d78707f9490f47de0403c [file] [log] [blame]
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +01001import {MDCTooltip} from '@material/tooltip';
2import {waitFor} from 'poll-until-promise';
3
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +01004import {parseUrl} from '../../common/commonUtils.js';
Adrià Vilanova Martínez17106dc2022-01-24 23:48:36 +01005import OptionsWatcher from '../../common/optionsWatcher.js';
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +01006import {createPlainTooltip} from '../../common/tooltip.js';
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +01007
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +01008import {createExtBadge, getDisplayLanguage} from './utils/common.js';
Adrià Vilanova Martínez69c30502022-01-28 20:47:08 +01009import PerForumStatsSection from './utils/PerForumStatsSection.js';
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +010010
11const kViewUnifiedUserResponseEvent = 'TWPT_ViewUnifiedUserResponse';
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +010012const kListCannedResponsesResponse = 'TWPT_ListCannedResponsesResponse';
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +010013const kViewThreadResponse = 'TWPT_ViewThreadResponse';
avm999632c112092022-03-11 15:18:26 +010014const kViewForumRequest = 'TWPT_ViewForumRequest';
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +010015const kViewForumResponse = 'TWPT_ViewForumResponse';
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +010016
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +010017// Used to match each category with the corresponding string.
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +010018const kAbuseCategories = [
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +010019 ['1', 'account'],
20 ['2', 'displayname'],
21 ['3', 'avatar'],
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +010022];
23const kAbuseViolationCategories = {
24 0: 'NO_VIOLATION',
25 1: 'COMMUNITY_POLICY_VIOLATION',
26 2: 'LEGAL_VIOLATION',
27 3: 'CSAI_VIOLATION',
28 4: 'OTHER_VIOLATION',
29};
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +010030const kAbuseViolationCategoriesI18n = {
31 0: 'noviolation',
32 1: 'communitypolicy',
33 2: 'legal',
34 3: 'csai',
35 4: 'other',
36};
37
38// The following array will appear in the interface as is (without being
39// translated).
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +010040const kAbuseViolationTypes = {
41 0: 'UNSPECIFIED',
42 23: 'ACCOUNT_DISABLED',
43 55: 'ACCOUNT_HAS_SERVICES_DISABLED',
44 35: 'ACCOUNT_HIJACKED',
45 96: 'ACCOUNT_LEAKED_CREDENTIALS',
46 92: 'ACCOUNT_NOT_SUPPORTED',
47 81: 'ARTISTIC_NUDITY',
48 66: 'BAD_BEHAVIOR_PATTERN',
49 78: 'BAD_ENGAGEMENT_BEHAVIOR_PATTERN',
50 79: 'BORDERLINE_HARASSMENT',
51 80: 'BORDERLINE_HATE_SPEECH',
52 38: 'BOTNET',
53 32: 'BRANDING_VIOLATION',
54 100: 'CAPITALIZING_TRAGIC_EVENTS',
55 105: 'CLOAKING',
56 49: 'COIN_MINING',
57 7: 'COMMERCIAL_CONTENT',
58 97: 'COPPA_REGULATED',
59 57: 'COPYRIGHT_CIRCUMVENTION',
60 8: 'COPYRIGHTED_CONTENT',
61 58: 'COURT_ORDER',
62 51: 'CSAI',
63 94: 'CSAI_INSPECT',
64 52: 'CSAI_CARTOON_HUMOR',
65 53: 'CSAI_SOLICITATION',
66 108: 'CSAI_NON_APPARENT',
67 67: 'DANGEROUS',
68 37: 'DATA_SCRAPING',
69 86: 'DECEPTIVE_OAUTH_IMPLEMENTATION',
70 46: 'DEFAMATORY_CONTENT',
71 36: 'DELINQUENT_BILLING',
72 30: 'DISRUPTION_ATTEMPT',
73 112: 'DOMESTIC_INTERFERENCE',
74 22: 'DOS',
75 9: 'DUPLICATE_CONTENT',
76 68: 'DUPLICATE_LOCAL_PAGE',
77 121: 'NON_QUALIFYING_ORGANIZATION',
78 115: 'EGREGIOUS_INTERACTION_WITH_MINOR',
79 83: 'ENGAGEMENT_COLLUSION',
80 41: 'EXPLOIT_ATTACKS',
81 65: 'FAKE_USER',
82 2: 'FRAUD',
83 21: 'FREE_TRIAL_VIOLATION',
84 43: 'GIBBERISH',
85 101: 'FOREIGN_INTERFERENCE',
86 59: 'GOVERNMENT_ORDER',
87 10: 'GRAPHICAL_VIOLENCE',
88 11: 'HARASSMENT',
89 12: 'HATE_SPEECH',
90 90: 'IDENTICAL_PRODUCT_NAME',
91 60: 'ILLEGAL_DRUGS',
92 13: 'IMPERSONATION',
93 69: 'IMPERSONATION_WITH_PII',
94 116: 'INAPPROPRIATE_INTERACTION_WITH_MINOR',
95 45: 'INAPPROPRIATE_CONTENT_SPEECH',
96 106: 'INTENTIONAL_THWARTING',
97 27: 'INTRUSION_ATTEMPT',
98 87: 'INVALID_API_USAGE',
99 14: 'INVALID_CONTENT',
100 20: 'INVALID_GCE_USAGE',
101 120: 'INVALID_STORAGE_USAGE',
102 15: 'INVALID_IMAGE_QUALITY',
103 88: 'INVALID_API_PRIVACY_POLICY_DISCLOSURE',
104 54: 'INVALID_USAGE_OF_IP_PROXYING',
105 99: 'KEYWORD_STUFFING',
106 61: 'LEGAL_COUNTERFEIT',
107 62: 'LEGAL_EXPORT',
108 63: 'LEGAL_PRIVACY',
109 33: 'LEGAL_REVIEW',
110 91: 'LEGAL_PROTECTED',
111 70: 'LOW_QUALITY_CONTENT',
112 93: 'LOW_REPUTATION_PHONE_NUMBER',
113 6: 'MALICIOUS_SOFTWARE',
114 40: 'MALWARE',
115 113: 'MISLEADING',
116 114: 'MISREP_OF_ID',
117 89: 'MEMBER_OF_ABUSIVE_GCE_NETWORK',
118 84: 'NON_CONSENSUAL_EXPLICIT_IMAGERY',
119 1: 'NONE',
120 102: 'OFF_TOPIC',
121 31: 'OPEN_PROXY',
122 28: 'PAYMENT_FRAUD',
123 16: 'PEDOPHILIA',
124 71: 'PERSONAL_INFORMATION_CONTENT',
125 25: 'PHISHING',
126 34: 'POLICY_REVIEW',
127 17: 'PORNOGRAPHY',
128 29: 'QUOTA_CIRCUMVENTION',
129 72: 'QUOTA_EXCEEDED',
130 73: 'REGULATED',
131 24: 'REPEATED_POLICY_VIOLATION',
132 104: 'RESOURCE_COMPROMISED',
133 107: 'REWARD_PROGRAMS_ABUSE',
134 74: 'ROGUE_PHARMA',
135 82: 'ESCORT',
136 75: 'SPAMMY_LOCAL_VERTICAL',
137 39: 'SEND_EMAIL_SPAM',
138 117: 'SEXTORTION',
139 118: 'SEX_TRAFFICKING',
140 44: 'SEXUALLY_EXPLICIT_CONTENT',
141 3: 'SHARDING',
142 95: 'SOCIAL_ENGINEERING',
143 109: 'SUSPICIOUS',
144 19: 'TRADEMARK_CONTENT',
145 50: 'TRAFFIC_PUMPING',
146 76: 'UNSAFE_RACY',
147 103: 'UNUSUAL_ACTIVITY_ALERT',
148 64: 'UNWANTED_CONTENT',
149 26: 'UNWANTED_SOFTWARE',
150 77: 'VIOLENT_EXTREMISM',
151 119: 'UNAUTH_IMAGES_OF_MINORS',
152 85: 'UNAUTHORIZED_SERVICE_RESELLING',
153 98: 'CSAI_EXTERNAL',
154 5: 'SPAM',
155 4: 'UNSAFE',
156 47: 'CHILD_PORNOGRAPHY_INCITATION',
157 18: 'TERRORISM_SUPPORT',
158 56: 'CSAI_WORST_OF_WORST',
159};
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100160
161// These values will be translated
162const kItemMetadataStateI18n = {
163 1: 'published',
164 2: 'draft',
avm999636ee7d832022-03-07 11:00:30 +0100165 3: 'automated_abuse_take_down_hide2',
166 4: 'automated_abuse_take_down_delete2',
167 13: 'automated_abuse_reinstate2',
168 10: 'automated_off_topic_hide2',
169 14: 'automated_flagged_pending_manual_review2',
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100170 5: 'user_flagged_pending_manual_review',
171 6: 'owner_deleted',
avm999636ee7d832022-03-07 11:00:30 +0100172 7: 'manual_take_down_hide2',
173 17: 'manual_profile_take_down_suspend2',
174 8: 'manual_take_down_delete2',
175 18: 'reinstate_profile_takedown2',
176 9: 'reinstate_abuse_takedown2',
177 11: 'clear_off_topic2',
178 12: 'confirm_off_topic2',
179 15: 'googler_off_topic_hide2',
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100180 16: 'expert_flagged_pending_manual_review',
181};
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100182const kItemMetadataState = {
183 0: 'UNDEFINED',
184 1: 'PUBLISHED',
185 2: 'DRAFT',
186 3: 'AUTOMATED_ABUSE_TAKE_DOWN_HIDE',
187 4: 'AUTOMATED_ABUSE_TAKE_DOWN_DELETE',
188 13: 'AUTOMATED_ABUSE_REINSTATE',
189 10: 'AUTOMATED_OFF_TOPIC_HIDE',
190 14: 'AUTOMATED_FLAGGED_PENDING_MANUAL_REVIEW',
191 5: 'USER_FLAGGED_PENDING_MANUAL_REVIEW',
192 6: 'OWNER_DELETED',
193 7: 'MANUAL_TAKE_DOWN_HIDE',
194 17: 'MANUAL_PROFILE_TAKE_DOWN_SUSPEND',
195 8: 'MANUAL_TAKE_DOWN_DELETE',
196 18: 'REINSTATE_PROFILE_TAKEDOWN',
197 9: 'REINSTATE_ABUSE_TAKEDOWN',
198 11: 'CLEAR_OFF_TOPIC',
199 12: 'CONFIRM_OFF_TOPIC',
200 15: 'GOOGLER_OFF_TOPIC_HIDE',
201 16: 'EXPERT_FLAGGED_PENDING_MANUAL_REVIEW',
202};
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100203
204export default class ExtraInfo {
205 constructor() {
206 this.lastProfile = {
207 body: {},
208 id: -1,
209 timestamp: 0,
210 };
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100211 this.lastCRsList = {
212 body: {},
213 id: -1,
214 duplicateNames: new Set(),
215 };
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100216 this.lastThread = {
217 body: {},
218 id: -1,
219 timestamp: 0,
220 };
avm999632c112092022-03-11 15:18:26 +0100221 // Threads currently loaded in the thread list
222 this.lastThreadListThreads = [];
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100223 this.lastThreadListTimestamp = 0;
avm999632c112092022-03-11 15:18:26 +0100224 this.lastThreadListIsFirst = null;
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100225 this.lastThreadListRequestId = -1;
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100226 this.displayLanguage = getDisplayLanguage();
227 this.optionsWatcher = new OptionsWatcher(['extrainfo', 'perforumstats']);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100228 this.setUpHandlers();
229 }
230
231 setUpHandlers() {
232 window.addEventListener(kViewUnifiedUserResponseEvent, e => {
233 if (e.detail.id < this.lastProfile.id) return;
234
235 this.lastProfile = {
236 body: e.detail.body,
237 id: e.detail.id,
238 timestamp: Date.now(),
239 };
240 });
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100241 window.addEventListener(kListCannedResponsesResponse, e => {
242 if (e.detail.id < this.lastCRsList.id) return;
243
244 // Look if there are duplicate names
245 const crs = e.detail.body?.['1'] ?? [];
246 const names = crs.map(cr => cr?.['7']).slice().sort();
247 let duplicateNames = new Set();
248 for (let i = 1; i < names.length; i++)
249 if (names[i - 1] == names[i]) duplicateNames.add(names[i]);
250
251 this.lastCRsList = {
252 body: e.detail.body,
253 id: e.detail.id,
254 duplicateNames,
255 };
256 });
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100257 window.addEventListener(kViewThreadResponse, e => {
258 if (e.detail.id < this.lastThread.id) return;
259
260 this.lastThread = {
261 body: e.detail.body,
262 id: e.detail.id,
263 timestamp: Date.now(),
264 };
265 });
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100266 window.addEventListener(kViewThreadResponse, e => {
267 if (e.detail.id < this.lastThread.id) return;
268
269 this.lastThread = {
270 body: e.detail.body,
271 id: e.detail.id,
272 timestamp: Date.now(),
273 };
274 });
avm999632c112092022-03-11 15:18:26 +0100275 window.addEventListener(kViewForumRequest, e => {
276 // Ignore ViewForum requests made by the chat feature and the "Mark as
277 // duplicate" dialog.
278 //
279 // All those requests have |maxNum| set to 10 and 20 respectively, while
280 // the requests that we want to handle are the ones to initially load the
281 // thread list (which currently requests 100 threads) and the ones to load
282 // more threads (which request 50 threads).
283 let maxNum = e.detail.body?.['2']?.['1']?.['2'];
284 if (maxNum != 10 && maxNum != 20) {
285 this.lastThreadListRequestId = e.detail.id;
286 this.lastThreadListIsFirst =
287 !e.detail.body?.['2']?.['1']?.['3']?.['2']; // Pagination token
288 }
289 });
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100290 window.addEventListener(kViewForumResponse, e => {
avm999632c112092022-03-11 15:18:26 +0100291 if (e.detail.id != this.lastThreadListRequestId) return;
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100292
avm999632c112092022-03-11 15:18:26 +0100293 let threads = e.detail.body?.['1']?.['2'] ?? [];
294 if (this.lastThreadListIsFirst)
295 this.lastThreadListThreads = threads;
296 else
297 this.lastThreadListThreads = this.lastThreadListThreads.concat(threads);
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100298 this.lastThreadListTimestamp = Date.now();
299 });
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100300 }
301
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100302 // Whether |feature| is enabled
303 isEnabled(feature) {
304 return this.optionsWatcher.isEnabled(feature);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100305 }
306
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100307 // Add chips which contain |info| to |node|. If |withContainer| is set to
308 // true, a container will contain all the chips.
309 addExtraInfoElement(info, node, withContainer = false) {
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100310 // Don't create if there's nothing to show
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100311 if (info.length == 0) return;
312
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100313 let container;
314 if (withContainer) {
315 container = document.createElement('div');
316 container.classList.add('TWPT-extrainfo-container');
317 } else {
318 container = node;
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100319 }
320
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100321 let tooltips = [];
322
323 for (const i of info) {
324 let chip = document.createElement('material-chip');
325 chip.classList.add('TWPT-extrainfo-chip');
326
327 let chipCont = document.createElement('div');
328 chipCont.classList.add('TWPT-chip-content-container');
329
330 let content = document.createElement('div');
331 content.classList.add('TWPT-content');
332
333 const [badge, badgeTooltip] = createExtBadge();
334
335 let span = document.createElement('span');
336 span.append(i);
337
338 content.append(badge, span);
339 chipCont.append(content);
340 chip.append(chipCont);
341 container.append(chip);
342
343 tooltips.push(badgeTooltip);
344 }
345
346 if (withContainer) node.append(container);
347
348 for (const tooltip of tooltips) new MDCTooltip(tooltip);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100349 }
350
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100351 /**
352 * Profile functionality
353 */
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100354 injectAtProfile(card) {
355 waitFor(
356 () => {
357 let now = Date.now();
358 if (now - this.lastProfile.timestamp < 15 * 1000)
359 return Promise.resolve(this.lastProfile);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100360 return Promise.reject(
361 new Error('Didn\'t receive profile information'));
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100362 },
363 {
364 interval: 500,
365 timeout: 15 * 1000,
366 })
367 .then(profile => {
368 let info = [];
369 const abuseViolationCategory = profile.body?.['1']?.['6'];
370 if (abuseViolationCategory) {
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100371 let avCat = document.createElement('span');
372 avCat.textContent = chrome.i18n.getMessage(
373 'inject_extrainfo_profile_abusecategory',
374 [chrome.i18n.getMessage(
375 'inject_extrainfo_profile_abusecategory_' +
376 kAbuseViolationCategoriesI18n[abuseViolationCategory]) ??
377 abuseViolationCategory]);
378 avCat.title = kAbuseViolationCategories[abuseViolationCategory] ??
379 abuseViolationCategory;
380 info.push(avCat);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100381 }
382
383 const profileAbuse = profile.body?.['1']?.['1']?.['8'];
384
385 for (const [index, category] of kAbuseCategories) {
386 const violation = profileAbuse?.[index]?.['1']?.['1'];
387 if (violation) {
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100388 info.push(chrome.i18n.getMessage(
389 'inject_extrainfo_profile_abuse_' + category,
390 [kAbuseViolationTypes[violation]]));
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100391 }
392 }
393
394 const appealCount = profileAbuse?.['4'];
395 if (appealCount !== undefined)
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100396 info.push(chrome.i18n.getMessage(
397 'inject_extrainfo_profile_appealsnum', [appealCount]));
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100398
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100399 this.addExtraInfoElement(info, card, true);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100400 })
401 .catch(err => {
402 console.error(
403 'extraInfo: error while injecting profile extra info: ', err);
404 });
405 }
406
407 injectAtProfileIfEnabled(card) {
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100408 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100409 if (isEnabled) return this.injectAtProfile(card);
410 });
411 }
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100412
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100413 /**
414 * Canned responses (CRs) functionality
415 */
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100416 getCRName(tags, isExpanded) {
417 if (!isExpanded)
418 return tags.parentNode?.querySelector?.('.text .name')?.textContent;
419
420 // https://www.youtube.com/watch?v=Z6_ZNW1DACE
421 return tags.parentNode?.parentNode?.parentNode?.parentNode?.parentNode
422 ?.parentNode?.parentNode?.querySelector?.('.text .name')
423 ?.textContent;
424 }
425
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100426 getUsageCountString(num) {
427 if (num == 1)
428 return chrome.i18n.getMessage('inject_extrainfo_crs_used_singular');
429
430 return chrome.i18n.getMessage('inject_extrainfo_crs_used_plural', [num]);
431 }
432
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100433 // Inject usage stats in the |tags| component of a CR
434 injectAtCR(tags, isExpanded) {
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100435 waitFor(
436 () => {
437 if (this.lastCRsList.id != -1)
438 return Promise.resolve(this.lastCRsList);
439 return Promise.reject(
440 new Error('Didn\'t receive canned responses list'));
441 },
442 {
443 interval: 500,
444 timeout: 15 * 1000,
445 })
446 .then(crs => {
447 let name = this.getCRName(tags, isExpanded);
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100448
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100449 // If another CR has the same name, there's no easy way to distinguish
450 // them, so don't show the usage stats.
451 if (crs.duplicateNames.has(name)) {
452 console.info(
453 'CR "' + name +
454 '" is duplicate, so skipping the injection of usage stats.');
455 return;
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100456 }
457
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100458 for (const cr of (crs.body?.['1'] ?? [])) {
459 if (cr['7'] == name) {
460 let tag = document.createElement('material-chip');
461 tag.classList.add('TWPT-tag');
462
463 let container = document.createElement('div');
464 container.classList.add('TWPT-chip-content-container');
465
466 let content = document.createElement('div');
467 content.classList.add('TWPT-content');
468
469 const [badge, badgeTooltip] = createExtBadge();
470
471 let label = document.createElement('span');
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100472 label.textContent = this.getUsageCountString(cr['8'] ?? '0');
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100473
474 content.append(badge, label);
475 container.append(content);
476 tag.append(container);
477 tags.append(tag);
478
479 new MDCTooltip(badgeTooltip);
480
481 if (cr['9']) {
482 const lastUsedTime = Math.floor(parseInt(cr['9']) / 1e3);
483 let date = (new Date(lastUsedTime)).toLocaleString();
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100484 createPlainTooltip(
485 label,
486 chrome.i18n.getMessage(
487 'inject_extrainfo_crs_lastused', [date]));
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100488 }
489
490 break;
491 }
492 }
493 })
494 .catch(err => {
495 console.error(
496 'extraInfo: error while injecting profile extra info: ', err);
497 });
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100498 }
499
500 injectAtCRIfEnabled(tags, isExpanded) {
501 // If the tag has already been injected, exit.
502 if (tags.querySelector('.TWPT-tag')) return;
503
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100504 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100505 if (isEnabled) return this.injectAtCR(tags, isExpanded);
506 });
507 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100508
509 /**
510 * Thread view functionality
511 */
512
513 getPendingStateInfo(endPendingStateTimestampMicros) {
514 const endPendingStateTimestamp =
515 Math.floor(endPendingStateTimestampMicros / 1e3);
516 const now = Date.now();
517 if (endPendingStateTimestampMicros && endPendingStateTimestamp > now) {
518 let span = document.createElement('span');
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100519 span.textContent =
520 chrome.i18n.getMessage('inject_extrainfo_message_pendingstate');
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100521
522 let date = new Date(endPendingStateTimestamp).toLocaleString();
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100523 let pendingTooltip = createPlainTooltip(
524 span,
525 chrome.i18n.getMessage(
526 'inject_extrainfo_message_pendingstate_tooltip', [date]),
527 false);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100528 return [span, pendingTooltip];
529 }
530
531 return [null, null];
532 }
533
534 getMetadataInfo(itemMetadata) {
535 let info = [];
536
537 const state = itemMetadata?.['1'];
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100538 if (state && state != 1) {
539 let span = document.createElement('span');
540 span.textContent = chrome.i18n.getMessage(
541 'inject_extrainfo_message_state',
542 [chrome.i18n.getMessage(
543 'inject_extrainfo_message_state_' +
544 kItemMetadataStateI18n[state]) ??
545 state]);
546 span.title = kItemMetadataState[state] ?? state;
547 info.push(span);
548 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100549
550 const shadowBlockInfo = itemMetadata?.['10'];
551 const blockedTimestampMicros = shadowBlockInfo?.['2'];
552 if (blockedTimestampMicros) {
553 const isBlocked = shadowBlockInfo?.['1'];
554 let span = document.createElement('span');
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100555 span.textContent = chrome.i18n.getMessage(
556 'inject_extrainfo_message_shadowblock' +
557 (isBlocked ? 'active' : 'notactive'));
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100558 if (isBlocked) span.classList.add('TWPT-extrainfo-bad');
559 info.push(span);
560 }
561
562 return info;
563 }
564
565 getLiveReviewStatusInfo(liveReviewStatus) {
566 const verdict = liveReviewStatus?.['1'];
567 if (!verdict) return [null, null];
568 let label, labelClass;
569 switch (verdict) {
570 case 1: // LIVE_REVIEW_RELEVANT
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100571 label = 'relevant';
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100572 labelClass = 'TWPT-extrainfo-good';
573 break;
574
575 case 2: // LIVE_REVIEW_OFF_TOPIC
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100576 label = 'offtopic';
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100577 labelClass = 'TWPT-extrainfo-bad';
578 break;
579
580 case 3: // LIVE_REVIEW_ABUSE
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100581 label = 'abuse';
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100582 labelClass = 'TWPT-extrainfo-bad';
583 break;
584 }
585 const reviewedBy = liveReviewStatus?.['2'];
586 const timestamp = liveReviewStatus?.['3'];
587 const date = (new Date(Math.floor(timestamp / 1e3))).toLocaleString();
588
589 let a = document.createElement('a');
590 a.href = 'https://support.google.com/s/community/user/' + reviewedBy;
591 a.classList.add(labelClass);
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100592 a.textContent = chrome.i18n.getMessage(
593 'inject_extrainfo_message_livereviewverdict',
594 [chrome.i18n.getMessage(
595 'inject_extrainfo_message_livereviewverdict_' + label)]);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100596 let liveReviewTooltip = createPlainTooltip(a, date, false);
597 return [a, liveReviewTooltip];
598 }
599
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100600 // Get an |info| array with the info related to the thread, and a |tooltips|
601 // array with the corresponding tooltips which should be initialized after the
602 // info is added to the DOM.
603 //
604 // This is used by the injectAtQuestion() and injectAtThreadList() functions.
605 getThreadInfo(thread) {
606 let info = [];
607 let tooltips = [];
608
609 const endPendingStateTimestampMicros = thread?.['2']?.['39'];
610 const [pendingStateInfo, pendingTooltip] =
611 this.getPendingStateInfo(endPendingStateTimestampMicros);
612 if (pendingStateInfo) info.push(pendingStateInfo);
613 if (pendingTooltip) tooltips.push(pendingTooltip);
614
615 const isTrending = thread?.['2']?.['25'];
616 const isTrendingAutoMarked = thread?.['39'];
617 if (isTrendingAutoMarked)
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100618 info.push(document.createTextNode(
619 chrome.i18n.getMessage('inject_extrainfo_thread_autotrending')));
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100620 else if (isTrending)
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100621 info.push(document.createTextNode(
622 chrome.i18n.getMessage('inject_extrainfo_thread_trending')));
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100623
624 const itemMetadata = thread?.['2']?.['12'];
625 const mdInfo = this.getMetadataInfo(itemMetadata);
626 info.push(...mdInfo);
627
628 const liveReviewStatus = thread?.['2']?.['38'];
629 const [liveReviewInfo, liveReviewTooltip] =
630 this.getLiveReviewStatusInfo(liveReviewStatus);
631 if (liveReviewInfo) info.push(liveReviewInfo);
632 if (liveReviewTooltip) tooltips.push(liveReviewTooltip);
633
634 return [info, tooltips];
635 }
636
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100637 injectAtQuestion(stateChips) {
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100638 let currentPage = parseUrl(location.href);
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100639 if (currentPage === false) {
640 console.error('extraInfo: couldn\'t parse current URL:', location.href);
641 return;
642 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100643
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100644 waitFor(
645 () => {
646 let now = Date.now();
647 let threadInfo = this.lastThread.body['1']?.['2']?.['1'];
648 if (now - this.lastThread.timestamp < 30 * 1000 &&
649 threadInfo?.['1'] == currentPage.thread &&
650 threadInfo?.['3'] == currentPage.forum)
651 return Promise.resolve(this.lastThread);
652 return Promise.reject(
653 new Error('Didn\'t receive thread information'));
654 },
655 {
656 interval: 500,
657 timeout: 30 * 1000,
658 })
659 .then(thread => {
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100660 const [info, tooltips] = this.getThreadInfo(thread.body?.['1']);
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100661 this.addExtraInfoElement(info, stateChips, false);
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100662 for (const tooltip of tooltips) new MDCTooltip(tooltip);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100663 })
664 .catch(err => {
665 console.error(
666 'extraInfo: error while injecting question extra info: ', err);
667 });
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100668 }
669
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100670 injectAtQuestionIfEnabled(stateChips) {
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100671 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100672 if (isEnabled) return this.injectAtQuestion(stateChips);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100673 });
674 }
675
676 getMessagesByType(thread, type) {
677 if (type === 'reply') return thread?.['1']?.['3'];
678 if (type === 'lastMessage') return thread?.['1']?.['17']?.['3'];
679 if (type === 'suggested') return thread?.['1']?.['17']?.['4'];
680 if (type === 'recommended') return thread?.['1']?.['17']?.['1'];
681 }
682
683 getMessageByTypeAndIndex(thread, type, index) {
684 return this.getMessagesByType(thread, type)?.[index];
685 }
686
687 // Returns true if the last message is included in the messages array (in the
688 // extension context, we say those messages are of the type "reply").
689 lastMessageInReplies(thread) {
690 const lastMessageId = thread?.['1']?.['17']?.['3']?.[0]?.['1']?.['1'];
691 if (!lastMessageId) return true;
692
693 // If the last message is included in the lastMessage array, check if it
694 // also exists in the messages/replies array.
695 const replies = thread?.['1']?.['3'];
696 if (!replies?.length) return false;
697 const lastReplyIndex = replies.length - 1;
698 const lastReplyId = replies[lastReplyIndex]?.['1']?.['1'];
699 return lastMessageId && lastMessageId == lastReplyId;
700 }
701
702 getMessageInfo(thread, message) {
703 const section = message.parentNode;
704
705 let type = 'reply';
706 if (section?.querySelector?.('.heading material-icon[icon="auto_awesome"]'))
707 type = 'suggested';
708 if (section?.querySelector?.('.heading material-icon[icon="check_circle"]'))
709 type = 'recommended';
710
711 let index = -1;
712 let messagesInDom = section.querySelectorAll('ec-message');
713
714 // Number of messages in the DOM.
715 const n = messagesInDom.length;
716
717 if (type !== 'reply') {
718 for (let i = 0; i < n; ++i) {
719 if (message.isEqualNode(messagesInDom[i])) {
720 index = i;
721 break;
722 }
723 }
724 } else {
725 // If the type of the message is a reply, things are slightly more
726 // complex, since replies are paginated and the last message should be
727 // treated separately (it is included diferently in the API response).
728 let lastMessageInReplies = this.lastMessageInReplies(thread);
729 if (message.isEqualNode(messagesInDom[n - 1]) && !lastMessageInReplies) {
730 type = 'lastMessage';
731 index = 0
732 } else {
733 // Number of messages in the current API response.
734 const messagesInResponse = this.getMessagesByType(thread, type);
735 const m = messagesInResponse.length;
736 // If the last message is included in the replies array, we also have to
737 // consider the last message in the DOM.
738 let modifier = lastMessageInReplies ? 1 : 0;
739 for (let k = 0; k < m; ++k) {
740 let i = n - 2 - k + modifier;
741 if (message.isEqualNode(messagesInDom[i])) {
742 index = m - 1 - k;
743 break;
744 }
745 }
746 }
747 }
748
749 return [type, index];
750 }
751
752 injectAtMessage(messageNode) {
753 let currentPage = parseUrl(location.href);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100754 if (currentPage === false) {
755 console.error('extraInfo: couldn\'t parse current URL:', location.href);
756 return;
757 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100758
759 let footer = messageNode.querySelector('.footer-fill');
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100760 if (!footer) {
761 console.error('extraInfo: message doesn\'t have a footer:', messageNode);
762 return;
763 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100764
765 const [type, index] =
766 this.getMessageInfo(this.lastThread.body, messageNode);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100767 if (index == -1) {
768 console.error('extraInfo: this.getMessageInfo() returned index -1.');
769 return;
770 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100771
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100772 waitFor(
773 () => {
774 let now = Date.now();
775 let threadInfo = this.lastThread.body['1']?.['2']?.['1'];
776 if (now - this.lastThread.timestamp < 30 * 1000 &&
777 threadInfo?.['1'] == currentPage.thread &&
778 threadInfo?.['3'] == currentPage.forum) {
779 const message = this.getMessageByTypeAndIndex(
780 this.lastThread.body, type, index);
781 if (message) return Promise.resolve(message);
782 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100783
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100784 return Promise.reject(new Error(
785 'Didn\'t receive thread information (type: ' + type +
786 ', index: ' + index + ')'));
787 },
788 {
789 interval: 1000,
790 timeout: 30 * 1000,
791 })
792 .then(message => {
793 let info = [];
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100794
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100795 const endPendingStateTimestampMicros = message['1']?.['17'];
796 const [pendingStateInfo, pendingTooltip] =
797 this.getPendingStateInfo(endPendingStateTimestampMicros);
798 if (pendingStateInfo) info.push(pendingStateInfo);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100799
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100800 const itemMetadata = message['1']?.['5'];
801 const mdInfo = this.getMetadataInfo(itemMetadata);
802 info.push(...mdInfo);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100803
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100804 const liveReviewStatus = message['1']?.['36'];
805 const [liveReviewInfo, liveReviewTooltip] =
806 this.getLiveReviewStatusInfo(liveReviewStatus);
807 if (liveReviewInfo) info.push(liveReviewInfo);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100808
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100809 this.addExtraInfoElement(info, footer, true);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100810 if (pendingTooltip) new MDCTooltip(pendingTooltip);
811 if (liveReviewTooltip) new MDCTooltip(liveReviewTooltip);
812 })
813 .catch(err => {
814 console.error(
815 'extraInfo: error while injecting message extra info: ', err);
816 });
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100817 }
818
819 injectAtMessageIfEnabled(message) {
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100820 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100821 if (isEnabled) return this.injectAtMessage(message);
822 });
823 }
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100824
825 /**
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100826 * Thread list functionality
827 */
828 injectAtThreadList(li) {
829 waitFor(
830 () => {
avm99963d08c37e2022-09-25 20:41:58 +0200831 const headerContent = li.querySelector(
832 'ec-thread-summary .main-header .header a.header-content');
833 if (headerContent === null) {
834 return Promise.reject(new Error(
835 'extraInfo: Header is not present in the thread item\'s DOM.'));
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100836 }
837
avm99963d08c37e2022-09-25 20:41:58 +0200838 const threadInfo = parseUrl(headerContent.href);
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100839 if (threadInfo === false) {
avm99963d08c37e2022-09-25 20:41:58 +0200840 return Promise.reject(
841 new Error('extraInfo: Thread\'s link cannot be parsed.'));
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100842 }
843
844 let authorLine = li.querySelector(
845 'ec-thread-summary .header-content .top-row .author-line');
846 if (!authorLine) {
avm99963d08c37e2022-09-25 20:41:58 +0200847 return Promise.reject(new Error(
848 'extraInfo: Author line is not present in the thread item\'s DOM.'));
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100849 }
850
avm999632c112092022-03-11 15:18:26 +0100851 let thread = this.lastThreadListThreads?.find?.(t => {
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100852 return t?.['2']?.['1']?.['1'] == threadInfo.thread &&
853 t?.['2']?.['1']?.['3'] == threadInfo.forum;
854 });
855 if (thread) return Promise.resolve([thread, authorLine]);
856 return Promise.reject(
857 new Error('Didn\'t receive thread information'));
858 },
859 {
860 interval: 500,
861 timeout: 7 * 1000,
862 })
863 .then(response => {
864 const [thread, authorLine] = response;
865 const state = thread?.['2']?.['12']?.['1'];
866 if (state && ![1, 13, 18, 9].includes(state)) {
867 let label = document.createElement('div');
868 label.classList.add('TWPT-label');
869
870 const [badge, badgeTooltip] = createExtBadge();
871
872 let span = document.createElement('span');
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100873 span.textContent = chrome.i18n.getMessage(
874 'inject_extrainfo_message_state_' +
875 kItemMetadataStateI18n[state]) ??
876 state;
877 span.title = kItemMetadataState[state] ?? state;
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100878
879 label.append(badge, span);
880 authorLine.prepend(label);
881 new MDCTooltip(badgeTooltip);
882 }
883 })
884 .catch(err => {
885 console.error(
886 'extraInfo: error while injecting thread list extra info: ', err);
887 });
888 }
889
890 injectAtThreadListIfEnabled(li) {
891 this.isEnabled('extrainfo').then(isEnabled => {
892 if (isEnabled) this.injectAtThreadList(li);
893 });
894 }
895
896 injectAtExpandedThreadList(toolbelt) {
avm99963d08c37e2022-09-25 20:41:58 +0200897 const headerContent =
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100898 toolbelt?.parentNode?.parentNode?.parentNode?.querySelector?.(
avm99963d08c37e2022-09-25 20:41:58 +0200899 '.main-header .header a.header-content');
900 if (headerContent === null) {
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100901 console.error(
902 'extraInfo: Header is not present in the thread item\'s DOM.');
903 return;
904 }
905
avm99963d08c37e2022-09-25 20:41:58 +0200906 const threadInfo = parseUrl(headerContent.href);
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100907 if (threadInfo === false) {
908 console.error('extraInfo: Thread\'s link cannot be parsed.');
909 return;
910 }
911
912 waitFor(
913 () => {
avm999632c112092022-03-11 15:18:26 +0100914 let thread = this.lastThreadListThreads?.find?.(t => {
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100915 return t?.['2']?.['1']?.['1'] == threadInfo.thread &&
916 t?.['2']?.['1']?.['3'] == threadInfo.forum;
917 });
918 if (thread) return Promise.resolve(thread);
919 return Promise.reject(
920 new Error('Didn\'t receive thread information'));
921 },
922 {
923 interval: 500,
924 timeout: 7 * 1000,
925 })
926 .then(thread => {
927 const [info, tooltips] = this.getThreadInfo(thread);
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100928 this.addExtraInfoElement(info, toolbelt, true);
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100929 for (const tooltip of tooltips) new MDCTooltip(tooltip);
930 })
931 .catch(err => {
932 console.error(
933 'extraInfo: error while injecting thread list extra info: ', err);
934 });
935 }
936
937 injectAtExpandedThreadListIfEnabled(toolbelt) {
938 this.isEnabled('extrainfo').then(isEnabled => {
939 if (isEnabled) this.injectAtExpandedThreadList(toolbelt);
940 });
941 }
942
943 /**
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100944 * Per-forum stats in user profiles.
945 */
946
947 injectPerForumStats(chart) {
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100948 waitFor(
949 () => {
950 let now = Date.now();
951 if (now - this.lastProfile.timestamp < 15 * 1000)
952 return Promise.resolve(this.lastProfile);
953 return Promise.reject(new Error(
954 'Didn\'t receive profile information (for per-profile stats)'));
955 },
956 {
957 interval: 500,
958 timeout: 15 * 1000,
959 })
960 .then(profile => {
Adrià Vilanova Martínez69c30502022-01-28 20:47:08 +0100961 new PerForumStatsSection(
avm9996337601bc2022-02-21 10:36:45 +0100962 chart?.parentNode, profile.body, this.displayLanguage,
963 /* isCommunityConsole = */ true);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100964 })
965 .catch(err => {
966 console.error(
967 'extraInfo: error while preparing to inject per-forum stats: ',
968 err);
969 });
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100970 }
971
972 injectPerForumStatsIfEnabled(chart) {
973 this.isEnabled('perforumstats').then(isEnabled => {
974 if (isEnabled) this.injectPerForumStats(chart);
975 });
976 }
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100977}