blob: 6a1a4912e5582695837014b60818660bd0934952 [file] [log] [blame]
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +02001import {createExtBadge} from './utils/common.js';
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +02002import {getAuthUser} from '../../common/communityConsoleUtils.js';
3
4var authuser = getAuthUser();
5
6export var autoRefresh = {
7 isLookingForUpdates: false,
8 isUpdatePromptShown: false,
9 lastTimestamp: null,
10 filter: null,
11 path: null,
12 snackbar: null,
13 interval: null,
14 firstCallTimeout: null,
15 intervalMs: 3 * 60 * 1000, // 3 minutes
16 firstCallDelayMs: 3 * 1000, // 3 seconds
17 getStartupData() {
18 return JSON.parse(
19 document.querySelector('html').getAttribute('data-startup'));
20 },
21 isOrderedByTimestampDescending() {
22 var startup = this.getStartupData();
23 // Returns orderOptions.by == TIMESTAMP && orderOptions.desc == true
24 return (
25 startup?.[1]?.[1]?.[3]?.[14]?.[1] == 1 &&
26 startup?.[1]?.[1]?.[3]?.[14]?.[2] == true);
27 },
28 getCustomFilter(path) {
29 var searchRegex = /^\/s\/community\/search\/([^\/]*)/;
30 var matches = path.match(searchRegex);
31 if (matches !== null && matches.length > 1) {
32 var search = decodeURIComponent(matches[1]);
33 var params = new URLSearchParams(search);
34 return params.get('query') || '';
35 }
36
37 return '';
38 },
39 filterHasOverride(filter, override) {
40 var escapedOverride = override.replace(/([^\w\d\s])/gi, '\\$1');
41 var regex = new RegExp('[^a-zA-Z0-9]?' + escapedOverride + ':');
42 return regex.test(filter);
43 },
44 getFilter(path) {
45 var query = this.getCustomFilter(path);
46
47 // Note: This logic has been copied and adapted from the
48 // _buildQuery$1$threadId function in the Community Console
49 var conditions = '';
50 var startup = this.getStartupData();
51
52 // TODO(avm99963): if the selected forums are changed without reloading the
53 // page, this will get the old selected forums. Fix this.
54 var forums = startup?.[1]?.[1]?.[3]?.[8] ?? [];
55 if (!this.filterHasOverride(query, 'forum') && forums !== null &&
56 forums.length > 0)
57 conditions += ' forum:(' + forums.join(' | ') + ')';
58
59 var langs = startup?.[1]?.[1]?.[3]?.[5] ?? [];
60 if (!this.filterHasOverride(query, 'lang') && langs !== null &&
61 langs.length > 0)
62 conditions += ' lang:(' + langs.map(l => '"' + l + '"').join(' | ') + ')';
63
64 if (query.length !== 0 && conditions.length !== 0)
65 return '(' + query + ')' + conditions;
66 return query + conditions;
67 },
68 getLastTimestamp() {
69 var APIRequestUrl = 'https://support.google.com/s/community/api/ViewForum' +
70 (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
71
72 return fetch(APIRequestUrl, {
73 'headers': {
74 'content-type': 'text/plain; charset=utf-8',
75 },
76 'body': JSON.stringify({
77 1: '0', // TODO: Change, when only a forum is selected, it
78 // should be set here
79 2: {
80 1: {
81 2: 2,
82 },
83 2: {
84 1: 1,
85 2: true,
86 },
87 12: this.filter,
88 },
89 }),
90 'method': 'POST',
91 'mode': 'cors',
92 'credentials': 'include',
93 })
94 .then(res => {
95 if (res.status == 200 || res.status == 400) {
96 return res.json().then(data => ({
97 status: res.status,
98 body: data,
99 }));
100 } else {
101 throw new Error('Status code ' + res.status + ' was not expected.');
102 }
103 })
104 .then(res => {
105 if (res.status == 400) {
106 throw new Error(
107 res.body[4] ||
108 ('Response status: 400. Error code: ' + res.body[2]));
109 }
110
111 return res.body;
112 })
113 .then(body => {
114 var timestamp = body?.[1]?.[2]?.[0]?.[2]?.[17];
115 if (timestamp === undefined)
116 throw new Error(
117 'Unexpected body of response (' +
118 (body?.[1]?.[2]?.[0] === undefined ?
119 'no threads were returned' :
120 'the timestamp value is not present in the first thread') +
121 ').');
122
123 return timestamp;
124 });
125 // TODO(avm99963): Add retry mechanism (sometimes thread lists are empty,
126 // but when loading the next page the thread appears).
127 //
128 // NOTE(avm99963): It seems like loading the first 2 threads instead of only
129 // the first one fixes this (empty lists are now rarely returned).
130 },
131 unregister() {
132 console.debug('autorefresh_list: unregistering');
133
134 if (!this.isLookingForUpdates) return;
135
136 window.clearTimeout(this.firstCallTimeout);
137 window.clearInterval(this.interval);
138 this.isUpdatePromptShown = false;
139 this.isLookingForUpdates = false;
140 },
141 showUpdatePrompt() {
142 this.snackbar.classList.remove('TWPT-hidden');
143 document.title = '[!!!] ' + document.title.replace('[!!!] ', '');
144 this.isUpdatePromptShown = true;
145 },
146 hideUpdatePrompt() {
147 this.snackbar.classList.add('TWPT-hidden');
148 document.title = document.title.replace('[!!!] ', '');
149 this.isUpdatePromptShown = false;
150 },
151 injectUpdatePrompt() {
152 var pane = document.createElement('div');
153 pane.classList.add('TWPT-pane-for-snackbar');
154
155 var snackbar = document.createElement('material-snackbar-panel');
156 snackbar.classList.add('TWPT-snackbar');
157 snackbar.classList.add('TWPT-hidden');
158
159 var ac = document.createElement('div');
160 ac.classList.add('TWPT-animation-container');
161
162 var nb = document.createElement('div');
163 nb.classList.add('TWPT-notification-bar');
164
165 var ft = document.createElement('focus-trap');
166
167 var content = document.createElement('div');
168 content.classList.add('TWPT-focus-content-wrapper');
169
170 var badge = createExtBadge();
171
172 var message = document.createElement('div');
173 message.classList.add('TWPT-message');
174 message.textContent =
175 chrome.i18n.getMessage('inject_autorefresh_list_snackbar_message');
176
177 var action = document.createElement('div');
178 action.classList.add('TWPT-action');
179 action.textContent =
180 chrome.i18n.getMessage('inject_autorefresh_list_snackbar_action');
181
182 action.addEventListener('click', e => {
183 this.hideUpdatePrompt();
184 document.querySelector('.app-title-button').click();
185 });
186
187 content.append(badge, message, action);
188 ft.append(content);
189 nb.append(ft);
190 ac.append(nb);
191 snackbar.append(ac);
192 pane.append(snackbar);
193 document.getElementById('default-acx-overlay-container').append(pane);
194 this.snackbar = snackbar;
195 },
196 checkUpdate() {
197 if (location.pathname != this.path) {
198 this.unregister();
199 return;
200 }
201
202 if (this.isUpdatePromptShown) return;
203
204 console.debug('Checking for update at: ', new Date());
205
206 this.getLastTimestamp()
207 .then(timestamp => {
208 if (timestamp != this.lastTimestamp) this.showUpdatePrompt();
209 })
210 .catch(
211 err => console.error(
212 'Coudln\'t get last timestamp (while updating): ', err));
213 },
214 firstCall() {
215 console.debug(
216 'autorefresh_list: now performing first call to finish setup (filter: [' +
217 this.filter + '])');
218
219 if (location.pathname != this.path) {
220 this.unregister();
221 return;
222 }
223
224 this.getLastTimestamp()
225 .then(timestamp => {
226 this.lastTimestamp = timestamp;
227 var checkUpdateCallback = this.checkUpdate.bind(this);
228 this.interval =
229 window.setInterval(checkUpdateCallback, this.intervalMs);
230 })
231 .catch(
232 err => console.error(
233 'Couldn\'t get last timestamp (while setting up): ', err));
234 },
235 setUp() {
236 if (!this.isOrderedByTimestampDescending()) return;
237
238 this.unregister();
239
240 console.debug('autorefresh_list: starting set up...');
241
242 if (this.snackbar === null) this.injectUpdatePrompt();
243 this.isLookingForUpdates = true;
244 this.path = location.pathname;
245 this.filter = this.getFilter(this.path);
246
247 var firstCall = this.firstCall.bind(this);
248 this.firstCallTimeout = window.setTimeout(firstCall, this.firstCallDelayMs);
249 },
250};