blob: 955108a05785e0f209da1b4a9e1fad5f5aa80508 [file] [log] [blame]
AdriĆ  Vilanova61f782f2024-05-31 22:57:24 +00001import {MDCTooltip} from '@material/tooltip';
2import {waitFor} from 'poll-until-promise';
3
4import {parseUrl} from '../../common/commonUtils.js';
5import {injectStylesheet} from '../../common/contentScriptsUtils';
6import {getDocURL} from '../../common/extUtils.js';
7import {getOptions} from '../../common/options/optionsUtils.js';
8
9import {createExtBadge} from './utils/common.js';
10
11const kSMEINestedReplies = 15;
12const kViewThreadResponse = 'TWPT_ViewThreadResponse';
13
14export default class ThreadPageDesignWarning {
15 constructor() {
16 this.lastThread = {
17 body: {},
18 id: -1,
19 timestamp: 0,
20 };
21 this.setUpHandler();
22
23 // We have to save whether the old UI was enabled at startup, since that's
24 // the moment when it takes effect. If the option changes while the tab is
25 // open, it won't take effect.
26 getOptions([
27 'interopthreadpage', 'interopthreadpage_mode'
28 ]).then(options => {
29 this.shouldShowWarning = options.interopthreadpage &&
30 options.interopthreadpage_mode == 'previous';
31
32 if (this.shouldShowWarning) {
33 injectStylesheet(
34 chrome.runtime.getURL('css/thread_page_design_warning.css'));
35 } else {
36 this.removeHandler();
37 }
38 });
39
40 this.isExperimentEnabled = this.isNestedRepliesExperimentEnabled();
41 }
42
43 isNestedRepliesExperimentEnabled() {
44 if (!document.documentElement.hasAttribute('data-startup')) return false;
45
46 let startup =
47 JSON.parse(document.documentElement.getAttribute('data-startup'));
48 return startup?.[1]?.[6]?.includes?.(kSMEINestedReplies);
49 }
50
51 eventHandler(e) {
52 if (e.detail.id < this.lastThread.id) return;
53
54 this.lastThread = {
55 body: e.detail.body,
56 id: e.detail.id,
57 timestamp: Date.now(),
58 };
59 }
60
61 setUpHandler() {
62 window.addEventListener(kViewThreadResponse, this.eventHandler.bind(this));
63 }
64
65 removeHandler() {
66 window.removeEventListener(
67 kViewThreadResponse, this.eventHandler.bind(this));
68 }
69
70 injectWarning(content) {
71 let div = document.createElement('div');
72 div.classList.add('TWPT-warning');
73
74 let icon = document.createElement('material-icon');
75 icon.classList.add('TWPT-warning--icon');
76
77 let iconContent = document.createElement('i');
78 iconContent.classList.add('material-icon-i', 'material-icons-extended');
79 iconContent.setAttribute('role', 'img');
80 iconContent.setAttribute('aria-hidden', 'true');
81 iconContent.textContent = 'warning';
82
83 icon.append(iconContent);
84
85 let text = document.createElement('div');
86 text.classList.add('TWPT-warning--text');
87 text.textContent =
88 chrome.i18n.getMessage('inject_threadpagedesign_warning');
89
90 let btn = document.createElement('a');
91 btn.classList.add('TWPT-warning--btn');
92 btn.href =
93 getDocURL('features.md#Thread-page-design-in-the-Community-Console');
94 btn.setAttribute('target', '_blank');
95 btn.setAttribute('rel', 'noopener noreferrer');
96
97 const [badge, badgeTooltip] = createExtBadge();
98
99 let btnText = document.createElement('div');
100 btnText.textContent = chrome.i18n.getMessage('btn_learnmore');
101
102 btn.append(badge, btnText);
103
104 div.append(icon, text, btn);
105 content.prepend(div);
106
107 new MDCTooltip(badgeTooltip);
108 }
109
110 injectWarningIfApplicable(content) {
111 return waitFor(
112 () => {
113 if (this.shouldShowWarning === undefined)
114 return Promise.reject(
115 new Error('shouldShowWarning is not defined.'));
116
117 return Promise.resolve({result: this.shouldShowWarning});
118 },
119 {
120 interval: 500,
121 timeout: 10 * 1000,
122 })
123 .then(preShouldShowWarning => {
124 if (!preShouldShowWarning.result) return;
125
126 // If the global SMEI experiment is enabled, all threads use nested
127 // replies, so we'll skip the per-thread check and always show the
128 // warning banner.
129 if (this.isExperimentEnabled) return Promise.resolve({result: true});
130
131 let currentThread = parseUrl(location.href);
132 if (currentThread === false)
133 throw new Error('current thread id cannot be parsed.');
134
135 return waitFor(() => {
136 let now = Date.now();
137 let lastThreadInfo = this.lastThread.body['1']?.['2']?.['1'];
138 if (now - this.lastThread.timestamp > 30 * 1000 ||
139 lastThreadInfo?.['1'] != currentThread.thread ||
140 lastThreadInfo?.['3'] != currentThread.forum)
141 throw new Error(
142 'cannot obtain information about current thread.');
143
144 // If the messageOrGap field contains any items, the thread is using
145 // nested replies. Otherwise, it probably isn't using them.
146 return Promise.resolve(
147 {result: this.lastThread.body['1']?.['40']?.length > 0});
148 }, {
149 interval: 500,
150 timeout: 10 * 1000,
151 });
152 })
153 .then(shouldShowWarning => {
154 if (shouldShowWarning.result) this.injectWarning(content);
155 })
156 .catch(err => {
157 console.error(
158 '[threadPageDesignWarning] An error ocurred while trying to decide whether to show warning: ',
159 err);
160 });
161 }
162}