AdriĆ Vilanova | 61f782f | 2024-05-31 22:57:24 +0000 | [diff] [blame] | 1 | import {MDCTooltip} from '@material/tooltip'; |
| 2 | import {waitFor} from 'poll-until-promise'; |
| 3 | |
| 4 | import {parseUrl} from '../../common/commonUtils.js'; |
| 5 | import {injectStylesheet} from '../../common/contentScriptsUtils'; |
| 6 | import {getDocURL} from '../../common/extUtils.js'; |
| 7 | import {getOptions} from '../../common/options/optionsUtils.js'; |
| 8 | |
| 9 | import {createExtBadge} from './utils/common.js'; |
| 10 | |
| 11 | const kSMEINestedReplies = 15; |
| 12 | const kViewThreadResponse = 'TWPT_ViewThreadResponse'; |
| 13 | |
| 14 | export 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 | } |