├── icon128.png ├── icon18.png ├── icon48.png ├── ajax-loader.gif ├── plsdeletebackground.html ├── content-styles.css ├── LICENSE ├── manifest.json ├── popup.html ├── options.js ├── style.css ├── options.html ├── .gitignore ├── README.markdown ├── background.js ├── content-script.js └── popup.js /icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swyxio/HNX/HEAD/icon128.png -------------------------------------------------------------------------------- /icon18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swyxio/HNX/HEAD/icon18.png -------------------------------------------------------------------------------- /icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swyxio/HNX/HEAD/icon48.png -------------------------------------------------------------------------------- /ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swyxio/HNX/HEAD/ajax-loader.gif -------------------------------------------------------------------------------- /plsdeletebackground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /content-styles.css: -------------------------------------------------------------------------------- 1 | #hnx-anchor { 2 | color: black 3 | } 4 | 5 | #hnx-anchor a { 6 | color: darkcyan 7 | } 8 | #hnx-anchor a:hover { 9 | color: darkgoldenrod 10 | } 11 | 12 | /* User blocking styles */ 13 | .hn_bl_separator { 14 | color: #828282; 15 | } 16 | 17 | .hn_block_action { 18 | color: #828282; 19 | text-decoration: none; 20 | font-size: 11px; 21 | } 22 | 23 | .hn_block_action:hover { 24 | color: #000; 25 | text-decoration: underline; 26 | } 27 | 28 | .blocked { 29 | color: #828282; 30 | font-style: italic; 31 | font-size: 11px; 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 swyx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "update_url": "https://clients2.google.com/service/update2/crx", 3 | 4 | "manifest_version": 3, 5 | "name": "Swyx - HNX", 6 | "version": "3.21.0", 7 | "description": "Displays recent stories from Hacker News - swyx's fork", 8 | "icons": { "48": "icon48.png", 9 | "128": "icon128.png" }, 10 | "action": { 11 | "default_title": "Hacker News", 12 | "default_icon": "icon18.png", 13 | "default_popup": "popup.html" 14 | }, 15 | "background": { 16 | "service_worker": "background.js" 17 | }, 18 | "content_scripts": [ 19 | { 20 | "matches": ["https://news.ycombinator.com/*"], 21 | "css": ["content-styles.css"], 22 | "js": ["content-script.js"] 23 | } 24 | ], 25 | "options_page":"options.html", 26 | "permissions": [ 27 | "storage", 28 | "tabs" 29 | ], 30 | "host_permissions": [ 31 | "https://news.ycombinator.com/" 32 | ], 33 | "web_accessible_resources": [ 34 | { 35 | "resources": [ "injectedUI/*" ], 36 | "matches": [] 37 | } 38 | ] 39 | // "content_security_policy": { 40 | // "extension_pages": "script-src 'self' 'unsafe-eval' https://news.ycombinator.com; object-src 'self' 'unsafe-eval' https://news.ycombinator.com" 41 | // } 42 | } 43 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 16 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function(){ 2 | restoreOptions(); 3 | document.getElementById("SaveButton").addEventListener('click', saveOptions, false); 4 | }); 5 | 6 | var selectReqInterval; 7 | var radioBackgroundTabs; 8 | 9 | function initVariables() { 10 | selectReqInterval = document.getElementById("RequestInterval"); 11 | radioBackgroundTabs = document.getElementsByName("BackgroundTabs"); 12 | } 13 | 14 | function restoreOptions() { 15 | initVariables(); 16 | var reqInterval = localStorage["HN.RequestInterval"]; 17 | for (var i=0; i 2 | 3 | 4 | HNX - Swyx Options 5 | 6 | 7 | 8 | 9 | 10 |
11 | 15 |
16 | 17 |


26 |
27 | Yes 28 | No
29 |

30 | 31 |

32 |

33 | Report an Issue 34 |

35 |

:: Created by Adam Albrecht :: www.adamalbrecht.com

36 |

37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ## Hacker News Extended 2 | 3 | This is an extension for Google Chrome that: 4 | 5 | - displays the latest links from Y Combinator's [Hacker News](https://news.ycombinator.com) 6 | - Offers a link for submitting the url of the current tab. 7 | - shows previous results when submitting in case someone recently submitted 8 | - **User Blocking**: Block/unblock users on Hacker News comment threads to hide their comments 9 | 10 | ![https://pbs.twimg.com/media/FWO1kRkXoAMsBwE?format=jpg&name=4096x4096](https://pbs.twimg.com/media/FWO1kRkXoAMsBwE?format=jpg&name=4096x4096) 11 | 12 | ## Usage instructions 13 | 14 | This extension has not yet been published. To use this you'll have to: 15 | 16 | - clone this repo 17 | - open `chrome://extensions/` and enable Developer mode 18 | - "Load unpacked" and open the folder that contains this repo 19 | 20 | Now you should have the orange "Y" square in your chrome extensions list (I use it often enough that I "pin" it to the top.) 21 | 22 | - click it to see the HN front page, and search 23 | - whatever page you are on, you can use this extension to submit that current page 24 | 25 | ## User Blocking Feature 26 | 27 | The extension includes a user blocking feature that works on Hacker News comment threads (`/item` and `/threads` pages). 28 | 29 | ### How it works: 30 | - **Block users**: Click the "block" link next to any username in comment threads 31 | - **Unblock users**: Click "unblock" to restore visibility of a blocked user's comments 32 | - **Visual indicators**: Blocked comments are hidden and show a "[blocked]" indicator 33 | - **Persistent storage**: Blocked users are remembered across browser sessions 34 | 35 | ### Where data is stored: 36 | User blocking preferences are stored locally in your browser using Chrome's `chrome.storage.local` API under the key `"hn_banned"`. The data structure includes: 37 | ```json 38 | { 39 | "username": { 40 | "username": "example_user", 41 | "blocked": true, 42 | "timestamp": 1697234567890 43 | } 44 | } 45 | ``` 46 | 47 | This data persists locally on your machine and is not synced across devices or shared externally. 48 | 49 | ## See also 50 | 51 | My other fave extension: https://github.com/sw-yx/Twitter-Links-beta 52 | 53 | ## Acknowledgements 54 | 55 | The starter code for this extension came from https://chrome.google.com/webstore/detail/hacker-news/geancnifhbkbjijfkcjjdnfemppmcjmk 56 | 57 | Migrated to v3 with https://blog.shahednasser.com/chrome-extension-tutorial-migrating-to-manifest-v3-from-v2/#from-background-scripts-to-service-workers 58 | 59 | other helpful: 60 | 61 | - https://developer.chrome.com/docs/extensions/mv3/content_scripts/ 62 | - https://stackoverflow.com/questions/9444926/chrome-extension-in-tabs-doc-dont-exist-this-chrome-tabs-getselected-but-i-s 63 | - https://groups.google.com/a/chromium.org/g/chromium-extensions/c/gywzLNsOMVI?pli=1 64 | - loading js files (i gave up) 65 | - https://stackoverflow.com/questions/48104433/how-to-import-es6-modules-in-content-script-for-chrome-extension 66 | - https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/web_accessible_resources 67 | - lastChangedWindow -> currentWindow -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | // main code 2 | 3 | var firstRequest = true; 4 | async function startRequest() { 5 | await UpdateIfReady(firstRequest); 6 | firstRequest = false; 7 | setTimeout(startRequest, 60000); 8 | } 9 | //If any options are not already set, they will be set to defaults here 10 | SetInitialOption("HN.RequestInterval", 600000); 11 | SetInitialOption("HN.BackgroundTabs", false); 12 | 13 | startRequest(); 14 | 15 | // CORE.js 16 | 17 | var maxFeedItems = 15; 18 | var req; 19 | var buildPopupAfterResponse = false; 20 | var OnFeedSuccess = null; 21 | var OnFeedFail = null; 22 | var retryMilliseconds = 60000; 23 | 24 | function SetInitialOption(key, value) { 25 | chrome.storage.local.get([key], function(result) { 26 | if (result == null) { 27 | chrome.storage.local.set({[key]: value}, function() { 28 | console.log(`Value ${key} is initialized to ` + value); 29 | }); 30 | } 31 | }); 32 | } 33 | 34 | async function localStorage(key) { 35 | return new Promise((resolve, reject) => { 36 | chrome.storage.local.get([key], function(result) { 37 | resolve(result[key]); 38 | }); 39 | }); 40 | } 41 | async function setLocalStorage(key, val) { 42 | return new Promise((resolve, reject) => { 43 | chrome.storage.local.set({[key]: val}, resolve); 44 | }); 45 | } 46 | 47 | async function UpdateIfReady(force) { 48 | var lastRefresh = parseFloat(await localStorage("HN.LastRefresh")); 49 | var interval = parseFloat(await localStorage("HN.RequestInterval")); 50 | var nextRefresh = lastRefresh + interval; 51 | var curTime = parseFloat((new Date()).getTime()); 52 | var isReady = (curTime > nextRefresh); 53 | var isNull = (await localStorage("HN.LastRefresh") == null); 54 | if ((force == true) || (await localStorage("HN.LastRefresh") == null)) { 55 | await UpdateFeed(); 56 | } 57 | else { 58 | if (isReady) { 59 | await UpdateFeed(); 60 | } 61 | } 62 | } 63 | 64 | async function UpdateFeed() { 65 | return fetch('https://news.ycombinator.com/rss') 66 | .then(response => response.text()) 67 | .then(text => onRssSuccess(text)) 68 | .catch(error => onRssError(error)); 69 | // var xhr = new XMLHttpRequest(); 70 | // xhr.open('GET', ); 71 | // xhr.onload = await (async function() { 72 | // if (xhr.status === 200) { 73 | // await onRssSuccess(xhr.responseText); 74 | // } 75 | // else { 76 | // await onRssError(); 77 | // } 78 | // }()); 79 | // xhr.send(); 80 | } 81 | 82 | async function onRssSuccess(doc) { 83 | console.log('doc', doc); 84 | if (!doc) { 85 | handleFeedParsingFailed("Not a valid feed."); 86 | return; 87 | } 88 | links = parseHNLinks(doc); 89 | await SaveLinksToLocalStorage(links); 90 | if (buildPopupAfterResponse == true) { 91 | buildPopup(links); 92 | buildPopupAfterResponse = false; 93 | } 94 | return setLocalStorage("HN.LastRefresh", new Date().getTime()); 95 | } 96 | 97 | 98 | async function onRssError(xhr, type, error) { 99 | return handleFeedParsingFailed('Failed to fetch RSS feed.'); 100 | } 101 | 102 | async function handleFeedParsingFailed(error) { 103 | //var feed = document.getElementById("feed"); 104 | //feed.className = "error" 105 | //feed.innerText = "Error: " + error; 106 | const newthing = (await localStorage("HN.LastRefresh")) + retryMilliseconds; 107 | return setLocalStorage("HN.LastRefresh", newthing); 108 | } 109 | 110 | function parseXml(xml) { 111 | var xmlDoc; 112 | try { 113 | xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); 114 | xmlDoc.async = false; 115 | xmlDoc.loadXML(xml); 116 | } 117 | catch (e) { 118 | xmlDoc = (new DOMParser).parseFromString(xml, 'text/xml'); 119 | } 120 | 121 | return xmlDoc; 122 | } 123 | 124 | function parseHNLinks(rawXmlStr) { 125 | var parser = new DOMParser(); 126 | var doc = parser.parseFromString(rawXmlStr, "text/xml"); 127 | var entries = doc.getElementsByTagName('entry'); 128 | if (entries.length == 0) { 129 | entries = doc.getElementsByTagName('item'); 130 | } 131 | var count = Math.min(entries.length, maxFeedItems); 132 | var links = new Array(); 133 | for (var i=0; i< count; i++) { 134 | item = entries.item(i); 135 | var hnLink = new Object(); 136 | //Grab the title 137 | var itemTitle = item.getElementsByTagName('title')[0]; 138 | if (itemTitle) { 139 | hnLink.Title = itemTitle.textContent; 140 | } else { 141 | hnLink.Title = "Unknown Title"; 142 | } 143 | 144 | //Grab the Link 145 | var itemLink = item.getElementsByTagName('link')[0]; 146 | if (!itemLink) { 147 | itemLink = item.getElementsByTagName('comments')[0]; 148 | } 149 | if (itemLink) { 150 | hnLink.Link = itemLink.textContent; 151 | } else { 152 | hnLink.Link = ''; 153 | } 154 | 155 | //Grab the comments link 156 | var commentsLink = item.getElementsByTagName('comments')[0]; 157 | if (commentsLink) { 158 | hnLink.CommentsLink = commentsLink.textContent; 159 | } else { 160 | hnLink.CommentsLink = ''; 161 | } 162 | 163 | links.push(hnLink); 164 | } 165 | return links; 166 | } 167 | 168 | async function SaveLinksToLocalStorage(links) { 169 | await setLocalStorage("HN.NumLinks", links.length); 170 | for (var i=0; i { 118 | // const src = chrome.runtime.getURL('injectedUI/index.js'); 119 | // console.log('src', src); 120 | // const contentMain = await import(src); 121 | // contentMain.load(); 122 | // })(); 123 | 124 | 125 | 126 | 127 | const rtf = new Intl.RelativeTimeFormat("en", { 128 | localeMatcher: "best fit", // other values: "lookup" 129 | numeric: "always", // other values: "auto" 130 | style: "long", // other values: "short" or "narrow" 131 | }); 132 | function getDifferenceInDays(fromDate, toDate) { 133 | const diff = Math.floor((fromDate - toDate) / (1000 * 60 * 60 * 24)); 134 | return rtf.format(diff, "day"); 135 | } 136 | 137 | 138 | 139 | if (location.pathname === '/submitlink') { 140 | const anchor = document.createElement('div') 141 | anchor.id = 'hnx-anchor' 142 | document.getElementById('hnmain').parentElement.append(anchor) 143 | 144 | // parse 145 | var abc = new URL(location) 146 | var submittedURL = abc.searchParams.get('u') 147 | // https://hn.algolia.com/api 148 | // https%3A%2F%2Fhn.algolia.com%2Fapi&sort=byPopularity&type=story 149 | // https://hn.algolia.com/?dateRange=all&page=0&prefix=false&query= 150 | fetch('https://hn.algolia.com/api/v1/search_by_date?query=' + encodeURIComponent(submittedURL)) 151 | .then(res => res.json()) 152 | .then(res => console.log(res) || res) 153 | .then(({hits}) => { 154 | if (hits.length) { 155 | anchor.append(document.createElement('hr')) 156 | // create h1 element with text "hi" 157 | const h1 = document.createElement('h1') 158 | h1.innerText = hits.length + ' previous results found:' 159 | anchor.append(h1); 160 | anchor.append(document.createElement('br')) 161 | const ul = document.createElement('ul') 162 | anchor.append(ul) 163 | // display 164 | hits.forEach(({title, comment_text, points, objectID, author, created_at}) => { 165 | const li = document.createElement('li') 166 | const datediff = getDifferenceInDays(new Date(created_at), new Date()) 167 | li.innerHTML = ` 168 | ${datediff} 169 | ${comment_text ? 170 | `
[${author}${points ? `, ${points}pts` : ''}]: comment ${comment_text}
` : 171 | ` [${author}${points ? `, ${points}pts` : ''}]: ${title}` 172 | } 173 | ` 174 | ul.appendChild(li) 175 | li.style = 'text-align: left' 176 | }) 177 | } else { 178 | // display 179 | anchor.append('no previous submissions found'); 180 | } 181 | }) 182 | 183 | 184 | } -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | window.onload = function(){ 2 | main(); 3 | setupEvents(); 4 | }; 5 | function setupEvents() { 6 | document.getElementById("submitLink").addEventListener('click', submitCurrentTab, false); 7 | document.getElementById("refresh").addEventListener('click', refreshLinks, false); 8 | document.getElementById("options").addEventListener('click', openOptions, false); 9 | document.getElementById("searchbox").addEventListener('keydown', function(event) { 10 | if (event.which === 13) { 11 | search(); 12 | } 13 | }); 14 | } 15 | function main() { 16 | if (localStorage['HN.NumLinks'] == null) { 17 | buildPopupAfterResponse = true; 18 | UpdateFeed(); 19 | } 20 | else { 21 | buildPopup(RetrieveLinksFromLocalStorage()); 22 | } 23 | } 24 | 25 | function buildPopup(links) { 26 | var header = document.getElementById("header"); 27 | var feed = document.getElementById("feed"); 28 | var issueLink = document.getElementById("issues"); 29 | issueLink.addEventListener("click", openLinkFront); 30 | 31 | //Setup Title Link 32 | var title = document.getElementById("title"); 33 | title.addEventListener("click", openLink); 34 | 35 | //Setup search button 36 | var searchButton = document.getElementById("searchbutton"); 37 | searchButton.addEventListener("click", search); 38 | 39 | for (var i=0; i 0) { 70 | var search_url = "https://hn.algolia.com/?query=" + keywords.replace(" ", "+"); 71 | openUrl(search_url, true); 72 | } 73 | } 74 | 75 | function refreshLinks() { 76 | var linkTable = document.getElementById("feed"); 77 | while(linkTable.hasChildNodes()) linkTable.removeChild(linkTable.firstChild); //Remove all current links 78 | toggle("container"); 79 | toggle("spinner"); 80 | buildPopupAfterResponse = true; 81 | UpdateFeed(); 82 | updateLastRefreshTime(); 83 | } 84 | 85 | 86 | function updateLastRefreshTime() { 87 | localStorage["HN.LastRefresh"] = (new Date()).getTime(); 88 | } 89 | 90 | //Submit the current tab 91 | function submitCurrentTab() { 92 | console.log('submitCurrentTab!'); 93 | chrome.tabs.query({ 94 | active: true, 95 | currentWindow: true // no longer lastChangedWindow 96 | }).then(tabs => { 97 | var tab = tabs[0]; 98 | console.log('tab', Object.keys(tab)) 99 | var submit_url = "https://news.ycombinator.com/submitlink?u=" + encodeURIComponent(tab.url) + "&t=" + encodeURIComponent(tab.title); 100 | console.log('submit_url', submit_url) 101 | // setTimeout(() => { // just for testing 102 | openUrl(submit_url, true); 103 | // }, 3000); // just for testing 104 | }); 105 | } 106 | 107 | 108 | /// ----------------- Helper Functions ----------------- /// 109 | 110 | 111 | function RetrieveLinksFromLocalStorage() { 112 | var numLinks = localStorage["HN.NumLinks"]; 113 | if (numLinks == null) { 114 | return null; 115 | } 116 | else { 117 | var links = new Array(); 118 | for (var i=0; i 11) { ap = "PM"; } 178 | if (hour > 12) { hour = hour - 12; } 179 | if (hour == 0) { hour = 12; } 180 | if (minute < 10) { minute = "0" + minute; } 181 | var timeString = hour + 182 | ':' + 183 | minute + 184 | " " + 185 | ap; 186 | return timeString; 187 | } 188 | 189 | 190 | // CORE.js 191 | 192 | var maxFeedItems = 15; 193 | var req; 194 | var buildPopupAfterResponse = false; 195 | var OnFeedSuccess = null; 196 | var OnFeedFail = null; 197 | var retryMilliseconds = 60000; 198 | 199 | function SetInitialOption(key, value) { 200 | chrome.storage.local.get([key], function(result) { 201 | if (result == null) { 202 | chrome.storage.local.set({[key]: value}, function() { 203 | console.log(`Value ${key} is initialized to ` + value); 204 | }); 205 | } 206 | }); 207 | } 208 | 209 | async function localStorage(key) { 210 | return new Promise((resolve, reject) => { 211 | chrome.storage.local.get([key], function(result) { 212 | resolve(result[key]); 213 | }); 214 | }); 215 | } 216 | async function setLocalStorage(key, val) { 217 | return new Promise((resolve, reject) => { 218 | chrome.storage.local.set({[key]: val}, resolve); 219 | }); 220 | } 221 | 222 | async function UpdateIfReady(force) { 223 | var lastRefresh = parseFloat(await localStorage("HN.LastRefresh")); 224 | var interval = parseFloat(await localStorage("HN.RequestInterval")); 225 | var nextRefresh = lastRefresh + interval; 226 | var curTime = parseFloat((new Date()).getTime()); 227 | var isReady = (curTime > nextRefresh); 228 | var isNull = (await localStorage("HN.LastRefresh") == null); 229 | if ((force == true) || (await localStorage("HN.LastRefresh") == null)) { 230 | await UpdateFeed(); 231 | } 232 | else { 233 | if (isReady) { 234 | await UpdateFeed(); 235 | } 236 | } 237 | } 238 | 239 | async function UpdateFeed() { 240 | return fetch('https://news.ycombinator.com/rss') 241 | .then(response => response.text()) 242 | .then(text => onRssSuccess(text)) 243 | .catch(error => onRssError(error)); 244 | // var xhr = new XMLHttpRequest(); 245 | // xhr.open('GET', ); 246 | // xhr.onload = await (async function() { 247 | // if (xhr.status === 200) { 248 | // await onRssSuccess(xhr.responseText); 249 | // } 250 | // else { 251 | // await onRssError(); 252 | // } 253 | // }()); 254 | // xhr.send(); 255 | } 256 | 257 | async function onRssSuccess(doc) { 258 | console.log('doc', doc); 259 | if (!doc) { 260 | handleFeedParsingFailed("Not a valid feed."); 261 | return; 262 | } 263 | links = parseHNLinks(doc); 264 | await SaveLinksToLocalStorage(links); 265 | if (buildPopupAfterResponse == true) { 266 | buildPopup(links); 267 | buildPopupAfterResponse = false; 268 | } 269 | return setLocalStorage("HN.LastRefresh", new Date().getTime()); 270 | } 271 | 272 | 273 | async function onRssError(xhr, type, error) { 274 | return handleFeedParsingFailed('Failed to fetch RSS feed.'); 275 | } 276 | 277 | async function handleFeedParsingFailed(error) { 278 | //var feed = document.getElementById("feed"); 279 | //feed.className = "error" 280 | //feed.innerText = "Error: " + error; 281 | const newthing = (await localStorage("HN.LastRefresh")) + retryMilliseconds; 282 | return setLocalStorage("HN.LastRefresh", newthing); 283 | } 284 | 285 | function parseXml(xml) { 286 | var xmlDoc; 287 | try { 288 | xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); 289 | xmlDoc.async = false; 290 | xmlDoc.loadXML(xml); 291 | } 292 | catch (e) { 293 | xmlDoc = (new DOMParser).parseFromString(xml, 'text/xml'); 294 | } 295 | 296 | return xmlDoc; 297 | } 298 | 299 | function parseHNLinks(rawXmlStr) { 300 | var parser = new DOMParser(); 301 | var doc = parser.parseFromString(rawXmlStr, "text/xml"); 302 | var entries = doc.getElementsByTagName('entry'); 303 | if (entries.length == 0) { 304 | entries = doc.getElementsByTagName('item'); 305 | } 306 | var count = Math.min(entries.length, maxFeedItems); 307 | var links = new Array(); 308 | for (var i=0; i< count; i++) { 309 | item = entries.item(i); 310 | var hnLink = new Object(); 311 | //Grab the title 312 | var itemTitle = item.getElementsByTagName('title')[0]; 313 | if (itemTitle) { 314 | hnLink.Title = itemTitle.textContent; 315 | } else { 316 | hnLink.Title = "Unknown Title"; 317 | } 318 | 319 | //Grab the Link 320 | var itemLink = item.getElementsByTagName('link')[0]; 321 | if (!itemLink) { 322 | itemLink = item.getElementsByTagName('comments')[0]; 323 | } 324 | if (itemLink) { 325 | hnLink.Link = itemLink.textContent; 326 | } else { 327 | hnLink.Link = ''; 328 | } 329 | 330 | //Grab the comments link 331 | var commentsLink = item.getElementsByTagName('comments')[0]; 332 | if (commentsLink) { 333 | hnLink.CommentsLink = commentsLink.textContent; 334 | } else { 335 | hnLink.CommentsLink = ''; 336 | } 337 | 338 | links.push(hnLink); 339 | } 340 | return links; 341 | } 342 | 343 | async function SaveLinksToLocalStorage(links) { 344 | await setLocalStorage("HN.NumLinks", links.length); 345 | for (var i=0; i