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.
- package/.claude/settings.local.json +9 -0
- package/.output/chrome-mv3/_locales/de/messages.json +214 -0
- package/.output/chrome-mv3/_locales/en/messages.json +214 -0
- package/.output/chrome-mv3/_locales/es/messages.json +214 -0
- package/.output/chrome-mv3/_locales/fr/messages.json +214 -0
- package/.output/chrome-mv3/_locales/hi/messages.json +214 -0
- package/.output/chrome-mv3/_locales/id/messages.json +214 -0
- package/.output/chrome-mv3/_locales/it/messages.json +214 -0
- package/.output/chrome-mv3/_locales/nl/messages.json +214 -0
- package/.output/chrome-mv3/_locales/pl/messages.json +214 -0
- package/.output/chrome-mv3/_locales/pt_BR/messages.json +214 -0
- package/.output/chrome-mv3/_locales/pt_PT/messages.json +214 -0
- package/.output/chrome-mv3/_locales/tr/messages.json +214 -0
- package/.output/chrome-mv3/assets/popup-Z_g1HFs5.css +1 -0
- package/.output/chrome-mv3/background.js +42 -0
- package/.output/chrome-mv3/chunks/popup-IxiPwS1E.js +42 -0
- package/.output/chrome-mv3/content-scripts/content.js +179 -0
- package/.output/chrome-mv3/icon-128.png +0 -0
- package/.output/chrome-mv3/icon-16.png +0 -0
- package/.output/chrome-mv3/icon-48.png +0 -0
- package/.output/chrome-mv3/icon.svg +9 -0
- package/.output/chrome-mv3/manifest.json +1 -0
- package/.output/chrome-mv3/popup.html +247 -0
- package/.wxt/eslint-auto-imports.mjs +56 -0
- package/.wxt/tsconfig.json +28 -0
- package/.wxt/types/globals.d.ts +15 -0
- package/.wxt/types/i18n.d.ts +593 -0
- package/.wxt/types/imports-module.d.ts +20 -0
- package/.wxt/types/imports.d.ts +50 -0
- package/.wxt/types/paths.d.ts +32 -0
- package/.wxt/wxt.d.ts +7 -0
- package/entrypoints/background.ts +112 -0
- package/entrypoints/content.ts +656 -0
- package/entrypoints/popup/main.ts +452 -0
- package/entrypoints/popup/modules/auth-modal.ts +219 -0
- package/entrypoints/popup/modules/settings.ts +78 -0
- package/entrypoints/popup/modules/ui-state.ts +95 -0
- package/entrypoints/popup/style.css +844 -0
- package/entrypoints/popup.html +261 -0
- package/lib/constants.ts +9 -0
- package/lib/device-meta.ts +173 -0
- package/lib/i18n.ts +201 -0
- package/lib/license.ts +470 -0
- package/lib/selectors.ts +24 -0
- package/lib/storage.ts +95 -0
- package/lib/telemetry.ts +94 -0
- package/package.json +30 -0
- package/public/_locales/de/messages.json +214 -0
- package/public/_locales/en/messages.json +214 -0
- package/public/_locales/es/messages.json +214 -0
- package/public/_locales/fr/messages.json +214 -0
- package/public/_locales/hi/messages.json +214 -0
- package/public/_locales/id/messages.json +214 -0
- package/public/_locales/it/messages.json +214 -0
- package/public/_locales/nl/messages.json +214 -0
- package/public/_locales/pl/messages.json +214 -0
- package/public/_locales/pt_BR/messages.json +214 -0
- package/public/_locales/pt_PT/messages.json +214 -0
- package/public/_locales/tr/messages.json +214 -0
- package/public/icon-128.png +0 -0
- package/public/icon-16.png +0 -0
- package/public/icon-48.png +0 -0
- package/public/icon.svg +9 -0
- package/tsconfig.json +3 -0
- 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
|
+
}
|