├── 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 |
14 |

15 |
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 | 
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