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.