|
103 | 103 | let autoExpandMountObserver = null; |
104 | 104 | let calendarActionObserver = null; |
105 | 105 |
|
| 106 | + // Reaction enhancements |
| 107 | + let sortReactionsByCount = true; |
| 108 | + let showReactionCountTooltip = false; |
| 109 | + let reactionTypesCache = null; // { TYPE: { color, unicode } } |
| 110 | + let reactionEnhancerObserver = null; |
| 111 | + |
106 | 112 | const MESSENGER_PANEL_WIDTH_MIN_PERCENT = 50; |
107 | 113 | const MESSENGER_PANEL_WIDTH_MAX_PERCENT = 125; |
108 | 114 | const MESSENGER_PANEL_WIDTH_DEFAULT_PERCENT = 100; |
|
1215 | 1221 | debugLog('[AutoExpand] Mount observer installed (debounced 200ms)'); |
1216 | 1222 | } |
1217 | 1223 |
|
| 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 | + |
1218 | 1411 | // Initialize |
1219 | 1412 | init(); |
1220 | 1413 |
|
|
1325 | 1518 | if (!autoExpandMountObserver) { |
1326 | 1519 | setupAutoExpandMountObserver(); |
1327 | 1520 | } |
| 1521 | + reapplyReactionEnhancements(); |
1328 | 1522 | sendResponse({ success: true }); |
1329 | 1523 | }); |
1330 | 1524 | return true; |
|
1362 | 1556 | autoExpandShowMoreLists(); |
1363 | 1557 | setupAutoExpandMountObserver(); |
1364 | 1558 |
|
| 1559 | + // Reaction enhancements (sort by count, hover tooltip) |
| 1560 | + if (sortReactionsByCount || showReactionCountTooltip) { |
| 1561 | + setupReactionEnhancerObserver(); |
| 1562 | + } |
| 1563 | + |
1365 | 1564 | // Replace generic channel avatars and process date/times |
1366 | 1565 | setTimeout(() => { |
1367 | 1566 | if (isExtensionContextValid()) { |
|
1402 | 1601 | const rawDelay = parseInt(settings.autoExpandDelayMs, 10); |
1403 | 1602 | autoExpandDelayMs = isNaN(rawDelay) ? 300 : Math.max(100, Math.min(1000, rawDelay)); |
1404 | 1603 | autoExpandScope = normalizeAutoExpandScope(settings.autoExpandScope); |
| 1604 | + sortReactionsByCount = settings.sortReactionsByCount !== false; |
| 1605 | + showReactionCountTooltip = settings.showReactionCountTooltip === true; |
1405 | 1606 | const messengerPanelWidthPercent = clampMessengerPanelWidthPercent(settings.messengerPanelWidthPercent); |
1406 | 1607 | debugLog('[Content] keepMessengerExpanded setting:', settings.keepMessengerExpanded); |
1407 | 1608 | debugLog('[Content] messengerPanelWidthPercent setting:', messengerPanelWidthPercent); |
|
1433 | 1634 | autoExpandClicksPerList = 3; |
1434 | 1635 | autoExpandDelayMs = 300; |
1435 | 1636 | autoExpandScope = 'both'; |
| 1637 | + sortReactionsByCount = true; |
| 1638 | + showReactionCountTooltip = false; |
1436 | 1639 | } |
1437 | 1640 | } else { |
1438 | 1641 | debugLog('Cannot load settings: extension context invalid'); |
|
1452 | 1655 | autoExpandClicksPerList = 3; |
1453 | 1656 | autoExpandDelayMs = 300; |
1454 | 1657 | autoExpandScope = 'both'; |
| 1658 | + sortReactionsByCount = true; |
| 1659 | + showReactionCountTooltip = false; |
1455 | 1660 | } |
1456 | 1661 | } catch (e) { |
1457 | 1662 | console.error('Failed to load settings:', e); |
|
1470 | 1675 | autoExpandClicksPerList = 3; |
1471 | 1676 | autoExpandDelayMs = 300; |
1472 | 1677 | autoExpandScope = 'both'; |
| 1678 | + sortReactionsByCount = true; |
| 1679 | + showReactionCountTooltip = false; |
1473 | 1680 | } |
1474 | 1681 | } |
1475 | 1682 |
|
|
0 commit comments