Flatten threads: add UI components to messages
- A quote is added to messages to show which was the previous message in
the reply chain.
- A "reply" button is added to messages to allow users to compose a
comment which is added at the end of the reply chain.
- A bug is fixed in calculating the parent reply ID in the extra info
object. Now parent reply means the first message in a reply chain,
while previous reply means the previous message in the reply chain.
Bug: twpowertools:153
Change-Id: I699507ade52e80287dd634e61f835d53af6a904d
diff --git a/package-lock.json b/package-lock.json
index b00199b..6464798 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"@material/tooltip": "^12.0.0",
"@material/web": "^0.1.0-alpha.0",
"async-mutex": "^0.3.2",
+ "dompurify": "^2.4.1",
"google-protobuf": "^3.19.3",
"grpc-web": "^1.2.1",
"idb": "^6.1.2",
@@ -1727,6 +1728,11 @@
"node": ">=6.0.0"
}
},
+ "node_modules/dompurify": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz",
+ "integrity": "sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA=="
+ },
"node_modules/electron-to-chromium": {
"version": "1.3.772",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.772.tgz",
@@ -5491,6 +5497,11 @@
"esutils": "^2.0.2"
}
},
+ "dompurify": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz",
+ "integrity": "sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA=="
+ },
"electron-to-chromium": {
"version": "1.3.772",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.772.tgz",
diff --git a/package.json b/package.json
index 5771ded..5e9a073 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
"@material/tooltip": "^12.0.0",
"@material/web": "^0.1.0-alpha.0",
"async-mutex": "^0.3.2",
+ "dompurify": "^2.4.1",
"google-protobuf": "^3.19.3",
"grpc-web": "^1.2.1",
"idb": "^6.1.2",
diff --git a/src/contentScripts/communityConsole/flattenThreads/components/QuoteAuthor.js b/src/contentScripts/communityConsole/flattenThreads/components/QuoteAuthor.js
new file mode 100644
index 0000000..2f1fb95
--- /dev/null
+++ b/src/contentScripts/communityConsole/flattenThreads/components/QuoteAuthor.js
@@ -0,0 +1,60 @@
+import '@material/web/icon/icon.js';
+import '@material/web/iconbutton/standard-icon-button.js';
+
+import {css, html, LitElement} from 'lit';
+
+import {SHARED_MD3_STYLES} from '../../../../common/styles/md3.js';
+
+export default class TwptFlattenThreadQuoteAuthor extends LitElement {
+ static properties = {
+ prevMessage: {type: Object},
+ };
+
+ static styles = [
+ SHARED_MD3_STYLES,
+ css`
+ :host {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ max-width: max(25%, 150px);
+ color: var(--TWPT-interop-secondary-text, #444746);
+ }
+
+ :host > *:not(:last-child) {
+ margin-right: 4px;
+ }
+
+ .name {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ `,
+ ];
+
+ constructor() {
+ super();
+ this.prevMessage = {};
+ }
+
+ render() {
+ return html`
+ <md-icon>reply</md-icon>
+ <span class="name">${this.prevMessage?.author?.[1]?.[1]}</span>
+ <md-standard-icon-button
+ icon="arrow_upward"
+ @click=${this.focusParent}>
+ </md-standard-icon-button>
+ `;
+ }
+
+ focusParent() {
+ const parentNode = document.querySelector(
+ '[data-twpt-message-id="' + this.prevMessage?.id + '"]');
+ parentNode.focus({preventScroll: true});
+ parentNode.scrollIntoView({behavior: 'smooth', block: 'start'});
+ }
+}
+window.customElements.define(
+ 'twpt-flatten-thread-quote-author', TwptFlattenThreadQuoteAuthor);
diff --git a/src/contentScripts/communityConsole/flattenThreads/components/ReplyButton.js b/src/contentScripts/communityConsole/flattenThreads/components/ReplyButton.js
new file mode 100644
index 0000000..681c433
--- /dev/null
+++ b/src/contentScripts/communityConsole/flattenThreads/components/ReplyButton.js
@@ -0,0 +1,99 @@
+import '@material/web/button/outlined-button.js';
+
+import {css, html, LitElement} from 'lit';
+import {waitFor} from 'poll-until-promise';
+
+import {SHARED_MD3_STYLES} from '../../../../common/styles/md3.js';
+import {getExtraInfoNodes} from '../flattenThreads.js';
+
+export default class TwptFlattenThreadReplyButton extends LitElement {
+ static properties = {
+ extraInfo: {type: Object},
+ };
+
+ static styles = [
+ SHARED_MD3_STYLES,
+ css`
+ md-outlined-button {
+ --md-outlined-button-container-shape: 0.25rem;
+ --md-outlined-button-container-height: 38px;
+ }
+ `,
+ ];
+
+ constructor() {
+ super();
+ this.extraInfo = {};
+ }
+
+ render() {
+ return html`
+ <md-outlined-button
+ label="Reply"
+ @click=${this.openReplyEditor}>
+ </md-outlined-button>
+ `;
+ }
+
+ #defaultReply(messagePayload) {
+ const quoteHeader = document.createElement('div');
+ const italics = document.createElement('i');
+ italics.textContent = this.extraInfo?.authorName + ' said:';
+ quoteHeader.append(italics);
+
+ const quote = document.createElement('blockquote');
+ quote.innerHTML = messagePayload;
+ getExtraInfoNodes(quote)?.forEach?.(node => {
+ node.parentNode.removeChild(node);
+ });
+
+ const br1 = document.createElement('br');
+ const br2 = document.createElement('br');
+
+ return [quoteHeader, quote, br1, br2];
+ }
+
+ openReplyEditor() {
+ const messageId = this.extraInfo?.id;
+ const messagePayload = document.querySelector(
+ '[data-twpt-message-id="' + messageId +
+ '"] .scTailwindThreadPostcontentroot html-blob');
+ if (!messagePayload) {
+ console.error('[flattenThreads] Payload not found.');
+ return;
+ }
+
+ const parentId = this.extraInfo?.parentId ?? this.extraInfo?.id;
+ const parentNodeReply =
+ document.querySelector('[data-twpt-message-id="' + parentId + '"]')
+ ?.closest?.('sc-tailwind-thread-message-message-card');
+ const parentNodeReplyButton = parentNodeReply?.querySelector?.(
+ '.scTailwindThreadMessageMessagecardadd-comment button');
+ if (!parentNodeReplyButton) {
+ // This is not critical: the reply button might already have been clicked
+ // (so it no longer exists), or the thread might be locked so replying is
+ // disabled and the button does'nt exist.
+ console.debug('[flattenThreads] Reply button not found.');
+ return;
+ }
+
+ // Click the reply button.
+ parentNodeReplyButton.click();
+
+ // Fill in the default reply text (it includes a quote of the message the
+ // user wishes to reply to).
+ waitFor(() => {
+ const editor =
+ parentNodeReply?.querySelector('sc-tailwind-thread-reply-editor');
+ if (editor) return Promise.resolve(editor);
+ return Promise.reject(new Error('Editor not found.'));
+ }, {interval: 75, timeout: 10 * 1000}).then(editor => {
+ const payload =
+ editor?.querySelector('.scTailwindSharedRichtexteditoreditor');
+
+ payload.prepend(...this.#defaultReply(messagePayload.innerHTML));
+ });
+ }
+}
+window.customElements.define(
+ 'twpt-flatten-thread-reply-button', TwptFlattenThreadReplyButton);
diff --git a/src/contentScripts/communityConsole/flattenThreads/components/index.js b/src/contentScripts/communityConsole/flattenThreads/components/index.js
new file mode 100644
index 0000000..08f1d18
--- /dev/null
+++ b/src/contentScripts/communityConsole/flattenThreads/components/index.js
@@ -0,0 +1,136 @@
+import '@material/web/button/tonal-button.js';
+
+import './QuoteAuthor.js';
+
+// Other components imported so they are also injected:
+import './ReplyButton.js';
+
+import * as DOMPurify from 'dompurify';
+import {css, html, LitElement} from 'lit';
+import {classMap} from 'lit/directives/class-map.js';
+import {unsafeHTML} from 'lit/directives/unsafe-html.js';
+
+import {SHARED_MD3_STYLES} from '../../../../common/styles/md3.js';
+
+export default class TwptFlattenThreadQuote extends LitElement {
+ static properties = {
+ prevMessage: {type: Object},
+ _expanded: {type: Boolean},
+ };
+
+ static styles = [
+ SHARED_MD3_STYLES,
+ css`
+ :host {
+ display: block;
+ }
+
+ .quote-container {
+ position: relative;
+ background-color: var(--TWPT-secondary-background, rgba(242, 242, 242, 0.502));
+ margin-bottom: 16px;
+ padding: 0 12px 12px 12px;
+ border-radius: 12px;
+ }
+
+ .payload-container {
+ padding-top: 12px;
+ }
+
+ .quote-container:not(.quote-container--expanded) .payload-container {
+ max-height: 4rem;
+ overflow: hidden;
+ }
+
+ .payload-container twpt-flatten-thread-quote-author {
+ float: right;
+ margin-left: 12px;
+ margin-top: -12px;
+ shape-outside: inset(0 10px 10px 0);
+ }
+
+ .payload {
+ display: inline;
+ }
+
+ .payload img {
+ max-width: 100%;
+ max-height: calc(100vh - 2*64px);
+ }
+
+ .payload blockquote {
+ border-left: 1px solid #757575;
+ margin: 0 0 0 4px;
+ padding: 0 0 0 4px;
+ }
+
+ .buttons-row {
+ position: absolute;
+ width: calc(100% - 24px);
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ bottom: -20px;
+ transition: opacity .25s;
+ }
+
+ .quote-container:not(:hover) .buttons-row:not(:has(md-tonal-button:focus)) {
+ opacity: 0;
+ }
+
+ .buttons-row md-tonal-button {
+ --md-tonal-button-container-color: var(--TWPT-dark-flatten-replies-more-bg, rgba(222, 222, 222, 0.9));
+ --md-tonal-button-label-text-color: var(--TWPT-md-sys-color-on-surface);
+ --md-tonal-button-hover-label-text-color: var(--TWPT-md-sys-color-on-surface);
+ --md-tonal-button-focus-label-text-color: var(--TWPT-md-sys-color-on-surface);
+ --md-tonal-button-pressed-label-text-color: var(--TWPT-md-sys-color-on-surface);
+ --md-tonal-button-with-icon-icon-color: var(--TWPT-md-sys-color-on-surface);
+ --md-tonal-button-with-icon-hover-icon-color: var(--TWPT-md-sys-color-on-surface);
+ --md-tonal-button-with-icon-focus-icon-color: var(--TWPT-md-sys-color-on-surface);
+ --md-tonal-button-with-icon-pressed-icon-color: var(--TWPT-md-sys-color-on-surface);
+ }
+ `,
+ ];
+
+ constructor() {
+ super();
+ this.prevMessage = {};
+ this._expanded = false;
+ }
+
+ getTrustedPayload() {
+ return DOMPurify.sanitize(this.prevMessage?.payload ?? '');
+ }
+
+ render() {
+ const containerClasses = classMap({
+ 'quote-container': true,
+ 'quote-container--expanded': this._expanded,
+ });
+ return html`
+ <div class=${containerClasses}>
+ <div class="payload-container">
+ <twpt-flatten-thread-quote-author
+ .prevMessage=${this.prevMessage}>
+ </twpt-flatten-thread-quote-author>
+ <div class="payload">
+ ${unsafeHTML(this.getTrustedPayload())}
+ </div>
+ </div>
+ <div class="buttons-row">
+ <md-tonal-button
+ icon="${this._expanded ? 'expand_less' : 'expand_more'}"
+ label="${this._expanded ? 'Less' : 'More'}"
+ @click=${this.toggleExpanded}>
+ </md-tonal-button>
+ </div>
+ </div>
+ `;
+ }
+
+ toggleExpanded() {
+ this._expanded = !this._expanded;
+ }
+}
+window.customElements.define(
+ 'twpt-flatten-thread-quote', TwptFlattenThreadQuote);
diff --git a/src/contentScripts/communityConsole/flattenThreads/flattenThreads.js b/src/contentScripts/communityConsole/flattenThreads/flattenThreads.js
index 91bcfbb..6fc3eac 100644
--- a/src/contentScripts/communityConsole/flattenThreads/flattenThreads.js
+++ b/src/contentScripts/communityConsole/flattenThreads/flattenThreads.js
@@ -4,6 +4,24 @@
export const kReplyPayloadSelector =
'.scTailwindThreadMessageMessagecardcontent:not(.scTailwindThreadMessageMessagecardpromoted) .scTailwindThreadPostcontentroot html-blob';
+export const kReplyActionButtonsSelector =
+ '.scTailwindThreadMessageMessagecardcontent:not(.scTailwindThreadMessageMessagecardpromoted) sc-tailwind-thread-message-message-actions';
+export const kMatchingSelectors = [
+ kReplyPayloadSelector,
+ kReplyActionButtonsSelector,
+];
+
+export function getExtraInfoNodes(node) {
+ const confirmedNodes = [];
+ const possibleExtraInfoNodes =
+ node.querySelectorAll('span[style*=\'display\'][style*=\'none\']');
+ for (const candidate of possibleExtraInfoNodes) {
+ const content = candidate.textContent;
+ const matches = content.match(kAdditionalInfoRegex);
+ if (matches) confirmedNodes.push(candidate);
+ }
+ return confirmedNodes;
+}
export default class FlattenThreads {
construct() {}
@@ -33,13 +51,18 @@
injectQuote(node, extraInfo) {
const content = node.closest('.scTailwindThreadPostcontentroot');
- // @TODO: Change this by the actual quote component
- const quote = document.createElement('div');
- quote.textContent = 'QUOTE(' + extraInfo.parentMessage.id + ')';
+ const quote = document.createElement('twpt-flatten-thread-quote');
+ quote.setAttribute('prevMessage', JSON.stringify(extraInfo.prevMessage));
content.prepend(quote);
}
- injectIfApplicable(node) {
+ injectReplyBtn(node, extraInfo) {
+ const btn = document.createElement('twpt-flatten-thread-reply-button');
+ btn.setAttribute('extraInfo', JSON.stringify(extraInfo));
+ node.prepend(btn);
+ }
+
+ injectQuoteIfApplicable(node) {
// If we injected the additional information, it means the flatten threads
// feature is enabled and in actual use, so we should inject the quote.
const extraInfo = this.getExtraInfo(node);
@@ -49,7 +72,24 @@
if (extraInfo.isComment) this.injectQuote(node, extraInfo);
}
- shouldInject(node) {
+ shouldInjectQuote(node) {
return node.matches(kReplyPayloadSelector);
}
+
+ injectReplyBtnIfApplicable(node) {
+ // If we injected the additional information, it means the flatten threads
+ // feature is enabled and in actual use, so we should inject the reply
+ // button.
+ const root =
+ node.closest('.scTailwindThreadMessageMessagecardcontent')
+ .querySelector('.scTailwindThreadMessageMessagecardbody html-blob');
+ const extraInfo = this.getExtraInfo(root);
+ if (!extraInfo) return;
+
+ this.injectReplyBtn(node, extraInfo);
+ }
+
+ shouldInjectReplyBtn(node) {
+ return node.matches(kReplyActionButtonsSelector);
+ }
}
diff --git a/src/contentScripts/communityConsole/main.js b/src/contentScripts/communityConsole/main.js
index a3b1bb7..2061834 100644
--- a/src/contentScripts/communityConsole/main.js
+++ b/src/contentScripts/communityConsole/main.js
@@ -8,7 +8,7 @@
// #!if ['chromium', 'chromium_mv3'].includes(browser_target)
import {applyDragAndDropFixIfEnabled} from './dragAndDropFix.js';
// #!endif
-import {default as FlattenThreads, kReplyPayloadSelector} from './flattenThreads/flattenThreads.js';
+import {default as FlattenThreads, kMatchingSelectors as kFlattenThreadMatchingSelectors} from './flattenThreads/flattenThreads.js';
import InfiniteScroll from './infiniteScroll.js';
import {kRepliesSectionSelector} from './threadToolbar/constants.js';
import ThreadToolbar from './threadToolbar/threadToolbar.js';
@@ -79,7 +79,7 @@
kRepliesSectionSelector,
// Reply payload (for the flatten threads UI)
- kReplyPayloadSelector,
+ ...kFlattenThreadMatchingSelectors,
];
function handleCandidateNode(node) {
@@ -220,20 +220,33 @@
}
// Inject parent reply quote
- if (flattenThreads.shouldInject(node)) {
- flattenThreads.injectIfApplicable(node);
+ if (flattenThreads.shouldInjectQuote(node)) {
+ flattenThreads.injectQuoteIfApplicable(node);
+ }
+
+ // Inject reply button in non-nested view
+ if (flattenThreads.shouldInjectReplyBtn(node)) {
+ flattenThreads.injectReplyBtnIfApplicable(node);
}
}
}
-function handleRemovedNode(node) {
+function handleRemovedNode(mutation, node) {
+ if (!('tagName' in node)) return;
+
// Remove snackbar when exiting thread list view
- if ('tagName' in node && node.tagName == 'EC-THREAD-LIST') {
+ if (node.tagName == 'EC-THREAD-LIST') {
window.TWPTAutoRefresh.hideUpdatePrompt();
}
+
+ // Readd reply button when the Community Console removes it
+ if (node.tagName == 'TWPT-FLATTEN-THREAD-REPLY-BUTTON') {
+ flattenThreads.injectReplyBtn(
+ mutation.target, JSON.parse(node.getAttribute('extraInfo')));
+ }
}
-function mutationCallback(mutationList, observer) {
+function mutationCallback(mutationList) {
mutationList.forEach((mutation) => {
if (mutation.type == 'childList') {
mutation.addedNodes.forEach(function(node) {
@@ -241,7 +254,7 @@
});
mutation.removedNodes.forEach(function(node) {
- handleRemovedNode(node);
+ handleRemovedNode(mutation, node);
});
}
});
@@ -325,4 +338,6 @@
injectScript(chrome.runtime.getURL('litComponentsInject.bundle.js'));
// Thread toolbar
injectStylesheet(chrome.runtime.getURL('css/thread_toolbar.css'));
+ // Flatten threads
+ injectStylesheet(chrome.runtime.getURL('css/flatten_threads.css'));
});
diff --git a/src/injections/litComponentsInject.js b/src/injections/litComponentsInject.js
index f4d59bd..9506c8c 100644
--- a/src/injections/litComponentsInject.js
+++ b/src/injections/litComponentsInject.js
@@ -4,6 +4,7 @@
// because `window.customElements` doesn't exist in content scripts.
import '../contentScripts/communityConsole/workflows/components/index.js';
import '../contentScripts/communityConsole/threadToolbar/components/index.js';
+import '../contentScripts/communityConsole/flattenThreads/components/index.js';
import {injectStylesheet} from '../common/contentScriptsUtils.js';
diff --git a/src/static/css/ccdarktheme.css b/src/static/css/ccdarktheme.css
index 0282658..a793a51 100644
--- a/src/static/css/ccdarktheme.css
+++ b/src/static/css/ccdarktheme.css
@@ -40,6 +40,9 @@
--TWPT-md-ripple-hover-state-layer-color: white;
--TWPT-md-ripple-pressed-state-layer-color: white;
--TWPT-custom-md-icon-color: var(--TWPT-subtle-button-background);
+
+ /* TWPT features variables */
+ --TWPT-dark-flatten-replies-more-bg: rgba(89, 89, 89, 0.9);
}
body {
diff --git a/src/static/css/flatten_threads.css b/src/static/css/flatten_threads.css
new file mode 100644
index 0000000..8a06ef3
--- /dev/null
+++ b/src/static/css/flatten_threads.css
@@ -0,0 +1,12 @@
+body.TWPT-flattenthreads-enabled ec-thread sc-tailwind-thread-message-message-list:last-child sc-tailwind-thread-message-message-actions {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 0.75rem;
+ margin-top: 1rem;
+}
+
+body.TWPT-flattenthreads-enabled ec-thread sc-tailwind-thread-message-message-list:last-child sc-tailwind-thread-message-message-actions > * {
+ margin: 0;
+ padding: 0;
+}
diff --git a/src/static/css/thread_toolbar.css b/src/static/css/thread_toolbar.css
index cd1ac48..4c1b35f 100644
--- a/src/static/css/thread_toolbar.css
+++ b/src/static/css/thread_toolbar.css
@@ -4,6 +4,28 @@
}
/* Hide reply button when a thread is flattened, since it might not work correctly */
-body.TWPT-flattenthreads-enabled ec-thread sc-tailwind-thread-message-message-list:last-child .scTailwindThreadMessageMessagecardsub-content {
- display: none;
+@supports selector(:has(div)) {
+ body.TWPT-flattenthreads-enabled ec-thread sc-tailwind-thread-message-message-list:last-child .scTailwindThreadMessageMessagecardsub-content:not(:has(.scTailwindThreadMessageMessagecardreply-editor)) {
+ display: none;
+ }
+}
+/* Fallback support for browsers which do not support :has(...) */
+@supports not selector(:has(div)) {
+ body.TWPT-flattenthreads-enabled ec-thread sc-tailwind-thread-message-message-list:last-child .scTailwindThreadMessageMessagecardsub-content .scTailwindThreadMessageMessagecardaction {
+ padding: 0;
+ }
+
+ body.TWPT-flattenthreads-enabled ec-thread sc-tailwind-thread-message-message-list:last-child .scTailwindThreadMessageMessagecardsub-content .scTailwindThreadMessageMessagecardaction .scTailwindThreadMessageMessagecardadd-comment {
+ display: none;
+ }
+
+ body.TWPT-flattenthreads-enabled ec-thread sc-tailwind-thread-message-message-list:last-child .scTailwindThreadMessageMessagecardsub-content .scTailwindThreadMessageMessagecardaction .scTailwindThreadMessageMessagecardreply-editor {
+ margin: 0.5rem;
+ }
+
+ @media (min-width: 37.5rem) {
+ body.TWPT-flattenthreads-enabled ec-thread sc-tailwind-thread-message-message-list:last-child .scTailwindThreadMessageMessagecardsub-content .scTailwindThreadMessageMessagecardaction .scTailwindThreadMessageMessagecardreply-editor {
+ margin: 0.5rem 3.5rem;
+ }
+ }
}
diff --git a/src/xhrInterceptor/responseModifiers/flattenThread.js b/src/xhrInterceptor/responseModifiers/flattenThread.js
index cece8b4..6828772 100644
--- a/src/xhrInterceptor/responseModifiers/flattenThread.js
+++ b/src/xhrInterceptor/responseModifiers/flattenThread.js
@@ -27,8 +27,14 @@
// Add some message data to the payload so the extension can show the parent
// comment/reply in the case of comments.
+ let prevReplyId;
+ let prevReplyParentId;
mogs.forEach(m => {
- const info = this.getAdditionalInformation(m, mogs);
+ const info = this.getAdditionalInformation(
+ m, mogs, prevReplyId, prevReplyParentId);
+ prevReplyId = m.getId();
+ prevReplyParentId = info.parentId;
+
const span = document.createElement('span');
span.textContent = kAdditionalInfoPrefix + JSON.stringify(info);
span.setAttribute('style', 'display: none');
@@ -52,25 +58,36 @@
response[1][8] = response[1][40].length;
return response;
},
- getAdditionalInformation(message, mogs) {
+ getAdditionalInformation(message, mogs, prevReplyId, prevReplyParentId) {
const id = message.getId();
const parentId = message.getParentMessageId();
- const parentMessage =
- parentId ? mogs.find(m => m.getId() === parentId) : null;
- if (!parentMessage) {
+ const authorName = message.getAuthor()?.[1]?.[1];
+ if (!parentId) {
return {
isComment: false,
id,
+ authorName,
};
}
+ let prevId;
+ if (parentId === prevReplyParentId && prevReplyParentId)
+ prevId = prevReplyId;
+ else
+ prevId = parentId;
+
+ const prevMessage =
+ prevId ? mogs.find(m => m.getId() === prevId) : null;
+
return {
isComment: true,
id,
- parentMessage: {
- id: parentId,
- payload: parentMessage.getPayload(),
- author: parentMessage.getAuthor(),
+ authorName,
+ parentId,
+ prevMessage: {
+ id: prevId,
+ payload: prevMessage.getPayload(),
+ author: prevMessage.getAuthor(),
},
};
}
diff --git a/templates/manifest.gjson b/templates/manifest.gjson
index c8d2ffb..5aabcf1 100644
--- a/templates/manifest.gjson
+++ b/templates/manifest.gjson
@@ -102,6 +102,7 @@
"css/ui_spacing/twbasic.css",
"css/thread_page_design_warning.css",
"css/thread_toolbar.css",
+ "css/flatten_threads.css",
"communityConsoleMain.bundle.js.map",
"communityConsoleStart.bundle.js.map",