PageSourceSearch

https://www.mitti.se/app/ndtagsub_customized_v4.js

js mitti.se collected 2026-09-24 08:58:34 UTC 11,186 bytes, 375 lines download raw bytes

1const ndTagSub = (function () {
2
3	const SUBS_STORAGE_KEY = 'agTagSub';
4	const SUBS_CHECKSUM_STORAGE_KEY = 'agTagSubChecksum';
5	const STORED_USERID_KEY = "AG_STORED_USER_ID";
6	const TAG_CONTAINER_NODE_SELECTOR = '.agTextTagContainer';
7	const TAG_CONTAINER_NODE_ACTION_ATTR = 'data-actionurl';
8	const TAG_NODE_SELECTOR = '.agTextTag';
9	const TAG_NODE_BUTTON_SELECTOR = '.agTextTagButton';
10	const SITE_CLOUD_CONTAINER_NODE_SELECTOR = '.agSiteTagContainer';
11
12	let userSpecificSubs = undefined;
13
14	async function handleLocalSubsMigration() {
15		const localSubs = JSON.parse(localStorage.getItem(SUBS_STORAGE_KEY));
16		if (!localSubs) {
17			return;
18		}
19
20		const ndTagSubMigrationPrompt = 'ndTagSubMigrationPrompt' in window ? window.ndTagSubMigrationPrompt : undefined;
21
22		if (!ndTagSubMigrationPrompt) {
23			// Just empty local subsriptions and return
24			localStorage.removeItem(SUBS_STORAGE_KEY);
25			return;
26		}
27
28		const localExtraSubs = localSubs.filter(localSub => userSpecificSubs.findIndex(userSub => userSub.id === localSub.id) == -1);
29		if (localExtraSubs.length) {
30			const migrate = await ndTagSubMigrationPrompt();
31			if (migrate) {
32				userSpecificSubs = userSpecificSubs.concat(localExtraSubs);
33				storeSubscriptions(userSpecificSubs);
34			}
35		}
36		localStorage.removeItem(SUBS_STORAGE_KEY);
37	}
38	
39	const decodeJWTPayload = function(token) {
40		try {
41			// Split the token into its parts (header, payload, signature)
42			const parts = token.split('.');
43			if (parts.length !== 3) {
44				throw new Error('Invalid JWT format');
45			}
46
47			// Decode the payload (second part of the token)
48			const payload = parts[1];
49			const decodedPayload = atob(payload); // Decode Base64Url to a string
50
51			// Parse the JSON string into an object
52			return JSON.parse(decodedPayload);
53		} catch (error) {
54			console.error('Failed to decode JWT payload:', error);
55			return null;
56		}
57	}
58	
59	const debugElement = document.querySelector('.prenly-debug');
60	if (debugElement) {
61		debugElement.innerText += `Debug:\n`;
62		if (prenlyAppSDK) {
63			debugElement.innerText += `JSBridge version: ${prenlyAppSDK.supportedVersion}\n`;
64		} else {
65			debugElement.innerText += `JSBridge not initialized\n`;
66		}
67	
68		debugElement.style.display='block'
69	}
70	
71	const debug = function(line) {
72		
73		if (!debugElement) {
74			return;
75		}
76		
77		debugElement.innerText += line + '\n';
78	}
79
80	// Allow only one promise at a time
81	let _cached = null;
82	function loadUserSpecificSubsWithSideEffects() {
83		if (_cached !== null) {
84			return _cached;
85		}
86
87		_cached = (async () => {
88			let url = '/neodirect/getusersubs';
89
90			const storedChecksum = localStorage.getItem(SUBS_CHECKSUM_STORAGE_KEY);
91
92			url += '?checksum=' + (storedChecksum ? storedChecksum : '0');
93			try {
94				const params = { method: 'GET' };
95				
96				if (prenlyAppSDK && prenlyAppSDK.api) {
97						try {
98						  const jwt = await prenlyAppSDK.api.getUserJwt();
99							if (jwt) {
100								// debugElement.innerText += 'jwt: ' + jwt.jwt + '\n';
101								const userId = decodeJWTPayload(jwt.jwt).sub;
102								debug('user id from jwt: ' + userId);
103								// window.prenlyUserId = userId;
104								params.headers = {
105									'X-Prenly-Logged-In-User-Id': userId
106								}
107														  
108							} else {
109								debug('no JWT\n');
110							}
111						}
112						 catch (error) {
113							debug('error fetching JWT: ' + error);
114						  // What to do here? Some message to the user? Or do we come here if user is not logged in?
115						}
116				} else {
117					const userId = localStorage.getItem(STORED_USERID_KEY);
118					if (userId) {
119						params.headers = {
120							'X-Prenly-Logged-In-User-Id': userId
121						}
122					}
123				}
124
125				const response = await fetch(url, params);
126				if (!response.ok) {
127					throw new Error(`Response status: ${response.status}`);
128				}
129				const respJ = await response.json();
130				userSpecificSubs = respJ.subs;
131				
132				debug('User subs: ' + JSON.stringify(userSpecificSubs));
133				localStorage.setItem(SUBS_CHECKSUM_STORAGE_KEY, respJ.checksum);
134				await handleLocalSubsMigration();
135			} catch (error) {
136				userSpecificSubs = null;
137				debug('Error fetching user  subs');
138			}
139			_cached = null;
140		})();
141
142		return _cached;
143	}
144
145	async function saveUserSpecificSubs() {
146		const url = '/neodirect/setusersubs';
147		try {
148				const params = { method: 'POST', headers: { 'Content-Type': 'application/json' }};
149				
150				if (prenlyAppSDK && prenlyAppSDK.api) {
151						try {
152						  const jwt = await prenlyAppSDK.api.getUserJwt();
153							if (jwt) {
154								// debugElement.innerText += 'jwt: ' + jwt.jwt + '\n';
155								const userId = decodeJWTPayload(jwt.jwt).sub;
156								// debugElement.innerText += 'user id from jwt: ' + userId + '\n';
157								// window.prenlyUserId = userId;
158								params.headers = {
159									...params.headers,
160									'X-Prenly-Logged-In-User-Id': userId
161								}
162														  
163							} else {
164								// debugElement.innerText += 'no JWT\n';
165							}
166						}
167						 catch (error) {
168							// debugElement.innerText += 'error fetching JWT: ' + error + '\n';
169						  // What to do here? Some message to the user? Or do we come here if user is not logged in?
170						}
171				} else {
172					const userId = localStorage.getItem(STORED_USERID_KEY);
173					if (userId) {
174						params.headers = {
175							...params.headers,
176							'X-Prenly-Logged-In-User-Id': userId
177						}
178					}
179				}
180			const response = await fetch(url, { ...params,
181				body: JSON.stringify({ subs: userSpecificSubs })
182			});
183			if (!response.ok) {
184				throw new Error(`Response status: ${response.status}`);
185			}
186			
187			localStorage.setItem(SUBS_CHECKSUM_STORAGE_KEY, null);
188		} catch (error) {
189		}
190	}
191
192	async function getSubscriptions() {
193		if (userSpecificSubs === undefined) {
194			await loadUserSpecificSubsWithSideEffects();
195		}
196
197		if (userSpecificSubs) {
198			return userSpecificSubs;
199		} else {
200			const subs = localStorage.getItem(SUBS_STORAGE_KEY);
201			return subs ? JSON.parse(subs) : [];
202		}
203	}
204
205	function storeSubscriptions(subs) {
206		if (userSpecificSubs) {
207			userSpecificSubs = subs;
208			saveUserSpecificSubs();
209		} else {
210			localStorage.setItem(SUBS_STORAGE_KEY, JSON.stringify(subs));
211		}
212	}
213
214	function resolveNodeValues(tagContainerNode) {
215		const tagId = parseInt(tagContainerNode.dataset.tagid);
216		const actionUrl = tagContainerNode.dataset.actionurl;
217		const tagNode = tagContainerNode.querySelector(TAG_NODE_SELECTOR);
218		const value = tagNode.textContent;
219
220		return [tagId, value, actionUrl];
221	}
222
223	async function isSubscribed(id) {
224		let subs = await getSubscriptions();
225		return subs.some(tagObj => tagObj.id === id);
226	}
227
228	function postSubscription(url, actionParam) {
229		const actionParamKey = 'action=';
230		const urlWithQuery = url + (url.indexOf('?') > -1 ? '&' + actionParamKey + actionParam : '?' + actionParamKey + actionParam);
231		fetch(urlWithQuery, {
232			method: 'POST',
233			body: JSON.stringify({
234				'dummy': 'dummy'
235			})
236		});
237	}
238
239	async function toggleTagSubscription(id, value, actionUrl) {
240		let subs = await getSubscriptions();
241		const isSubscribed = subs.some(tagObj => tagObj.id === id);
242
243		let actionParam = '';
244
245		if (isSubscribed) {
246			subs = subs.filter(tagObj => tagObj.id !== id);
247			if (actionUrl) {
248				actionParam = 'unsubscribe';
249			}
250		} else {
251			subs.push({ id: id, value: value });
252			if (actionUrl) {
253				actionParam = 'subscribe';
254			}
255		}
256
257		if (actionParam) {
258			postSubscription(actionUrl, actionParam);
259		}
260
261		storeSubscriptions(subs);
262		updateTagStates();
263	}
264
265	function toggleTagNodeSubscription(tagContainerNode) {
266		const [id, value, actionUrl] = resolveNodeValues(tagContainerNode);
267
268		toggleTagSubscription(id, value, actionUrl);
269	}
270
271	async function updateTagStates() {
272
273		const subs = await getSubscriptions();
274
275		const tagContainerNodes = document.querySelectorAll(TAG_CONTAINER_NODE_SELECTOR);
276
277		tagContainerNodes.forEach(containerNode => {
278			const buttonNode = containerNode.querySelector(TAG_NODE_BUTTON_SELECTOR);
279			if (!buttonNode) {
280				return;
281			}
282
283			const tagId = parseInt(containerNode.dataset.tagid);		
284			const isSubscribed = subs.some(tagObj => tagObj.id === tagId);
285			const placeHolderSpan = buttonNode.firstElementChild;
286			placeHolderSpan.className = isSubscribed ? 'subscribed' : '';
287			placeHolderSpan.textContent = isSubscribed ? '-' : '+';
288			buttonNode.onclick = () => { toggleTagNodeSubscription(containerNode); };
289		});
290	}
291
292	function formSiteTagCloud(node, tagJson) {
293		// This structure is ridiculous but consistent with article specific tag clouds
294		// TODO: Change later on to functional control with search functionality etc.
295
296		const agTextTagCloud = document.createElement('div');
297		agTextTagCloud.classList.add('agTextTagCloud');
298
299		tagJson.forEach(tag => {
300			const agTextTagContainer = document.createElement('div');
301			agTextTagContainer.classList.add('agTextTagContainer');
302			agTextTagContainer.setAttribute('data-tagid', tag.id);
303			
304			const agTextTag = document.createElement('span');
305			agTextTag.classList.add('agTextTag');
306			agTextTag.textContent = tag.name;
307
308			if (tag.url) {
309				const tagLink = document.createElement('a');
310				tagLink.href = tag.url;
311				tagLink.appendChild(agTextTag);
312				agTextTagContainer.appendChild(tagLink);
313			} else {
314				agTextTagContainer.appendChild(agTextTag);
315			}
316
317			if (tag.actionurl) {
318				agTextTagContainer.setAttribute(TAG_CONTAINER_NODE_ACTION_ATTR, tag.actionurl);
319				const agTextTagButton = document.createElement('button');
320				agTextTagButton.classList.add('agTextTagButton');
321				const placeHolder = document.createElement('span');
322				agTextTagButton.appendChild(placeHolder);
323				agTextTagContainer.appendChild(agTextTagButton);
324			}
325
326			agTextTagCloud.appendChild(agTextTagContainer);
327		});
328
329		node.appendChild(agTextTagCloud);
330	}
331
332	function initSiteClouds() {
333		const containers = document.querySelectorAll(SITE_CLOUD_CONTAINER_NODE_SELECTOR);
334
335		containers.forEach(node => {
336			const req = new XMLHttpRequest();
337			req.addEventListener('load', () => {
338				const tagJson = JSON.parse(req.responseText);
339				const tagcloudFactory = 'customTagCloudFactory' in window ? window.customTagCloudFactory : formSiteTagCloud;
340				tagcloudFactory(node, tagJson);
341				updateTagStates();
342			});
343			req.open('GET', node.dataset.url);
344			req.send();
345		});
346	}
347
348	async function getTagSubQueryString() {
349		const subs = await getSubscriptions();
350
351		if (subs.length === 0) {
352			return '';
353		}
354
355		// Normalize querystring
356		subs.sort(function (a, b) { return a.id - b.id; });
357
358		return '&subscribedtags=' + subs.map(tagObj => tagObj.id).join(',');
359	}
360
361	// Update states on initialization
362	updateTagStates();
363
364	// Initialize site tag clouds
365	initSiteClouds();
366
367	return {
368		updateTagStates: updateTagStates,
369		toggleTagSubscription: toggleTagSubscription,
370		isSubscribed: isSubscribed,
371		getSubscriptions: getSubscriptions,
372		getTagSubQueryString: getTagSubQueryString
373	};
374
375})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.