Add extra info to thread lists
Fixed: twpowertools:103
Change-Id: I957d94d352113b7d81f0f31a014e130b13e0046c
diff --git a/src/contentScripts/communityConsole/extraInfo.js b/src/contentScripts/communityConsole/extraInfo.js
index 8419ea2..acea6fe 100644
--- a/src/contentScripts/communityConsole/extraInfo.js
+++ b/src/contentScripts/communityConsole/extraInfo.js
@@ -11,6 +11,7 @@
const kViewUnifiedUserResponseEvent = 'TWPT_ViewUnifiedUserResponse';
const kListCannedResponsesResponse = 'TWPT_ListCannedResponsesResponse';
const kViewThreadResponse = 'TWPT_ViewThreadResponse';
+const kViewForumResponse = 'TWPT_ViewForumResponse';
const kAbuseCategories = [
['1', 'Account'],
@@ -187,6 +188,9 @@
id: -1,
timestamp: 0,
};
+ this.lastThreadList = null;
+ this.lastThreadListTimestamp = 0;
+ this.lastThreadListRequestId = -1;
this.displayLanguage = getDisplayLanguage();
this.optionsWatcher = new OptionsWatcher(['extrainfo', 'perforumstats']);
this.setUpHandlers();
@@ -227,6 +231,21 @@
timestamp: Date.now(),
};
});
+ window.addEventListener(kViewThreadResponse, e => {
+ if (e.detail.id < this.lastThread.id) return;
+
+ this.lastThread = {
+ body: e.detail.body,
+ id: e.detail.id,
+ timestamp: Date.now(),
+ };
+ });
+ window.addEventListener(kViewForumResponse, e => {
+ if (e.detail.id < this.lastThreadListRequestId) return;
+
+ this.lastThreadList = e.detail.body;
+ this.lastThreadListTimestamp = Date.now();
+ });
}
// Whether |feature| is enabled
@@ -489,12 +508,53 @@
return [a, liveReviewTooltip];
}
+ // Get an |info| array with the info related to the thread, and a |tooltips|
+ // array with the corresponding tooltips which should be initialized after the
+ // info is added to the DOM.
+ //
+ // This is used by the injectAtQuestion() and injectAtThreadList() functions.
+ getThreadInfo(thread) {
+ let info = [];
+ let tooltips = [];
+
+ const endPendingStateTimestampMicros = thread?.['2']?.['39'];
+ const [pendingStateInfo, pendingTooltip] =
+ this.getPendingStateInfo(endPendingStateTimestampMicros);
+ if (pendingStateInfo) info.push(pendingStateInfo);
+ if (pendingTooltip) tooltips.push(pendingTooltip);
+
+ const isTrending = thread?.['2']?.['25'];
+ const isTrendingAutoMarked = thread?.['39'];
+ if (isTrendingAutoMarked)
+ info.push(document.createTextNode('Automatically marked as trending'));
+ else if (isTrending)
+ info.push(document.createTextNode('Trending'));
+
+ const itemMetadata = thread?.['2']?.['12'];
+ const mdInfo = this.getMetadataInfo(itemMetadata);
+ info.push(...mdInfo);
+
+ const liveReviewStatus = thread?.['2']?.['38'];
+ const [liveReviewInfo, liveReviewTooltip] =
+ this.getLiveReviewStatusInfo(liveReviewStatus);
+ if (liveReviewInfo) info.push(liveReviewInfo);
+ if (liveReviewTooltip) tooltips.push(liveReviewTooltip);
+
+ return [info, tooltips];
+ }
+
injectAtQuestion(question) {
let currentPage = parseUrl(location.href);
- if (currentPage === false) return;
+ if (currentPage === false) {
+ console.error('extraInfo: couldn\'t parse current URL:', location.href);
+ return;
+ }
let content = question.querySelector('ec-question > .content');
- if (!content) return;
+ if (!content) {
+ console.error('extraInfo: question doesn\'t have .content:', messageNode);
+ return;
+ }
waitFor(
() => {
@@ -512,34 +572,9 @@
timeout: 30 * 1000,
})
.then(thread => {
- let info = [];
-
- const endPendingStateTimestampMicros =
- thread.body['1']?.['2']?.['39'];
- const [pendingStateInfo, pendingTooltip] =
- this.getPendingStateInfo(endPendingStateTimestampMicros);
- if (pendingStateInfo) info.push(pendingStateInfo);
-
- const isTrending = thread.body['1']?.['2']?.['25'];
- const isTrendingAutoMarked = thread.body['1']?.['39'];
- if (isTrendingAutoMarked)
- info.push(
- document.createTextNode('Automatically marked as trending'));
- else if (isTrending)
- info.push(document.createTextNode('Trending'));
-
- const itemMetadata = thread.body['1']?.['2']?.['12'];
- const mdInfo = this.getMetadataInfo(itemMetadata);
- info.push(...mdInfo);
-
- const liveReviewStatus = thread.body['1']?.['2']?.['38'];
- const [liveReviewInfo, liveReviewTooltip] =
- this.getLiveReviewStatusInfo(liveReviewStatus);
- if (liveReviewInfo) info.push(liveReviewInfo);
-
+ const [info, tooltips] = this.getThreadInfo(thread.body?.['1']);
this.addExtraInfoElement(info, content);
- if (pendingTooltip) new MDCTooltip(pendingTooltip);
- if (liveReviewTooltip) new MDCTooltip(liveReviewTooltip);
+ for (const tooltip of tooltips) new MDCTooltip(tooltip);
})
.catch(err => {
console.error(
@@ -703,6 +738,122 @@
}
/**
+ * Thread list functionality
+ */
+ injectAtThreadList(li) {
+ waitFor(
+ () => {
+ const header = li.querySelector(
+ 'ec-thread-summary .main-header .panel-description a.header');
+ if (header === null) {
+ console.error(
+ 'extraInfo: Header is not present in the thread item\'s DOM.');
+ return;
+ }
+
+ const threadInfo = parseUrl(header.href);
+ if (threadInfo === false) {
+ console.error('extraInfo: Thread\'s link cannot be parsed.');
+ return;
+ }
+
+ let authorLine = li.querySelector(
+ 'ec-thread-summary .header-content .top-row .author-line');
+ if (!authorLine) {
+ console.error(
+ 'extraInfo: Author line is not present in the thread item\'s DOM.');
+ return;
+ }
+
+ let thread = this.lastThreadList?.['1']?.['2']?.find?.(t => {
+ return t?.['2']?.['1']?.['1'] == threadInfo.thread &&
+ t?.['2']?.['1']?.['3'] == threadInfo.forum;
+ });
+ if (thread) return Promise.resolve([thread, authorLine]);
+ return Promise.reject(
+ new Error('Didn\'t receive thread information'));
+ },
+ {
+ interval: 500,
+ timeout: 7 * 1000,
+ })
+ .then(response => {
+ const [thread, authorLine] = response;
+ const state = thread?.['2']?.['12']?.['1'];
+ if (state && ![1, 13, 18, 9].includes(state)) {
+ let label = document.createElement('div');
+ label.classList.add('TWPT-label');
+
+ const [badge, badgeTooltip] = createExtBadge();
+
+ let span = document.createElement('span');
+ span.textContent = kItemMetadataState[state] ?? 'State ' + state;
+
+ label.append(badge, span);
+ authorLine.prepend(label);
+ new MDCTooltip(badgeTooltip);
+ }
+ })
+ .catch(err => {
+ console.error(
+ 'extraInfo: error while injecting thread list extra info: ', err);
+ });
+ }
+
+ injectAtThreadListIfEnabled(li) {
+ this.isEnabled('extrainfo').then(isEnabled => {
+ if (isEnabled) this.injectAtThreadList(li);
+ });
+ }
+
+ injectAtExpandedThreadList(toolbelt) {
+ const header =
+ toolbelt?.parentNode?.parentNode?.parentNode?.querySelector?.(
+ '.main-header .panel-description a.header');
+ if (header === null) {
+ console.error(
+ 'extraInfo: Header is not present in the thread item\'s DOM.');
+ return;
+ }
+
+ const threadInfo = parseUrl(header.href);
+ if (threadInfo === false) {
+ console.error('extraInfo: Thread\'s link cannot be parsed.');
+ return;
+ }
+
+ waitFor(
+ () => {
+ let thread = this.lastThreadList?.['1']?.['2']?.find?.(t => {
+ return t?.['2']?.['1']?.['1'] == threadInfo.thread &&
+ t?.['2']?.['1']?.['3'] == threadInfo.forum;
+ });
+ if (thread) return Promise.resolve(thread);
+ return Promise.reject(
+ new Error('Didn\'t receive thread information'));
+ },
+ {
+ interval: 500,
+ timeout: 7 * 1000,
+ })
+ .then(thread => {
+ const [info, tooltips] = this.getThreadInfo(thread);
+ this.addExtraInfoElement(info, toolbelt);
+ for (const tooltip of tooltips) new MDCTooltip(tooltip);
+ })
+ .catch(err => {
+ console.error(
+ 'extraInfo: error while injecting thread list extra info: ', err);
+ });
+ }
+
+ injectAtExpandedThreadListIfEnabled(toolbelt) {
+ this.isEnabled('extrainfo').then(isEnabled => {
+ if (isEnabled) this.injectAtExpandedThreadList(toolbelt);
+ });
+ }
+
+ /**
* Per-forum stats in user profiles.
*/
diff --git a/src/contentScripts/communityConsole/main.js b/src/contentScripts/communityConsole/main.js
index 75de36f..d140484 100644
--- a/src/contentScripts/communityConsole/main.js
+++ b/src/contentScripts/communityConsole/main.js
@@ -40,9 +40,12 @@
'ec-bulk-actions material-button[debugid="mark-read-button"]',
'ec-bulk-actions material-button[debugid="mark-unread-button"]',
- // Thread list items (used to inject the avatars)
+ // Thread list items (used to inject the avatars and extra info)
'li',
+ // Thread list item toolbelt (used for the extra info feature)
+ 'ec-thread-summary .main .toolbelt',
+
// Thread list (used for the autorefresh feature)
'ec-thread-list',
@@ -132,9 +135,17 @@
// Inject avatar links to threads in the thread list. injectIfEnabled is
// responsible of determining whether it should run or not depending on its
// current setting.
+ //
+ // Also, inject extra info in the thread list.
if (('tagName' in node) && (node.tagName == 'LI') &&
node.querySelector('ec-thread-summary') !== null) {
avatars.injectIfEnabled(node);
+ window.TWPTExtraInfo.injectAtThreadListIfEnabled(node);
+ }
+
+ // Inject extra info in the toolbelt of an expanded thread list item.
+ if (node.matches('ec-thread-summary .main .toolbelt')) {
+ window.TWPTExtraInfo.injectAtExpandedThreadListIfEnabled(node);
}
// Set up the autorefresh list feature. The setUp function is responsible
@@ -212,7 +223,7 @@
infiniteScroll = new InfiniteScroll();
workflows = new Workflows();
- // autoRefresh is initialized in start.js
+ // autoRefresh and extraInfo are initialized in start.js
// Before starting the mutation Observer, check whether we missed any
// mutations by manually checking whether some watched nodes already
diff --git a/src/static/css/extrainfo.css b/src/static/css/extrainfo.css
index 23b0afd..6813195 100644
--- a/src/static/css/extrainfo.css
+++ b/src/static/css/extrainfo.css
@@ -59,6 +59,12 @@
padding-inline-start: 6px;
}
+ec-thread-summary .main .toolbelt .TWPT-extrainfo-container {
+ margin-inline-start: 115px;
+ margin-bottom: 16px;
+ font-size: 12px;
+}
+
/* Special styles for good/bad labels */
.TWPT-extrainfo-good {
color: var(--TWPT-good-text, green)!important;
@@ -107,6 +113,19 @@
cursor: help;
}
+/* Special component for the thread list */
+ec-thread-summary .header-content .TWPT-label {
+ display: flex;
+ align-items: center;
+ padding-inline-end: 10px;
+ font-weight: 500;
+}
+
+ec-thread-summary .header-content .TWPT-label .TWPT-badge {
+ --icon-size: 9.5px;
+ margin-right: 2px;
+}
+
/* Per-forum stats section */
.TWPT-scTailwindSharedActivitychartroot .scTailwindSharedActivitychartchart {
margin-top: 26px;
@@ -118,7 +137,7 @@
}
.TWPT-scTailwindSharedActivitychartroot .scTailwindSharedActivitycharttitle .TWPT-badge {
- margin-right: 6px;
+ margin-inline-end: 6px;
}
.TWPT-scTailwindSharedActivitychartroot .TWPT-select-container {
diff --git a/src/static/css/reposition_expand_thread.css b/src/static/css/reposition_expand_thread.css
index 1af6917..ea26942 100644
--- a/src/static/css/reposition_expand_thread.css
+++ b/src/static/css/reposition_expand_thread.css
@@ -17,3 +17,7 @@
ec-thread-summary .panel .main .content-wrapper > .content > .content {
padding-inline: 121px 8px;
}
+
+ec-thread-summary .main .toolbelt .TWPT-extrainfo-container {
+ margin-inline-start: 145px!important;
+}