1let lastAuthorArticles = (function () { 2 const is_dev = window.location.href.indexOf('dev.rtvslo.si') > -1; 3 const LAST_ARTICLE_KEY = 'notification:last_article_id'; 4 const LAST_ARTICLES_DATA = 'notification:last_article_data'; 5 const LAST_ARTICLE_RESET = 'notification:last_article_reset'; 6 const CLIENT_ID = '82013fb3a531d5414f478747c1aca622'; 7 const SUBSCRIPTIONS_API_URL = is_dev 8 ? 'https://api.dev.rtvslo.si/news/getSubscriptions?client_id=' + CLIENT_ID + '&kindId=0&pageNumber=0&pageSize=1000' 9 : 'https://api.rtvslo.si/news/getSubscriptions?client_id=' + CLIENT_ID + '&kindId=0&pageNumber=0&pageSize=1000'; 10 // Cache for subscribed author IDs 11 let subscribedAuthorIds = null; 12 13 /** 14 * Fetches subscribed authors from the API 15 * @returns {Promise<string[]>} Array of subscribed author IDs 16 */ 17 async function fetchSubscribedAuthors() { 18 try { 19 const response = await fetch(SUBSCRIPTIONS_API_URL, { 20 credentials: 'include', 21 method: 'GET' 22 }); 23 if (!response.ok) { 24 throw new Error(`HTTP error! status: ${response.status}`); 25 } 26 const data = await response.json(); 27 28 // Extract author_ids from the nested items array 29 const items = data.response?.items || []; 30 const authorIds = []; 31 32 items.forEach(itemGroup => { 33 if (Array.isArray(itemGroup)) { 34 itemGroup.forEach(author => { 35 if (author.author_id) { 36 authorIds.push(author.author_id.toString()); 37 } 38 }); 39 } 40 }); 41 42 subscribedAuthorIds = authorIds; 43 //console.log("Subscribed authors fetched:", authorIds); 44 return authorIds; 45 } catch (error) { 46 console.error('Error fetching subscribed authors:', error); 47 return []; 48 } 49 } 50 51 /** 52 * Filters articles to only include those from subscribed authors 53 */ 54 function filterArticlesBySubscribedAuthors(articles, authorIds) { 55 if (!authorIds || authorIds.length === 0) { 56 return []; 57 } 58 59 return articles.filter(article => { 60 const articleAuthorId = article.author?.id?.toString(); 61 return articleAuthorId && authorIds.includes(articleAuthorId); 62 }); 63 } 64 65 /** 66 * Checks if there are new articles from subscribed authors 67 * @returns {Promise<{hasNew: boolean, newArticles: Array}>} 68 */ 69 async function checkForNewArticles() { 70 const articles = window._globalLastAuthorArticles; 71 72 // 1. Basic validation 73 if (!articles || !Array.isArray(articles) || articles.length === 0) { 74 console.log("No global articles found."); 75 return { hasNew: false, newArticles: [] }; 76 } 77 78 // 2. OPTIMIZATION: Get stored ID and compare immediately 79 // before doing expensive API calls or filtering. 80 const storedId = localStorage.getItem(LAST_ARTICLE_KEY); 81 const lastSeenId = storedId ? parseInt(storedId, 10) : 0; 82 83 // Assumes articles are sorted descending (newest first). 84 // If the newest global article is not newer than what we have stored, stop here. 85 const globalNewestId = parseInt(articles[0].id, 10); 86 87 if (globalNewestId <= lastSeenId) { 88 console.log("No new content at all (subscribed or otherwise)"); 89 // No new content at all (subscribed or otherwise) 90 91 let hasClientCleared = localStorage.getItem(LAST_ARTICLE_RESET) === 'true'; 92 93 if( hasClientCleared ) { 94 // If the client has cleared, reset the last seen ID to the global newest 95 localStorage.setItem(LAST_ARTICLE_KEY, globalNewestId.toString()); 96 return { hasNew: false, newArticles: [] }; 97 } 98 return { hasNew: false, newArticles: JSON.parse(localStorage.getItem(LAST_ARTICLES_DATA)) || [] }; 99 } 100 101 localStorage.removeItem(LAST_ARTICLE_RESET); // Clear reset flag if we have new content 102 103 // 3. Fetch subscribed authors if not cached 104 if (subscribedAuthorIds === null) {
105 await fetchSubscribedAuthors(); 106 } 107 108 if (!subscribedAuthorIds || subscribedAuthorIds.length === 0) { 109 console.log("No subscribed authors found."); 110 return { hasNew: false, newArticles: [] }; 111 } 112 113 // 4. Filter articles to only those from subscribed authors 114 const subscribedArticles = filterArticlesBySubscribedAuthors(articles, subscribedAuthorIds); 115 116 if (subscribedArticles.length === 0) { 117 // We have new global articles, but none are from subscribed authors. 118 // We should update the ID to the global newest to prevent re-checking this batch? 119 // Depending on logic, usually we leave the ID alone until we find a match, 120 // OR update it to skip checks. 121 // For now, based on your request, we only update if we find a subscribed article. 122 return { hasNew: false, newArticles: [] }; 123 } 124 125 // 5. Find new articles (those with ID greater than last seen) 126 const newArticles = subscribedArticles.filter(article => { 127 const articleId = parseInt(article.id, 10); 128 return articleId > lastSeenId; 129 }); 130 131 if (newArticles.length > 0) { 132 const newestArticle = newArticles[0]; 133 const newestId = parseInt(newestArticle.id, 10); 134 135 console.log(`New content available. Newest: ${newestId}, Last Seen: ${lastSeenId}, Count: ${newArticles.length}`); 136 137 // 6. AUTO-SAVE: Update storage immediately 138 localStorage.setItem(LAST_ARTICLE_KEY, newestId.toString()); 139 localStorage.setItem(LAST_ARTICLES_DATA, JSON.stringify(newArticles)); 140 141 return { hasNew: true, newArticles: newArticles }; 142 } 143 144 return { hasNew: false, newArticles: [] }; 145 } 146 147 /** 148 * Clears the cached subscribed authors 149 */ 150 function clearSubscriptionCache() { 151 subscribedAuthorIds = null; 152 } 153 154 /** 155 * Gets the current cached subscribed author IDs 156 */ 157 function getSubscribedAuthorIds() { 158 return subscribedAuthorIds; 159 } 160 161 function clientHasCleared() { 162 localStorage.setItem(LAST_ARTICLE_RESET, 'true'); 163 } 164 165 return { 166 checkForNewArticles: checkForNewArticles, 167 fetchSubscribedAuthors: fetchSubscribedAuthors, 168 clearSubscriptionCache: clearSubscriptionCache, 169 getSubscribedAuthorIds: getSubscribedAuthorIds, 170 clientHasCleared: clientHasCleared 171 // Removed manual updateLastSeen as it is now handled automatically 172 }; 173 174})(); 175let notificationsModule = (function(cookieHelper) { 176 177 let is_dev = window.location.href.indexOf('dev.rtvslo.si') > -1; 178 const CLIENT_ID = '?client_id=82013fb3a531d5414f478747c1aca622'; 179 180 async function fetchWithCache(url, options) { 181 return new Promise(function(resolve, reject) { 182 // Check if supports Cache API 183 if ('caches' in window) { 184 185 caches.open('notifications-cache').then((cache) => { 186 cache.match(url).then(response => { 187 // If cache exists use it from cache 188 if (response) { 189 //console.log('Iz pomnilnika:', response); 190 resolve(response); 191 // If cache doesn't exists fetch 192 } else { 193 var session = cookieHelper.getCookie('APISESSION_USER_ID'); 194 if (!session){ 195 //console.log('session', session); 196 return reject('No session'); 197 } 198 199 fetch(url, options).then((response) => { 200 console.log('Pomnilnik prazen. Dodajam:', response); 201 cache.put(url, response.clone()); 202 resolve(response); 203 }, 204 (reason) => resolve(reason)); 205 } 206 }); 207 }); 208 } else { 209 fetch(url, options).then((response) => resolve(response), (reason) => resolve(reason)); 210 } 211 }); 212 } 213 214 async function getNotifications(clb) { 215 return new Promise(function(resolve, reject) { 216 var session = cookieHelper.getCookie('APISESSION_USER_ID'); 217 if (!session) resolve('No session'); 218 219 let url = 'https://api.' + (is_dev ? 'dev.' : '') + 'rtvslo.si/notification' + CLIENT_ID; 220 221 fetchWithCache(url, { 222 credentials: 'include', 223 }) 224 .then((response) => response.json(), (reason) => { console.log('Error fetching: ' + reason); resolve(reason); }) 225 .then((data) => { 226 227 if(data && data.response){ 228 if (clb) clb(data.response); 229 resolve(data.response); 230 } 231 resolve('No data'); 232 }); 233 }); 234 } 235 236 function clearNotifications() { 237 var session = cookieHelper.getCookie('APISESSION_USER_ID'); 238 if(!session) return; 239 240 let url = 'https://api.' + (is_dev ? 'dev.' : '') + 'rtvslo.si/notification/readAll'+ CLIENT_ID; 241 242 fetch(url, { 243 credentials: 'include', 244 method: 'POST' 245 }) 246 .then((response) => response.json(), (reason) => { console.log('Error fetching: ' + reason) }) 247 .then((data) => { 248 //console.log('clearNotifications',data);
249 if ('caches' in window) caches.delete('notifications-cache'); // Delete cache 250 }); 251 252 } 253 254 function deleteAllNotifications() { 255 var session = cookieHelper.getCookie('APISESSION_USER_ID'); 256 if(!session) return; 257 258 let url = 'https://api.' + (is_dev ? 'dev.' : '') + 'rtvslo.si/notification/deleteAll' + CLIENT_ID; 259 260 fetch(url, { 261 credentials: 'include', 262 method: 'DELETE' 263 }) 264 .then((response) => response.json(), (reason) => { console.log('Error fetching: ' + reason) }) 265 .then((data) => { 266 console.log('deleteAllNotifications',data); 267 if ('caches' in window) caches.delete('notifications-cache'); // Delete cache 268 }); 269 270 } 271 272 273 return { 274 getNotifications: getNotifications, 275 clearNotifications: clearNotifications, 276 deleteAllNotifications: deleteAllNotifications 277 } 278 279})(cookieHelper); 280let lastSeenModule = (function (cookieHelper) { 281 282 const last_seen_timestamp = 'last_seen_timestamp'; 283 const last_seen_nr = 'last_seen_nr'; 284 285 function resetLastSeen() { 286 if (!cookieHelper) return; 287 cookieHelper.deleteCookie(last_seen_timestamp); 288 cookieHelper.deleteCookie(last_seen_nr); 289 } 290 291 //Detect changes 292 function checkForNewArticles(UNIX_timestamp) { 293 294 //console.log("checkForNewArticles", UNIX_timestamp); 295 //console.log("_globalLastPublishedItems", window._globalLastPublishedItems); 296 297 try { 298 if (!Array.isArray(window._globalLastPublishedItems)) return -1; 299 } 300 catch (e) { 301 console.log("checkForNewArticles error", e); 302 return -1; 303 } 304 305 let last_seen = cookieHelper.getCookie(last_seen_timestamp); 306 307 //last_seen = window._globalLastPublishedItems[5];//REMOVE THIS IN PRODUCTION 308 //console.debug("last_seen DEBUG MODE REMOVE THIS IN PRODUCTION", last_seen); 309 310 if (!last_seen) { 311 cookieHelper.setCookie(last_seen_timestamp, UNIX_timestamp, 1); 312 return -1; 313 } 314 315 let items = window._globalLastPublishedItems; 316 items.sort(function (a, b) { return b - a; }); 317 let nmr = 20; 318 items.map(function (item, i) { if (nmr === 20 && last_seen > item) nmr = i; }); 319 320 321 if (nmr === 0) return -1; 322 if (nmr > 15) { 323 cookieHelper.deleteCookie(last_seen_timestamp); 324 return -1; 325 } 326 327 cookieHelper.setCookie(last_seen_nr, nmr, 1); 328 if (nmr > 9) return '9+'; 329 330 return nmr; 331 } 332 333 function resetLastSeenPage() { 334 335 if (!cookieHelper) return; 336 let pathname = window.location.pathname; 337 338 339 if (pathname == '/zadnje') { 340 let last_seen = cookieHelper.getCookie(last_seen_nr); 341 last_seen = parseInt(last_seen, 10); 342 try { 343 if (Number.isInteger(last_seen)) { 344 if (last_seen > 15) return; 345 if (last_seen === 0) return; 346 let mainContainer = document.getElementById("main-container"); 347 let articleArchiveItem = mainContainer.getElementsByClassName('article-archive-item'); 348 //console.log("articleArchiveItem", articleArchiveItem); 349 let index = 0; 350 for (let el of articleArchiveItem) { 351 if (index >= last_seen) break; 352 el.classList.add("notification-dot"); 353 index++; 354 } 355 } 356 } 357 catch (e) { 358 console.log("resetLastSeenPage error", e); 359 } 360 361 resetLastSeen(); 362 } 363 } 364 365 366 return { 367 checkForNewArticles: checkForNewArticles, 368 resetLastSeenPage: resetLastSeenPage, 369 resetLastSeen: resetLastSeen 370 } 371 372 373})(cookieHelper); 374let faviconModule = (function() { 375 return { 376 current: 0, 377 result: null, 378 favicon: null, 379 380 init: function() { 381 this.favicon = new Favico({ 382 animation: 'none' 383 }); 384 }, 385 386 add: function() { 387 this.current++; 388 (this.current > 9) ? this.result = '9+' : this.result = this.current; 389 this.favicon.badge(this.result); 390 }, 391 392 reset: function() { 393 this.favicon.reset(); 394 } 395 }; 396})(); 397 398(function (cookieHelper, lastSeenModule, notificationsModule, faviconModule, lastAuthorArticles) { 399 400 var notificationWidget = null; 401 402 function unixTimestamp2AgoConverter(UNIX_timestamp) { 403 var result = ''; 404 var date = new Date(UNIX_timestamp * 1000); 405 //console.log(date); 406 if (timeRelative) result = timeRelative.calc(date, false); 407 408 return result; 409 } 410 411 //2023-11-12 22:08:14 to unix timestamp 412 function date2Timestamp(date) { 413 var timestamp = Date.parse(date); 414 return timestamp / 1000; 415 } 416 417 function clearCache(minutes) { 418 var now = new Date().getTime(); 419 var cacheSetupTime = localStorage.getItem('cacheSetupTime'); 420 if (cacheSetupTime == null) { 421 localStorage.setItem('cacheSetupTime', now) 422 } else { 423 if( now - cacheSetupTime > minutes*60*1000) {
424 if ('caches' in window) caches.delete('notifications-cache'); 425 localStorage.setItem('cacheSetupTime', now); 426 } 427 } 428 } 429 430 // Set global because is called from DOM directly 431 window.setMutedNotificationsCookie = function ( duration ) { 432 if ( duration == 0 ) { 433 docCookies.removeItem('mutedNotifications', '/'); 434 return; 435 } 436 437 var d = new Date(); 438 d.setTime(d.getTime() + ( duration * 1000) ); 439 docCookies.setItem('mutedNotifications', d.toISOString() , d.toUTCString(), '/'); 440 441 setMutedNotificationsState( d.toISOString() ); 442 } 443 444 function setMutedNotificationsState( time ) { 445 if ( notificationWidget ) { 446 notificationWidget.mute(true); 447 } 448 449 document.querySelectorAll('.notifications-muted-duration').forEach( function( el ) { 450 el.innerText = timeRelative.calc( time ); 451 }); 452 } 453 454 455 function Init() { 456 457 if(!document.body.classList.contains('advanced-user')){ 458 //return console.log('skip ...'); 459 } 460 461 notificationWidget = new Notifications({ 462 463 notificationsSeenHook: () => { 464 notificationsModule.clearNotifications(); 465 faviconModule.reset(); 466 if (typeof recordUIEvent == 'function') recordUIEvent('notifications', 'action=open_notifications'); 467 }, 468 469 markAsReadHook: () => { 470 //DEPRECATED 471 if (typeof recordUIEvent == 'function') recordUIEvent('notifications', 'action=mark_read'); 472 }, 473 474 clearAllHook: () => { 475 lastSeenModule.resetLastSeen() 476 notificationsModule.deleteAllNotifications(); 477 lastAuthorArticles.clientHasCleared(); 478 if (typeof recordUIEvent == 'function') recordUIEvent('notifications', 'action=clear_all'); 479 } 480 481 }); 482 483 // If muted notifications cookie exists set muted state and return / exit 484 var mutedNotifications = docCookies.getItem('mutedNotifications'); 485 if (mutedNotifications) { 486 setMutedNotificationsState(mutedNotifications); 487 return; 488 } 489 490 faviconModule.init(); 491 lastSeenModule.resetLastSeenPage(); 492 clearCache(3); // Clear every 2 minutes 493 494 495 notificationsModule.getNotifications() 496 .then((notifications) => { 497 return new Promise ((resolve, reject) => { 498 499 if(!Array.isArray(notifications.items)) { 500 resolve(); 501 return; 502 } 503 504 notifications.items.map(function(item) { 505 if(typeof item.type !== 'string') { 506 resolve(); 507 return; 508 } 509 510 let content = item.content; 511 try { 512 content = JSON.parse(content); 513 } 514 catch(e) { 515 console.log('parse error', e); 516 resolve(); 517 } 518 519 switch (item.type) { 520 case "comment_replay": 521 notificationWidget.insertNotification(item.unread, '<strong>' + content.username + '</strong> je odgovoril na vaš komentar', null, null, '//img.rtvslo.si/_up/avatars/' + content.user_id + '.png', '/komentar/' + content.comment_id, 'comment_reply'); 522 if (item.unread) { 523 notificationWidget.addUnreadCount(); 524 faviconModule.add(); 525 } 526 break; 527 case "recording": 528 //console.log('content', content); 529 let thumb = "https://365.rtvslo.si/icons/android-chrome-192x192.png"; 530 let path = 'https://img.rtvcdn.si/_up/ava/ava_misc/show_logos/'; 531 if(typeof content.thumb === 'string' && content.thumb.length > 0){ 532 thumb = path + content.thumb; 533 } 534 let publish_date = content.publish_date; 535 //check if string 536 if (typeof publish_date !== 'string') { 537 break; 538 } 539 //replace white space with T 540 publish_date = publish_date.replace(/\s/g, 'T'); 541 notificationWidget.insertNotification(item.unread, '<strong>' + content.name + '</strong><br>' 542 + content.show_name + ', ' + unixTimestamp2AgoConverter(date2Timestamp(publish_date)) , null, null, thumb, 'https://365.rtvslo.si/arhiv/n/' + content.id, 'ava'); 543 544 if (item.unread) { 545 notificationWidget.addUnreadCount(); 546 faviconModule.add(); 547 } 548 break; 549 default: 550 break; 551 } 552 }); 553 resolve(); 554 }) 555 }) 556 .then(() => { 557 // Last seen news is fetched separately 558 fetch('https://api.rtvslo.si/misc/time') 559 .then((response) => response.json(), (reason) => { console.log('Error fetching: ' + reason) }) 560 .then((data) => { 561 if (!data.response) return; 562 //let isJournalist = cookieHelper.getCookie('isJournalist'); 563 //if (isJournalist) return; 564 565 var unreadArticles = lastSeenModule.checkForNewArticles(data.response); 566 //console.log("unreadArticles", unreadArticles); 567 568 if (unreadArticles !== -1) { 569
570 // If loged out & unseen ecists show login notice right after unseen notice 571 if (typeof cookieHelper === 'object' && cookieHelper.getCookie('APISESSION_USER_NAME') == null) { 572 notificationWidget.insertNotification(false,' Za veÄ obvestil se prosimo <strong>prijavite</strong>.', null, 'user', null, '/prijava', 'login'); 573 } 574 575 notificationWidget.insertNotification(true, 576 unreadArticles + ' novih <strong>neprebranih novic</strong>', 577 'Zadnja objava: ' + unixTimestamp2AgoConverter(window._globalLastPublishedItems[0]), 'history', null, '/zadnje', 'last_seen'); 578 notificationWidget.addUnreadCount(); 579 faviconModule.add(); 580 } 581 //window.plusOneTest.add(); 582 }) 583 .catch((error) => { 584 console.log('Error fetching last seen news:', error); 585 }); 586 587 // Check for new articles from subscribed authors (runs independently) 588 if (typeof lastAuthorArticles === 'object' && typeof lastAuthorArticles.checkForNewArticles === 'function') { 589 lastAuthorArticles.checkForNewArticles() 590 .then((result) => { 591 if (result.hasNew && result.newArticles.length > 0) { 592 const count = result.newArticles.length; 593 const newestArticle = result.newArticles[0]; 594 const authorName = newestArticle.author?.title || 'naroÄenega avtorja'; 595 596 notificationWidget.insertNotification( 597 true, 598 count + ' novih Älankov od <strong>naroÄenih avtorjev</strong>', 599 'Zadnji Älanek: ' + authorName, 600 'user', 601 null, 602 '/mojrtv/moje-rtv-vsebine/avtorji', 603 'author_articles' 604 ); 605 606 if(result.hasNew) { 607 notificationWidget.addUnreadCount(); 608 faviconModule.add(); 609 } 610 } 611 }) 612 .catch((error) => { 613 console.log('Error checking for author articles:', error); 614 }); 615 } 616 }) 617 618 } 619 620 (typeof Notifications === 'function' ) ? Init() : window.addEventListener('jsbundleload', Init); 621 622 623})(cookieHelper, lastSeenModule, notificationsModule, faviconModule, lastAuthorArticles); 624 625 626 627 628
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.