| 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); |
| }); |
| } |
| } |