blob: f58112ee32092ee66f750a4a25fae9ef8d33bd25 [file] [log] [blame]
import {MDCTooltip} from '@material/tooltip';
import {waitFor} from 'poll-until-promise';
import {parseUrl} from '../../common/commonUtils.js';
import {injectStylesheet} from '../../common/contentScriptsUtils.js';
import {getDocURL} from '../../common/extUtils.js';
import {getOptions} from '../../common/optionsUtils.js';
import {createExtBadge} from './utils/common.js';
const kSMEINestedReplies = 15;
const kViewThreadResponse = 'TWPT_ViewThreadResponse';
export default class ThreadPageDesignWarning {
constructor() {
this.lastThread = {
body: {},
id: -1,
timestamp: 0,
};
this.setUpHandler();
// We have to save whether the old UI was enabled at startup, since that's
// the moment when it takes effect. If the option changes while the tab is
// open, it won't take effect.
getOptions([
'interopthreadpage', 'interopthreadpage_mode'
]).then(options => {
this.shouldShowWarning = options.interopthreadpage &&
options.interopthreadpage_mode == 'previous';
if (this.shouldShowWarning) {
injectStylesheet(
chrome.runtime.getURL('css/thread_page_design_warning.css'));
} else {
this.removeHandler();
}
});
this.isExperimentEnabled = this.isNestedRepliesExperimentEnabled();
}
isNestedRepliesExperimentEnabled() {
if (!document.documentElement.hasAttribute('data-startup')) return false;
let startup =
JSON.parse(document.documentElement.getAttribute('data-startup'));
return startup?.[1]?.[6]?.includes?.(kSMEINestedReplies);
}
eventHandler(e) {
if (e.detail.id < this.lastThread.id) return;
this.lastThread = {
body: e.detail.body,
id: e.detail.id,
timestamp: Date.now(),
};
}
setUpHandler() {
window.addEventListener(kViewThreadResponse, this.eventHandler.bind(this));
}
removeHandler() {
window.removeEventListener(
kViewThreadResponse, this.eventHandler.bind(this));
}
injectWarning(content) {
let div = document.createElement('div');
div.classList.add('TWPT-warning');
let icon = document.createElement('material-icon');
icon.classList.add('TWPT-warning--icon');
let iconContent = document.createElement('i');
iconContent.classList.add('material-icon-i', 'material-icons-extended');
iconContent.setAttribute('role', 'img');
iconContent.setAttribute('aria-hidden', 'true');
iconContent.textContent = 'warning';
icon.append(iconContent);
let text = document.createElement('div');
text.classList.add('TWPT-warning--text');
text.textContent =
chrome.i18n.getMessage('inject_threadpagedesign_warning');
let btn = document.createElement('a');
btn.classList.add('TWPT-warning--btn');
btn.href =
getDocURL('features.md#Thread-page-design-in-the-Community-Console');
btn.setAttribute('target', '_blank');
btn.setAttribute('rel', 'noopener noreferrer');
const [badge, badgeTooltip] = createExtBadge();
let btnText = document.createElement('div');
btnText.textContent = chrome.i18n.getMessage('btn_learnmore');
btn.append(badge, btnText);
div.append(icon, text, btn);
content.prepend(div);
new MDCTooltip(badgeTooltip);
}
injectWarningIfApplicable(content) {
return waitFor(
() => {
if (this.shouldShowWarning === undefined)
return Promise.reject(
new Error('shouldShowWarning is not defined.'));
return Promise.resolve({result: this.shouldShowWarning});
},
{
interval: 500,
timeout: 10 * 1000,
})
.then(preShouldShowWarning => {
if (!preShouldShowWarning.result) return;
// If the global SMEI experiment is enabled, all threads use nested
// replies, so we'll skip the per-thread check and always show the
// warning banner.
if (this.isExperimentEnabled) return Promise.resolve(true);
let currentThread = parseUrl(location.href);
if (currentThread === false)
throw new Error('current thread id cannot be parsed.');
return waitFor(() => {
let now = Date.now();
let lastThreadInfo = this.lastThread.body['1']?.['2']?.['1'];
if (now - this.lastThread.timestamp > 30 * 1000 ||
lastThreadInfo?.['1'] != currentThread.thread ||
lastThreadInfo?.['3'] != currentThread.forum)
throw new Error(
'cannot obtain information about current thread.');
// If the messageOrGap field contains any items, the thread is using
// nested replies. Otherwise, it probably isn't using them.
return Promise.resolve(
{result: this.lastThread.body['1']?.['40']?.length > 0});
}, {
interval: 500,
timeout: 10 * 1000,
});
})
.then(shouldShowWarning => {
if (shouldShowWarning.result) this.injectWarning(content);
})
.catch(err => {
console.error(
'[threadPageDesignWarning] An error ocurred while trying to decide whether to show warning: ',
err);
});
}
}