Add threadListAvatars experiment
This experiment adds avatars of the users who have participated in a
thread in the thread list, next to each thread.
Change-Id: I259b103a7d3462201013ab2027866bbcce476901
diff --git a/src/_locales/ca/messages.json b/src/_locales/ca/messages.json
index 2c3ed92..9bf0e5c 100644
--- a/src/_locales/ca/messages.json
+++ b/src/_locales/ca/messages.json
@@ -99,6 +99,10 @@
"message": "Posa el botó \"expandir fil\" a l'esquerra del tot en les llistes de fils de la Consola de la Comunitat.",
"description": "Feature checkbox in the options page"
},
+ "options_threadlistavatars": {
+ "message": "Mostra fotos de perfil a les llistes de fils de la Consola de la Comunitat.",
+ "description": "Feature checkbox in the options page"
+ },
"options_profileindicator_header": {
"message": "Punt indicador",
"description": "Heading for the profile indicator feature options"
diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json
index 7c8b45b..b8fbd5b 100644
--- a/src/_locales/en/messages.json
+++ b/src/_locales/en/messages.json
@@ -103,6 +103,10 @@
"message": "Place the \"expand thread\" button all the way to the left in the Community Console thread lists.",
"description": "Feature checkbox in the options page"
},
+ "options_threadlistavatars": {
+ "message": "Show avatars in thread lists in the Community Console.",
+ "description": "Feature checkbox in the options page"
+ },
"options_profileindicator_header": {
"message": "Indicator dot",
"description": "Heading for the profile indicator feature options"
diff --git a/src/_locales/es/messages.json b/src/_locales/es/messages.json
index ae9fbdd..aec514e 100644
--- a/src/_locales/es/messages.json
+++ b/src/_locales/es/messages.json
@@ -103,6 +103,10 @@
"message": "Punto indicador",
"description": "Heading for the profile indicator feature options"
},
+ "options_threadlistavatars": {
+ "message": "Muestra fotos de perfil en las listas de hilos de la Consola de la Comunidad.",
+ "description": "Feature checkbox in the options page"
+ },
"options_profileindicator_moreinfo": {
"message": "+info sobre las 2 opciones anteriores",
"description": "Link to learn more about the profile indicator feature"
diff --git a/src/common/common.js b/src/common/common.js
index 205cdc9..5703dbc 100644
--- a/src/common/common.js
+++ b/src/common/common.js
@@ -82,7 +82,10 @@
},
// Experiments:
-
+ 'threadlistavatars': {
+ defaultValue: false,
+ context: 'experiments',
+ },
// Internal options:
'ccdarktheme_switch_enabled': {
diff --git a/src/content_scripts/console_inject.js b/src/content_scripts/console_inject.js
index c042f92..ae10881 100644
--- a/src/content_scripts/console_inject.js
+++ b/src/content_scripts/console_inject.js
@@ -13,6 +13,20 @@
return getNParent(node.parentNode, n - 1);
}
+function parseUrl(url) {
+ var forum_a = url.match(/forum\/([0-9]+)/i);
+ var thread_a = url.match(/thread\/([0-9]+)/i);
+
+ if (forum_a === null || thread_a === null) {
+ return false;
+ }
+
+ return {
+ 'forum': forum_a[1],
+ 'thread': thread_a[1],
+ };
+}
+
function createExtBadge() {
var badge = document.createElement('div');
badge.classList.add('TWPT-badge');
@@ -221,6 +235,93 @@
clone, (readToggle.nextSibling || readToggle));
}
+// TODO(avm99963): This is a prototype. DON'T FORGET TO ADD ERROR HANDLING.
+function injectAvatars(node) {
+ var header = node.querySelector(
+ 'ec-thread-summary .main-header .panel-description a.header');
+ if (header === null) return;
+
+ var link = parseUrl(header.href);
+ if (link === false) return;
+
+ var APIRequestUrl = 'https://support.google.com/s/community/api/ViewThread' +
+ (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
+
+ fetch(APIRequestUrl, {
+ 'headers': {
+ 'content-type': 'text/plain; charset=utf-8',
+ },
+ 'body': JSON.stringify({
+ 1: link.forum,
+ 2: link.thread,
+ 3: {
+ 1: {2: 15},
+ 3: true,
+ 5: true,
+ 10: true,
+ 16: true,
+ 18: true,
+ }
+ }),
+ 'method': 'POST',
+ 'mode': 'cors',
+ 'credentials': 'omit',
+ })
+ .then(res => {
+ if (res.status == 200 || res.status == 400) {
+ return res.json().then(data => ({
+ status: res.status,
+ body: data,
+ }));
+ } else {
+ throw new Error('Status code ' + res.status + ' was not expected.');
+ }
+ })
+ .then(res => {
+ if (res.status == 400) {
+ throw new Error(
+ res.body[4] ||
+ ('Response status: 400. Error code: ' + res.body[2]));
+ }
+
+ return res.body;
+ })
+ .then(data => {
+ if (!('1' in data) || !('8' in data['1'])) return false;
+
+ var messages = data['1']['8'];
+ if (messages == 0) return;
+
+ var avatarUrls = [];
+
+ if (!('3' in data['1'])) return false;
+ for (var m of data['1']['3']) {
+ if (!('3' in m) || !('1' in m['3']) || !('2' in m['3']['1']))
+ continue;
+
+ var url = m['3']['1']['2'];
+
+ if (!avatarUrls.includes(url)) avatarUrls.push(url);
+
+ if (avatarUrls.length == 3) break;
+ }
+
+ var avatarsContainer = document.createElement('div');
+ avatarsContainer.classList.add('TWPT-avatars');
+
+ var count = Math.floor(Math.random() * 4);
+
+ for (var i = 0; i < avatarUrls.length; ++i) {
+ var avatar = document.createElement('div');
+ avatar.classList.add('TWPT-avatar');
+ avatar.style.backgroundImage = 'url(\''+avatarUrls[i]+'\')';
+ avatarsContainer.appendChild(avatar);
+ }
+
+ header.appendChild(avatarsContainer);
+ });
+}
+
function injectPreviousPostsLinks(nameElement) {
var mainCardContent = getNParent(nameElement, 3);
if (mainCardContent === null) {
@@ -272,6 +373,9 @@
// Read/unread bulk action in the list of thread, for the batch lock feature
'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)
+ 'li',
];
function handleCandidateNode(node) {
@@ -339,6 +443,13 @@
if (options.batchlock && nodeIsReadToggleBtn(node)) {
addBatchLockBtn(node);
}
+
+ // Inject avatar links to threads in the thread list
+ if (options.threadlistavatars && ('tagName' in node) &&
+ (node.tagName == 'LI') &&
+ node.querySelector('ec-thread-summary') !== null) {
+ injectAvatars(node);
+ }
}
}
@@ -417,4 +528,9 @@
if (options.batchlock) {
injectScript(chrome.runtime.getURL('injections/batchlock_inject.js'));
}
+
+ if (options.threadlistavatars) {
+ injectStylesheet(
+ chrome.runtime.getURL('injections/thread_list_avatars.css'));
+ }
});
diff --git a/src/injections/thread_list_avatars.css b/src/injections/thread_list_avatars.css
new file mode 100644
index 0000000..a418005
--- /dev/null
+++ b/src/injections/thread_list_avatars.css
@@ -0,0 +1,27 @@
+.TWPT-avatars {
+ display: flex;
+ flex-direction: row-reverse;
+ width: 102px;
+ overflow-x: hidden;
+ margin-left: 8px;
+}
+
+.TWPT-avatars .TWPT-avatar {
+ height: 28px;
+ width: 28px;
+ align-self: center;
+ border-width: 0;
+ border-radius: 50%;
+ margin-left: 6px;
+ background-color: white;
+ background-position: center;
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+/*
+ * Changing styles of existing elements so the avatars fit.
+ */
+ec-thread-summary .main-header .panel-description a.header .header-content {
+ width: calc(100% - 204px);
+}
diff --git a/src/options/experiments.html b/src/options/experiments.html
index a49e6c9..e25254b 100644
--- a/src/options/experiments.html
+++ b/src/options/experiments.html
@@ -12,6 +12,7 @@
<h1 data-i18n="experiments_title"></h1>
<p data-i18n="experiments_description"></p>
<form>
+ <div class="option"><input type="checkbox" id="threadlistavatars"> <label for="threadlistavatars" data-i18n="threadlistavatars"></label></div>
<div class="actions"><button id="save" data-i18n="save"></button></div>
</form>
<div id="save-indicator"></div>
diff --git a/templates/manifest.gjson b/templates/manifest.gjson
index 5a91bdf..83edfd8 100644
--- a/templates/manifest.gjson
+++ b/templates/manifest.gjson
@@ -72,7 +72,8 @@
"injections/ccdarktheme.css",
"injections/batchlock_inject.js",
"injections/enhanced_announcements_dot.css",
- "injections/reposition_expand_thread.css"
+ "injections/reposition_expand_thread.css",
+ "injections/thread_list_avatars.css"
#if defined(CHROMIUM_MV3)
],
"matches": [