Skip to content

Commit f68fd9f

Browse files
nichu42Copilot
andcommitted
feat: sort reaction emojis by count and add hover tooltip
- Add sortReactionsByCount setting (default: true): reorders the cat-icon emoji pills in coyo-reactions-info so the most-used reaction appears first instead of Haiilo's arbitrary order. - Add showReactionCountTooltip setting (default: false): injects a breakdown like '👍22 💡2' into the existing (otherwise empty) cat-tooltip on the reactions summary button. - Reaction types are fetched once per page load from /web/reaction-targets/types and cached; per-post counts are fetched from /web/reaction-targets/{type}?ids={id}. - Icons are identified by the unique SVG fill color in their shadow DOM (each reaction type has a distinct color in the types API). - A MutationObserver watches for [data-reaction-target-id] elements so the feature works on timeline infinite scroll and SPA navigation. - Settings flow through the existing normalizeSettings/syncToCloud/ pullFromCloud/resetSettings/export/import machinery automatically. - Bump version to 0.5.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 28a6635 commit f68fd9f

6 files changed

Lines changed: 235 additions & 4 deletions

File tree

background.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,9 @@ const DEFAULT_SETTINGS = {
183183
autoExpandClicksPerList: 3, // Max number of "Show more" clicks per list (0-10)
184184
autoExpandDelayMs: 300, // Delay between clicks in ms (100-1000)
185185
autoExpandScope: 'both', // Which lists to expand: 'both', 'workspaces', or 'pages'
186-
cloudSync: false // Sync settings and muted users via browser account (opt-in)
186+
cloudSync: false, // Sync settings and muted users via browser account (opt-in)
187+
sortReactionsByCount: true, // Sort reaction emojis by count (most used first)
188+
showReactionCountTooltip: false // Show reaction count breakdown on hover
187189
};
188190

189191
function clampMessengerPanelWidthPercent(value) {

content.js

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@
103103
let autoExpandMountObserver = null;
104104
let calendarActionObserver = null;
105105

106+
// Reaction enhancements
107+
let sortReactionsByCount = true;
108+
let showReactionCountTooltip = false;
109+
let reactionTypesCache = null; // { TYPE: { color, unicode } }
110+
let reactionEnhancerObserver = null;
111+
106112
const MESSENGER_PANEL_WIDTH_MIN_PERCENT = 50;
107113
const MESSENGER_PANEL_WIDTH_MAX_PERCENT = 125;
108114
const MESSENGER_PANEL_WIDTH_DEFAULT_PERCENT = 100;
@@ -1215,6 +1221,193 @@
12151221
debugLog('[AutoExpand] Mount observer installed (debounced 200ms)');
12161222
}
12171223

1224+
// ── Reaction enhancements ──────────────────────────────────────────────────
1225+
1226+
// Fetch and cache the reaction type metadata (color fingerprint + unicode emoji).
1227+
async function getReactionTypes() {
1228+
if (reactionTypesCache) return reactionTypesCache;
1229+
try {
1230+
const res = await fetch('/web/reaction-targets/types');
1231+
if (!res.ok) return null;
1232+
const types = await res.json();
1233+
reactionTypesCache = {};
1234+
for (const t of types) {
1235+
reactionTypesCache[t.reactionType] = {
1236+
color: t.color,
1237+
unicode: t.fallbackUnicode
1238+
};
1239+
}
1240+
return reactionTypesCache;
1241+
} catch (e) {
1242+
debugLog('[Reactions] Failed to fetch reaction types:', e);
1243+
return null;
1244+
}
1245+
}
1246+
1247+
// Identify a cat-icon element's reaction type by matching the unique fill
1248+
// color from the types API against the SVG rendered in its shadow DOM.
1249+
function identifyReactionIcon(icon, fingerprintMap) {
1250+
try {
1251+
const shadow = icon.shadowRoot;
1252+
if (!shadow) return null;
1253+
const html = shadow.innerHTML;
1254+
for (const [type, info] of Object.entries(fingerprintMap)) {
1255+
if (html.includes(info.color)) return type;
1256+
}
1257+
} catch (e) { /* shadow DOM read error */ }
1258+
return null;
1259+
}
1260+
1261+
// Reorder the cat-icon reaction summary icons to match sortedTypes order.
1262+
function reorderReactionIcons(icons, sortedTypes, fingerprintMap) {
1263+
if (icons.length < 2) return;
1264+
const identified = icons.map(icon => ({
1265+
icon,
1266+
type: identifyReactionIcon(icon, fingerprintMap)
1267+
}));
1268+
// Build target order: known types first (by sortedTypes), unknowns appended
1269+
const targetOrder = [];
1270+
for (const type of sortedTypes) {
1271+
const match = identified.find(i => i.type === type);
1272+
if (match) targetOrder.push(match);
1273+
}
1274+
for (const item of identified) {
1275+
if (!targetOrder.includes(item)) targetOrder.push(item);
1276+
}
1277+
// Check if already in correct order
1278+
const alreadyCorrect = targetOrder.every((item, idx) => item.icon === icons[idx]);
1279+
if (alreadyCorrect) return;
1280+
// Re-insert in sorted order before the first icon's current position
1281+
const parent = icons[0].parentNode;
1282+
const insertBefore = icons[0];
1283+
for (const item of targetOrder) {
1284+
parent.insertBefore(item.icon, insertBefore);
1285+
}
1286+
debugLog('[Reactions] Reordered icons:', targetOrder.map(i => i.type).join(', '));
1287+
}
1288+
1289+
// Inject (or update) a count tooltip into the cat-tooltip of a coyo-reactions-info.
1290+
function injectReactionTooltip(reactionsInfo, sortedData, fingerprintMap) {
1291+
const tooltip = reactionsInfo.querySelector('cat-tooltip');
1292+
if (!tooltip) return;
1293+
// Remove any previously injected tooltip content
1294+
const existing = tooltip.querySelector('.haiilo-enhancer-reaction-tooltip');
1295+
if (existing) existing.remove();
1296+
const text = sortedData
1297+
.map(({ reactionType, count }) => {
1298+
const unicode = fingerprintMap[reactionType]?.unicode || reactionType;
1299+
return `${unicode}${count}`;
1300+
})
1301+
.join(' ');
1302+
const p = document.createElement('p');
1303+
p.slot = 'content';
1304+
p.className = 'haiilo-enhancer-reaction-tooltip';
1305+
p.textContent = text;
1306+
tooltip.appendChild(p);
1307+
debugLog('[Reactions] Injected tooltip:', text);
1308+
}
1309+
1310+
// Process a single [data-reaction-target-id] anchor element.
1311+
async function processReactionTarget(dataEl) {
1312+
if (dataEl.dataset.haiiloEnhancerReactionsDone) return;
1313+
dataEl.dataset.haiiloEnhancerReactionsDone = '1';
1314+
1315+
const targetId = dataEl.dataset.reactionTargetId;
1316+
const targetType = dataEl.dataset.reactionTargetType;
1317+
const count = parseInt(dataEl.dataset.reactionCount, 10);
1318+
if (!targetId || !targetType || count < 2) return;
1319+
1320+
const fingerprintMap = await getReactionTypes();
1321+
if (!fingerprintMap) return;
1322+
1323+
// Fetch the summary for this target
1324+
let sortedData;
1325+
try {
1326+
const res = await fetch(`/web/reaction-targets/${targetType}?ids=${targetId}`);
1327+
if (!res.ok) return;
1328+
const json = await res.json();
1329+
const entry = json[targetId];
1330+
if (!entry || !Array.isArray(entry.allReactionsByCount)) return;
1331+
// Sort descending by count
1332+
sortedData = [...entry.allReactionsByCount].sort((a, b) => b.count - a.count);
1333+
} catch (e) {
1334+
debugLog('[Reactions] Failed to fetch summary for', targetId, e);
1335+
return;
1336+
}
1337+
1338+
// Find the enclosing coyo-reactions-info
1339+
const reactionsInfo = dataEl.closest('coyo-reactions-info') ||
1340+
dataEl.parentElement?.querySelector('coyo-reactions-info') ||
1341+
dataEl.closest('[data-test="info-container"]')?.querySelector('coyo-reactions-info');
1342+
if (!reactionsInfo) return;
1343+
1344+
// Wait briefly for icons to render if they haven't yet
1345+
let icons = [...reactionsInfo.querySelectorAll('cat-icon[data-test="reactions-info-icon"]')];
1346+
if (icons.length === 0) {
1347+
await new Promise(r => setTimeout(r, 150));
1348+
icons = [...reactionsInfo.querySelectorAll('cat-icon[data-test="reactions-info-icon"]')];
1349+
}
1350+
1351+
const sortedTypes = sortedData.map(d => d.reactionType);
1352+
1353+
if (sortReactionsByCount && icons.length >= 2) {
1354+
reorderReactionIcons(icons, sortedTypes, fingerprintMap);
1355+
}
1356+
1357+
if (showReactionCountTooltip) {
1358+
injectReactionTooltip(reactionsInfo, sortedData, fingerprintMap);
1359+
}
1360+
}
1361+
1362+
function setupReactionEnhancerObserver() {
1363+
if (reactionEnhancerObserver) reactionEnhancerObserver.disconnect();
1364+
1365+
// Process any already-present targets on the page
1366+
document.querySelectorAll('[data-reaction-target-id]').forEach(el => {
1367+
processReactionTarget(el).catch(() => {});
1368+
});
1369+
1370+
reactionEnhancerObserver = new MutationObserver(mutations => {
1371+
for (const mutation of mutations) {
1372+
for (const node of mutation.addedNodes) {
1373+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
1374+
// Direct match
1375+
if (node.hasAttribute && node.hasAttribute('data-reaction-target-id')) {
1376+
processReactionTarget(node).catch(() => {});
1377+
}
1378+
// Descendants
1379+
if (node.querySelectorAll) {
1380+
node.querySelectorAll('[data-reaction-target-id]').forEach(el => {
1381+
processReactionTarget(el).catch(() => {});
1382+
});
1383+
}
1384+
}
1385+
}
1386+
});
1387+
1388+
reactionEnhancerObserver.observe(document.body, { childList: true, subtree: true });
1389+
debugLog('[Reactions] Observer installed');
1390+
}
1391+
1392+
// Re-run reaction enhancements after settings change (clear processed flags first).
1393+
function reapplyReactionEnhancements() {
1394+
if (!sortReactionsByCount && !showReactionCountTooltip) return;
1395+
// Clear done flags so existing elements are reprocessed
1396+
document.querySelectorAll('[data-reaction-target-id][data-haiilo-enhancer-reactions-done]')
1397+
.forEach(el => {
1398+
delete el.dataset.haiiloEnhancerReactionsDone;
1399+
// Also clear injected tooltips if feature disabled
1400+
if (!showReactionCountTooltip) {
1401+
const ri = el.closest('coyo-reactions-info') ||
1402+
el.closest('[data-test="info-container"]')?.querySelector('coyo-reactions-info');
1403+
if (ri) ri.querySelector('.haiilo-enhancer-reaction-tooltip')?.remove();
1404+
}
1405+
});
1406+
setupReactionEnhancerObserver();
1407+
}
1408+
1409+
// ── End Reaction enhancements ──────────────────────────────────────────────
1410+
12181411
// Initialize
12191412
init();
12201413

@@ -1325,6 +1518,7 @@
13251518
if (!autoExpandMountObserver) {
13261519
setupAutoExpandMountObserver();
13271520
}
1521+
reapplyReactionEnhancements();
13281522
sendResponse({ success: true });
13291523
});
13301524
return true;
@@ -1362,6 +1556,11 @@
13621556
autoExpandShowMoreLists();
13631557
setupAutoExpandMountObserver();
13641558

1559+
// Reaction enhancements (sort by count, hover tooltip)
1560+
if (sortReactionsByCount || showReactionCountTooltip) {
1561+
setupReactionEnhancerObserver();
1562+
}
1563+
13651564
// Replace generic channel avatars and process date/times
13661565
setTimeout(() => {
13671566
if (isExtensionContextValid()) {
@@ -1402,6 +1601,8 @@
14021601
const rawDelay = parseInt(settings.autoExpandDelayMs, 10);
14031602
autoExpandDelayMs = isNaN(rawDelay) ? 300 : Math.max(100, Math.min(1000, rawDelay));
14041603
autoExpandScope = normalizeAutoExpandScope(settings.autoExpandScope);
1604+
sortReactionsByCount = settings.sortReactionsByCount !== false;
1605+
showReactionCountTooltip = settings.showReactionCountTooltip === true;
14051606
const messengerPanelWidthPercent = clampMessengerPanelWidthPercent(settings.messengerPanelWidthPercent);
14061607
debugLog('[Content] keepMessengerExpanded setting:', settings.keepMessengerExpanded);
14071608
debugLog('[Content] messengerPanelWidthPercent setting:', messengerPanelWidthPercent);
@@ -1433,6 +1634,8 @@
14331634
autoExpandClicksPerList = 3;
14341635
autoExpandDelayMs = 300;
14351636
autoExpandScope = 'both';
1637+
sortReactionsByCount = true;
1638+
showReactionCountTooltip = false;
14361639
}
14371640
} else {
14381641
debugLog('Cannot load settings: extension context invalid');
@@ -1452,6 +1655,8 @@
14521655
autoExpandClicksPerList = 3;
14531656
autoExpandDelayMs = 300;
14541657
autoExpandScope = 'both';
1658+
sortReactionsByCount = true;
1659+
showReactionCountTooltip = false;
14551660
}
14561661
} catch (e) {
14571662
console.error('Failed to load settings:', e);
@@ -1470,6 +1675,8 @@
14701675
autoExpandClicksPerList = 3;
14711676
autoExpandDelayMs = 300;
14721677
autoExpandScope = 'both';
1678+
sortReactionsByCount = true;
1679+
showReactionCountTooltip = false;
14731680
}
14741681
}
14751682

manifest.firefox.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 2,
33
"name": "Haiilo Enhancer",
4-
"version": "0.4.1",
4+
"version": "0.5.0",
55
"description": "Enhance your Haiilo experience - mute users, customize your feed, and more",
66
"homepage_url": "https://github.com/nichu42/haiilo-enhancer",
77
"permissions": [

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "Haiilo Enhancer",
4-
"version": "0.4.1",
4+
"version": "0.5.0",
55
"description": "Enhance your Haiilo experience - mute users, customize your feed, and more",
66
"homepage_url": "https://github.com/nichu42/haiilo-enhancer",
77
"permissions": [

options.html

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,23 @@ <h2>Custom Homepage Settings</h2>
206206
</section>
207207

208208
<section>
209-
<h2>Auto-Expand Sidebar Lists</h2>
209+
<h2>Reactions</h2>
210+
<p class="description">Improve how emoji reactions are displayed on posts and comments.</p>
211+
<div class="form-group checkbox-group">
212+
<label>
213+
<input type="checkbox" id="sortReactionsByCount">
214+
<span>Sort reaction emojis by count (most used first)</span>
215+
</label>
216+
</div>
217+
<div class="form-group checkbox-group">
218+
<label>
219+
<input type="checkbox" id="showReactionCountTooltip">
220+
<span>Show reaction count breakdown on hover (e.g. 👍22 💡2)</span>
221+
</label>
222+
</div>
223+
</section>
224+
225+
<section>
210226
<p class="description">Haiilo's left sidebar shows a limited number of Workspaces and Pages before hiding the rest behind a "Show more" button. Enable this to auto-click that button on page load so all entries are visible at once.</p>
211227
<div class="form-group checkbox-group">
212228
<label>

options.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ async function loadSettings() {
142142

143143
// Auto-expand sidebar lists
144144
document.getElementById('autoExpandEnabled').checked = settings.autoExpandEnabled === true;
145+
document.getElementById('sortReactionsByCount').checked = settings.sortReactionsByCount !== false;
146+
document.getElementById('showReactionCountTooltip').checked = settings.showReactionCountTooltip === true;
145147
document.getElementById('autoExpandClicksPerList').value = settings.autoExpandClicksPerList !== undefined ? settings.autoExpandClicksPerList : 3;
146148
document.getElementById('autoExpandDelayMs').value = settings.autoExpandDelayMs !== undefined ? settings.autoExpandDelayMs : 300;
147149
const scope = settings.autoExpandScope;
@@ -367,6 +369,8 @@ function setupEventListeners() {
367369
// Auto-expand settings
368370
document.getElementById('autoExpandEnabled').addEventListener('change', saveSettings);
369371
document.getElementById('autoExpandScope').addEventListener('change', saveSettings);
372+
document.getElementById('sortReactionsByCount').addEventListener('change', saveSettings);
373+
document.getElementById('showReactionCountTooltip').addEventListener('change', saveSettings);
370374

371375
// Cloud sync toggle
372376
const cloudSyncCheckbox = document.getElementById('cloudSync');
@@ -599,6 +603,8 @@ async function saveSettings() {
599603
autoExpandClicksPerList: parseInt(document.getElementById('autoExpandClicksPerList').value, 10) || 3,
600604
autoExpandDelayMs: parseInt(document.getElementById('autoExpandDelayMs').value, 10) || 300,
601605
autoExpandScope: document.getElementById('autoExpandScope').value,
606+
sortReactionsByCount: document.getElementById('sortReactionsByCount').checked,
607+
showReactionCountTooltip: document.getElementById('showReactionCountTooltip').checked,
602608
cloudSync: document.getElementById('cloudSync') ? document.getElementById('cloudSync').checked : false
603609
};
604610

0 commit comments

Comments
 (0)