linkfeed-pro 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.output/chrome-mv3/_locales/de/messages.json +214 -0
  3. package/.output/chrome-mv3/_locales/en/messages.json +214 -0
  4. package/.output/chrome-mv3/_locales/es/messages.json +214 -0
  5. package/.output/chrome-mv3/_locales/fr/messages.json +214 -0
  6. package/.output/chrome-mv3/_locales/hi/messages.json +214 -0
  7. package/.output/chrome-mv3/_locales/id/messages.json +214 -0
  8. package/.output/chrome-mv3/_locales/it/messages.json +214 -0
  9. package/.output/chrome-mv3/_locales/nl/messages.json +214 -0
  10. package/.output/chrome-mv3/_locales/pl/messages.json +214 -0
  11. package/.output/chrome-mv3/_locales/pt_BR/messages.json +214 -0
  12. package/.output/chrome-mv3/_locales/pt_PT/messages.json +214 -0
  13. package/.output/chrome-mv3/_locales/tr/messages.json +214 -0
  14. package/.output/chrome-mv3/assets/popup-Z_g1HFs5.css +1 -0
  15. package/.output/chrome-mv3/background.js +42 -0
  16. package/.output/chrome-mv3/chunks/popup-IxiPwS1E.js +42 -0
  17. package/.output/chrome-mv3/content-scripts/content.js +179 -0
  18. package/.output/chrome-mv3/icon-128.png +0 -0
  19. package/.output/chrome-mv3/icon-16.png +0 -0
  20. package/.output/chrome-mv3/icon-48.png +0 -0
  21. package/.output/chrome-mv3/icon.svg +9 -0
  22. package/.output/chrome-mv3/manifest.json +1 -0
  23. package/.output/chrome-mv3/popup.html +247 -0
  24. package/.wxt/eslint-auto-imports.mjs +56 -0
  25. package/.wxt/tsconfig.json +28 -0
  26. package/.wxt/types/globals.d.ts +15 -0
  27. package/.wxt/types/i18n.d.ts +593 -0
  28. package/.wxt/types/imports-module.d.ts +20 -0
  29. package/.wxt/types/imports.d.ts +50 -0
  30. package/.wxt/types/paths.d.ts +32 -0
  31. package/.wxt/wxt.d.ts +7 -0
  32. package/entrypoints/background.ts +112 -0
  33. package/entrypoints/content.ts +656 -0
  34. package/entrypoints/popup/main.ts +452 -0
  35. package/entrypoints/popup/modules/auth-modal.ts +219 -0
  36. package/entrypoints/popup/modules/settings.ts +78 -0
  37. package/entrypoints/popup/modules/ui-state.ts +95 -0
  38. package/entrypoints/popup/style.css +844 -0
  39. package/entrypoints/popup.html +261 -0
  40. package/lib/constants.ts +9 -0
  41. package/lib/device-meta.ts +173 -0
  42. package/lib/i18n.ts +201 -0
  43. package/lib/license.ts +470 -0
  44. package/lib/selectors.ts +24 -0
  45. package/lib/storage.ts +95 -0
  46. package/lib/telemetry.ts +94 -0
  47. package/package.json +30 -0
  48. package/public/_locales/de/messages.json +214 -0
  49. package/public/_locales/en/messages.json +214 -0
  50. package/public/_locales/es/messages.json +214 -0
  51. package/public/_locales/fr/messages.json +214 -0
  52. package/public/_locales/hi/messages.json +214 -0
  53. package/public/_locales/id/messages.json +214 -0
  54. package/public/_locales/it/messages.json +214 -0
  55. package/public/_locales/nl/messages.json +214 -0
  56. package/public/_locales/pl/messages.json +214 -0
  57. package/public/_locales/pt_BR/messages.json +214 -0
  58. package/public/_locales/pt_PT/messages.json +214 -0
  59. package/public/_locales/tr/messages.json +214 -0
  60. package/public/icon-128.png +0 -0
  61. package/public/icon-16.png +0 -0
  62. package/public/icon-48.png +0 -0
  63. package/public/icon.svg +9 -0
  64. package/tsconfig.json +3 -0
  65. package/wxt.config.ts +50 -0
@@ -0,0 +1,656 @@
1
+ import { getLicenseState, getCachedLicense } from '../lib/license';
2
+ import type { FeatureFlags } from '@linkfeed/shared';
3
+ import { settingsStorage, cachedLicenseStorage, type Settings } from '../lib/storage';
4
+ import { LNK_SELECTORS } from '../lib/selectors';
5
+
6
+ const FREE_FEATURES: FeatureFlags = { wideFeed: false, fontSize: false, autoExpandPosts: false };
7
+ let currentLicenseFeatures: FeatureFlags = { ...FREE_FEATURES };
8
+ let licenseRefreshInFlight: Promise<void> | null = null;
9
+
10
+ const REMOVED_FROM_FEED_MARKERS = [
11
+ 'post removed from your feed',
12
+ 'post removed form your feed',
13
+ 'removed from your feed',
14
+ 'removed form your feed',
15
+ 'publication supprimee de votre fil',
16
+ 'supprime de votre fil',
17
+ 'publicacion eliminada de tu feed',
18
+ 'beitrag wurde aus deinem feed entfernt',
19
+ 'post rimosso dal tuo feed',
20
+ 'post verwijderd uit je feed',
21
+ 'post removido do seu feed',
22
+ 'post removido do teu feed',
23
+ 'gonderi akisindan kaldirildi',
24
+ ];
25
+
26
+ const UNDO_MARKERS = [
27
+ 'undo',
28
+ 'annuler',
29
+ 'deshacer',
30
+ 'ruckgangig',
31
+ 'annulla',
32
+ 'ongedaan',
33
+ 'desfazer',
34
+ 'geri al',
35
+ 'batalkan',
36
+ ];
37
+
38
+ const FEEDBACK_MARKERS = [
39
+ 'not interested',
40
+ 'not appropriate',
41
+ 'pas interesse',
42
+ 'pas approprie',
43
+ 'no me interesa',
44
+ 'inappropriate for linkedin',
45
+ 'no es apropiado para linkedin',
46
+ 'nicht interessiert',
47
+ 'non interessato',
48
+ ];
49
+
50
+ const PROMOTED_MARKERS = [
51
+ 'promoted',
52
+ 'sponsored',
53
+ 'sponsorise',
54
+ 'patrocinado',
55
+ 'gesponsert',
56
+ 'sponsorizzato',
57
+ ];
58
+
59
+ const PROMOTED_LABEL_SELECTORS = [
60
+ '.update-components-actor__sub-description',
61
+ '.update-components-actor__description',
62
+ '.feed-shared-actor__sub-description',
63
+ '.feed-shared-actor__description',
64
+ ];
65
+
66
+ export default defineContentScript({
67
+ matches: ['https://www.linkedin.com/*'],
68
+ runAt: 'document_idle',
69
+
70
+ async main() {
71
+ // Load settings from storage
72
+ let settings = await settingsStorage.getValue();
73
+
74
+ // Apply cached license immediately, then refresh
75
+ await applyCachedLicense(settings);
76
+ void refreshLicense(settings);
77
+
78
+ // Listen for settings changes
79
+ settingsStorage.watch(async (newSettings) => {
80
+ if (newSettings) {
81
+ await applyLicenseFeatures(newSettings, currentLicenseFeatures);
82
+ void refreshLicense(newSettings);
83
+ settings = newSettings;
84
+ }
85
+ });
86
+
87
+ // Re-check license if license data changed
88
+ cachedLicenseStorage.watch(async (cached) => {
89
+ const currentSettings = await settingsStorage.getValue();
90
+ await applyCachedLicense(currentSettings, cached ?? undefined);
91
+ });
92
+ },
93
+ });
94
+
95
+ async function applyCachedLicense(settings: Settings, cached?: Awaited<ReturnType<typeof getCachedLicense>>) {
96
+ const resolved = cached ?? await getCachedLicense();
97
+ if (resolved?.features) {
98
+ await applyLicenseFeatures(settings, resolved.features);
99
+ } else {
100
+ await applyLicenseFeatures(settings, FREE_FEATURES);
101
+ }
102
+ }
103
+
104
+ async function refreshLicense(settings: Settings) {
105
+ if (!settings.globalEnabled) return;
106
+ if (licenseRefreshInFlight) return;
107
+
108
+ licenseRefreshInFlight = (async () => {
109
+ const licenseState = await getLicenseState();
110
+ currentLicenseFeatures = licenseState.features;
111
+ await applyLicenseFeatures(settings, currentLicenseFeatures);
112
+ })().finally(() => {
113
+ licenseRefreshInFlight = null;
114
+ });
115
+ }
116
+
117
+ // Migration logic is now handled in settingsStorage.getValue() via migrate property in storage.ts
118
+ // (Actually I reverted migrate and put it in popup, but content script should also be clean)
119
+ // Since I want best practices, I'll rely on settingsStorage giving me valid data.
120
+
121
+ /**
122
+ * Injects the base styles once with CSS variables
123
+ */
124
+ function injectBaseStyles() {
125
+ if (document.getElementById('linkfeed-styles')) return;
126
+
127
+ const style = document.createElement('style');
128
+ style.id = 'linkfeed-styles';
129
+ style.textContent = `
130
+ :root {
131
+ --lf-feed-width: 800px;
132
+ --lf-font-size: 16px;
133
+ --lf-feed-gap: 24px;
134
+ }
135
+
136
+ /* Global Disable Handle */
137
+ html:not(.lf-global-enabled) .lf-premium-feature,
138
+ html:not(.lf-global-enabled) .lf-sidebar-feature {
139
+ /* Reset or do nothing - handled by class toggles */
140
+ }
141
+
142
+ .feed-shared-inline-show-more-text:focus,
143
+ .feed-shared-inline-show-more-text:focus-visible,
144
+ .feed-shared-inline-show-more-text__see-more-less-toggle:focus {
145
+ outline: none !important;
146
+ }
147
+
148
+ /* Sidebars */
149
+ html.lf-hide-sidebars ${LNK_SELECTORS.SIDEBAR_RIGHT},
150
+ html.lf-hide-sidebars ${LNK_SELECTORS.SIDEBAR_LEFT} {
151
+ display: none !important;
152
+ }
153
+
154
+ /* Wide Feed */
155
+ html.lf-wide-feed body.render-mode-BIGPIPE {
156
+ display: flex !important;
157
+ justify-content: center !important;
158
+ }
159
+
160
+ html.lf-wide-feed .authentication-outlet,
161
+ html.lf-wide-feed .application-outlet,
162
+ html.lf-wide-feed .scaffold-layout,
163
+ html.lf-wide-feed .scaffold-layout__inner,
164
+ html.lf-wide-feed ${LNK_SELECTORS.MAIN_GRID},
165
+ html.lf-wide-feed .scaffold-layout__main,
166
+ html.lf-wide-feed .core-rail,
167
+ html.lf-wide-feed .scaffold-finite-scroll {
168
+ max-width: var(--lf-feed-width) !important;
169
+ width: 100% !important;
170
+ justify-content: center !important;
171
+ }
172
+
173
+ html.lf-wide-feed ${LNK_SELECTORS.MAIN_GRID} {
174
+ display: block !important;
175
+ margin: 0 auto !important;
176
+ }
177
+
178
+ html.lf-wide-feed ${LNK_SELECTORS.SCROLL_CONTENT} {
179
+ width: 100% !important;
180
+ max-width: 100% !important;
181
+ display: flex !important;
182
+ flex-direction: column !important;
183
+ align-items: center !important;
184
+ gap: var(--lf-feed-gap) !important;
185
+ }
186
+
187
+ html.lf-wide-feed ${LNK_SELECTORS.SCROLL_CONTENT} > * {
188
+ width: 100% !important;
189
+ max-width: 100% !important;
190
+ margin-left: 0 !important;
191
+ margin-right: 0 !important;
192
+ }
193
+
194
+ html.lf-wide-feed ${LNK_SELECTORS.SCROLL_CONTENT} > [data-finite-scroll-hotkey-item],
195
+ html.lf-wide-feed ${LNK_SELECTORS.SCROLL_CONTENT} > .scaffold-finite-scroll__content-item,
196
+ html.lf-wide-feed ${LNK_SELECTORS.SCROLL_CONTENT} > .feed-shared-update-v2,
197
+ html.lf-wide-feed ${LNK_SELECTORS.SCROLL_CONTENT} > .feed-shared-update-v1 {
198
+ border-radius: 8px !important;
199
+ }
200
+
201
+ html.lf-wide-feed ${LNK_SELECTORS.SCROLL_CONTENT} > ${LNK_SELECTORS.FEED_SKIP_LINK} {
202
+ display: none !important;
203
+ margin: 0 !important;
204
+ padding: 0 !important;
205
+ min-height: 0 !important;
206
+ height: 0 !important;
207
+ border: 0 !important;
208
+ }
209
+
210
+ html.lf-wide-feed ${LNK_SELECTORS.POST_CONTAINER},
211
+ html.lf-wide-feed .feed-shared-update-v2__description-wrapper {
212
+ width: 100% !important;
213
+ max-width: 100% !important;
214
+ margin: 0 auto !important;
215
+ }
216
+
217
+ /* Feature Visibility */
218
+ html.lf-hide-start-post ${LNK_SELECTORS.SHARE_BOX} {
219
+ display: none !important;
220
+ }
221
+
222
+ html.lf-hide-messenger ${LNK_SELECTORS.MESSENGER} {
223
+ display: none !important;
224
+ }
225
+
226
+ html.lf-hide-nav-bar ${LNK_SELECTORS.NAV_BAR} {
227
+ display: none !important;
228
+ }
229
+
230
+ html.lf-hide-nav-bar body.render-mode-BIGPIPE {
231
+ padding-top: 0 !important;
232
+ }
233
+
234
+ html.lf-hide-nav-bar main {
235
+ margin-top: 0 !important;
236
+ padding-top: 0 !important;
237
+ }
238
+
239
+ /* Font Size */
240
+ html.lf-custom-font ${LNK_SELECTORS.POST_TEXT} {
241
+ font-size: var(--lf-font-size) !important;
242
+ line-height: 1.6 !important;
243
+ font-weight: 400 !important;
244
+ color: var(--text-color-primary) !important;
245
+ }
246
+
247
+ html.lf-custom-font .update-components-text .break-words,
248
+ html.lf-custom-font .update-components-text span[dir="ltr"],
249
+ html.lf-custom-font .feed-shared-inline-show-more-text .break-words,
250
+ html.lf-custom-font .feed-shared-inline-show-more-text span[dir="ltr"] {
251
+ font-size: var(--lf-font-size) !important;
252
+ line-height: 1.6 !important;
253
+ }
254
+
255
+ html.lf-custom-font.lf-auto-expand .feed-shared-inline-show-more-text {
256
+ width: 100% !important;
257
+ max-width: 100% !important;
258
+ padding-right: 24px !important;
259
+ box-sizing: border-box !important;
260
+ }
261
+
262
+ html.lf-custom-font.lf-auto-expand .feed-shared-update-v2__description-wrapper {
263
+ padding-right: 24px !important;
264
+ }
265
+ `;
266
+ document.head.appendChild(style);
267
+ }
268
+
269
+ async function applyStyles(settings: Settings, licenseFeatures: FeatureFlags) {
270
+ injectBaseStyles();
271
+
272
+ const root = document.documentElement;
273
+
274
+ // Toggle global visibility
275
+ root.classList.toggle('lf-global-enabled', settings.globalEnabled);
276
+
277
+ if (!settings.globalEnabled) {
278
+ root.className = root.className.split(' ').filter(c => !c.startsWith('lf-')).join(' ');
279
+ return;
280
+ }
281
+
282
+ // Toggle features
283
+ const hideSidebars = settings.hideSidebars ?? (settings as Settings & { removeSidebars?: boolean }).removeSidebars ?? true;
284
+ root.classList.toggle('lf-hide-sidebars', hideSidebars);
285
+ root.classList.toggle('lf-hide-promoted', settings.hidePromoted ?? false);
286
+ root.classList.toggle('lf-hide-start-post', settings.hideStartPost);
287
+ root.classList.toggle('lf-hide-messenger', settings.hideMessenger);
288
+ root.classList.toggle('lf-hide-nav-bar', settings.hideNavBar);
289
+
290
+ // Wide Feed (Premium)
291
+ const isWideEnabled = licenseFeatures.wideFeed && settings.feedWidth > 600;
292
+ root.classList.toggle('lf-wide-feed', isWideEnabled);
293
+ if (isWideEnabled) {
294
+ root.style.setProperty('--lf-feed-width', `${settings.feedWidth}px`);
295
+ root.style.setProperty('--lf-feed-gap', `${settings.feedSpacing ?? 24}px`);
296
+ }
297
+
298
+ // Font Size (Premium)
299
+ const isFontEnabled = licenseFeatures.fontSize && settings.fontSize !== 14;
300
+ root.classList.toggle('lf-custom-font', isFontEnabled);
301
+ if (isFontEnabled) {
302
+ root.style.setProperty('--lf-font-size', `${settings.fontSize}px`);
303
+ }
304
+
305
+ // Auto-expand (Premium) – used for conditional layout tweaks
306
+ const isAutoExpandEnabled = licenseFeatures.autoExpandPosts && settings.autoExpandPosts;
307
+ root.classList.toggle('lf-auto-expand', isAutoExpandEnabled);
308
+ }
309
+
310
+ async function applyLicenseFeatures(settings: Settings, features: FeatureFlags) {
311
+ currentLicenseFeatures = features;
312
+ await applyStyles(settings, features);
313
+
314
+ const hasPremiumAccess = features.autoExpandPosts || features.fontSize || features.wideFeed;
315
+ const hidePromotedEnabled = settings.hidePromoted ?? false;
316
+ const hideRemovedFeedCardsEnabled = (settings.hideRemovedFeedCards ?? true) && hasPremiumAccess;
317
+ shouldHidePromotedPosts = hidePromotedEnabled;
318
+ shouldHideDismissedFeedCards = hideRemovedFeedCardsEnabled;
319
+
320
+ if (settings.globalEnabled && (hidePromotedEnabled || hideRemovedFeedCardsEnabled)) {
321
+ observeDismissedPosts();
322
+ } else {
323
+ stopDismissedPostObserver();
324
+ }
325
+
326
+ if (features.autoExpandPosts && settings.globalEnabled && settings.autoExpandPosts) {
327
+ observeNewPosts();
328
+ } else {
329
+ stopAutoExpandObserving();
330
+ }
331
+ }
332
+
333
+ let expansionObserver: IntersectionObserver | null = null;
334
+ let mutationObserver: MutationObserver | null = null;
335
+ let dismissedPostObserver: MutationObserver | null = null;
336
+ let dismissedClickHandler: ((event: MouseEvent) => void) | null = null;
337
+ let dismissedScanTimeout: ReturnType<typeof setTimeout> | null = null;
338
+ let shouldHideDismissedFeedCards = false;
339
+ let shouldHidePromotedPosts = false;
340
+
341
+ /**
342
+ * Clicks a "See More" button efficiently
343
+ */
344
+ function expandButton(button: HTMLElement) {
345
+ const text = button.textContent?.toLowerCase() || '';
346
+ // Check for English "more" or French "plus", or just click if it's the right class
347
+ if (text.includes('more') || text.includes('plus') || button.matches(LNK_SELECTORS.SEE_MORE_BTN)) {
348
+ button.click();
349
+ button.blur();
350
+ const parentContainer = button.closest('.feed-shared-inline-show-more-text') as HTMLElement;
351
+ if (parentContainer) {
352
+ parentContainer.blur();
353
+ }
354
+ }
355
+ }
356
+
357
+ function normalizeLinkedInText(value: string | null | undefined) {
358
+ if (!value) return '';
359
+ return value
360
+ .toLowerCase()
361
+ .normalize('NFD')
362
+ .replace(/[\u0300-\u036f]/g, '')
363
+ .replace(/\s+/g, ' ')
364
+ .trim();
365
+ }
366
+
367
+ function hasMarker(text: string, markers: string[]) {
368
+ return markers.some((marker) => text.includes(marker));
369
+ }
370
+
371
+ function isUndoControl(element: HTMLElement) {
372
+ const normalized = normalizeLinkedInText(`${element.textContent ?? ''} ${element.getAttribute('aria-label') ?? ''}`);
373
+ return normalized.length > 0 && hasMarker(normalized, UNDO_MARKERS);
374
+ }
375
+
376
+ function isDismissedFeedCard(element: HTMLElement) {
377
+ const normalized = normalizeLinkedInText(element.innerText || element.textContent);
378
+ if (!normalized) return false;
379
+ const hasRemovedText = hasMarker(normalized, REMOVED_FROM_FEED_MARKERS);
380
+ const hasUndoText = hasMarker(normalized, UNDO_MARKERS);
381
+ const hasFeedbackText = hasMarker(normalized, FEEDBACK_MARKERS);
382
+ return hasRemovedText && (hasUndoText || hasFeedbackText);
383
+ }
384
+
385
+ function isPromotedFeedCard(element: HTMLElement) {
386
+ const promotedLabels = element.querySelectorAll(PROMOTED_LABEL_SELECTORS.join(','));
387
+ for (const label of promotedLabels) {
388
+ if (!(label instanceof HTMLElement)) continue;
389
+ const normalized = normalizeLinkedInText(label.innerText || label.textContent);
390
+ if (normalized && hasMarker(normalized, PROMOTED_MARKERS)) {
391
+ return true;
392
+ }
393
+ }
394
+
395
+ const actorArea = element.querySelector('header, .update-components-actor, .feed-shared-actor');
396
+ if (actorArea instanceof HTMLElement) {
397
+ const normalized = normalizeLinkedInText(actorArea.innerText || actorArea.textContent);
398
+ if (normalized && hasMarker(normalized, PROMOTED_MARKERS)) {
399
+ return true;
400
+ }
401
+ }
402
+
403
+ return false;
404
+ }
405
+
406
+ function getTopLevelFeedItem(element: HTMLElement) {
407
+ const feedRoot = element.closest(LNK_SELECTORS.SCROLL_CONTENT) as HTMLElement | null;
408
+ if (!feedRoot) return element;
409
+
410
+ let current: HTMLElement | null = element;
411
+ while (current?.parentElement && current.parentElement !== feedRoot) {
412
+ current = current.parentElement;
413
+ }
414
+
415
+ return current?.parentElement === feedRoot ? current : element;
416
+ }
417
+
418
+ function findDismissedFeedCard(start: HTMLElement) {
419
+ const nearestFeedItem = start.closest(LNK_SELECTORS.FEED_ITEM) as HTMLElement | null;
420
+ if (nearestFeedItem && isDismissedFeedCard(nearestFeedItem)) {
421
+ return nearestFeedItem;
422
+ }
423
+
424
+ let current: HTMLElement | null = start;
425
+ for (let depth = 0; current && depth < 8; depth += 1) {
426
+ if (isDismissedFeedCard(current)) {
427
+ return current;
428
+ }
429
+ current = current.parentElement;
430
+ }
431
+
432
+ return null;
433
+ }
434
+
435
+ function removeFeedItemFromFeed(card: HTMLElement) {
436
+ if (card.dataset.lfDismissedCardHidden === '1') return;
437
+ const topLevelFeedItem = getTopLevelFeedItem(card);
438
+ const previousSibling = topLevelFeedItem.previousElementSibling as HTMLElement | null;
439
+ if (previousSibling?.matches(LNK_SELECTORS.FEED_SKIP_LINK)) {
440
+ previousSibling.remove();
441
+ }
442
+ topLevelFeedItem.dataset.lfDismissedCardHidden = '1';
443
+ topLevelFeedItem.remove();
444
+ }
445
+
446
+ function removeOrphanFeedSkipLinks(root: ParentNode) {
447
+ const feedRoot =
448
+ root instanceof HTMLElement && root.matches(LNK_SELECTORS.SCROLL_CONTENT)
449
+ ? root
450
+ : root instanceof Document
451
+ ? root.querySelector(LNK_SELECTORS.SCROLL_CONTENT)
452
+ : root instanceof HTMLElement
453
+ ? root.closest(LNK_SELECTORS.SCROLL_CONTENT) || root.querySelector(LNK_SELECTORS.SCROLL_CONTENT)
454
+ : null;
455
+
456
+ if (!(feedRoot instanceof HTMLElement)) return;
457
+
458
+ const children = Array.from(feedRoot.children);
459
+ children.forEach((child) => {
460
+ if (!(child instanceof HTMLElement) || !child.matches(LNK_SELECTORS.FEED_SKIP_LINK)) return;
461
+
462
+ const next = child.nextElementSibling as HTMLElement | null;
463
+ const isNextFeedItem = Boolean(next?.hasAttribute('data-finite-scroll-hotkey-item'));
464
+ if (!isNextFeedItem) {
465
+ child.remove();
466
+ }
467
+ });
468
+ }
469
+
470
+ function scheduleDismissedCardScan(delayMs = 180) {
471
+ if (dismissedScanTimeout) {
472
+ clearTimeout(dismissedScanTimeout);
473
+ }
474
+ dismissedScanTimeout = setTimeout(() => {
475
+ dismissedScanTimeout = null;
476
+ removeDismissedFeedCardsFromRoot(document);
477
+ }, delayMs);
478
+ }
479
+
480
+ function removeDismissedFeedCardsFromRoot(root: ParentNode) {
481
+ const actionButtons = new Set<HTMLElement>();
482
+ const feedItems = new Set<HTMLElement>();
483
+
484
+ if (root instanceof HTMLElement && root.matches(LNK_SELECTORS.ACTION_BUTTON)) {
485
+ actionButtons.add(root);
486
+ }
487
+
488
+ if ('querySelectorAll' in root) {
489
+ root.querySelectorAll(LNK_SELECTORS.ACTION_BUTTON).forEach((el) => {
490
+ if (el instanceof HTMLElement) {
491
+ actionButtons.add(el);
492
+ }
493
+ });
494
+ }
495
+
496
+ if (root instanceof HTMLElement && root.matches(LNK_SELECTORS.FEED_ITEM)) {
497
+ feedItems.add(root);
498
+ }
499
+
500
+ if ('querySelectorAll' in root) {
501
+ root.querySelectorAll(LNK_SELECTORS.FEED_ITEM).forEach((el) => {
502
+ if (el instanceof HTMLElement) {
503
+ feedItems.add(el);
504
+ }
505
+ });
506
+ }
507
+
508
+ actionButtons.forEach((button) => {
509
+ if (!shouldHideDismissedFeedCards) return;
510
+ if (!isUndoControl(button)) return;
511
+ const card = findDismissedFeedCard(button);
512
+ if (card) {
513
+ removeFeedItemFromFeed(card);
514
+ }
515
+ });
516
+
517
+ feedItems.forEach((item) => {
518
+ if (shouldHideDismissedFeedCards && isDismissedFeedCard(item)) {
519
+ removeFeedItemFromFeed(item);
520
+ return;
521
+ }
522
+ if (shouldHidePromotedPosts && isPromotedFeedCard(item)) {
523
+ removeFeedItemFromFeed(item);
524
+ }
525
+ });
526
+
527
+ removeOrphanFeedSkipLinks(root);
528
+ }
529
+
530
+ function observeDismissedPosts() {
531
+ if (dismissedPostObserver || !document.body) return;
532
+
533
+ removeDismissedFeedCardsFromRoot(document);
534
+
535
+ dismissedPostObserver = new MutationObserver((mutations) => {
536
+ mutations.forEach((mutation) => {
537
+ if (mutation.type === 'characterData') {
538
+ const target = mutation.target.parentElement;
539
+ if (target) {
540
+ removeDismissedFeedCardsFromRoot(target);
541
+ }
542
+ }
543
+
544
+ mutation.addedNodes.forEach((node) => {
545
+ if (node instanceof HTMLElement) {
546
+ removeDismissedFeedCardsFromRoot(node);
547
+ }
548
+ });
549
+ });
550
+ });
551
+
552
+ dismissedPostObserver.observe(document.body, {
553
+ childList: true,
554
+ characterData: true,
555
+ subtree: true,
556
+ });
557
+
558
+ if (!dismissedClickHandler) {
559
+ dismissedClickHandler = (event: MouseEvent) => {
560
+ const target = event.target as HTMLElement | null;
561
+ if (!target) return;
562
+ if (!target.closest(LNK_SELECTORS.ACTION_BUTTON)) return;
563
+ scheduleDismissedCardScan();
564
+ };
565
+ document.addEventListener('click', dismissedClickHandler, true);
566
+ }
567
+ }
568
+
569
+ function stopDismissedPostObserver() {
570
+ if (!dismissedPostObserver) return;
571
+ dismissedPostObserver.disconnect();
572
+ dismissedPostObserver = null;
573
+
574
+ if (dismissedClickHandler) {
575
+ document.removeEventListener('click', dismissedClickHandler, true);
576
+ dismissedClickHandler = null;
577
+ }
578
+
579
+ if (dismissedScanTimeout) {
580
+ clearTimeout(dismissedScanTimeout);
581
+ dismissedScanTimeout = null;
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Stops auto-expand observers
587
+ */
588
+ function stopAutoExpandObserving() {
589
+ if (expansionObserver) {
590
+ expansionObserver.disconnect();
591
+ expansionObserver = null;
592
+ }
593
+ if (mutationObserver) {
594
+ mutationObserver.disconnect();
595
+ mutationObserver = null;
596
+ }
597
+ }
598
+
599
+ function observeNewPosts() {
600
+ if (mutationObserver) return; // Already observing
601
+
602
+ initExpansionObserver();
603
+
604
+ mutationObserver = new MutationObserver((mutations) => {
605
+ for (const mutation of mutations) {
606
+ if (mutation.addedNodes.length > 0) {
607
+ mutation.addedNodes.forEach(node => {
608
+ if (node instanceof HTMLElement) {
609
+ const buttons = node.querySelectorAll(LNK_SELECTORS.SEE_MORE_BTN);
610
+ buttons.forEach(btn => {
611
+ expansionObserver?.observe(btn);
612
+ });
613
+
614
+ // Also check if the node itself is the button (rare but possible)
615
+ if (node.classList.contains(LNK_SELECTORS.SEE_MORE_BTN.substring(1))) {
616
+ expansionObserver?.observe(node);
617
+ }
618
+ }
619
+ });
620
+ }
621
+ }
622
+ });
623
+
624
+ const feedContainer = document.querySelector(LNK_SELECTORS.MAIN_GRID);
625
+ if (feedContainer) {
626
+ mutationObserver.observe(feedContainer, {
627
+ childList: true,
628
+ subtree: true,
629
+ });
630
+ }
631
+ }
632
+
633
+ /**
634
+ * Initializes IntersectionObserver to expand posts as they enter viewport
635
+ */
636
+ function initExpansionObserver() {
637
+ if (expansionObserver) expansionObserver.disconnect();
638
+
639
+ expansionObserver = new IntersectionObserver((entries) => {
640
+ entries.forEach(entry => {
641
+ if (entry.isIntersecting) {
642
+ const button = entry.target as HTMLElement;
643
+ expandButton(button);
644
+ // Once clicked, we don't need to observe this specific button anymore
645
+ expansionObserver?.unobserve(button);
646
+ }
647
+ });
648
+ }, {
649
+ rootMargin: '200px', // Expand slightly before it becomes visible
650
+ threshold: 0
651
+ });
652
+
653
+ // Start by observing existing buttons
654
+ const buttons = document.querySelectorAll(LNK_SELECTORS.SEE_MORE_BTN);
655
+ buttons.forEach(btn => expansionObserver?.observe(btn));
656
+ }