├── .github └── FUNDING.yml ├── .gitignore ├── .vscode └── extensions.json ├── LICENSE ├── NOTICE ├── PRIVACY_POLICY.md ├── README.md ├── _locales └── en │ └── messages.json ├── background.js ├── content.js ├── icons ├── chrome-web-store-icon.png ├── icon.svg ├── icon128.png ├── icon16.png ├── icon256.png ├── icon48.png ├── icon512.png ├── icon600.png ├── icon64.png ├── icon96.png ├── toolbar-icon16.png ├── toolbar-icon19.png ├── toolbar-icon32.png ├── toolbar-icon38.png ├── toolbar-icon48.png └── toolbar-icon72.png ├── jsconfig.json ├── manifest.mv2.json ├── manifest.mv3.json ├── options.css ├── options.html ├── options.js ├── package.json ├── promo ├── app-store.png ├── chrome_small_promo_tile.png └── draw-the-rest-of-the-owl.gif ├── safari ├── .gitignore ├── Comments Owl for Hacker News.xcodeproj │ └── project.pbxproj ├── Shared (App) │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── appicon1024-fullbleed.png │ │ │ ├── appicon1024.png │ │ │ ├── appicon128.png │ │ │ ├── appicon16.png │ │ │ ├── appicon256.png │ │ │ ├── appicon32.png │ │ │ ├── appicon512.png │ │ │ └── appicon64.png │ │ ├── Contents.json │ │ └── LargeIcon.imageset │ │ │ └── Contents.json │ ├── Base.lproj │ │ └── Main.html │ ├── Resources │ │ ├── Ad.png │ │ ├── Icon.png │ │ ├── Script.js │ │ └── Style.css │ └── ViewController.swift ├── Shared (Extension) │ ├── Resources │ │ └── manifest.json │ └── SafariWebExtensionHandler.swift ├── iOS (App) │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── SceneDelegate.swift ├── iOS (Extension) │ └── Info.plist ├── macOS (App) │ ├── AppDelegate.swift │ ├── Base.lproj │ │ └── Main.storyboard │ └── Comments Owl for Hacker News.entitlements └── macOS (Extension) │ ├── Comments Owl for Hacker News.entitlements │ └── Info.plist ├── scripts ├── build.js ├── copy.js ├── create-browser-action.js └── release.js └── types.d.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: jbscript 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /manifest.json 3 | browser_action.html 4 | node_modules/ 5 | web-ext-artifacts/ -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "santacodes.santacodes-region-viewer" 4 | ] 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Jonny Buchanan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | content.js includes the dedent() function from https://github.com/victornpb/tiny-dedent 2 | 3 | MIT License 4 | 5 | Copyright (c) 2021 Victor 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /PRIVACY_POLICY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | No data or personal information is collected by Comments Owl for Hacker News. 4 | 5 | ### Contact 6 | 7 | If you have any questions or suggestions regarding this privacy policy, please email [extensions@soitis.dev](mailto:extensions@soitis.dev). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Comments Owl for Hacker News](https://soitis.dev/comments-owl-for-hacker-news) 2 | 3 | [![](promo/draw-the-rest-of-the-owl.gif)](https://soitis.dev/comments-owl-for-hacker-news) 4 | 5 | **Comments Owl for Hacker News is a browser extension which makes it easer to follow comment threads on [Hacker News ](https://news.ycombinator.com) across multiple visits, adds the ability to annotate and mute other users, plus other UI and UX tweaks** 6 | 7 | > [!IMPORTANT] 8 | > This is the support repository for Comments Owl for Hacker News - for installation links, information about the extension, screenshots, and FAQs, please visit the [Comments Owl for Hacker News website](https://soitis.dev/comments-owl-for-hacker-news). 9 | 10 | Follow [@soitis.dev](https://bsky.app/profile/soitis.dev) on Bluesky for extension updates and other announcements. 11 | 12 | Check the availability of the latest updates for your browser on the [releases page](https://github.com/insin/comments-owl-for-hacker-news/releases). 13 | 14 | ## Support 15 | 16 | To report a bug, [create a new Issue](https://github.com/insin/comments-owl-for-hacker-news/issues/new). 17 | 18 | Please include: 19 | 20 | - The version of the extension you're using 21 | - The browser and operating system you're using it on 22 | - Relevant URLs if applicable 23 | - Relevant screenshots if applicable 24 | 25 | If you don't have a GitHub account, post bug details or feature requests to [@soitis.dev](https://bsky.app/profile/soitis.dev) on Bluesky. 26 | 27 | If you don't have a Bluesky account, or want to provide more information than a post allows, send an email to [extensions@soitis.dev](mailto:extensions@soitis.dev). 28 | 29 | ## Icon Attribution 30 | 31 | Icon adapted from "Owl icon" by [Lorc](https://lorcblog.blogspot.com/) from [game-icons.net](https://game-icons.net), [CC 3.0 BY](https://creativecommons.org/licenses/by/3.0/) 32 | -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Comments Owl for Hacker News" 4 | }, 5 | "extensionDescription": { 6 | "message": "Highlight new comments, mute users, and other tweaks for Hacker News", 7 | "description": "<= 112 characters" 8 | }, 9 | "addUpvotedToHeader": { 10 | "message": "Add \"upvoted\"" 11 | }, 12 | "autoCollapseNotNew": { 13 | "message": "Auto collapse threads without new comments" 14 | }, 15 | "autoHighlightNew": { 16 | "message": "Auto highlight new comments" 17 | }, 18 | "commentPagesOptions": { 19 | "message": "Comment pages" 20 | }, 21 | "hideCommentsNav": { 22 | "message": "Hide \"comments\"" 23 | }, 24 | "hideJobsNav": { 25 | "message": "Hide \"jobs\"" 26 | }, 27 | "hidePastNav": { 28 | "message": "Hide \"past\"" 29 | }, 30 | "hideReplyLinks": { 31 | "message": "Hide \"reply\" links under comments" 32 | }, 33 | "hideSubmitNav": { 34 | "message": "Hide \"submit\"" 35 | }, 36 | "listPageAccidentallyInfo": { 37 | "message": "Prevent accidental flagging/hiding on mobile" 38 | }, 39 | "listPageFlagging": { 40 | "message": "Flagging" 41 | }, 42 | "listPageHiding": { 43 | "message": "Hiding" 44 | }, 45 | "listPagesOptions": { 46 | "message": "List pages" 47 | }, 48 | "makeSubmissionTextReadable": { 49 | "message": "Increase contrast of submission text" 50 | }, 51 | "navigationOptions": { 52 | "message": "Navigation" 53 | }, 54 | "option_confirm": { 55 | "message": "Confirm" 56 | }, 57 | "option_disabled": { 58 | "message": "Disabled" 59 | }, 60 | "option_enabled": { 61 | "message": "Enabled" 62 | } 63 | } -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | const TOGGLE_REPLY_LINKS = 'toggle-reply-links' 2 | 3 | let hidingReplyLinks = false 4 | 5 | chrome.storage.local.get({hideReplyLinks: false}, ({hideReplyLinks}) => { 6 | hidingReplyLinks = hideReplyLinks 7 | chrome.contextMenus.create({ 8 | id: TOGGLE_REPLY_LINKS, 9 | type: 'checkbox', 10 | contexts: ['page'], 11 | checked: hideReplyLinks, 12 | title: 'Hide reply links', 13 | documentUrlPatterns: ['https://news.ycombinator.com/item*'], 14 | }) 15 | }) 16 | 17 | chrome.contextMenus.onClicked.addListener((info) => { 18 | if (info.menuItemId == TOGGLE_REPLY_LINKS) { 19 | hidingReplyLinks = !hidingReplyLinks 20 | chrome.storage.local.set({hideReplyLinks: hidingReplyLinks}) 21 | } 22 | }) 23 | 24 | chrome.storage.local.onChanged.addListener((changes) => { 25 | if ('hideReplyLinks' in changes) { 26 | hidingReplyLinks = changes['hideReplyLinks'].newValue 27 | chrome.contextMenus.update(TOGGLE_REPLY_LINKS, {checked: hidingReplyLinks}) 28 | } 29 | }) -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Comments Owl for Hacker News 3 | // @description Highlight new comments, mute users, and other tweaks for Hacker News 4 | // @namespace https://github.com/insin/comments-owl-for-hacker-news/ 5 | // @match https://news.ycombinator.com/* 6 | // @version 48 7 | // ==/UserScript== 8 | let debug = false 9 | let isSafari = navigator.userAgent.includes('Safari/') && !/Chrom(e|ium)\//.test(navigator.userAgent) 10 | 11 | const HIGHLIGHT_COLOR = '#ffffde' 12 | const TOGGLE_HIDE = '[–]' 13 | const TOGGLE_SHOW = '[+]' 14 | const MUTED_USERS_KEY = 'mutedUsers' 15 | const USER_NOTES_KEY = 'userNotes' 16 | const LOGGED_OUT_USER_PAGE = ` 17 | 18 | 19 | 20 | 21 | Muted | Comments Owl for Hacker News 22 | 23 | 24 |
25 | 26 | 27 | 28 | 51 | 52 | 53 | 54 | 67 | 68 | 69 |
29 | 30 | 31 | 32 | 37 | 42 | 47 | 48 | 49 |
33 | 34 | 35 | 36 | 38 | Hacker News 39 | new | past | comments | ask | show | jobs 40 | 41 | 43 | 44 | login 45 | 46 |
50 |
55 | 56 | 57 | 58 | 59 | 62 | 63 | 64 |
user: 60 | anonymous comments owl user 61 |
65 |

66 |
70 |
71 | ` 72 | 73 | //#region Config 74 | /** @type {import("./types").Config} */ 75 | let config = { 76 | addUpvotedToHeader: true, 77 | autoCollapseNotNew: true, 78 | autoHighlightNew: true, 79 | hideCommentsNav: false, 80 | hideJobsNav: false, 81 | hidePastNav: false, 82 | hideReplyLinks: false, 83 | hideSubmitNav: false, 84 | listPageFlagging: 'enabled', 85 | listPageHiding: 'enabled', 86 | makeSubmissionTextReadable: true, 87 | } 88 | //#endregion 89 | 90 | //#region Storage 91 | class Visit { 92 | constructor({commentCount, maxCommentId, time}) { 93 | /** @type {number} */ 94 | this.commentCount = commentCount 95 | /** @type {number} */ 96 | this.maxCommentId = maxCommentId 97 | /** @type {Date} */ 98 | this.time = time 99 | } 100 | 101 | toJSON() { 102 | return { 103 | c: this.commentCount, 104 | m: this.maxCommentId, 105 | t: this.time.getTime(), 106 | } 107 | } 108 | } 109 | 110 | Visit.fromJSON = function(obj) { 111 | return new Visit({ 112 | commentCount: obj.c, 113 | maxCommentId: obj.m, 114 | time: new Date(obj.t), 115 | }) 116 | } 117 | 118 | function getLastVisit(itemId) { 119 | let json = localStorage.getItem(itemId) 120 | if (json == null) return null 121 | return Visit.fromJSON(JSON.parse(json)) 122 | } 123 | 124 | function storeVisit(itemId, visit) { 125 | log('storing visit', visit) 126 | localStorage.setItem(itemId, JSON.stringify(visit)) 127 | } 128 | 129 | /** @returns {Set} */ 130 | function getMutedUsers(json = localStorage[MUTED_USERS_KEY]) { 131 | return new Set(JSON.parse(json || '[]')) 132 | } 133 | 134 | /** @returns {Record} */ 135 | function getUserNotes(json = localStorage[USER_NOTES_KEY]) { 136 | return JSON.parse(json || '{}') 137 | } 138 | 139 | function storeMutedUsers(mutedUsers) { 140 | localStorage[MUTED_USERS_KEY] = JSON.stringify(Array.from(mutedUsers)) 141 | } 142 | 143 | function storeUserNotes(userNotes) { 144 | localStorage[USER_NOTES_KEY] = JSON.stringify(userNotes) 145 | } 146 | //#endregion 147 | 148 | //#region Utility functions 149 | /** 150 | * @param {string} role 151 | * @param {...string} css 152 | */ 153 | function addStyle(role, ...css) { 154 | let $style = document.createElement('style') 155 | $style.dataset.insertedBy = 'comments-owl' 156 | $style.dataset.role = role 157 | if (css.length > 0) { 158 | $style.textContent = css.filter(Boolean).map(dedent).join('\n') 159 | } 160 | document.querySelector('head').appendChild($style) 161 | return $style 162 | } 163 | 164 | const autosizeTextArea = (() => { 165 | /** @type {Number} */ 166 | let textAreaPadding 167 | 168 | return function autosizeTextarea($textArea) { 169 | if (textAreaPadding == null) { 170 | textAreaPadding = Number(getComputedStyle($textArea).paddingTop.replace('px', '')) * 2 171 | } 172 | $textArea.style.height = '0px' 173 | $textArea.style.height = $textArea.scrollHeight + textAreaPadding + 'px' 174 | } 175 | })() 176 | 177 | function checkbox(attributes, label) { 178 | return h('label', null, 179 | h('input', { 180 | style: {verticalAlign: 'middle'}, 181 | type: 'checkbox', 182 | ...attributes, 183 | }), 184 | ' ', 185 | label, 186 | ) 187 | } 188 | 189 | /** 190 | * @param {string} str 191 | * @return {string} 192 | */ 193 | function dedent(str) { 194 | str = str.replace(/^[ \t]*\r?\n/, '') 195 | let indent = /^[ \t]+/m.exec(str) 196 | if (indent) str = str.replace(new RegExp('^' + indent[0], 'gm'), '') 197 | return str.replace(/(\r?\n)[ \t]+$/, '$1') 198 | } 199 | 200 | /** 201 | * Create an element. 202 | * @param {string} tagName 203 | * @param {{[key: string]: any}} [attributes] 204 | * @param {...any} children 205 | * @returns {HTMLElement} 206 | */ 207 | function h(tagName, attributes, ...children) { 208 | let $el = document.createElement(tagName) 209 | 210 | if (attributes) { 211 | for (let [prop, value] of Object.entries(attributes)) { 212 | if (prop.indexOf('on') === 0) { 213 | $el.addEventListener(prop.slice(2).toLowerCase(), value) 214 | } 215 | else if (prop.toLowerCase() == 'style') { 216 | for (let [styleProp, styleValue] of Object.entries(value)) { 217 | $el.style[styleProp] = styleValue 218 | } 219 | } 220 | else { 221 | $el[prop] = value 222 | } 223 | } 224 | } 225 | 226 | for (let child of children) { 227 | if (child == null || child === false) { 228 | continue 229 | } 230 | if (child instanceof Node) { 231 | $el.appendChild(child) 232 | } 233 | else { 234 | $el.insertAdjacentText('beforeend', String(child)) 235 | } 236 | } 237 | 238 | return $el 239 | } 240 | 241 | function log(...args) { 242 | if (debug) { 243 | console.log('🦉', ...args) 244 | } 245 | } 246 | 247 | function warn(...args) { 248 | if (debug) { 249 | console.log('❗', ...args) 250 | } 251 | } 252 | 253 | /** 254 | * @param {number} count 255 | * @param {string} suffixes 256 | * @returns {string} 257 | */ 258 | function s(count, suffixes = ',s') { 259 | if (!suffixes.includes(',')) { 260 | suffixes = `,${suffixes}` 261 | } 262 | return suffixes.split(',')[count === 1 ? 0 : 1] 263 | } 264 | 265 | /** 266 | * @param {HTMLElement} $el 267 | * @param {boolean} hidden 268 | */ 269 | function toggleDisplay($el, hidden) { 270 | $el.classList.toggle('noshow', hidden) 271 | // We need to enforce display setting as the page's own script expands all 272 | // comments on page load. 273 | $el.style.display = hidden ? 'none' : '' 274 | } 275 | 276 | /** 277 | * @param {HTMLElement} $el 278 | * @param {boolean} hidden 279 | */ 280 | function toggleVisibility($el, hidden) { 281 | $el.classList.toggle('nosee', hidden) 282 | // We need to enforce visibility setting as the page's own script expands 283 | // all comments on page load. 284 | $el.style.visibility = hidden ? 'hidden' : 'visible' 285 | } 286 | //#endregion 287 | 288 | //#region Navigation 289 | function tweakNav() { 290 | let $pageTop = document.querySelector('span.pagetop') 291 | if (!$pageTop) { 292 | warn('pagetop not found') 293 | return 294 | } 295 | 296 | //#region CSS 297 | addStyle('nav-static', ` 298 | .desktopnav { 299 | display: inline; 300 | } 301 | .mobilenav { 302 | display: none; 303 | } 304 | @media only screen and (min-width : 300px) and (max-width : 750px) { 305 | .desktopnav { 306 | display: none; 307 | } 308 | .mobilenav { 309 | display: revert; 310 | } 311 | } 312 | `) 313 | 314 | let $style = addStyle('nav-dynamic') 315 | 316 | function configureCss() { 317 | let hideNavSelectors = [ 318 | config.hidePastNav && 'span.past-sep, span.past-sep + a', 319 | config.hideCommentsNav && 'span.comments-sep, span.comments-sep + a', 320 | config.hideJobsNav && 'span.jobs-sep, span.jobs-sep + a', 321 | config.hideSubmitNav && 'span.submit-sep, span.submit-sep + a', 322 | !config.addUpvotedToHeader && 'span.upvoted-sep, span.upvoted-sep + a', 323 | ].filter(Boolean) 324 | $style.textContent = hideNavSelectors.length == 0 ? '' : dedent(` 325 | ${hideNavSelectors.join(',\n')} { 326 | display: none; 327 | } 328 | `) 329 | } 330 | //#endregion 331 | 332 | //#region Main 333 | // Add a 'muted' link next to 'login' for logged-out users 334 | let $loginLink = document.querySelector('span.pagetop a[href^="login"]') 335 | if ($loginLink) { 336 | $loginLink.parentElement.append( 337 | h('a', {href: `muted`}, 'muted'), 338 | ' | ', 339 | $loginLink, 340 | ) 341 | } 342 | 343 | // Add /upvoted if we're not on it and the user is logged in 344 | if (!location.pathname.startsWith('/upvoted')) { 345 | let $userLink = document.querySelector('span.pagetop a[href^="user?id"]') 346 | if ($userLink) { 347 | let $submit = $pageTop.querySelector('a[href="submit"]') 348 | $submit.insertAdjacentElement('afterend', h('a', {href: `upvoted?id=${$userLink.textContent}`}, 'upvoted')) 349 | $submit.insertAdjacentElement('afterend', h('span', {className: 'upvoted-sep'}, ' | ')) 350 | } 351 | } 352 | 353 | // Wrap separators in elements so they can be used to hide items 354 | Array.from($pageTop.childNodes) 355 | .filter(n => n.nodeType == Node.TEXT_NODE && n.nodeValue == ' | ') 356 | .forEach(n => n.replaceWith(h('span', {className: `${n.nextSibling?.textContent}-sep`}, ' | '))) 357 | 358 | // Create a new row for mobile nav 359 | let $mobileNav = /** @type {HTMLTableCellElement} */ ($pageTop.parentElement.cloneNode(true)) 360 | $mobileNav.querySelector('b')?.remove() 361 | $mobileNav.colSpan = 3 362 | $pageTop.closest('tbody').append(h('tr', {className: 'mobilenav'}, $mobileNav)) 363 | 364 | // Move everything after b.hnname into a desktop nav wrapper 365 | $pageTop.appendChild(h('span', {className: 'desktopnav'}, ...Array.from($pageTop.childNodes).slice(1))) 366 | 367 | configureCss() 368 | 369 | chrome.storage.local.onChanged.addListener((changes) => { 370 | for (let [configProp, change] of Object.entries(changes)) { 371 | if (['hidePastNav', 'hideCommentsNav', 'hideJobsNav', 'hideSubmitNav', 'addUpvotedToHeader'].includes(configProp)) { 372 | config[configProp] = change.newValue 373 | configureCss() 374 | } 375 | } 376 | }) 377 | //#endregion 378 | } 379 | //#endregion 380 | 381 | //#region Comment page 382 | /** 383 | * Each comment on a comment page has the following structure: 384 | * 385 | * ```html 386 | * (wrapper) 387 | * 388 | * 389 | * 390 | * 393 | * (vote up/down controls) 394 | * … (rank, upvote control, title/link and domain) 1102 | * 1103 | * (spacer) 1104 | * 1107 | * 1108 | * … 1109 | * ``` 1110 | * 1111 | * Using the comment count stored when you visit a comment page, we'll display 1112 | * the number of new comments in the subtext section and provide a link which 1113 | * will automatically highlight new comments and collapse comment trees without 1114 | * new comments. 1115 | * 1116 | * For regular stories, the subtext element contains points, user, age (in 1117 | * a link to the comments page), flag/hide controls and finally the number of 1118 | * comments (in another link to the comments page). We'll look for the latter 1119 | * to detemine the current number of comments and the item id. 1120 | * 1121 | * For job postings, the subtext element only contains age (in 1122 | * a link to the comments page) and a hide control, so we'll try to ignore 1123 | * those. 1124 | */ 1125 | function itemListPage() { 1126 | log('item list page') 1127 | 1128 | //#region CSS 1129 | let $style = addStyle('list-dynamic') 1130 | 1131 | function configureCss() { 1132 | $style.textContent = [ 1133 | // Hide flag links 1134 | config.listPageFlagging == 'disabled' && ` 1135 | .flag-sep, .flag-sep + a { 1136 | display: none; 1137 | } 1138 | `, 1139 | // Hide hide links 1140 | config.listPageHiding == 'disabled' && ` 1141 | .hide-sep, .hide-sep + a { 1142 | display: none; 1143 | } 1144 | ` 1145 | ].filter(Boolean).map(dedent).join('\n') 1146 | } 1147 | //#endregion 1148 | 1149 | //#region Functions 1150 | function confirmFlag(e) { 1151 | if (config.listPageFlagging != 'confirm') return 1152 | let title = e.target.closest('tr').previousElementSibling.querySelector('.titleline a')?.textContent || 'this item' 1153 | if (!confirm(`Are you sure you want to flag "${title}"?`)) { 1154 | e.stopPropagation() 1155 | e.stopImmediatePropagation() 1156 | e.preventDefault() 1157 | return false 1158 | } 1159 | } 1160 | 1161 | function confirmHide(e) { 1162 | if (config.listPageHiding != 'confirm') return 1163 | let title = e.target.closest('tr').previousElementSibling.querySelector('.titleline a')?.textContent || 'this item' 1164 | if (!confirm(`Are you sure you want to hide "${title}"?`)) { 1165 | e.stopPropagation() 1166 | e.stopImmediatePropagation() 1167 | e.preventDefault() 1168 | return false 1169 | } 1170 | } 1171 | //#endregion 1172 | 1173 | //#region Main 1174 | if (location.pathname != '/flagged') { 1175 | for (let $flagLink of document.querySelectorAll('span.subline > a[href^="flag"]')) { 1176 | // Wrap the '|' before flag links in an element so they can be hidden 1177 | $flagLink.previousSibling.replaceWith(h('span', {className: 'flag-sep'}, ' | ')) 1178 | $flagLink.addEventListener('click', confirmFlag, true) 1179 | } 1180 | } 1181 | 1182 | if (location.pathname != '/hidden') { 1183 | for (let $hideLink of document.querySelectorAll('span.subline > a[href^="hide"]')) { 1184 | // Wrap the '|' before hide links in an element so they can be hidden 1185 | $hideLink.previousSibling.replaceWith(h('span', {className: 'hide-sep'}, ' | ')) 1186 | $hideLink.addEventListener('click', confirmHide, true) 1187 | } 1188 | } 1189 | 1190 | let commentLinks = /** @type {NodeListOf} */ (document.querySelectorAll('span.subline > a[href^="item?id="]:last-child')) 1191 | log('number of comments/discuss links', commentLinks.length) 1192 | 1193 | let noCommentsCount = 0 1194 | let noLastVisitCount = 0 1195 | 1196 | for (let $commentLink of commentLinks) { 1197 | let id = $commentLink.href.split('=').pop() 1198 | 1199 | let commentCountMatch = /^(\d+)/.exec($commentLink.textContent) 1200 | if (commentCountMatch == null) { 1201 | noCommentsCount++ 1202 | continue 1203 | } 1204 | 1205 | let lastVisit = getLastVisit(id) 1206 | if (lastVisit == null) { 1207 | noLastVisitCount++ 1208 | continue 1209 | } 1210 | 1211 | let commentCount = Number(commentCountMatch[1]) 1212 | if (commentCount <= lastVisit.commentCount) { 1213 | log(`${id} doesn't have any new comments`, lastVisit) 1214 | continue 1215 | } 1216 | 1217 | $commentLink.insertAdjacentElement('afterend', 1218 | h('span', null, 1219 | ' (', 1220 | h('a', { 1221 | href: `item?shownew&id=${id}`, 1222 | style: {fontWeight: 'bold'}, 1223 | }, 1224 | commentCount - lastVisit.commentCount, 1225 | ' new' 1226 | ), 1227 | ')', 1228 | ) 1229 | ) 1230 | } 1231 | 1232 | if (noCommentsCount > 0) { 1233 | log(`${noCommentsCount} item${s(noCommentsCount, " doesn't,s don't")} have any comments`) 1234 | } 1235 | if (noLastVisitCount > 0) { 1236 | log(`${noLastVisitCount} item${s(noLastVisitCount, " doesn't,s don't")} have a last visit stored`) 1237 | } 1238 | 1239 | configureCss() 1240 | 1241 | chrome.storage.local.onChanged.addListener((changes) => { 1242 | if ('listPageFlagging' in changes) { 1243 | config.listPageFlagging = changes['listPageFlagging'].newValue 1244 | configureCss() 1245 | } 1246 | if ('listPageHiding' in changes) { 1247 | config.listPageHiding = changes['listPageHiding'].newValue 1248 | configureCss() 1249 | } 1250 | }) 1251 | //#endregion 1252 | } 1253 | //#endregion 1254 | 1255 | //#region Profile page 1256 | function userProfilePage() { 1257 | log('user profile page') 1258 | 1259 | let $userLink = /** @type {HTMLAnchorElement} */ (document.querySelector('a.hnuser')) 1260 | if ($userLink == null) { 1261 | warn('not a valid user') 1262 | return 1263 | } 1264 | 1265 | let userId = $userLink.innerText 1266 | let $currentUserLink = /** @type {HTMLAnchorElement} */ (document.querySelector('a#me')) 1267 | let currentUser = $currentUserLink?.innerText ?? '' 1268 | let mutedUsers = getMutedUsers() 1269 | let userNotes = getUserNotes() 1270 | let $table = $userLink.closest('table') 1271 | 1272 | if (userId == currentUser || location.pathname.startsWith('/muted')) { 1273 | //#region Logged-in user's profile 1274 | let $mutedUsers = createMutedUsers() 1275 | 1276 | function createMutedUsers() { 1277 | if (mutedUsers.size == 0) { 1278 | return h('tbody', null, 1279 | h('tr', null, 1280 | h('td', {valign: 'top'}, 'muted:'), 1281 | h('td', null, 'No muted users.') 1282 | ) 1283 | ) 1284 | } 1285 | 1286 | let first = 0 1287 | return h('tbody', null, 1288 | ...Array.from(mutedUsers).map((mutedUserId) => h('tr', null, 1289 | h('td', {valign: 'top'}, first++ == 0 ? 'muted:' : ''), 1290 | h('td', null, 1291 | h('a', {href: `user?id=${mutedUserId}`}, mutedUserId), 1292 | h('a', { 1293 | href: '#', 1294 | onClick: function(e) { 1295 | e.preventDefault() 1296 | mutedUsers = getMutedUsers() 1297 | mutedUsers.delete(mutedUserId) 1298 | storeMutedUsers(mutedUsers) 1299 | replaceMutedUsers() 1300 | } 1301 | }, 1302 | ' (', h('u', null, 'unmute'), ')' 1303 | ), 1304 | userNotes[mutedUserId] ? ` - ${userNotes[mutedUserId].split(/\r?\n/)[0]}` : null, 1305 | ), 1306 | )) 1307 | ) 1308 | } 1309 | 1310 | function replaceMutedUsers() { 1311 | let $newMutedUsers = createMutedUsers() 1312 | $mutedUsers.replaceWith($newMutedUsers) 1313 | $mutedUsers = $newMutedUsers 1314 | } 1315 | 1316 | $table.append($mutedUsers) 1317 | 1318 | window.addEventListener('storage', (e) => { 1319 | if (e.storageArea !== localStorage || 1320 | e.newValue == null || 1321 | e.key != MUTED_USERS_KEY && e.key != USER_NOTES_KEY) { 1322 | return 1323 | } 1324 | 1325 | if (e.key == MUTED_USERS_KEY) { 1326 | mutedUsers = getMutedUsers(e.newValue) 1327 | } 1328 | else if (e.key == USER_NOTES_KEY) { 1329 | userNotes = getUserNotes(e.newValue) 1330 | } 1331 | 1332 | replaceMutedUsers() 1333 | }) 1334 | //#endregion 1335 | } 1336 | else { 1337 | //#region Other user profile 1338 | addStyle('profile-static', ` 1339 | .saved { 1340 | color: #000; 1341 | opacity: 0; 1342 | } 1343 | .saved.show { 1344 | animation: flash 2s forwards; 1345 | } 1346 | @keyframes flash { 1347 | from { 1348 | opacity: 0; 1349 | } 1350 | 15% { 1351 | opacity: 1; 1352 | animation-timing-function: ease-in; 1353 | } 1354 | 75% { 1355 | opacity: 1; 1356 | } 1357 | to { 1358 | opacity: 0; 1359 | animation-timing-function: ease-out; 1360 | } 1361 | } 1362 | .notes { 1363 | display: flex; 1364 | flex-direction: column; 1365 | align-items: flex-start; 1366 | gap: 3px; 1367 | } 1368 | `) 1369 | 1370 | function getMutedStatusText() { 1371 | return mutedUsers.has(userId) ? 'unmute' : 'mute' 1372 | } 1373 | 1374 | function getUserNote() { 1375 | return userNotes[userId] || '' 1376 | } 1377 | 1378 | function userHasNote() { 1379 | return userNotes.hasOwnProperty(userId) 1380 | } 1381 | 1382 | function saveNotes() { 1383 | userNotes = getUserNotes() 1384 | let note = $textArea.value.trim() 1385 | 1386 | // Don't save initial blanks or duplicates 1387 | if (userNotes[userId] == note || note == '' && !userHasNote()) return 1388 | 1389 | userNotes[userId] = $textArea.value.trim() 1390 | storeUserNotes(userNotes) 1391 | 1392 | if ($saved.classList.contains('show')) { 1393 | $saved.classList.remove('show') 1394 | $saved.offsetHeight 1395 | } 1396 | $saved.classList.add('show') 1397 | } 1398 | 1399 | let $textArea = /** @type {HTMLTextAreaElement} */ (h('textarea', { 1400 | cols: 60, 1401 | value: userNotes[userId] || '', 1402 | className: 'notes', 1403 | style: {resize: 'none'}, 1404 | onInput() { 1405 | autosizeTextArea(this) 1406 | }, 1407 | onKeydown(e) { 1408 | // Save on Use Ctrl+Enter / Cmd+Return 1409 | if (e.key == 'Enter' && (e.ctrlKey || e.metaKey)) { 1410 | e.preventDefault() 1411 | saveNotes() 1412 | } 1413 | }, 1414 | onBlur() { 1415 | saveNotes() 1416 | } 1417 | })) 1418 | 1419 | let $muted = h('u', null, getMutedStatusText()) 1420 | let $saved = h('span', {className: 'saved'}, 'saved') 1421 | 1422 | $table.querySelector('tbody').append( 1423 | h('tr', null, 1424 | h('td'), 1425 | h('td', null, 1426 | h('a', { 1427 | href: '#', 1428 | onClick: function(e) { 1429 | e.preventDefault() 1430 | if (mutedUsers.has(userId)) { 1431 | mutedUsers = getMutedUsers() 1432 | mutedUsers.delete(userId) 1433 | this.firstElementChild.innerText = 'mute' 1434 | } 1435 | else { 1436 | mutedUsers = getMutedUsers() 1437 | mutedUsers.add(userId) 1438 | this.firstElementChild.innerText = 'unmute' 1439 | } 1440 | storeMutedUsers(mutedUsers) 1441 | } 1442 | }, 1443 | $muted 1444 | ) 1445 | ) 1446 | ), 1447 | h('tr', null, 1448 | h('td', {vAlign: 'top'}, 'notes:'), 1449 | h('td', {className: 'notes'}, $textArea, $saved), 1450 | ), 1451 | ) 1452 | 1453 | autosizeTextArea($textArea) 1454 | 1455 | window.addEventListener('storage', (e) => { 1456 | if (e.storageArea !== localStorage || e.newValue == null) return 1457 | 1458 | if (e.key == MUTED_USERS_KEY) { 1459 | mutedUsers = getMutedUsers(e.newValue) 1460 | if ($muted.textContent != getMutedStatusText()) { 1461 | $muted.textContent = getMutedStatusText() 1462 | } 1463 | } 1464 | else if (e.key == USER_NOTES_KEY) { 1465 | userNotes = getUserNotes(e.newValue) 1466 | if (userHasNote() && $textArea.value.trim() != getUserNote()) { 1467 | $textArea.value = getUserNote() 1468 | } 1469 | } 1470 | }) 1471 | //#endregion 1472 | } 1473 | } 1474 | //#endregion 1475 | 1476 | //#region Main 1477 | function main() { 1478 | log('config', config) 1479 | 1480 | if (location.pathname.startsWith('/login')) { 1481 | log('login screen') 1482 | if (isSafari) { 1483 | log('trying to prevent Safari zooming in on the autofocused input') 1484 | addStyle('login-safari', `input[type="text"], input[type="password"] { font-size: 16px; }`) 1485 | setTimeout(() => { 1486 | document.querySelector('input[type="password"]').focus() 1487 | document.querySelector('input[type="text"]').focus() 1488 | }) 1489 | } 1490 | return 1491 | } 1492 | 1493 | if (location.pathname.startsWith('/muted')) { 1494 | document.documentElement.innerHTML = LOGGED_OUT_USER_PAGE 1495 | // Safari on macOS has a default dark background in dark mode 1496 | if (isSafari) { 1497 | addStyle('muted-safari', 'html { background-color: #fff; }') 1498 | } 1499 | } 1500 | 1501 | tweakNav() 1502 | 1503 | let path = location.pathname.slice(1) 1504 | 1505 | if (/^($|active|ask|best($|\?)|flagged|front|hidden|invited|launches|news|newest|noobstories|pool|show|submitted|upvoted)/.test(path) || 1506 | /^favorites/.test(path) && !location.search.includes('&comments=t')) { 1507 | itemListPage() 1508 | } 1509 | else if (/^item/.test(path)) { 1510 | commentPage() 1511 | } 1512 | else if (/^(user|muted)/.test(path)) { 1513 | userProfilePage() 1514 | } 1515 | } 1516 | 1517 | if ( 1518 | typeof GM == 'undefined' && 1519 | typeof chrome != 'undefined' && 1520 | typeof chrome.storage != 'undefined' 1521 | ) { 1522 | chrome.storage.local.get((storedConfig) => { 1523 | Object.assign(config, storedConfig) 1524 | main() 1525 | }) 1526 | } 1527 | else { 1528 | main() 1529 | } 1530 | //#endregion -------------------------------------------------------------------------------- /icons/chrome-web-store-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/icons/chrome-web-store-icon.png -------------------------------------------------------------------------------- /icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/icons/icon128.png -------------------------------------------------------------------------------- /icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/icons/icon16.png -------------------------------------------------------------------------------- /icons/icon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/icons/icon256.png -------------------------------------------------------------------------------- /icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/icons/icon48.png -------------------------------------------------------------------------------- /icons/icon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/icons/icon512.png -------------------------------------------------------------------------------- /icons/icon600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/icons/icon600.png -------------------------------------------------------------------------------- /icons/icon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/icons/icon64.png -------------------------------------------------------------------------------- /icons/icon96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/icons/icon96.png -------------------------------------------------------------------------------- /icons/toolbar-icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/icons/toolbar-icon16.png -------------------------------------------------------------------------------- /icons/toolbar-icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/icons/toolbar-icon19.png -------------------------------------------------------------------------------- /icons/toolbar-icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/icons/toolbar-icon32.png -------------------------------------------------------------------------------- /icons/toolbar-icon38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/icons/toolbar-icon38.png -------------------------------------------------------------------------------- /icons/toolbar-icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/icons/toolbar-icon48.png -------------------------------------------------------------------------------- /icons/toolbar-icon72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/icons/toolbar-icon72.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "module": "NodeNext", 5 | "moduleDetection": "force", 6 | "moduleResolution": "nodenext", 7 | "target": "ES2022" 8 | }, 9 | "exclude": [ 10 | "node_modules" 11 | ] 12 | } -------------------------------------------------------------------------------- /manifest.mv2.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "default_locale": "en", 4 | "name": "__MSG_extensionName__", 5 | "description": "__MSG_extensionDescription__", 6 | "homepage_url": "https://soitis.dev/comments-owl-for-hacker-news", 7 | "version": "3.0.0", 8 | "icons": { 9 | "16": "icons/icon16.png", 10 | "48": "icons/icon48.png", 11 | "64": "icons/icon64.png", 12 | "96": "icons/icon96.png", 13 | "128": "icons/icon128.png" 14 | }, 15 | "background": { 16 | "scripts": [ 17 | "background.js" 18 | ] 19 | }, 20 | "content_scripts": [ 21 | { 22 | "matches": [ 23 | "https://news.ycombinator.com/*" 24 | ], 25 | "js": [ 26 | "content.js" 27 | ] 28 | } 29 | ], 30 | "options_ui": { 31 | "browser_style": true, 32 | "chrome_style": false, 33 | "page": "options.html" 34 | }, 35 | "browser_action": { 36 | "browser_style": true, 37 | "default_title": "__MSG_extensionName__", 38 | "default_popup": "browser_action.html" 39 | }, 40 | "permissions": [ 41 | "contextMenus", 42 | "storage" 43 | ], 44 | "browser_specific_settings": { 45 | "gecko_android": { 46 | "strict_min_version": "113.0" 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /manifest.mv3.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "default_locale": "en", 4 | "name": "__MSG_extensionName__", 5 | "description": "__MSG_extensionDescription__", 6 | "homepage_url": "https://soitis.dev/comments-owl-for-hacker-news", 7 | "version": "3.0.0", 8 | "icons": { 9 | "16": "icons/icon16.png", 10 | "48": "icons/icon48.png", 11 | "64": "icons/icon64.png", 12 | "96": "icons/icon96.png", 13 | "128": "icons/icon128.png" 14 | }, 15 | "background": { 16 | "service_worker": "background.js" 17 | }, 18 | "content_scripts": [ 19 | { 20 | "matches": [ 21 | "https://news.ycombinator.com/*" 22 | ], 23 | "js": [ 24 | "content.js" 25 | ] 26 | } 27 | ], 28 | "options_ui": { 29 | "page": "options.html" 30 | }, 31 | "action": { 32 | "default_title": "__MSG_extensionName__", 33 | "default_popup": "browser_action.html" 34 | }, 35 | "permissions": [ 36 | "contextMenus", 37 | "storage" 38 | ] 39 | } -------------------------------------------------------------------------------- /options.css: -------------------------------------------------------------------------------- 1 | :root { 2 | color-scheme: light dark; 3 | --border-color: #f0f0f0; 4 | } 5 | 6 | body { 7 | user-select: none; 8 | padding: 0; 9 | margin: 0; 10 | /* XXX These are defaults for Chrome, do something else instead */ 11 | font-size: 13px; 12 | font-family: system-ui, sans-serif; 13 | } 14 | 15 | body.browserAction { 16 | min-width: 400px; 17 | } 18 | body.iOS.browserAction { 19 | min-width: 0; 20 | } 21 | body.macOS.browserAction { 22 | min-width: 340px; 23 | } 24 | 25 | body:not(.iOS.safari) .toggle { 26 | display: none; 27 | } 28 | 29 | label { 30 | display: flex; 31 | justify-content: space-between; 32 | align-items: center; 33 | padding: 4px 12px; 34 | margin: 8px 0; 35 | cursor: pointer; 36 | } 37 | 38 | input[type=checkbox], 39 | select { 40 | cursor: pointer; 41 | } 42 | 43 | input[type=checkbox] { 44 | margin-left: 12px; 45 | flex-shrink: 0; 46 | } 47 | 48 | section.group > label:not(.checkbox) { 49 | cursor: default; 50 | } 51 | 52 | section:not(:first-of-type) { 53 | border-top: 1px solid var(--border-color); 54 | } 55 | 56 | section.group > section { 57 | margin-left: 40px; 58 | } 59 | 60 | section.group > label { 61 | margin-bottom: 12px; 62 | } 63 | 64 | form > section.group:first-of-type > label { 65 | margin-bottom: 8px; 66 | } 67 | 68 | section.group > section > * { 69 | color: rgb(95, 99, 104); 70 | padding-left: 0; 71 | } 72 | 73 | section.group > p { 74 | margin: 0 0 12px 40px; 75 | font-size: 12px; 76 | } 77 | 78 | section.group > section > p { 79 | margin: 0 40px 12px 0; 80 | font-size: 12px; 81 | } 82 | #version { 83 | text-align: center; 84 | font-size: 75%; 85 | margin-top: 12px; 86 | } 87 | 88 | /* Firefox overrides */ 89 | @-moz-document url-prefix() { 90 | body { 91 | font-family: inherit; 92 | font-size: 15px; 93 | } 94 | body.browserAction { 95 | min-width: auto; 96 | max-width: 400px; 97 | } 98 | section { 99 | --border-color: #d7d7db; 100 | } 101 | section.group > p, 102 | section.group > section > * { 103 | color: rgb(91, 91, 102); 104 | } 105 | section.group > p, 106 | section.group > section > p { 107 | font-size: 14px; 108 | } 109 | } 110 | 111 | /* Edge overrides */ 112 | body.edge { 113 | font-size: 14px; 114 | } 115 | body.edge section { 116 | --border-color: #B6B6B6; 117 | } 118 | body.edge section.group > label { 119 | font-weight: 600; 120 | } 121 | body.edge section.group > p, 122 | body.edge section.group > section > * { 123 | color: #767676; 124 | } 125 | 126 | /* Safari overrides */ 127 | body.safari { 128 | -webkit-user-select: none; 129 | } 130 | 131 | /* Safari overrides (macOS) */ 132 | body.macOS.safari form { 133 | padding: 4px 0; 134 | } 135 | /* Space option groups */ 136 | body.macOS.safari form > section:not(:first-of-type) { 137 | margin-top: 20px; 138 | } 139 | /* Add colons to option group labels */ 140 | body.macOS.safari section.group > label:not(.checkbox)::after { 141 | content: ":"; 142 | } 143 | body.macOS.safari form > section.group > label { 144 | margin-bottom: 0; 145 | } 146 | /* Indent options to align with the toggle all checkbox if there is one */ 147 | body.macOS.safari section.group > section { 148 | margin-left: 32px; 149 | } 150 | /* Align option group help text with options */ 151 | body.macOS.safari form > section.group > p { 152 | margin: 12px 12px 12px 32px; 153 | } 154 | /* Don't display dividing lines between option groups or options */ 155 | body.macOS.safari section { 156 | border-top: none; 157 | } 158 | /* Indent nested options to align with checkbox labels */ 159 | body.macOS.safari section.group > section > section { 160 | margin-left: 8px; 161 | } 162 | /* Put controls next to their labels... */ 163 | body.macOS.safari label { 164 | justify-content: start; 165 | align-items: center; 166 | padding-top: 0; 167 | padding-bottom: 0; 168 | } 169 | /* Put checkboxes before their labels */ 170 | body.macOS.safari section.checkbox > label, 171 | body.macOS.safari label.checkbox { 172 | flex-direction: row-reverse; 173 | } 174 | body.macOS.safari input[type=checkbox] { 175 | margin-left: 0; 176 | margin-right: 6px; 177 | } 178 | /* Add colons to dropdown labels */ 179 | body.macOS.safari section.select label { 180 | display: block; 181 | } 182 | body.macOS.safari section.select span::after { 183 | content: ": "; 184 | display: inline-block 185 | } 186 | /* Align help text with checkbox labels */ 187 | body.macOS.safari section.checkbox > p { 188 | margin-left: 20px; 189 | margin-right: 12px; 190 | } 191 | body.macOS.safari p { 192 | color: rgb(123, 123, 123) !important; 193 | } 194 | body.macOS.safari section.group > section > * { 195 | color: inherit; 196 | } 197 | 198 | /* Safari overrides (iOS) */ 199 | body.iOS.safari { 200 | background-color: rgb(240, 240, 245); 201 | padding: 18px; 202 | font-size: inherit; 203 | } 204 | body.iOS.safari label { 205 | /* Prevent flash when labels are tapped */ 206 | -webkit-tap-highlight-color: transparent; 207 | padding-left: 0; 208 | padding-right: 18px; 209 | } 210 | /* Option groups should contain their options in a rounded box */ 211 | body.iOS.safari form > section { 212 | background-color: white; 213 | border-radius: 10px; 214 | padding-left: 18px; 215 | padding-bottom: 1px; 216 | padding-top: 1px; 217 | border-top: none !important; 218 | } 219 | body.iOS.safari form > section:not(:first-of-type) { 220 | margin-top: 34px; 221 | } 222 | /* All options should have a dividing line */ 223 | body.iOS.safari section.group section { 224 | border-top: 1px solid rgb(236, 236, 237); 225 | } 226 | /* Options should not be indented by default */ 227 | body.iOS.safari section.group > section { 228 | margin-left: 0; 229 | } 230 | /* Groups which have a toggle for all their options should indent their options */ 231 | body.iOS.safari section.group > label.checkbox ~ section, 232 | /* Nested groups should indent their options */ 233 | body.iOS.safari section.group > section.group > section { 234 | margin-left: 18px; 235 | } 236 | /* Labelled option groups should display their label above the box */ 237 | body.iOS.safari form > section.labelled { 238 | position: relative; 239 | margin-top: 46px; 240 | } 241 | body.iOS.safari form > section:first-child.labelled { 242 | margin-top: 12px; 243 | } 244 | body.iOS.safari form > section.labelled > label { 245 | position: absolute; 246 | top: -32px; 247 | text-transform: uppercase; 248 | font-size: 75%; 249 | color: rgb(133, 133, 135); 250 | } 251 | body.iOS.safari form > section.labelled > label + section, 252 | body.iOS.safari form > section.labelled > label + section.desktop + section { 253 | border-top: none; 254 | } 255 | body.iOS.safari form > section.labelled > label + p { 256 | margin-top: 12px; 257 | margin-left: 0; 258 | } 259 | /* A checkbox options with nested options should indent their names */ 260 | body.iOS.safari section.checkbox > section:not(.group) > label { 261 | padding-left: 18px 262 | } 263 | /* A checkbox option with nested group should indent it */ 264 | body.iOS.safari section.checkbox > section.group { 265 | margin-left: 18px 266 | } 267 | body.iOS.safari summary { 268 | padding-left: 0; 269 | } 270 | body.iOS.safari p { 271 | font-size: inherit !important; 272 | color: rgb(95, 99, 104) !important; 273 | margin-right: 18px; 274 | } 275 | body.iOS.safari section.group > section > * { 276 | color: inherit; 277 | } 278 | 279 | /* iOS-style toggles */ 280 | body.iOS.safari .checkbox input[type="checkbox"] { 281 | position: absolute; 282 | overflow: hidden; 283 | clip: rect(0 0 0 0); 284 | height: 1px; 285 | width: 1px; 286 | margin: -1px; 287 | padding: 0; 288 | border: 0; 289 | } 290 | body.iOS.safari .checkbox .toggle { 291 | position: relative; 292 | display: inline-block; 293 | min-width: 46px; 294 | height: 26px; 295 | background-color: #e6e6e6; 296 | border-radius: 23px; 297 | vertical-align: text-bottom; 298 | transition: all 0.3s linear; 299 | margin-left: 8px; 300 | } 301 | body.iOS.safari .checkbox .toggle::before { 302 | content: ""; 303 | position: absolute; 304 | left: 0; 305 | width: 42px; 306 | height: 22px; 307 | background-color: #fff; 308 | border-radius: 11px; 309 | transform: translate3d(2px, 2px, 0) scale3d(1, 1, 1); 310 | transition: all 0.25s linear; 311 | } 312 | body.iOS.safari .checkbox .toggle::after { 313 | content: ""; 314 | position: absolute; 315 | left: 0; 316 | width: 22px; 317 | height: 22px; 318 | background-color: #fff; 319 | border-radius: 11px; 320 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.24); 321 | transform: translate3d(2px, 2px, 0); 322 | transition: all 0.2s ease-in-out; 323 | } 324 | body.iOS.safari .checkbox label:active .toggle::after, 325 | body.iOS.safari label.checkbox:active .toggle:after { 326 | width: 28px; 327 | transform: translate3d(2px, 2px, 0); 328 | } 329 | body.iOS.safari .checkbox label:active input:checked + .toggle::after, 330 | body.iOS.safari label.checkbox:active input:checked + .toggle::after { 331 | transform: translate3d(16px, 2px, 0); 332 | } 333 | body.iOS.safari input:checked + .toggle { 334 | background-color: #4BD763; 335 | } 336 | body.iOS.safari input:checked + .toggle::before { 337 | transform: translate3d(18px, 2px, 0) scale3d(0, 0, 0); 338 | } 339 | body.iOS.safari input:checked + .toggle::after { 340 | transform: translate3d(22px, 2px, 0); 341 | } 342 | body.iOS.safari .checkbox input:focus + .toggle { 343 | outline: 5px auto Highlight; 344 | outline: 5px auto -webkit-focus-ring-color; 345 | } 346 | 347 | /* Dark mode overrides */ 348 | @media (prefers-color-scheme: dark) { 349 | body { 350 | background-color: #292a2d; 351 | color: #e8eaed; 352 | } 353 | section { 354 | --border-color: #3f4042; 355 | } 356 | section.group > p, 357 | section.group > section > * { 358 | color: rgb(154, 160, 166); 359 | } 360 | 361 | /* Edge dark mode overrides */ 362 | body.edge { 363 | background-color: #3B3B3B; 364 | color: #A7A7A7; 365 | } 366 | body.edge section.group > label { 367 | color: #fff; 368 | } 369 | body.edge section { 370 | --border-color: #737373; 371 | } 372 | body.edge section.group > p, 373 | body.edge section.group > section > * { 374 | color: #A7A7A7; 375 | } 376 | 377 | /* Firefox dark mode overrides */ 378 | @-moz-document url-prefix() { 379 | body { 380 | background-color: #23222b; 381 | } 382 | section { 383 | --border-color: #4e4d54; 384 | } 385 | section.group > p, 386 | section.group > section > * { 387 | color: rgb(191, 191, 201); 388 | } 389 | } 390 | 391 | /* Safari dark mode overrides */ 392 | body.macOS.safari p { 393 | color: rgb(184, 184, 184) !important; 394 | } 395 | body.iOS.safari { 396 | background-color: rgb(0, 0, 0); 397 | } 398 | body.iOS.safari form > section { 399 | background-color: rgb(28, 28, 30); 400 | } 401 | body.iOS.safari form > section.labelled > label { 402 | color: rgb(115, 115, 121); 403 | } 404 | body.iOS.safari section.group section { 405 | --border-color: rgb(35, 35, 37); 406 | } 407 | body.iOS.safari p { 408 | color: rgb(132, 132, 138) !important; 409 | } 410 | body.iOS.safari .checkbox .toggle { 411 | background-color: rgb(57, 57, 61); 412 | } 413 | body.iOS.safari .checkbox .toggle::before { 414 | background-color: rgb(57, 57, 61); 415 | } 416 | } -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | 15 |
16 | 24 | 32 |

Prevent accidental flagging/hiding on mobile

33 |
34 |
35 |
36 | 39 |
40 | 45 |
46 |
47 | 52 |
53 |
54 | 59 |
60 |
61 | 66 |
67 |
68 |
69 | 72 |
73 | 78 |
79 |
80 | 85 |
86 |
87 | 92 |
93 |
94 | 99 |
100 |
101 | 106 |
107 |
108 |
v3.0.0
109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | document.title = chrome.i18n.getMessage('extensionName') 2 | 3 | for (let optionValue of [ 4 | 'confirm', 5 | 'disabled', 6 | 'enabled', 7 | ]) { 8 | let label = chrome.i18n.getMessage(`option_${optionValue}`) 9 | for (let $option of document.querySelectorAll(`option[value="${optionValue}"]`)) { 10 | $option.textContent = label 11 | } 12 | } 13 | 14 | for (let translationId of [ 15 | 'addUpvotedToHeader', 16 | 'autoCollapseNotNew', 17 | 'autoHighlightNew', 18 | 'commentPagesOptions', 19 | 'hideCommentsNav', 20 | 'hideJobsNav', 21 | 'hidePastNav', 22 | 'hideReplyLinks', 23 | 'hideSubmitNav', 24 | 'listPageAccidentallyInfo', 25 | 'listPageFlagging', 26 | 'listPageHiding', 27 | 'listPagesOptions', 28 | 'makeSubmissionTextReadable', 29 | 'navigationOptions', 30 | ]) { 31 | document.getElementById(translationId).textContent = chrome.i18n.getMessage(translationId) 32 | } 33 | 34 | let $body = document.body 35 | let $form = document.querySelector('form') 36 | 37 | if (navigator.userAgent.includes('Safari/') && !/Chrom(e|ium)\//.test(navigator.userAgent)) { 38 | $body.classList.add('safari', /iP(ad|hone)/.test(navigator.userAgent) ? 'iOS' : 'macOS') 39 | } else { 40 | $body.classList.toggle('edge', navigator.userAgent.includes('Edg/')) 41 | } 42 | 43 | function setFormValue(prop, value) { 44 | if (!$form.elements.hasOwnProperty(prop)) return 45 | 46 | let $el = /** @type {HTMLInputElement} */ ($form.elements[prop]) 47 | if ($el.type == 'checkbox') { 48 | $el.checked = value 49 | } else { 50 | $el.value = value 51 | } 52 | } 53 | 54 | /** @type {import("./types").Config} */ 55 | let defaultConfig = { 56 | addUpvotedToHeader: true, 57 | autoCollapseNotNew: true, 58 | autoHighlightNew: true, 59 | hideCommentsNav: false, 60 | hideJobsNav: false, 61 | hidePastNav: false, 62 | hideReplyLinks: false, 63 | hideSubmitNav: false, 64 | listPageFlagging: 'enabled', 65 | listPageHiding: 'enabled', 66 | makeSubmissionTextReadable: true, 67 | } 68 | 69 | /** @type {import("./types").Config} */ 70 | let optionsConfig 71 | 72 | chrome.storage.local.get((storedConfig) => { 73 | optionsConfig = {...defaultConfig, ...storedConfig} 74 | 75 | for (let [prop, value] of Object.entries(optionsConfig)) { 76 | setFormValue(prop, value) 77 | } 78 | 79 | $form.addEventListener('change', (e) => { 80 | let $el = /** @type {HTMLInputElement} */ (e.target) 81 | let prop = $el.name 82 | let value = $el.type == 'checkbox' ? $el.checked : $el.value 83 | chrome.storage.local.set({[prop]: value}) 84 | }) 85 | 86 | chrome.storage.local.onChanged.addListener((changes) => { 87 | for (let prop in changes) { 88 | optionsConfig[prop] = changes[prop].newValue 89 | setFormValue(prop, changes[prop].newValue) 90 | } 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "create-browser-action": "node ./scripts/create-browser-action.js", 4 | "release": "node ./scripts/release.js", 5 | "prebuild": "npm run create-browser-action", 6 | "build": "node ./scripts/build.js", 7 | "build-mv2": "node ./scripts/build.js 2", 8 | "build-mv3": "node ./scripts/build.js 3", 9 | "lint-mv2": "npm run copy-mv2 && web-ext lint", 10 | "lint-mv3": "npm run copy-mv3 && web-ext lint", 11 | "copy-mv2": "node ./scripts/copy.js manifest.mv2.json manifest.json", 12 | "copy-mv3": "node ./scripts/copy.js manifest.mv3.json manifest.json" 13 | }, 14 | "webExt": { 15 | "ignoreFiles": [ 16 | "*.md", 17 | "icons/chrome-web-store-icon.png", 18 | "icons/*.svg", 19 | "icons/icon256.png", 20 | "icons/icon512.png", 21 | "icons/icon600.png", 22 | "icons/toolbar-*.png", 23 | "jsconfig.json", 24 | "manifest.mv2.json", 25 | "manifest.mv3.json", 26 | "package.json", 27 | "promo/", 28 | "safari/", 29 | "screenshots/", 30 | "scripts/", 31 | "types.d.ts" 32 | ] 33 | }, 34 | "devDependencies": { 35 | "@types/chrome": "0.0.x", 36 | "@types/greasemonkey": "4.x", 37 | "semver": "7.x", 38 | "web-ext": "7.x" 39 | } 40 | } -------------------------------------------------------------------------------- /promo/app-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/promo/app-store.png -------------------------------------------------------------------------------- /promo/chrome_small_promo_tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/promo/chrome_small_promo_tile.png -------------------------------------------------------------------------------- /promo/draw-the-rest-of-the-owl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/promo/draw-the-rest-of-the-owl.gif -------------------------------------------------------------------------------- /safari/.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ 2 | *.xcworkspace 3 | build/ -------------------------------------------------------------------------------- /safari/Comments Owl for Hacker News.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FB6A6CF12A595D2600636A10 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB6A6CF02A595D2600636A10 /* AppDelegate.swift */; }; 11 | FB6A6CF32A595D2600636A10 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB6A6CF22A595D2600636A10 /* SceneDelegate.swift */; }; 12 | FB6A6CF62A595D2600636A10 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6CF42A595D2600636A10 /* LaunchScreen.storyboard */; }; 13 | FB6A6CF92A595D2600636A10 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6CF72A595D2600636A10 /* Main.storyboard */; }; 14 | FB6A6D022A595D2600636A10 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB6A6D012A595D2600636A10 /* AppDelegate.swift */; }; 15 | FB6A6D052A595D2600636A10 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D032A595D2600636A10 /* Main.storyboard */; }; 16 | FB6A6D0C2A595D2600636A10 /* Comments Owl for Hacker News Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = FB6A6D0B2A595D2600636A10 /* Comments Owl for Hacker News Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 17 | FB6A6D162A595D2600636A10 /* Comments Owl for Hacker News Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = FB6A6D152A595D2600636A10 /* Comments Owl for Hacker News Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 18 | FB6A6D1C2A595D2600636A10 /* Main.html in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6CD72A595D2500636A10 /* Main.html */; }; 19 | FB6A6D1D2A595D2600636A10 /* Main.html in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6CD72A595D2500636A10 /* Main.html */; }; 20 | FB6A6D1E2A595D2600636A10 /* Icon.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6CD92A595D2500636A10 /* Icon.png */; }; 21 | FB6A6D1F2A595D2600636A10 /* Icon.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6CD92A595D2500636A10 /* Icon.png */; }; 22 | FB6A6D202A595D2600636A10 /* Style.css in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6CDA2A595D2500636A10 /* Style.css */; }; 23 | FB6A6D212A595D2600636A10 /* Style.css in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6CDA2A595D2500636A10 /* Style.css */; }; 24 | FB6A6D222A595D2600636A10 /* Script.js in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6CDB2A595D2500636A10 /* Script.js */; }; 25 | FB6A6D232A595D2600636A10 /* Script.js in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6CDB2A595D2500636A10 /* Script.js */; }; 26 | FB6A6D242A595D2600636A10 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB6A6CDC2A595D2500636A10 /* ViewController.swift */; }; 27 | FB6A6D252A595D2600636A10 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB6A6CDC2A595D2500636A10 /* ViewController.swift */; }; 28 | FB6A6D262A595D2600636A10 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6CDD2A595D2600636A10 /* Assets.xcassets */; }; 29 | FB6A6D272A595D2600636A10 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6CDD2A595D2600636A10 /* Assets.xcassets */; }; 30 | FB6A6D282A595D2600636A10 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB6A6CDF2A595D2600636A10 /* SafariWebExtensionHandler.swift */; }; 31 | FB6A6D292A595D2600636A10 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB6A6CDF2A595D2600636A10 /* SafariWebExtensionHandler.swift */; }; 32 | FB6A6D2E2A595D2600636A10 /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6CE32A595D2600636A10 /* manifest.json */; }; 33 | FB6A6D2F2A595D2600636A10 /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6CE32A595D2600636A10 /* manifest.json */; }; 34 | FB6A6D522A599C9800636A10 /* options.css in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D4A2A599C9700636A10 /* options.css */; }; 35 | FB6A6D532A599C9800636A10 /* options.css in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D4A2A599C9700636A10 /* options.css */; }; 36 | FB6A6D542A599C9800636A10 /* content.js in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D4B2A599C9800636A10 /* content.js */; }; 37 | FB6A6D552A599C9800636A10 /* content.js in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D4B2A599C9800636A10 /* content.js */; }; 38 | FB6A6D562A599C9800636A10 /* options.html in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D4C2A599C9800636A10 /* options.html */; }; 39 | FB6A6D572A599C9800636A10 /* options.html in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D4C2A599C9800636A10 /* options.html */; }; 40 | FB6A6D582A599C9800636A10 /* browser_action.html in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D4D2A599C9800636A10 /* browser_action.html */; }; 41 | FB6A6D592A599C9800636A10 /* browser_action.html in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D4D2A599C9800636A10 /* browser_action.html */; }; 42 | FB6A6D5C2A599C9800636A10 /* _locales in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D4F2A599C9800636A10 /* _locales */; }; 43 | FB6A6D5D2A599C9800636A10 /* _locales in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D4F2A599C9800636A10 /* _locales */; }; 44 | FB6A6D5E2A599C9800636A10 /* options.js in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D502A599C9800636A10 /* options.js */; }; 45 | FB6A6D5F2A599C9800636A10 /* options.js in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D502A599C9800636A10 /* options.js */; }; 46 | FB6A6D602A599C9800636A10 /* background.js in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D512A599C9800636A10 /* background.js */; }; 47 | FB6A6D612A599C9800636A10 /* background.js in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D512A599C9800636A10 /* background.js */; }; 48 | FB6A6D662A599D2F00636A10 /* icon256.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D622A599D2F00636A10 /* icon256.png */; }; 49 | FB6A6D672A599D2F00636A10 /* icon256.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D622A599D2F00636A10 /* icon256.png */; }; 50 | FB6A6D682A599D2F00636A10 /* icon96.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D632A599D2F00636A10 /* icon96.png */; }; 51 | FB6A6D692A599D2F00636A10 /* icon96.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D632A599D2F00636A10 /* icon96.png */; }; 52 | FB6A6D6A2A599D2F00636A10 /* icon128.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D642A599D2F00636A10 /* icon128.png */; }; 53 | FB6A6D6B2A599D2F00636A10 /* icon128.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D642A599D2F00636A10 /* icon128.png */; }; 54 | FB6A6D6C2A599D2F00636A10 /* icon48.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D652A599D2F00636A10 /* icon48.png */; }; 55 | FB6A6D6D2A599D2F00636A10 /* icon48.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D652A599D2F00636A10 /* icon48.png */; }; 56 | FB6A6D812A599E9B00636A10 /* icon512.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D802A599E9B00636A10 /* icon512.png */; }; 57 | FB6A6D822A599E9B00636A10 /* icon512.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D802A599E9B00636A10 /* icon512.png */; }; 58 | FB6A6D892A59ABA800636A10 /* toolbar-icon19.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D832A59ABA800636A10 /* toolbar-icon19.png */; }; 59 | FB6A6D8A2A59ABA800636A10 /* toolbar-icon19.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D832A59ABA800636A10 /* toolbar-icon19.png */; }; 60 | FB6A6D8B2A59ABA800636A10 /* toolbar-icon48.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D842A59ABA800636A10 /* toolbar-icon48.png */; }; 61 | FB6A6D8C2A59ABA800636A10 /* toolbar-icon48.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D842A59ABA800636A10 /* toolbar-icon48.png */; }; 62 | FB6A6D8D2A59ABA800636A10 /* toolbar-icon72.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D852A59ABA800636A10 /* toolbar-icon72.png */; }; 63 | FB6A6D8E2A59ABA800636A10 /* toolbar-icon72.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D852A59ABA800636A10 /* toolbar-icon72.png */; }; 64 | FB6A6D8F2A59ABA800636A10 /* toolbar-icon16.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D862A59ABA800636A10 /* toolbar-icon16.png */; }; 65 | FB6A6D902A59ABA800636A10 /* toolbar-icon16.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D862A59ABA800636A10 /* toolbar-icon16.png */; }; 66 | FB6A6D912A59ABA800636A10 /* toolbar-icon32.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D872A59ABA800636A10 /* toolbar-icon32.png */; }; 67 | FB6A6D922A59ABA800636A10 /* toolbar-icon32.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D872A59ABA800636A10 /* toolbar-icon32.png */; }; 68 | FB6A6D932A59ABA800636A10 /* toolbar-icon38.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D882A59ABA800636A10 /* toolbar-icon38.png */; }; 69 | FB6A6D942A59ABA800636A10 /* toolbar-icon38.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D882A59ABA800636A10 /* toolbar-icon38.png */; }; 70 | FB6A6D9B2A60DCAA00636A10 /* Ad.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D982A60DC2200636A10 /* Ad.png */; }; 71 | FB6A6D9C2A60DCAB00636A10 /* Ad.png in Resources */ = {isa = PBXBuildFile; fileRef = FB6A6D982A60DC2200636A10 /* Ad.png */; }; 72 | /* End PBXBuildFile section */ 73 | 74 | /* Begin PBXContainerItemProxy section */ 75 | FB6A6D0D2A595D2600636A10 /* PBXContainerItemProxy */ = { 76 | isa = PBXContainerItemProxy; 77 | containerPortal = FB6A6CD12A595D2500636A10 /* Project object */; 78 | proxyType = 1; 79 | remoteGlobalIDString = FB6A6D0A2A595D2600636A10; 80 | remoteInfo = "Comments Owl for Hacker News Extension (iOS)"; 81 | }; 82 | FB6A6D172A595D2600636A10 /* PBXContainerItemProxy */ = { 83 | isa = PBXContainerItemProxy; 84 | containerPortal = FB6A6CD12A595D2500636A10 /* Project object */; 85 | proxyType = 1; 86 | remoteGlobalIDString = FB6A6D142A595D2600636A10; 87 | remoteInfo = "Comments Owl for Hacker News Extension (macOS)"; 88 | }; 89 | /* End PBXContainerItemProxy section */ 90 | 91 | /* Begin PBXCopyFilesBuildPhase section */ 92 | FB6A6D3F2A595D2600636A10 /* Embed Foundation Extensions */ = { 93 | isa = PBXCopyFilesBuildPhase; 94 | buildActionMask = 2147483647; 95 | dstPath = ""; 96 | dstSubfolderSpec = 13; 97 | files = ( 98 | FB6A6D0C2A595D2600636A10 /* Comments Owl for Hacker News Extension.appex in Embed Foundation Extensions */, 99 | ); 100 | name = "Embed Foundation Extensions"; 101 | runOnlyForDeploymentPostprocessing = 0; 102 | }; 103 | FB6A6D462A595D2600636A10 /* Embed Foundation Extensions */ = { 104 | isa = PBXCopyFilesBuildPhase; 105 | buildActionMask = 2147483647; 106 | dstPath = ""; 107 | dstSubfolderSpec = 13; 108 | files = ( 109 | FB6A6D162A595D2600636A10 /* Comments Owl for Hacker News Extension.appex in Embed Foundation Extensions */, 110 | ); 111 | name = "Embed Foundation Extensions"; 112 | runOnlyForDeploymentPostprocessing = 0; 113 | }; 114 | /* End PBXCopyFilesBuildPhase section */ 115 | 116 | /* Begin PBXFileReference section */ 117 | FB6A6CD82A595D2500636A10 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = Base; path = ../Base.lproj/Main.html; sourceTree = ""; }; 118 | FB6A6CD92A595D2500636A10 /* Icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Icon.png; sourceTree = ""; }; 119 | FB6A6CDA2A595D2500636A10 /* Style.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = Style.css; sourceTree = ""; }; 120 | FB6A6CDB2A595D2500636A10 /* Script.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = Script.js; sourceTree = ""; }; 121 | FB6A6CDC2A595D2500636A10 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 122 | FB6A6CDD2A595D2600636A10 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 123 | FB6A6CDF2A595D2600636A10 /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = ""; }; 124 | FB6A6CE32A595D2600636A10 /* manifest.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = manifest.json; sourceTree = ""; }; 125 | FB6A6CED2A595D2600636A10 /* Comments Owl for Hacker News.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Comments Owl for Hacker News.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 126 | FB6A6CF02A595D2600636A10 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 127 | FB6A6CF22A595D2600636A10 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 128 | FB6A6CF52A595D2600636A10 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 129 | FB6A6CF82A595D2600636A10 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 130 | FB6A6CFA2A595D2600636A10 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 131 | FB6A6CFF2A595D2600636A10 /* Comments Owl for Hacker News.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Comments Owl for Hacker News.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 132 | FB6A6D012A595D2600636A10 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 133 | FB6A6D042A595D2600636A10 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 134 | FB6A6D062A595D2600636A10 /* Comments Owl for Hacker News.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Comments Owl for Hacker News.entitlements"; sourceTree = ""; }; 135 | FB6A6D0B2A595D2600636A10 /* Comments Owl for Hacker News Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Comments Owl for Hacker News Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 136 | FB6A6D102A595D2600636A10 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 137 | FB6A6D152A595D2600636A10 /* Comments Owl for Hacker News Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Comments Owl for Hacker News Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 138 | FB6A6D1A2A595D2600636A10 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 139 | FB6A6D1B2A595D2600636A10 /* Comments Owl for Hacker News.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Comments Owl for Hacker News.entitlements"; sourceTree = ""; }; 140 | FB6A6D4A2A599C9700636A10 /* options.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = options.css; path = ../../../options.css; sourceTree = ""; }; 141 | FB6A6D4B2A599C9800636A10 /* content.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = content.js; path = ../../../content.js; sourceTree = ""; }; 142 | FB6A6D4C2A599C9800636A10 /* options.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = options.html; path = ../../../options.html; sourceTree = ""; }; 143 | FB6A6D4D2A599C9800636A10 /* browser_action.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = browser_action.html; path = ../../../browser_action.html; sourceTree = ""; }; 144 | FB6A6D4F2A599C9800636A10 /* _locales */ = {isa = PBXFileReference; lastKnownFileType = folder; name = _locales; path = ../../../_locales; sourceTree = ""; }; 145 | FB6A6D502A599C9800636A10 /* options.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = options.js; path = ../../../options.js; sourceTree = ""; }; 146 | FB6A6D512A599C9800636A10 /* background.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = background.js; path = ../../../background.js; sourceTree = ""; }; 147 | FB6A6D622A599D2F00636A10 /* icon256.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = icon256.png; path = ../../../icons/icon256.png; sourceTree = ""; }; 148 | FB6A6D632A599D2F00636A10 /* icon96.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = icon96.png; path = ../../../icons/icon96.png; sourceTree = ""; }; 149 | FB6A6D642A599D2F00636A10 /* icon128.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = icon128.png; path = ../../../icons/icon128.png; sourceTree = ""; }; 150 | FB6A6D652A599D2F00636A10 /* icon48.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = icon48.png; path = ../../../icons/icon48.png; sourceTree = ""; }; 151 | FB6A6D802A599E9B00636A10 /* icon512.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = icon512.png; path = ../../../icons/icon512.png; sourceTree = ""; }; 152 | FB6A6D832A59ABA800636A10 /* toolbar-icon19.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "toolbar-icon19.png"; path = "../../../icons/toolbar-icon19.png"; sourceTree = ""; }; 153 | FB6A6D842A59ABA800636A10 /* toolbar-icon48.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "toolbar-icon48.png"; path = "../../../icons/toolbar-icon48.png"; sourceTree = ""; }; 154 | FB6A6D852A59ABA800636A10 /* toolbar-icon72.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "toolbar-icon72.png"; path = "../../../icons/toolbar-icon72.png"; sourceTree = ""; }; 155 | FB6A6D862A59ABA800636A10 /* toolbar-icon16.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "toolbar-icon16.png"; path = "../../../icons/toolbar-icon16.png"; sourceTree = ""; }; 156 | FB6A6D872A59ABA800636A10 /* toolbar-icon32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "toolbar-icon32.png"; path = "../../../icons/toolbar-icon32.png"; sourceTree = ""; }; 157 | FB6A6D882A59ABA800636A10 /* toolbar-icon38.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "toolbar-icon38.png"; path = "../../../icons/toolbar-icon38.png"; sourceTree = ""; }; 158 | FB6A6D982A60DC2200636A10 /* Ad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Ad.png; sourceTree = ""; }; 159 | /* End PBXFileReference section */ 160 | 161 | /* Begin PBXFrameworksBuildPhase section */ 162 | FB6A6CEA2A595D2600636A10 /* Frameworks */ = { 163 | isa = PBXFrameworksBuildPhase; 164 | buildActionMask = 2147483647; 165 | files = ( 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | FB6A6CFC2A595D2600636A10 /* Frameworks */ = { 170 | isa = PBXFrameworksBuildPhase; 171 | buildActionMask = 2147483647; 172 | files = ( 173 | ); 174 | runOnlyForDeploymentPostprocessing = 0; 175 | }; 176 | FB6A6D082A595D2600636A10 /* Frameworks */ = { 177 | isa = PBXFrameworksBuildPhase; 178 | buildActionMask = 2147483647; 179 | files = ( 180 | ); 181 | runOnlyForDeploymentPostprocessing = 0; 182 | }; 183 | FB6A6D122A595D2600636A10 /* Frameworks */ = { 184 | isa = PBXFrameworksBuildPhase; 185 | buildActionMask = 2147483647; 186 | files = ( 187 | ); 188 | runOnlyForDeploymentPostprocessing = 0; 189 | }; 190 | /* End PBXFrameworksBuildPhase section */ 191 | 192 | /* Begin PBXGroup section */ 193 | FB6A6CD02A595D2500636A10 = { 194 | isa = PBXGroup; 195 | children = ( 196 | FB6A6CD52A595D2500636A10 /* Shared (App) */, 197 | FB6A6CDE2A595D2600636A10 /* Shared (Extension) */, 198 | FB6A6CEF2A595D2600636A10 /* iOS (App) */, 199 | FB6A6D002A595D2600636A10 /* macOS (App) */, 200 | FB6A6D0F2A595D2600636A10 /* iOS (Extension) */, 201 | FB6A6D192A595D2600636A10 /* macOS (Extension) */, 202 | FB6A6CEE2A595D2600636A10 /* Products */, 203 | ); 204 | sourceTree = ""; 205 | }; 206 | FB6A6CD52A595D2500636A10 /* Shared (App) */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | FB6A6CDC2A595D2500636A10 /* ViewController.swift */, 210 | FB6A6CDD2A595D2600636A10 /* Assets.xcassets */, 211 | FB6A6CD62A595D2500636A10 /* Resources */, 212 | ); 213 | path = "Shared (App)"; 214 | sourceTree = ""; 215 | }; 216 | FB6A6CD62A595D2500636A10 /* Resources */ = { 217 | isa = PBXGroup; 218 | children = ( 219 | FB6A6D982A60DC2200636A10 /* Ad.png */, 220 | FB6A6CD72A595D2500636A10 /* Main.html */, 221 | FB6A6CD92A595D2500636A10 /* Icon.png */, 222 | FB6A6CDA2A595D2500636A10 /* Style.css */, 223 | FB6A6CDB2A595D2500636A10 /* Script.js */, 224 | ); 225 | path = Resources; 226 | sourceTree = ""; 227 | }; 228 | FB6A6CDE2A595D2600636A10 /* Shared (Extension) */ = { 229 | isa = PBXGroup; 230 | children = ( 231 | FB6A6CDF2A595D2600636A10 /* SafariWebExtensionHandler.swift */, 232 | FB6A6CE02A595D2600636A10 /* Resources */, 233 | ); 234 | path = "Shared (Extension)"; 235 | sourceTree = ""; 236 | }; 237 | FB6A6CE02A595D2600636A10 /* Resources */ = { 238 | isa = PBXGroup; 239 | children = ( 240 | FB6A6D4F2A599C9800636A10 /* _locales */, 241 | FB6A6D512A599C9800636A10 /* background.js */, 242 | FB6A6D4D2A599C9800636A10 /* browser_action.html */, 243 | FB6A6D4B2A599C9800636A10 /* content.js */, 244 | FB6A6D652A599D2F00636A10 /* icon48.png */, 245 | FB6A6D632A599D2F00636A10 /* icon96.png */, 246 | FB6A6D642A599D2F00636A10 /* icon128.png */, 247 | FB6A6D622A599D2F00636A10 /* icon256.png */, 248 | FB6A6D802A599E9B00636A10 /* icon512.png */, 249 | FB6A6CE32A595D2600636A10 /* manifest.json */, 250 | FB6A6D4A2A599C9700636A10 /* options.css */, 251 | FB6A6D4C2A599C9800636A10 /* options.html */, 252 | FB6A6D502A599C9800636A10 /* options.js */, 253 | FB6A6D862A59ABA800636A10 /* toolbar-icon16.png */, 254 | FB6A6D832A59ABA800636A10 /* toolbar-icon19.png */, 255 | FB6A6D872A59ABA800636A10 /* toolbar-icon32.png */, 256 | FB6A6D882A59ABA800636A10 /* toolbar-icon38.png */, 257 | FB6A6D842A59ABA800636A10 /* toolbar-icon48.png */, 258 | FB6A6D852A59ABA800636A10 /* toolbar-icon72.png */, 259 | ); 260 | path = Resources; 261 | sourceTree = ""; 262 | }; 263 | FB6A6CEE2A595D2600636A10 /* Products */ = { 264 | isa = PBXGroup; 265 | children = ( 266 | FB6A6CED2A595D2600636A10 /* Comments Owl for Hacker News.app */, 267 | FB6A6CFF2A595D2600636A10 /* Comments Owl for Hacker News.app */, 268 | FB6A6D0B2A595D2600636A10 /* Comments Owl for Hacker News Extension.appex */, 269 | FB6A6D152A595D2600636A10 /* Comments Owl for Hacker News Extension.appex */, 270 | ); 271 | name = Products; 272 | sourceTree = ""; 273 | }; 274 | FB6A6CEF2A595D2600636A10 /* iOS (App) */ = { 275 | isa = PBXGroup; 276 | children = ( 277 | FB6A6CF02A595D2600636A10 /* AppDelegate.swift */, 278 | FB6A6CF22A595D2600636A10 /* SceneDelegate.swift */, 279 | FB6A6CF42A595D2600636A10 /* LaunchScreen.storyboard */, 280 | FB6A6CF72A595D2600636A10 /* Main.storyboard */, 281 | FB6A6CFA2A595D2600636A10 /* Info.plist */, 282 | ); 283 | path = "iOS (App)"; 284 | sourceTree = ""; 285 | }; 286 | FB6A6D002A595D2600636A10 /* macOS (App) */ = { 287 | isa = PBXGroup; 288 | children = ( 289 | FB6A6D012A595D2600636A10 /* AppDelegate.swift */, 290 | FB6A6D032A595D2600636A10 /* Main.storyboard */, 291 | FB6A6D062A595D2600636A10 /* Comments Owl for Hacker News.entitlements */, 292 | ); 293 | path = "macOS (App)"; 294 | sourceTree = ""; 295 | }; 296 | FB6A6D0F2A595D2600636A10 /* iOS (Extension) */ = { 297 | isa = PBXGroup; 298 | children = ( 299 | FB6A6D102A595D2600636A10 /* Info.plist */, 300 | ); 301 | path = "iOS (Extension)"; 302 | sourceTree = ""; 303 | }; 304 | FB6A6D192A595D2600636A10 /* macOS (Extension) */ = { 305 | isa = PBXGroup; 306 | children = ( 307 | FB6A6D1A2A595D2600636A10 /* Info.plist */, 308 | FB6A6D1B2A595D2600636A10 /* Comments Owl for Hacker News.entitlements */, 309 | ); 310 | path = "macOS (Extension)"; 311 | sourceTree = ""; 312 | }; 313 | /* End PBXGroup section */ 314 | 315 | /* Begin PBXNativeTarget section */ 316 | FB6A6CEC2A595D2600636A10 /* Comments Owl for Hacker News (iOS) */ = { 317 | isa = PBXNativeTarget; 318 | buildConfigurationList = FB6A6D402A595D2600636A10 /* Build configuration list for PBXNativeTarget "Comments Owl for Hacker News (iOS)" */; 319 | buildPhases = ( 320 | FB6A6CE92A595D2600636A10 /* Sources */, 321 | FB6A6CEA2A595D2600636A10 /* Frameworks */, 322 | FB6A6CEB2A595D2600636A10 /* Resources */, 323 | FB6A6D3F2A595D2600636A10 /* Embed Foundation Extensions */, 324 | ); 325 | buildRules = ( 326 | ); 327 | dependencies = ( 328 | FB6A6D0E2A595D2600636A10 /* PBXTargetDependency */, 329 | ); 330 | name = "Comments Owl for Hacker News (iOS)"; 331 | productName = "Comments Owl for Hacker News (iOS)"; 332 | productReference = FB6A6CED2A595D2600636A10 /* Comments Owl for Hacker News.app */; 333 | productType = "com.apple.product-type.application"; 334 | }; 335 | FB6A6CFE2A595D2600636A10 /* Comments Owl for Hacker News (macOS) */ = { 336 | isa = PBXNativeTarget; 337 | buildConfigurationList = FB6A6D472A595D2600636A10 /* Build configuration list for PBXNativeTarget "Comments Owl for Hacker News (macOS)" */; 338 | buildPhases = ( 339 | FB6A6CFB2A595D2600636A10 /* Sources */, 340 | FB6A6CFC2A595D2600636A10 /* Frameworks */, 341 | FB6A6CFD2A595D2600636A10 /* Resources */, 342 | FB6A6D462A595D2600636A10 /* Embed Foundation Extensions */, 343 | ); 344 | buildRules = ( 345 | ); 346 | dependencies = ( 347 | FB6A6D182A595D2600636A10 /* PBXTargetDependency */, 348 | ); 349 | name = "Comments Owl for Hacker News (macOS)"; 350 | productName = "Comments Owl for Hacker News (macOS)"; 351 | productReference = FB6A6CFF2A595D2600636A10 /* Comments Owl for Hacker News.app */; 352 | productType = "com.apple.product-type.application"; 353 | }; 354 | FB6A6D0A2A595D2600636A10 /* Comments Owl for Hacker News Extension (iOS) */ = { 355 | isa = PBXNativeTarget; 356 | buildConfigurationList = FB6A6D3C2A595D2600636A10 /* Build configuration list for PBXNativeTarget "Comments Owl for Hacker News Extension (iOS)" */; 357 | buildPhases = ( 358 | FB6A6D072A595D2600636A10 /* Sources */, 359 | FB6A6D082A595D2600636A10 /* Frameworks */, 360 | FB6A6D092A595D2600636A10 /* Resources */, 361 | ); 362 | buildRules = ( 363 | ); 364 | dependencies = ( 365 | ); 366 | name = "Comments Owl for Hacker News Extension (iOS)"; 367 | productName = "Comments Owl for Hacker News Extension (iOS)"; 368 | productReference = FB6A6D0B2A595D2600636A10 /* Comments Owl for Hacker News Extension.appex */; 369 | productType = "com.apple.product-type.app-extension"; 370 | }; 371 | FB6A6D142A595D2600636A10 /* Comments Owl for Hacker News Extension (macOS) */ = { 372 | isa = PBXNativeTarget; 373 | buildConfigurationList = FB6A6D432A595D2600636A10 /* Build configuration list for PBXNativeTarget "Comments Owl for Hacker News Extension (macOS)" */; 374 | buildPhases = ( 375 | FB6A6D112A595D2600636A10 /* Sources */, 376 | FB6A6D122A595D2600636A10 /* Frameworks */, 377 | FB6A6D132A595D2600636A10 /* Resources */, 378 | ); 379 | buildRules = ( 380 | ); 381 | dependencies = ( 382 | ); 383 | name = "Comments Owl for Hacker News Extension (macOS)"; 384 | productName = "Comments Owl for Hacker News Extension (macOS)"; 385 | productReference = FB6A6D152A595D2600636A10 /* Comments Owl for Hacker News Extension.appex */; 386 | productType = "com.apple.product-type.app-extension"; 387 | }; 388 | /* End PBXNativeTarget section */ 389 | 390 | /* Begin PBXProject section */ 391 | FB6A6CD12A595D2500636A10 /* Project object */ = { 392 | isa = PBXProject; 393 | attributes = { 394 | BuildIndependentTargetsInParallel = 1; 395 | LastSwiftUpdateCheck = 1420; 396 | LastUpgradeCheck = 1620; 397 | TargetAttributes = { 398 | FB6A6CEC2A595D2600636A10 = { 399 | CreatedOnToolsVersion = 14.2; 400 | }; 401 | FB6A6CFE2A595D2600636A10 = { 402 | CreatedOnToolsVersion = 14.2; 403 | }; 404 | FB6A6D0A2A595D2600636A10 = { 405 | CreatedOnToolsVersion = 14.2; 406 | }; 407 | FB6A6D142A595D2600636A10 = { 408 | CreatedOnToolsVersion = 14.2; 409 | }; 410 | }; 411 | }; 412 | buildConfigurationList = FB6A6CD42A595D2500636A10 /* Build configuration list for PBXProject "Comments Owl for Hacker News" */; 413 | compatibilityVersion = "Xcode 14.0"; 414 | developmentRegion = en; 415 | hasScannedForEncodings = 0; 416 | knownRegions = ( 417 | en, 418 | Base, 419 | ); 420 | mainGroup = FB6A6CD02A595D2500636A10; 421 | productRefGroup = FB6A6CEE2A595D2600636A10 /* Products */; 422 | projectDirPath = ""; 423 | projectRoot = ""; 424 | targets = ( 425 | FB6A6CEC2A595D2600636A10 /* Comments Owl for Hacker News (iOS) */, 426 | FB6A6CFE2A595D2600636A10 /* Comments Owl for Hacker News (macOS) */, 427 | FB6A6D0A2A595D2600636A10 /* Comments Owl for Hacker News Extension (iOS) */, 428 | FB6A6D142A595D2600636A10 /* Comments Owl for Hacker News Extension (macOS) */, 429 | ); 430 | }; 431 | /* End PBXProject section */ 432 | 433 | /* Begin PBXResourcesBuildPhase section */ 434 | FB6A6CEB2A595D2600636A10 /* Resources */ = { 435 | isa = PBXResourcesBuildPhase; 436 | buildActionMask = 2147483647; 437 | files = ( 438 | FB6A6D1E2A595D2600636A10 /* Icon.png in Resources */, 439 | FB6A6CF62A595D2600636A10 /* LaunchScreen.storyboard in Resources */, 440 | FB6A6D1C2A595D2600636A10 /* Main.html in Resources */, 441 | FB6A6D222A595D2600636A10 /* Script.js in Resources */, 442 | FB6A6D262A595D2600636A10 /* Assets.xcassets in Resources */, 443 | FB6A6CF92A595D2600636A10 /* Main.storyboard in Resources */, 444 | FB6A6D9C2A60DCAB00636A10 /* Ad.png in Resources */, 445 | FB6A6D202A595D2600636A10 /* Style.css in Resources */, 446 | ); 447 | runOnlyForDeploymentPostprocessing = 0; 448 | }; 449 | FB6A6CFD2A595D2600636A10 /* Resources */ = { 450 | isa = PBXResourcesBuildPhase; 451 | buildActionMask = 2147483647; 452 | files = ( 453 | FB6A6D1F2A595D2600636A10 /* Icon.png in Resources */, 454 | FB6A6D212A595D2600636A10 /* Style.css in Resources */, 455 | FB6A6D052A595D2600636A10 /* Main.storyboard in Resources */, 456 | FB6A6D232A595D2600636A10 /* Script.js in Resources */, 457 | FB6A6D9B2A60DCAA00636A10 /* Ad.png in Resources */, 458 | FB6A6D272A595D2600636A10 /* Assets.xcassets in Resources */, 459 | FB6A6D1D2A595D2600636A10 /* Main.html in Resources */, 460 | ); 461 | runOnlyForDeploymentPostprocessing = 0; 462 | }; 463 | FB6A6D092A595D2600636A10 /* Resources */ = { 464 | isa = PBXResourcesBuildPhase; 465 | buildActionMask = 2147483647; 466 | files = ( 467 | FB6A6D542A599C9800636A10 /* content.js in Resources */, 468 | FB6A6D5E2A599C9800636A10 /* options.js in Resources */, 469 | FB6A6D6C2A599D2F00636A10 /* icon48.png in Resources */, 470 | FB6A6D6A2A599D2F00636A10 /* icon128.png in Resources */, 471 | FB6A6D582A599C9800636A10 /* browser_action.html in Resources */, 472 | FB6A6D812A599E9B00636A10 /* icon512.png in Resources */, 473 | FB6A6D522A599C9800636A10 /* options.css in Resources */, 474 | FB6A6D8D2A59ABA800636A10 /* toolbar-icon72.png in Resources */, 475 | FB6A6D8B2A59ABA800636A10 /* toolbar-icon48.png in Resources */, 476 | FB6A6D912A59ABA800636A10 /* toolbar-icon32.png in Resources */, 477 | FB6A6D932A59ABA800636A10 /* toolbar-icon38.png in Resources */, 478 | FB6A6D602A599C9800636A10 /* background.js in Resources */, 479 | FB6A6D8F2A59ABA800636A10 /* toolbar-icon16.png in Resources */, 480 | FB6A6D662A599D2F00636A10 /* icon256.png in Resources */, 481 | FB6A6D2E2A595D2600636A10 /* manifest.json in Resources */, 482 | FB6A6D5C2A599C9800636A10 /* _locales in Resources */, 483 | FB6A6D562A599C9800636A10 /* options.html in Resources */, 484 | FB6A6D892A59ABA800636A10 /* toolbar-icon19.png in Resources */, 485 | FB6A6D682A599D2F00636A10 /* icon96.png in Resources */, 486 | ); 487 | runOnlyForDeploymentPostprocessing = 0; 488 | }; 489 | FB6A6D132A595D2600636A10 /* Resources */ = { 490 | isa = PBXResourcesBuildPhase; 491 | buildActionMask = 2147483647; 492 | files = ( 493 | FB6A6D552A599C9800636A10 /* content.js in Resources */, 494 | FB6A6D5F2A599C9800636A10 /* options.js in Resources */, 495 | FB6A6D6D2A599D2F00636A10 /* icon48.png in Resources */, 496 | FB6A6D6B2A599D2F00636A10 /* icon128.png in Resources */, 497 | FB6A6D592A599C9800636A10 /* browser_action.html in Resources */, 498 | FB6A6D822A599E9B00636A10 /* icon512.png in Resources */, 499 | FB6A6D532A599C9800636A10 /* options.css in Resources */, 500 | FB6A6D8E2A59ABA800636A10 /* toolbar-icon72.png in Resources */, 501 | FB6A6D8C2A59ABA800636A10 /* toolbar-icon48.png in Resources */, 502 | FB6A6D922A59ABA800636A10 /* toolbar-icon32.png in Resources */, 503 | FB6A6D942A59ABA800636A10 /* toolbar-icon38.png in Resources */, 504 | FB6A6D612A599C9800636A10 /* background.js in Resources */, 505 | FB6A6D902A59ABA800636A10 /* toolbar-icon16.png in Resources */, 506 | FB6A6D672A599D2F00636A10 /* icon256.png in Resources */, 507 | FB6A6D2F2A595D2600636A10 /* manifest.json in Resources */, 508 | FB6A6D5D2A599C9800636A10 /* _locales in Resources */, 509 | FB6A6D572A599C9800636A10 /* options.html in Resources */, 510 | FB6A6D8A2A59ABA800636A10 /* toolbar-icon19.png in Resources */, 511 | FB6A6D692A599D2F00636A10 /* icon96.png in Resources */, 512 | ); 513 | runOnlyForDeploymentPostprocessing = 0; 514 | }; 515 | /* End PBXResourcesBuildPhase section */ 516 | 517 | /* Begin PBXSourcesBuildPhase section */ 518 | FB6A6CE92A595D2600636A10 /* Sources */ = { 519 | isa = PBXSourcesBuildPhase; 520 | buildActionMask = 2147483647; 521 | files = ( 522 | FB6A6D242A595D2600636A10 /* ViewController.swift in Sources */, 523 | FB6A6CF12A595D2600636A10 /* AppDelegate.swift in Sources */, 524 | FB6A6CF32A595D2600636A10 /* SceneDelegate.swift in Sources */, 525 | ); 526 | runOnlyForDeploymentPostprocessing = 0; 527 | }; 528 | FB6A6CFB2A595D2600636A10 /* Sources */ = { 529 | isa = PBXSourcesBuildPhase; 530 | buildActionMask = 2147483647; 531 | files = ( 532 | FB6A6D252A595D2600636A10 /* ViewController.swift in Sources */, 533 | FB6A6D022A595D2600636A10 /* AppDelegate.swift in Sources */, 534 | ); 535 | runOnlyForDeploymentPostprocessing = 0; 536 | }; 537 | FB6A6D072A595D2600636A10 /* Sources */ = { 538 | isa = PBXSourcesBuildPhase; 539 | buildActionMask = 2147483647; 540 | files = ( 541 | FB6A6D282A595D2600636A10 /* SafariWebExtensionHandler.swift in Sources */, 542 | ); 543 | runOnlyForDeploymentPostprocessing = 0; 544 | }; 545 | FB6A6D112A595D2600636A10 /* Sources */ = { 546 | isa = PBXSourcesBuildPhase; 547 | buildActionMask = 2147483647; 548 | files = ( 549 | FB6A6D292A595D2600636A10 /* SafariWebExtensionHandler.swift in Sources */, 550 | ); 551 | runOnlyForDeploymentPostprocessing = 0; 552 | }; 553 | /* End PBXSourcesBuildPhase section */ 554 | 555 | /* Begin PBXTargetDependency section */ 556 | FB6A6D0E2A595D2600636A10 /* PBXTargetDependency */ = { 557 | isa = PBXTargetDependency; 558 | target = FB6A6D0A2A595D2600636A10 /* Comments Owl for Hacker News Extension (iOS) */; 559 | targetProxy = FB6A6D0D2A595D2600636A10 /* PBXContainerItemProxy */; 560 | }; 561 | FB6A6D182A595D2600636A10 /* PBXTargetDependency */ = { 562 | isa = PBXTargetDependency; 563 | target = FB6A6D142A595D2600636A10 /* Comments Owl for Hacker News Extension (macOS) */; 564 | targetProxy = FB6A6D172A595D2600636A10 /* PBXContainerItemProxy */; 565 | }; 566 | /* End PBXTargetDependency section */ 567 | 568 | /* Begin PBXVariantGroup section */ 569 | FB6A6CD72A595D2500636A10 /* Main.html */ = { 570 | isa = PBXVariantGroup; 571 | children = ( 572 | FB6A6CD82A595D2500636A10 /* Base */, 573 | ); 574 | name = Main.html; 575 | sourceTree = ""; 576 | }; 577 | FB6A6CF42A595D2600636A10 /* LaunchScreen.storyboard */ = { 578 | isa = PBXVariantGroup; 579 | children = ( 580 | FB6A6CF52A595D2600636A10 /* Base */, 581 | ); 582 | name = LaunchScreen.storyboard; 583 | sourceTree = ""; 584 | }; 585 | FB6A6CF72A595D2600636A10 /* Main.storyboard */ = { 586 | isa = PBXVariantGroup; 587 | children = ( 588 | FB6A6CF82A595D2600636A10 /* Base */, 589 | ); 590 | name = Main.storyboard; 591 | sourceTree = ""; 592 | }; 593 | FB6A6D032A595D2600636A10 /* Main.storyboard */ = { 594 | isa = PBXVariantGroup; 595 | children = ( 596 | FB6A6D042A595D2600636A10 /* Base */, 597 | ); 598 | name = Main.storyboard; 599 | sourceTree = ""; 600 | }; 601 | /* End PBXVariantGroup section */ 602 | 603 | /* Begin XCBuildConfiguration section */ 604 | FB6A6D3A2A595D2600636A10 /* Debug */ = { 605 | isa = XCBuildConfiguration; 606 | buildSettings = { 607 | ALWAYS_SEARCH_USER_PATHS = NO; 608 | CLANG_ANALYZER_NONNULL = YES; 609 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 610 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 611 | CLANG_ENABLE_MODULES = YES; 612 | CLANG_ENABLE_OBJC_ARC = YES; 613 | CLANG_ENABLE_OBJC_WEAK = YES; 614 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 615 | CLANG_WARN_BOOL_CONVERSION = YES; 616 | CLANG_WARN_COMMA = YES; 617 | CLANG_WARN_CONSTANT_CONVERSION = YES; 618 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 619 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 620 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 621 | CLANG_WARN_EMPTY_BODY = YES; 622 | CLANG_WARN_ENUM_CONVERSION = YES; 623 | CLANG_WARN_INFINITE_RECURSION = YES; 624 | CLANG_WARN_INT_CONVERSION = YES; 625 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 626 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 627 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 628 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 629 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 630 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 631 | CLANG_WARN_STRICT_PROTOTYPES = YES; 632 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 633 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 634 | CLANG_WARN_UNREACHABLE_CODE = YES; 635 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 636 | COPY_PHASE_STRIP = NO; 637 | CURRENT_PROJECT_VERSION = 5; 638 | DEAD_CODE_STRIPPING = YES; 639 | DEBUG_INFORMATION_FORMAT = dwarf; 640 | ENABLE_STRICT_OBJC_MSGSEND = YES; 641 | ENABLE_TESTABILITY = YES; 642 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 643 | GCC_C_LANGUAGE_STANDARD = gnu11; 644 | GCC_DYNAMIC_NO_PIC = NO; 645 | GCC_NO_COMMON_BLOCKS = YES; 646 | GCC_OPTIMIZATION_LEVEL = 0; 647 | GCC_PREPROCESSOR_DEFINITIONS = ( 648 | "DEBUG=1", 649 | "$(inherited)", 650 | ); 651 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 652 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 653 | GCC_WARN_UNDECLARED_SELECTOR = YES; 654 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 655 | GCC_WARN_UNUSED_FUNCTION = YES; 656 | GCC_WARN_UNUSED_VARIABLE = YES; 657 | MARKETING_VERSION = 3.0.0; 658 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 659 | MTL_FAST_MATH = YES; 660 | ONLY_ACTIVE_ARCH = YES; 661 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 662 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 663 | }; 664 | name = Debug; 665 | }; 666 | FB6A6D3B2A595D2600636A10 /* Release */ = { 667 | isa = XCBuildConfiguration; 668 | buildSettings = { 669 | ALWAYS_SEARCH_USER_PATHS = NO; 670 | CLANG_ANALYZER_NONNULL = YES; 671 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 672 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 673 | CLANG_ENABLE_MODULES = YES; 674 | CLANG_ENABLE_OBJC_ARC = YES; 675 | CLANG_ENABLE_OBJC_WEAK = YES; 676 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 677 | CLANG_WARN_BOOL_CONVERSION = YES; 678 | CLANG_WARN_COMMA = YES; 679 | CLANG_WARN_CONSTANT_CONVERSION = YES; 680 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 681 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 682 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 683 | CLANG_WARN_EMPTY_BODY = YES; 684 | CLANG_WARN_ENUM_CONVERSION = YES; 685 | CLANG_WARN_INFINITE_RECURSION = YES; 686 | CLANG_WARN_INT_CONVERSION = YES; 687 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 688 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 689 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 690 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 691 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 692 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 693 | CLANG_WARN_STRICT_PROTOTYPES = YES; 694 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 695 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 696 | CLANG_WARN_UNREACHABLE_CODE = YES; 697 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 698 | COPY_PHASE_STRIP = NO; 699 | CURRENT_PROJECT_VERSION = 5; 700 | DEAD_CODE_STRIPPING = YES; 701 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 702 | ENABLE_NS_ASSERTIONS = NO; 703 | ENABLE_STRICT_OBJC_MSGSEND = YES; 704 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 705 | GCC_C_LANGUAGE_STANDARD = gnu11; 706 | GCC_NO_COMMON_BLOCKS = YES; 707 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 708 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 709 | GCC_WARN_UNDECLARED_SELECTOR = YES; 710 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 711 | GCC_WARN_UNUSED_FUNCTION = YES; 712 | GCC_WARN_UNUSED_VARIABLE = YES; 713 | MARKETING_VERSION = 3.0.0; 714 | MTL_ENABLE_DEBUG_INFO = NO; 715 | MTL_FAST_MATH = YES; 716 | SWIFT_COMPILATION_MODE = wholemodule; 717 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 718 | }; 719 | name = Release; 720 | }; 721 | FB6A6D3D2A595D2600636A10 /* Debug */ = { 722 | isa = XCBuildConfiguration; 723 | buildSettings = { 724 | CODE_SIGN_STYLE = Automatic; 725 | DEVELOPMENT_TEAM = 2RDKJDLNY8; 726 | GENERATE_INFOPLIST_FILE = YES; 727 | INFOPLIST_FILE = "iOS (Extension)/Info.plist"; 728 | INFOPLIST_KEY_CFBundleDisplayName = "Comments Owl for Hacker News Extension"; 729 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 730 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 731 | LD_RUNPATH_SEARCH_PATHS = ( 732 | "$(inherited)", 733 | "@executable_path/Frameworks", 734 | "@executable_path/../../Frameworks", 735 | ); 736 | OTHER_LDFLAGS = ( 737 | "-framework", 738 | SafariServices, 739 | ); 740 | PRODUCT_BUNDLE_IDENTIFIER = "dev.jbscript.Comments-Owl-for-Hacker-News.Extension"; 741 | PRODUCT_NAME = "Comments Owl for Hacker News Extension"; 742 | SDKROOT = iphoneos; 743 | SKIP_INSTALL = YES; 744 | SWIFT_EMIT_LOC_STRINGS = YES; 745 | SWIFT_VERSION = 5.0; 746 | TARGETED_DEVICE_FAMILY = "1,2"; 747 | }; 748 | name = Debug; 749 | }; 750 | FB6A6D3E2A595D2600636A10 /* Release */ = { 751 | isa = XCBuildConfiguration; 752 | buildSettings = { 753 | CODE_SIGN_STYLE = Automatic; 754 | DEVELOPMENT_TEAM = 2RDKJDLNY8; 755 | GENERATE_INFOPLIST_FILE = YES; 756 | INFOPLIST_FILE = "iOS (Extension)/Info.plist"; 757 | INFOPLIST_KEY_CFBundleDisplayName = "Comments Owl for Hacker News Extension"; 758 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 759 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 760 | LD_RUNPATH_SEARCH_PATHS = ( 761 | "$(inherited)", 762 | "@executable_path/Frameworks", 763 | "@executable_path/../../Frameworks", 764 | ); 765 | OTHER_LDFLAGS = ( 766 | "-framework", 767 | SafariServices, 768 | ); 769 | PRODUCT_BUNDLE_IDENTIFIER = "dev.jbscript.Comments-Owl-for-Hacker-News.Extension"; 770 | PRODUCT_NAME = "Comments Owl for Hacker News Extension"; 771 | SDKROOT = iphoneos; 772 | SKIP_INSTALL = YES; 773 | SWIFT_EMIT_LOC_STRINGS = YES; 774 | SWIFT_VERSION = 5.0; 775 | TARGETED_DEVICE_FAMILY = "1,2"; 776 | VALIDATE_PRODUCT = YES; 777 | }; 778 | name = Release; 779 | }; 780 | FB6A6D412A595D2600636A10 /* Debug */ = { 781 | isa = XCBuildConfiguration; 782 | buildSettings = { 783 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 784 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 785 | CODE_SIGN_STYLE = Automatic; 786 | DEVELOPMENT_TEAM = 2RDKJDLNY8; 787 | GENERATE_INFOPLIST_FILE = YES; 788 | INFOPLIST_FILE = "iOS (App)/Info.plist"; 789 | INFOPLIST_KEY_CFBundleDisplayName = "Comments Owl for Hacker News"; 790 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 791 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 792 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 793 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 794 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 795 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 796 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 797 | LD_RUNPATH_SEARCH_PATHS = ( 798 | "$(inherited)", 799 | "@executable_path/Frameworks", 800 | ); 801 | OTHER_LDFLAGS = ( 802 | "-framework", 803 | SafariServices, 804 | "-framework", 805 | WebKit, 806 | ); 807 | PRODUCT_BUNDLE_IDENTIFIER = "dev.jbscript.Comments-Owl-for-Hacker-News"; 808 | PRODUCT_NAME = "Comments Owl for Hacker News"; 809 | SDKROOT = iphoneos; 810 | SWIFT_EMIT_LOC_STRINGS = YES; 811 | SWIFT_VERSION = 5.0; 812 | TARGETED_DEVICE_FAMILY = "1,2"; 813 | }; 814 | name = Debug; 815 | }; 816 | FB6A6D422A595D2600636A10 /* Release */ = { 817 | isa = XCBuildConfiguration; 818 | buildSettings = { 819 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 820 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 821 | CODE_SIGN_STYLE = Automatic; 822 | DEVELOPMENT_TEAM = 2RDKJDLNY8; 823 | GENERATE_INFOPLIST_FILE = YES; 824 | INFOPLIST_FILE = "iOS (App)/Info.plist"; 825 | INFOPLIST_KEY_CFBundleDisplayName = "Comments Owl for Hacker News"; 826 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 827 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 828 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 829 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 830 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 831 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 832 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 833 | LD_RUNPATH_SEARCH_PATHS = ( 834 | "$(inherited)", 835 | "@executable_path/Frameworks", 836 | ); 837 | OTHER_LDFLAGS = ( 838 | "-framework", 839 | SafariServices, 840 | "-framework", 841 | WebKit, 842 | ); 843 | PRODUCT_BUNDLE_IDENTIFIER = "dev.jbscript.Comments-Owl-for-Hacker-News"; 844 | PRODUCT_NAME = "Comments Owl for Hacker News"; 845 | SDKROOT = iphoneos; 846 | SWIFT_EMIT_LOC_STRINGS = YES; 847 | SWIFT_VERSION = 5.0; 848 | TARGETED_DEVICE_FAMILY = "1,2"; 849 | VALIDATE_PRODUCT = YES; 850 | }; 851 | name = Release; 852 | }; 853 | FB6A6D442A595D2600636A10 /* Debug */ = { 854 | isa = XCBuildConfiguration; 855 | buildSettings = { 856 | CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/Comments Owl for Hacker News.entitlements"; 857 | CODE_SIGN_STYLE = Automatic; 858 | DEAD_CODE_STRIPPING = YES; 859 | DEVELOPMENT_TEAM = 2RDKJDLNY8; 860 | ENABLE_HARDENED_RUNTIME = YES; 861 | GENERATE_INFOPLIST_FILE = YES; 862 | INFOPLIST_FILE = "macOS (Extension)/Info.plist"; 863 | INFOPLIST_KEY_CFBundleDisplayName = "Comments Owl for Hacker News Extension"; 864 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 865 | LD_RUNPATH_SEARCH_PATHS = ( 866 | "$(inherited)", 867 | "@executable_path/../Frameworks", 868 | "@executable_path/../../../../Frameworks", 869 | ); 870 | MACOSX_DEPLOYMENT_TARGET = 11.0; 871 | OTHER_LDFLAGS = ( 872 | "-framework", 873 | SafariServices, 874 | ); 875 | PRODUCT_BUNDLE_IDENTIFIER = "dev.jbscript.Comments-Owl-for-Hacker-News.Extension"; 876 | PRODUCT_NAME = "Comments Owl for Hacker News Extension"; 877 | SDKROOT = macosx; 878 | SKIP_INSTALL = YES; 879 | SWIFT_EMIT_LOC_STRINGS = YES; 880 | SWIFT_VERSION = 5.0; 881 | }; 882 | name = Debug; 883 | }; 884 | FB6A6D452A595D2600636A10 /* Release */ = { 885 | isa = XCBuildConfiguration; 886 | buildSettings = { 887 | CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/Comments Owl for Hacker News.entitlements"; 888 | CODE_SIGN_STYLE = Automatic; 889 | DEAD_CODE_STRIPPING = YES; 890 | DEVELOPMENT_TEAM = 2RDKJDLNY8; 891 | ENABLE_HARDENED_RUNTIME = YES; 892 | GENERATE_INFOPLIST_FILE = YES; 893 | INFOPLIST_FILE = "macOS (Extension)/Info.plist"; 894 | INFOPLIST_KEY_CFBundleDisplayName = "Comments Owl for Hacker News Extension"; 895 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 896 | LD_RUNPATH_SEARCH_PATHS = ( 897 | "$(inherited)", 898 | "@executable_path/../Frameworks", 899 | "@executable_path/../../../../Frameworks", 900 | ); 901 | MACOSX_DEPLOYMENT_TARGET = 11.0; 902 | OTHER_LDFLAGS = ( 903 | "-framework", 904 | SafariServices, 905 | ); 906 | PRODUCT_BUNDLE_IDENTIFIER = "dev.jbscript.Comments-Owl-for-Hacker-News.Extension"; 907 | PRODUCT_NAME = "Comments Owl for Hacker News Extension"; 908 | SDKROOT = macosx; 909 | SKIP_INSTALL = YES; 910 | SWIFT_EMIT_LOC_STRINGS = YES; 911 | SWIFT_VERSION = 5.0; 912 | }; 913 | name = Release; 914 | }; 915 | FB6A6D482A595D2600636A10 /* Debug */ = { 916 | isa = XCBuildConfiguration; 917 | buildSettings = { 918 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 919 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 920 | CODE_SIGN_ENTITLEMENTS = "macOS (App)/Comments Owl for Hacker News.entitlements"; 921 | CODE_SIGN_STYLE = Automatic; 922 | DEAD_CODE_STRIPPING = YES; 923 | DEVELOPMENT_TEAM = 2RDKJDLNY8; 924 | ENABLE_HARDENED_RUNTIME = YES; 925 | GENERATE_INFOPLIST_FILE = YES; 926 | INFOPLIST_KEY_CFBundleDisplayName = "Comments Owl for Hacker News"; 927 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 928 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 929 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 930 | LD_RUNPATH_SEARCH_PATHS = ( 931 | "$(inherited)", 932 | "@executable_path/../Frameworks", 933 | ); 934 | MACOSX_DEPLOYMENT_TARGET = 11.0; 935 | OTHER_LDFLAGS = ( 936 | "-framework", 937 | SafariServices, 938 | "-framework", 939 | WebKit, 940 | ); 941 | PRODUCT_BUNDLE_IDENTIFIER = "dev.jbscript.Comments-Owl-for-Hacker-News"; 942 | PRODUCT_NAME = "Comments Owl for Hacker News"; 943 | SDKROOT = macosx; 944 | SWIFT_EMIT_LOC_STRINGS = YES; 945 | SWIFT_VERSION = 5.0; 946 | }; 947 | name = Debug; 948 | }; 949 | FB6A6D492A595D2600636A10 /* Release */ = { 950 | isa = XCBuildConfiguration; 951 | buildSettings = { 952 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 953 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 954 | CODE_SIGN_ENTITLEMENTS = "macOS (App)/Comments Owl for Hacker News.entitlements"; 955 | CODE_SIGN_STYLE = Automatic; 956 | DEAD_CODE_STRIPPING = YES; 957 | DEVELOPMENT_TEAM = 2RDKJDLNY8; 958 | ENABLE_HARDENED_RUNTIME = YES; 959 | GENERATE_INFOPLIST_FILE = YES; 960 | INFOPLIST_KEY_CFBundleDisplayName = "Comments Owl for Hacker News"; 961 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 962 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 963 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 964 | LD_RUNPATH_SEARCH_PATHS = ( 965 | "$(inherited)", 966 | "@executable_path/../Frameworks", 967 | ); 968 | MACOSX_DEPLOYMENT_TARGET = 11.0; 969 | OTHER_LDFLAGS = ( 970 | "-framework", 971 | SafariServices, 972 | "-framework", 973 | WebKit, 974 | ); 975 | PRODUCT_BUNDLE_IDENTIFIER = "dev.jbscript.Comments-Owl-for-Hacker-News"; 976 | PRODUCT_NAME = "Comments Owl for Hacker News"; 977 | SDKROOT = macosx; 978 | SWIFT_EMIT_LOC_STRINGS = YES; 979 | SWIFT_VERSION = 5.0; 980 | }; 981 | name = Release; 982 | }; 983 | /* End XCBuildConfiguration section */ 984 | 985 | /* Begin XCConfigurationList section */ 986 | FB6A6CD42A595D2500636A10 /* Build configuration list for PBXProject "Comments Owl for Hacker News" */ = { 987 | isa = XCConfigurationList; 988 | buildConfigurations = ( 989 | FB6A6D3A2A595D2600636A10 /* Debug */, 990 | FB6A6D3B2A595D2600636A10 /* Release */, 991 | ); 992 | defaultConfigurationIsVisible = 0; 993 | defaultConfigurationName = Release; 994 | }; 995 | FB6A6D3C2A595D2600636A10 /* Build configuration list for PBXNativeTarget "Comments Owl for Hacker News Extension (iOS)" */ = { 996 | isa = XCConfigurationList; 997 | buildConfigurations = ( 998 | FB6A6D3D2A595D2600636A10 /* Debug */, 999 | FB6A6D3E2A595D2600636A10 /* Release */, 1000 | ); 1001 | defaultConfigurationIsVisible = 0; 1002 | defaultConfigurationName = Release; 1003 | }; 1004 | FB6A6D402A595D2600636A10 /* Build configuration list for PBXNativeTarget "Comments Owl for Hacker News (iOS)" */ = { 1005 | isa = XCConfigurationList; 1006 | buildConfigurations = ( 1007 | FB6A6D412A595D2600636A10 /* Debug */, 1008 | FB6A6D422A595D2600636A10 /* Release */, 1009 | ); 1010 | defaultConfigurationIsVisible = 0; 1011 | defaultConfigurationName = Release; 1012 | }; 1013 | FB6A6D432A595D2600636A10 /* Build configuration list for PBXNativeTarget "Comments Owl for Hacker News Extension (macOS)" */ = { 1014 | isa = XCConfigurationList; 1015 | buildConfigurations = ( 1016 | FB6A6D442A595D2600636A10 /* Debug */, 1017 | FB6A6D452A595D2600636A10 /* Release */, 1018 | ); 1019 | defaultConfigurationIsVisible = 0; 1020 | defaultConfigurationName = Release; 1021 | }; 1022 | FB6A6D472A595D2600636A10 /* Build configuration list for PBXNativeTarget "Comments Owl for Hacker News (macOS)" */ = { 1023 | isa = XCConfigurationList; 1024 | buildConfigurations = ( 1025 | FB6A6D482A595D2600636A10 /* Debug */, 1026 | FB6A6D492A595D2600636A10 /* Release */, 1027 | ); 1028 | defaultConfigurationIsVisible = 0; 1029 | defaultConfigurationName = Release; 1030 | }; 1031 | /* End XCConfigurationList section */ 1032 | }; 1033 | rootObject = FB6A6CD12A595D2500636A10 /* Project object */; 1034 | } 1035 | -------------------------------------------------------------------------------- /safari/Shared (App)/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "appicon1024-fullbleed.png", 5 | "idiom": "universal", 6 | "platform": "ios", 7 | "size": "1024x1024" 8 | }, 9 | { 10 | "filename": "appicon16.png", 11 | "idiom": "mac", 12 | "scale": "1x", 13 | "size": "16x16" 14 | }, 15 | { 16 | "filename": "appicon32.png", 17 | "idiom": "mac", 18 | "scale": "2x", 19 | "size": "16x16" 20 | }, 21 | { 22 | "filename": "appicon32.png", 23 | "idiom": "mac", 24 | "scale": "1x", 25 | "size": "32x32" 26 | }, 27 | { 28 | "filename": "appicon64.png", 29 | "idiom": "mac", 30 | "scale": "2x", 31 | "size": "32x32" 32 | }, 33 | { 34 | "filename": "appicon128.png", 35 | "idiom": "mac", 36 | "scale": "1x", 37 | "size": "128x128" 38 | }, 39 | { 40 | "filename": "appicon256.png", 41 | "idiom": "mac", 42 | "scale": "2x", 43 | "size": "128x128" 44 | }, 45 | { 46 | "filename": "appicon256.png", 47 | "idiom": "mac", 48 | "scale": "1x", 49 | "size": "256x256" 50 | }, 51 | { 52 | "filename": "appicon512.png", 53 | "idiom": "mac", 54 | "scale": "2x", 55 | "size": "256x256" 56 | }, 57 | { 58 | "filename": "appicon512.png", 59 | "idiom": "mac", 60 | "scale": "1x", 61 | "size": "512x512" 62 | }, 63 | { 64 | "filename": "appicon1024.png", 65 | "idiom": "mac", 66 | "scale": "2x", 67 | "size": "512x512" 68 | } 69 | ], 70 | "info": { 71 | "author": "xcode", 72 | "version": 1 73 | } 74 | } -------------------------------------------------------------------------------- /safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon1024-fullbleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon1024-fullbleed.png -------------------------------------------------------------------------------- /safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon1024.png -------------------------------------------------------------------------------- /safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon128.png -------------------------------------------------------------------------------- /safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon16.png -------------------------------------------------------------------------------- /safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon256.png -------------------------------------------------------------------------------- /safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon32.png -------------------------------------------------------------------------------- /safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon512.png -------------------------------------------------------------------------------- /safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon64.png -------------------------------------------------------------------------------- /safari/Shared (App)/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /safari/Shared (App)/Assets.xcassets/LargeIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "scale" : "3x" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /safari/Shared (App)/Base.lproj/Main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | Comments Owl for Hacker News 13 |
You can turn on Comments Owl for Hacker News’ Safari extension in Settings.
14 |
You can turn on Comments Owl for Hacker News’ extension in Safari Extensions preferences.
15 |
Comments Owl for Hacker News’ extension is currently on. You can turn it off in Safari Extensions preferences.
16 |
Comments Owl for Hacker News’ extension is currently off. You can turn it on in Safari Extensions preferences.
17 | 18 |
19 |
20 |
21 |
Ad
22 | Control Panel for Twitter 23 |
24 |
25 |
26 | Missing third-party Twitter clients on iOS? 27 | Ditch the app and take control of the web version with Control Panel for Twitter 28 |
29 |
30 | “Works beautifully in Safari. Beautiful UI, too” — @zeldman 31 |
32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /safari/Shared (App)/Resources/Ad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/safari/Shared (App)/Resources/Ad.png -------------------------------------------------------------------------------- /safari/Shared (App)/Resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/a51a58b4f6f27a227b656586ef702a62c4ed9aa6/safari/Shared (App)/Resources/Icon.png -------------------------------------------------------------------------------- /safari/Shared (App)/Resources/Script.js: -------------------------------------------------------------------------------- 1 | function show(platform, enabled, useSettingsInsteadOfPreferences) { 2 | document.body.classList.add(`platform-${platform}`) 3 | 4 | if (useSettingsInsteadOfPreferences) { 5 | document.querySelector('.platform-mac.state-on').innerText = 'Comments Owl for Hacker News’ extension is currently on. You can turn it off in the Extensions section of Safari Settings.' 6 | document.querySelector('.platform-mac.state-off').innerText = 'Comments Owl for Hacker News’ extension is currently off. You can turn it on in the Extensions section of Safari Settings.' 7 | document.querySelector('.platform-mac.state-unknown').innerText = 'You can turn on Comments Owl for Hacker News’ extension in the Extensions section of Safari Settings.' 8 | document.querySelector('.open-preferences').innerText = 'Quit and Open Safari Settings…' 9 | } 10 | 11 | if (typeof enabled === 'boolean') { 12 | document.body.classList.toggle('state-on', enabled) 13 | document.body.classList.toggle('state-off', !enabled) 14 | } else { 15 | document.body.classList.remove('state-on', 'state-off') 16 | } 17 | 18 | if (platform === 'ios') { 19 | document.querySelector('.open-preferences').innerText = 'Open Safari Extensions Preferences…' 20 | } 21 | } 22 | 23 | document.querySelector('button.open-preferences').addEventListener('click', () => { 24 | webkit.messageHandlers.controller.postMessage('open-preferences') 25 | }) 26 | 27 | document.querySelector('.ad').addEventListener('click', () =>{ 28 | webkit.messageHandlers.controller.postMessage('open-ad') 29 | }) 30 | -------------------------------------------------------------------------------- /safari/Shared (App)/Resources/Style.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-user-select: none; 3 | -webkit-user-drag: none; 4 | cursor: default; 5 | } 6 | 7 | :root { 8 | color-scheme: light dark; 9 | } 10 | 11 | html { 12 | height: 100%; 13 | } 14 | 15 | body { 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | height: 100%; 20 | font: -apple-system-short-body; 21 | } 22 | 23 | body:not(.platform-mac, .platform-ios) :is(.platform-mac, .platform-ios), 24 | body:not(.state-on, .state-off) :is(.state-on, .state-off), 25 | body.platform-ios .platform-mac, 26 | body.platform-mac .platform-ios, 27 | body.state-on :is(.state-off, .state-unknown), 28 | body.state-off :is(.state-on, .state-unknown) { 29 | display: none; 30 | } 31 | 32 | .app { 33 | flex: 1; 34 | display: flex; 35 | flex-direction: column; 36 | align-items: center; 37 | justify-content: center; 38 | gap: 1.25rem; 39 | text-align: center; 40 | } 41 | 42 | .ad { 43 | display: flex; 44 | flex-direction: row; 45 | align-items: stretch; 46 | gap: .5rem; 47 | border: 1px solid; 48 | border-radius: 1.5rem; 49 | font-size: .875rem; 50 | margin: 0 1rem 1rem 1rem; 51 | padding: .75rem 1rem 1rem .5rem; 52 | max-width: 26rem; 53 | } 54 | 55 | button { 56 | font-size: 1rem; 57 | } 58 | 59 | .flex { 60 | display: flex; 61 | } 62 | 63 | .flex-col { 64 | flex-direction: column; 65 | } 66 | 67 | .font-bold { 68 | font-weight: bold; 69 | } 70 | 71 | .justify-between { 72 | justify-content: space-between; 73 | } 74 | 75 | .-mb-2 { 76 | margin-bottom: -.5rem; 77 | } 78 | 79 | .mb-4 { 80 | margin-bottom: 1rem; 81 | } 82 | 83 | .text-center { 84 | text-align: center; 85 | } -------------------------------------------------------------------------------- /safari/Shared (App)/ViewController.swift: -------------------------------------------------------------------------------- 1 | import WebKit 2 | 3 | #if os(iOS) 4 | import UIKit 5 | typealias PlatformViewController = UIViewController 6 | #elseif os(macOS) 7 | import Cocoa 8 | import SafariServices 9 | typealias PlatformViewController = NSViewController 10 | #endif 11 | 12 | let extensionBundleIdentifier = "dev.jbscript.Comments-Owl-for-Hacker-News.Extension" 13 | 14 | class ViewController: PlatformViewController, WKNavigationDelegate, WKScriptMessageHandler { 15 | 16 | @IBOutlet var webView: WKWebView! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | self.webView.navigationDelegate = self 22 | 23 | #if os(iOS) 24 | self.webView.scrollView.isScrollEnabled = false 25 | #endif 26 | 27 | self.webView.configuration.userContentController.add(self, name: "controller") 28 | 29 | self.webView.loadFileURL(Bundle.main.url(forResource: "Main", withExtension: "html")!, allowingReadAccessTo: Bundle.main.resourceURL!) 30 | } 31 | 32 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 33 | #if os(iOS) 34 | webView.evaluateJavaScript("show('ios')") 35 | #elseif os(macOS) 36 | webView.evaluateJavaScript("show('mac')") 37 | 38 | SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in 39 | guard let state = state, error == nil else { 40 | // Insert code to inform the user that something went wrong. 41 | return 42 | } 43 | 44 | DispatchQueue.main.async { 45 | if #available(macOS 13, *) { 46 | webView.evaluateJavaScript("show('mac', \(state.isEnabled), true)") 47 | } else { 48 | webView.evaluateJavaScript("show('mac', \(state.isEnabled), false)") 49 | } 50 | } 51 | } 52 | #endif 53 | } 54 | 55 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 56 | #if os(iOS) 57 | if (message.body as! String == "open-ad") { 58 | let url = URL(string: "https://jbscript.dev/control-panel-for-twitter")! 59 | if UIApplication.shared.canOpenURL(url){ 60 | UIApplication.shared.open(url, options: [:], completionHandler: nil) 61 | } 62 | return 63 | } 64 | #endif 65 | 66 | if (message.body as! String != "open-preferences") { 67 | return 68 | } 69 | #if os(iOS) 70 | if #available(iOS 18.0, *) { 71 | let url = URL(string: "App-Prefs:com.apple.mobilesafari")! 72 | guard UIApplication.shared.canOpenURL(url) else { 73 | return 74 | } 75 | UIApplication.shared.open(url) 76 | } else { 77 | let url = URL(string: "App-Prefs:Safari&path=WEB_EXTENSIONS")! 78 | guard UIApplication.shared.canOpenURL(url) else { 79 | return 80 | } 81 | UIApplication.shared.open(url) 82 | } 83 | #elseif os(macOS) 84 | SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in 85 | guard error == nil else { 86 | // Insert code to inform the user that something went wrong. 87 | return 88 | } 89 | 90 | DispatchQueue.main.async { 91 | NSApplication.shared.terminate(nil) 92 | } 93 | } 94 | #endif 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /safari/Shared (Extension)/Resources/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "default_locale": "en", 4 | "name": "__MSG_extensionName__", 5 | "description": "__MSG_extensionDescription__", 6 | "homepage_url": "https://soitis.dev/comments-owl-for-hacker-news", 7 | "version": "3.0.0", 8 | "icons": { 9 | "48": "icon48.png", 10 | "96": "icon96.png", 11 | "128": "icon128.png", 12 | "256": "icon256.png", 13 | "512": "icon512.png" 14 | }, 15 | "background": { 16 | "service_worker": "background.js" 17 | }, 18 | "content_scripts": [ 19 | { 20 | "matches": [ 21 | "https://news.ycombinator.com/*" 22 | ], 23 | "js": [ 24 | "content.js" 25 | ] 26 | } 27 | ], 28 | "options_ui": { 29 | "page": "options.html" 30 | }, 31 | "action": { 32 | "default_title": "__MSG_extensionName__", 33 | "default_popup": "browser_action.html", 34 | "default_icon": { 35 | "16": "toolbar-icon16.png", 36 | "19": "toolbar-icon19.png", 37 | "32": "toolbar-icon32.png", 38 | "38": "toolbar-icon38.png", 39 | "48": "toolbar-icon48.png", 40 | "72": "toolbar-icon72.png" 41 | } 42 | }, 43 | "permissions": [ 44 | "contextMenus", 45 | "storage" 46 | ] 47 | } -------------------------------------------------------------------------------- /safari/Shared (Extension)/SafariWebExtensionHandler.swift: -------------------------------------------------------------------------------- 1 | import SafariServices 2 | import os.log 3 | 4 | let SFExtensionMessageKey = "message" 5 | 6 | class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { 7 | 8 | func beginRequest(with context: NSExtensionContext) { 9 | let item = context.inputItems[0] as! NSExtensionItem 10 | let message = item.userInfo?[SFExtensionMessageKey] 11 | os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg) 12 | 13 | let response = NSExtensionItem() 14 | response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ] 15 | 16 | context.completeRequest(returningItems: [response], completionHandler: nil) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /safari/iOS (App)/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | var window: UIWindow? 7 | 8 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 9 | // Override point for customization after application launch. 10 | return true 11 | } 12 | 13 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 14 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /safari/iOS (App)/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /safari/iOS (App)/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /safari/iOS (App)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /safari/iOS (App)/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 4 | 5 | var window: UIWindow? 6 | 7 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 8 | guard let _ = (scene as? UIWindowScene) else { return } 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /safari/iOS (Extension)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.Safari.web-extension 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /safari/macOS (App)/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | @main 4 | class AppDelegate: NSObject, NSApplicationDelegate { 5 | 6 | func applicationDidFinishLaunching(_ notification: Notification) { 7 | // Override point for customization after application launch. 8 | } 9 | 10 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 11 | return true 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /safari/macOS (App)/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /safari/macOS (App)/Comments Owl for Hacker News.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /safari/macOS (Extension)/Comments Owl for Hacker News.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /safari/macOS (Extension)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.Safari.web-extension 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const {execSync} = require('child_process') 2 | const path = require('path') 3 | const fs = require('fs') 4 | 5 | let manifestVersions = [2, 3] 6 | if (process.argv[2] && manifestVersions.includes(Number(process.argv[2]))) { 7 | manifestVersions = [Number(process.argv[2])] 8 | } 9 | 10 | for (let manifestVersion of manifestVersions) { 11 | console.log(`\nBuilding MV${manifestVersion} version`) 12 | let manifestFile = `manifest.mv${manifestVersion}.json` 13 | let manifestData = require(`../${manifestFile}`) 14 | fs.copyFileSync(`./${manifestFile}`, './manifest.json') 15 | execSync(`node_modules/.bin/web-ext${process.platform == 'win32' ? '.cmd' : ''} build`, {stdio: 'inherit'}) 16 | let renameTo = `./web-ext-artifacts/comments_owl_for_hacker_news-${manifestData['version']}.mv${manifestVersion}.zip` 17 | fs.renameSync( 18 | `./web-ext-artifacts/comments_owl_for_hacker_news-${manifestData['version']}.zip`, 19 | renameTo, 20 | ) 21 | console.log('Moved to:', path.resolve(renameTo)) 22 | fs.rmSync('./manifest.json') 23 | } -------------------------------------------------------------------------------- /scripts/copy.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | fs.copyFileSync(process.argv[2], process.argv[3]) -------------------------------------------------------------------------------- /scripts/create-browser-action.js: -------------------------------------------------------------------------------- 1 | // Creates browser_action.html, which is just options.html with styling to 2 | // control the popup width appropriately for each browser. 3 | const fs = require('fs') 4 | 5 | let options = fs.readFileSync('./options.html', {encoding: 'utf8'}) 6 | 7 | fs.writeFileSync( 8 | './browser_action.html', 9 | options.replace('', ''), 10 | {encoding: 'utf8'} 11 | ) -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const semver = require('semver') 4 | 5 | const contentPath = './content.js' 6 | const manifestPaths = ['./manifest.mv2.json', './manifest.mv3.json', './Safari/Shared (Extension)/Resources/manifest.json'] 7 | const optionsPath = './options.html' 8 | const safariProjectPath = './safari/Comments Owl for Hacker News.xcodeproj/project.pbxproj' 9 | 10 | let releaseType = process.argv[2] 11 | 12 | if (releaseType != 'patch' && releaseType != 'minor' && releaseType != 'major') { 13 | console.log(` 14 | Usage: 15 | npm run release (patch|minor|major) 16 | `.trim()) 17 | process.exit(1) 18 | } 19 | 20 | let currentVersion = JSON.parse(fs.readFileSync(manifestPaths[0], {encoding: 'utf8'})).version 21 | let nextVersion = semver.inc(currentVersion, releaseType) 22 | 23 | fs.writeFileSync( 24 | contentPath, 25 | fs.readFileSync(contentPath, {encoding: 'utf8'}) 26 | .replace(/@version (\d+)/g, (_, current) => `@version ${Number(current) + 1}`), 27 | {encoding: 'utf8'} 28 | ) 29 | 30 | for (let manifestPath of manifestPaths) { 31 | fs.writeFileSync( 32 | manifestPath, 33 | fs.readFileSync(manifestPath, {encoding: 'utf8'}) 34 | .replace(/"version": "[^"]+"/, `"version": "${nextVersion}"`), 35 | {encoding: 'utf8'} 36 | ) 37 | } 38 | 39 | fs.writeFileSync( 40 | optionsPath, 41 | fs.readFileSync(optionsPath, {encoding: 'utf8'}) 42 | .replace(/id="version">[^<]+v${nextVersion}<`), 43 | {encoding: 'utf8'} 44 | ) 45 | 46 | fs.writeFileSync( 47 | safariProjectPath, 48 | fs.readFileSync(safariProjectPath, {encoding: 'utf8'}) 49 | .replace(/CURRENT_PROJECT_VERSION = (\d+)/g, (_, current) => `CURRENT_PROJECT_VERSION = ${Number(current) + 1}`) 50 | .replace(/MARKETING_VERSION = [^;]+/g, `MARKETING_VERSION = ${nextVersion}`), 51 | {encoding: 'utf8'} 52 | ) 53 | 54 | console.log(`Bumped to v${nextVersion}`) -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | export type Config = { 2 | addUpvotedToHeader: boolean 3 | autoCollapseNotNew: boolean 4 | autoHighlightNew: boolean 5 | hideCommentsNav: boolean 6 | hideJobsNav: boolean 7 | hidePastNav: boolean 8 | hideReplyLinks: boolean 9 | hideSubmitNav: boolean 10 | listPageFlagging: 'enabled' | 'disabled' | 'confirm' 11 | listPageHiding: 'enabled' | 'disabled' | 'confirm' 12 | makeSubmissionTextReadable: boolean 13 | } --------------------------------------------------------------------------------
391 | * (indentation) 392 | * 395 | *
396 | *
(meta bar: user, age and folding control) 397 | * … 398 | *
399 | * (text and reply link) 400 | * ``` 401 | * 402 | * We want to be able to collapse comment trees which don't contain new comments 403 | * and highlight new comments, so for each wrapper we'll create a `HNComment` 404 | * object to manage this. 405 | * 406 | * Comments are rendered as a flat list of table rows, so we'll use the width of 407 | * the indentation spacer to determine which comments are descendants of a given 408 | * comment. 409 | * 410 | * Since we have to reimplement our own comment folding, we'll hide the built-in 411 | * folding controls and create new ones in a better position (on the left), with 412 | * a larger hitbox (larger font and an en dash [–] instead of a hyphen [-]). 413 | * 414 | * On each comment page view, we store the current comment count, the max 415 | * comment id on the page and the current time as the last visit time. 416 | */ 417 | function commentPage() { 418 | log('comment page') 419 | 420 | //#region CSS 421 | addStyle('comments-static', ` 422 | /* Hide default toggle and nav links */ 423 | a.togg { 424 | display: none; 425 | } 426 | .toggle { 427 | cursor: pointer; 428 | margin-right: 3px; 429 | background: transparent; 430 | border: 0; 431 | padding: 0; 432 | color: inherit; 433 | font-family: inherit; 434 | } 435 | /* Display the mute control on hover, unless the comment is collapsed */ 436 | .mute { 437 | display: none; 438 | } 439 | /* Prevent :hover causing double-tap on comment functionality in iOS Safari */ 440 | @media(hover: hover) and (pointer: fine) { 441 | tr.comtr:hover td.votelinks:not(.nosee) + td .mute { 442 | display: inline; 443 | } 444 | } 445 | /* Don't show notes on collapsed comments */ 446 | td.votelinks.nosee + td .note { 447 | display: none; 448 | } 449 | #timeTravel { 450 | margin-top: 1em; 451 | vertical-align: middle; 452 | } 453 | #timeTravelRange { 454 | width: 100%; 455 | } 456 | #timeTravelButton { 457 | margin-right: 1em; 458 | } 459 | 460 | @media only screen and (min-width: 300px) and (max-width: 750px) { 461 | td.votelinks:not(.nosee) + td .mute { 462 | display: inline; 463 | } 464 | /* Allow comments to go full-width */ 465 | .comment { 466 | max-width: unset; 467 | } 468 | /* Increase distance between upvote and downvote */ 469 | a[id^="down_"] { 470 | margin-top: 16px; 471 | } 472 | /* Increase hit-target */ 473 | .toggle { 474 | font-size: 14px; 475 | } 476 | #highlightControls label { 477 | display: block; 478 | } 479 | #highlightControls label + label { 480 | margin-top: .5rem; 481 | } 482 | #timeTravelRange { 483 | width: calc(100% - 32px); 484 | } 485 | } 486 | `) 487 | 488 | let $style = addStyle('comments-dynamic') 489 | 490 | function configureCss() { 491 | $style.textContent = [ 492 | config.hideReplyLinks && ` 493 | div.reply { 494 | margin-top: 8px; 495 | } 496 | div.reply p { 497 | display: none; 498 | } 499 | `, 500 | config.makeSubmissionTextReadable && ` 501 | div.toptext { 502 | color: #000; 503 | } 504 | `, 505 | ].filter(Boolean).map(dedent).join('\n') 506 | } 507 | //#endregion 508 | 509 | //#region Variables 510 | /** @type {boolean} */ 511 | let autoCollapseNotNew = config.autoCollapseNotNew || location.search.includes('?shownew') 512 | 513 | /** @type {boolean} */ 514 | let autoHighlightNew = config.autoHighlightNew || location.search.includes('?shownew') 515 | 516 | /** @type {HNComment[]} */ 517 | let comments = [] 518 | 519 | /** @type {Record} */ 520 | let commentsById = {} 521 | 522 | /** @type {boolean} */ 523 | let hasNewComments = false 524 | 525 | /** @type {string} */ 526 | let itemId = /id=(\d+)/.exec(location.search)[1] 527 | 528 | /** @type {Visit} */ 529 | let lastVisit 530 | 531 | /** @type {number} */ 532 | let maxCommentId 533 | 534 | /** @type {Set} */ 535 | let mutedUsers = getMutedUsers() 536 | 537 | /** @type {Record} */ 538 | let userNotes = getUserNotes() 539 | 540 | // Comment counts 541 | let commentCount = 0 542 | let mutedCommentCount = 0 543 | let newCommentCount = 0 544 | let replyToMutedCommentCount = 0 545 | //#endregion 546 | 547 | class HNComment { 548 | /** 549 | * returns {boolean} 550 | */ 551 | get isMuted() { 552 | return mutedUsers.has(this.user) 553 | } 554 | 555 | /** 556 | * @returns {HNComment[]} 557 | */ 558 | get childComments() { 559 | if (this._childComments == null) { 560 | this._childComments = [] 561 | for (let i = this.index + 1; i < comments.length; i++) { 562 | if (comments[i].indent <= this.indent) { 563 | break 564 | } 565 | this._childComments.push(comments[i]) 566 | } 567 | } 568 | return this._childComments 569 | } 570 | 571 | get collapsedChildrenText() { 572 | return this.childCommentCount == 0 ? '' : [ 573 | this.isDeleted ? '(' : ' | (', 574 | this.childCommentCount, 575 | ` child${s(this.childCommentCount, 'ren')})`, 576 | ].join('') 577 | } 578 | 579 | /** 580 | * @returns {HNComment[]} 581 | */ 582 | get nonMutedChildComments() { 583 | if (this._nonMutedChildComments == null) { 584 | let muteIndent = null 585 | this._nonMutedChildComments = this.childComments.filter(comment => { 586 | if (muteIndent != null) { 587 | if (comment.indent > muteIndent) { 588 | return false 589 | } 590 | muteIndent = null 591 | } 592 | 593 | if (comment.isMuted) { 594 | muteIndent = comment.indent 595 | return false 596 | } 597 | 598 | return true 599 | }) 600 | } 601 | return this._nonMutedChildComments 602 | } 603 | 604 | /** 605 | * returns {number} 606 | */ 607 | get childCommentCount() { 608 | return this.nonMutedChildComments.length 609 | } 610 | 611 | /** 612 | * @param {HTMLElement} $wrapper 613 | * @param {number} index 614 | */ 615 | constructor($wrapper, index) { 616 | /** @type {number} */ 617 | this.indent = Number( /** @type {HTMLImageElement} */ ($wrapper.querySelector('img[src="s.gif"]')).width) 618 | 619 | /** @type {number} */ 620 | this.index = index 621 | 622 | let $user = /** @type {HTMLElement} */ ($wrapper.querySelector('a.hnuser')) 623 | /** @type {string} */ 624 | this.user = $user?.innerText 625 | 626 | /** @type {HTMLElement} */ 627 | this.$comment = $wrapper.querySelector('div.comment') 628 | 629 | /** @type {HTMLElement} */ 630 | this.$topBar = $wrapper.querySelector('td.default > div') 631 | 632 | /** @type {HTMLElement} */ 633 | this.$voteLinks = $wrapper.querySelector('td.votelinks') 634 | 635 | /** @type {HTMLElement} */ 636 | this.$wrapper = $wrapper 637 | 638 | /** @private @type {HNComment[]} */ 639 | this._childComments = null 640 | 641 | /** @private @type {HNComment[]} */ 642 | this._nonMutedChildComments = null 643 | 644 | /** 645 | * The comment's id. 646 | * Will be `-1` for deleted comments. 647 | * @type {number} 648 | */ 649 | this.id = -1 650 | 651 | /** 652 | * Some flagged comments are collapsed by default. 653 | * @type {boolean} 654 | */ 655 | this.isCollapsed = $wrapper.classList.contains('coll') 656 | 657 | /** 658 | * Comments whose text has been removed but are still displayed may have 659 | * their text replaced with [flagged], [dead] or similar - we'll take any 660 | * word in square brackets as indication of this. 661 | * @type {boolean} 662 | */ 663 | this.isDeleted = /^\s*\[\w+]\s*$/.test(this.$comment.firstChild.nodeValue) 664 | 665 | /** 666 | * The displayed age of the comment; `${n} minutes/hours/days ago`, or 667 | * `on ${date}` for older comments. 668 | * Will be blank for deleted comments. 669 | * @type {string} 670 | */ 671 | this.when = '' 672 | 673 | /** @type {HTMLElement} */ 674 | this.$childCount = null 675 | 676 | /** @type {HTMLElement} */ 677 | this.$comhead = this.$topBar.querySelector('span.comhead') 678 | 679 | /** @type {HTMLElement} */ 680 | this.$toggleControl = h('button', { 681 | className: 'toggle', 682 | onclick: () => this.toggleCollapsed(), 683 | }, this.isCollapsed ? TOGGLE_SHOW : TOGGLE_HIDE) 684 | 685 | if (!this.isDeleted) { 686 | let $permalink = /** @type {HTMLAnchorElement} */ (this.$topBar.querySelector('a[href^=item]')) 687 | this.id = Number($permalink.href.split('=').pop()) 688 | this.when = $permalink?.textContent.replace('minute', 'min') 689 | } 690 | } 691 | 692 | addControls() { 693 | // We want to use the comment meta bar for the folding control, so put 694 | // it back above the deleted comment placeholder. 695 | if (this.isDeleted) { 696 | this.$topBar.style.marginBottom = '4px' 697 | } 698 | this.$topBar.insertAdjacentText('afterbegin', ' ') 699 | this.$topBar.insertAdjacentElement('afterbegin', this.$toggleControl) 700 | this.$comhead.append(...[ 701 | // User note 702 | userNotes[this.user] && h('span', {className: 'note'}, 703 | ` | nb: ${userNotes[this.user].split(/\r?\n/)[0]}`, 704 | ), 705 | // Mute control 706 | this.user && h('span', {className: 'mute'}, ' | ', h('a', { 707 | href: `mute?id=${this.user}`, 708 | onclick: (e) => { 709 | e.preventDefault() 710 | this.mute() 711 | } 712 | }, 'mute')) 713 | ].filter(Boolean)) 714 | } 715 | 716 | mute() { 717 | mutedUsers = getMutedUsers() 718 | mutedUsers.add(this.user) 719 | storeMutedUsers(mutedUsers) 720 | 721 | // Invalidate non-muted child caches and update child counts on any 722 | // comments which have been collapsed. 723 | for (let i = 0; i < comments.length; i++) { 724 | let comment = comments[i] 725 | 726 | if (comment.isMuted) { 727 | i += comment.childComments.length 728 | continue 729 | } 730 | 731 | comment._nonMutedChildComments = null 732 | if (comment.$childCount) { 733 | comment.$childCount.textContent = comment.collapsedChildrenText 734 | } 735 | } 736 | 737 | hideMutedUsers() 738 | } 739 | 740 | /** 741 | * @param {boolean} updateChildren 742 | */ 743 | updateDisplay(updateChildren = true) { 744 | // Show/hide this comment, preserving display of the meta bar 745 | toggleDisplay(this.$comment, this.isCollapsed) 746 | if (this.$voteLinks) { 747 | toggleVisibility(this.$voteLinks, this.isCollapsed) 748 | } 749 | this.$toggleControl.textContent = this.isCollapsed ? TOGGLE_SHOW : TOGGLE_HIDE 750 | 751 | // Show/hide the number of child comments when collapsed 752 | if (this.childCommentCount > 0) { 753 | if (this.isCollapsed && this.$childCount == null) { 754 | this.$childCount = h('span', null, this.collapsedChildrenText) 755 | this.$comhead.appendChild(this.$childCount) 756 | } 757 | toggleDisplay(this.$childCount, !this.isCollapsed) 758 | } 759 | 760 | if (updateChildren) { 761 | for (let i = 0; i < this.nonMutedChildComments.length; i++) { 762 | let child = this.nonMutedChildComments[i] 763 | toggleDisplay(child.$wrapper, this.isCollapsed) 764 | if (child.isCollapsed) { 765 | i += child.childComments.length 766 | } 767 | } 768 | } 769 | } 770 | 771 | /** 772 | * Completely hides this comment and its replies. 773 | */ 774 | hide() { 775 | toggleDisplay(this.$wrapper, true) 776 | this.childComments.forEach((child) => toggleDisplay(child.$wrapper, true)) 777 | } 778 | 779 | /** 780 | * @param {number} commentId 781 | * @returns {boolean} 782 | */ 783 | hasChildCommentsNewerThan(commentId) { 784 | return this.nonMutedChildComments.some((comment) => comment.isNewerThan(commentId)) 785 | } 786 | 787 | /** 788 | * @param {number} commentId 789 | * @returns {boolean} 790 | */ 791 | isNewerThan(commentId) { 792 | return this.id > commentId 793 | } 794 | 795 | /** 796 | * @param {boolean} isCollapsed 797 | */ 798 | toggleCollapsed(isCollapsed = !this.isCollapsed) { 799 | this.isCollapsed = isCollapsed 800 | this.updateDisplay() 801 | } 802 | 803 | /** 804 | * @param {boolean} highlight 805 | */ 806 | toggleHighlighted(highlight) { 807 | this.$wrapper.style.backgroundColor = highlight ? HIGHLIGHT_COLOR : 'transparent' 808 | } 809 | } 810 | 811 | //#region Functions 812 | function addHighlightCommentsControl($container) { 813 | let $highlightComments = h('span', null, ' | ', h('a', { 814 | href: '#', 815 | onClick(e) { 816 | e.preventDefault() 817 | addTimeTravelCommentControls($container) 818 | $highlightComments.remove() 819 | }, 820 | }, 'highlight comments')) 821 | 822 | $container.querySelector('.subline')?.append($highlightComments) 823 | } 824 | 825 | /** 826 | * Adds checkboxes to toggle folding and highlighting when there are new 827 | * comments on a comment page. 828 | * @param {HTMLElement} $container 829 | */ 830 | function addNewCommentControls($container) { 831 | $container.appendChild( 832 | h('div', null, 833 | h('p', null, 834 | `${newCommentCount} new comment${s(newCommentCount)} since ${lastVisit.time.toLocaleString()}` 835 | ), 836 | h('div', {id: 'highlightControls'}, 837 | checkbox({ 838 | checked: autoHighlightNew, 839 | onclick: (e) => { 840 | highlightNewComments(e.target.checked, lastVisit.maxCommentId) 841 | }, 842 | }, 'highlight new comments'), 843 | ' ', 844 | checkbox({ 845 | checked: autoCollapseNotNew, 846 | onclick: (e) => { 847 | collapseThreadsWithoutNewComments(e.target.checked, lastVisit.maxCommentId) 848 | }, 849 | }, 'collapse threads without new comments'), 850 | ), 851 | ) 852 | ) 853 | } 854 | 855 | /** 856 | * Adds the appropriate page controls depending on whether or not there are 857 | * new comments or any comments at all. 858 | */ 859 | function addPageControls() { 860 | let $container = /** @type {HTMLElement} */ (document.querySelector('td.subtext')) 861 | if (!$container) { 862 | warn('no container found for page controls') 863 | return 864 | } 865 | 866 | if (hasNewComments) { 867 | addNewCommentControls($container) 868 | } 869 | else if (commentCount > 1) { 870 | addHighlightCommentsControl($container) 871 | } 872 | } 873 | 874 | /** 875 | * Adds a range control and button to show the last X new comments. 876 | */ 877 | function addTimeTravelCommentControls($container) { 878 | let sortedCommentIds = [] 879 | for (let i = 0; i < comments.length; i++) { 880 | let comment = comments[i] 881 | if (comment.isMuted) { 882 | // Skip muted comments and their replies as they're always hidden 883 | i += comment.childComments.length 884 | continue 885 | } 886 | sortedCommentIds.push(comment.id) 887 | } 888 | sortedCommentIds.sort() 889 | 890 | let showNewCommentsAfter = Math.max(0, sortedCommentIds.length - 1) 891 | let howMany = sortedCommentIds.length - showNewCommentsAfter 892 | 893 | function getRangeDescription() { 894 | let fromWhen = commentsById[sortedCommentIds[showNewCommentsAfter]].when 895 | // Older comments display `on ${date}` instead of a relative time 896 | if (fromWhen.startsWith(' on')) { 897 | fromWhen = fromWhen.replace(' on', 'since') 898 | } 899 | else { 900 | fromWhen = `from ${fromWhen}` 901 | } 902 | return `${howMany} ${fromWhen}` 903 | } 904 | 905 | let $description = h('span', null, getRangeDescription()) 906 | 907 | let $range = h('input', { 908 | id: 'timeTravelRange', 909 | max: sortedCommentIds.length - 1, 910 | min: 1, 911 | oninput(e) { 912 | showNewCommentsAfter = Number(e.target.value) 913 | howMany = sortedCommentIds.length - showNewCommentsAfter 914 | $description.textContent = getRangeDescription() 915 | }, 916 | type: 'range', 917 | value: sortedCommentIds.length - 1, 918 | }) 919 | 920 | let $button = /** @type {HTMLInputElement} */ (h('input', { 921 | id: 'timeTravelButton', 922 | onclick() { 923 | let referenceCommentId = sortedCommentIds[showNewCommentsAfter - 1] 924 | log(`manually highlighting ${howMany} comments since ${referenceCommentId}`) 925 | highlightNewComments(true, referenceCommentId) 926 | collapseThreadsWithoutNewComments(true, referenceCommentId) 927 | $timeTravelControl.remove() 928 | }, 929 | type: 'button', 930 | value: 'highlight comments', 931 | })) 932 | 933 | let $timeTravelControl = h('div', { 934 | id: 'timeTravel', 935 | }, h('div', null, $range), $button, $description) 936 | 937 | $container.appendChild($timeTravelControl) 938 | } 939 | 940 | /** 941 | * Collapses threads which don't have any comments newer than the given 942 | * comment id. 943 | * @param {boolean} collapse 944 | * @param {number} referenceCommentId 945 | */ 946 | function collapseThreadsWithoutNewComments(collapse, referenceCommentId) { 947 | for (let i = 0; i < comments.length; i++) { 948 | let comment = comments[i] 949 | if (comment.isMuted) { 950 | // Skip muted comments and their replies as they're always hidden 951 | i += comment.childComments.length 952 | continue 953 | } 954 | if (!comment.isNewerThan(referenceCommentId) && 955 | !comment.hasChildCommentsNewerThan(referenceCommentId)) { 956 | comment.toggleCollapsed(collapse) 957 | // Skip replies as we've already checked them 958 | i += comment.childComments.length 959 | } 960 | } 961 | } 962 | 963 | function hideMutedUsers() { 964 | for (let i = 0; i < comments.length; i++) { 965 | let comment = comments[i] 966 | if (comment.isMuted) { 967 | comment.hide() 968 | // Skip replies as hide() already hid them 969 | i += comment.childComments.length 970 | } 971 | } 972 | } 973 | 974 | /** 975 | * Highlights comments newer than the given comment id. 976 | * @param {boolean} highlight 977 | * @param {number} referenceCommentId 978 | */ 979 | function highlightNewComments(highlight, referenceCommentId) { 980 | comments.forEach((comment) => { 981 | if (!comment.isMuted && comment.isNewerThan(referenceCommentId)) { 982 | comment.toggleHighlighted(highlight) 983 | } 984 | }) 985 | } 986 | 987 | function initComments() { 988 | let commentWrappers = /** @type {NodeListOf} */ (document.querySelectorAll('table.comment-tree tr.athing')) 989 | log('number of comment wrappers', commentWrappers.length) 990 | 991 | let commentIndex = 0 992 | for (let $wrapper of commentWrappers) { 993 | let comment = new HNComment($wrapper, commentIndex++) 994 | comments.push(comment) 995 | if (!comment.isMuted && !comment.isDeleted) { 996 | commentsById[comment.id] = comment 997 | } 998 | } 999 | 1000 | let lastVisitMaxCommentId = lastVisit?.maxCommentId ?? -1 1001 | for (let i = 0; i < comments.length; i++) { 1002 | let comment = comments[i] 1003 | 1004 | if (comment.isMuted) { 1005 | mutedCommentCount++ 1006 | for (let j = i + 1; j <= i + comment.childComments.length; j++) { 1007 | if (comments[j].isMuted) { 1008 | mutedCommentCount++ 1009 | } else { 1010 | replyToMutedCommentCount++ 1011 | } 1012 | } 1013 | // Skip child comments as we've already accounted for them 1014 | i += comment.childComments.length 1015 | // Don't consider muted comments or their replies when counting new 1016 | // comments, or add controls to them, as they'll all be hidden. 1017 | continue 1018 | } 1019 | 1020 | if (!comment.isDeleted && comment.isNewerThan(lastVisitMaxCommentId)) { 1021 | newCommentCount++ 1022 | } 1023 | 1024 | comment.addControls() 1025 | } 1026 | 1027 | maxCommentId = comments.map(comment => comment.id).sort().pop() 1028 | hasNewComments = lastVisit != null && newCommentCount > 0 1029 | } 1030 | 1031 | // TODO Only store visit data when the item header is present (i.e. not a comment permalink) 1032 | // TODO Only store visit data for commentable items (a reply box / reply links are visible) 1033 | // TODO Clear any existing stored visit if the item is no longer commentable 1034 | function storePageViewData() { 1035 | storeVisit(itemId, new Visit({ 1036 | commentCount, 1037 | maxCommentId, 1038 | time: new Date(), 1039 | })) 1040 | } 1041 | //#endregion 1042 | 1043 | //#region Main 1044 | lastVisit = getLastVisit(itemId) 1045 | 1046 | let $commentsLink = document.querySelector('span.subline > a[href^=item]') 1047 | if ($commentsLink && /^\d+/.test($commentsLink.textContent)) { 1048 | commentCount = Number($commentsLink.textContent.split(/\s/).shift()) 1049 | } else { 1050 | warn('number of comments link not found') 1051 | } 1052 | 1053 | configureCss() 1054 | initComments() 1055 | // Update display of any comments which were already collapsed by HN's own 1056 | // functionality, e.g. deleted comments 1057 | comments.filter(comment => comment.isCollapsed).forEach(comment => comment.updateDisplay(false)) 1058 | hideMutedUsers() 1059 | if (hasNewComments && (autoHighlightNew || autoCollapseNotNew)) { 1060 | if (autoHighlightNew) { 1061 | highlightNewComments(true, lastVisit.maxCommentId) 1062 | } 1063 | if (autoCollapseNotNew) { 1064 | collapseThreadsWithoutNewComments(true, lastVisit.maxCommentId) 1065 | } 1066 | } 1067 | addPageControls() 1068 | storePageViewData() 1069 | 1070 | log('page view data', { 1071 | autoHighlightNew, 1072 | commentCount, 1073 | mutedCommentCount, 1074 | replyToMutedCommentCount, 1075 | hasNewComments, 1076 | itemId, 1077 | lastVisit, 1078 | maxCommentId, 1079 | newCommentCount, 1080 | }) 1081 | 1082 | chrome.storage.local.onChanged.addListener((changes) => { 1083 | if ('hideReplyLinks' in changes) { 1084 | config.hideReplyLinks = changes['hideReplyLinks'].newValue 1085 | configureCss() 1086 | } 1087 | if ('makeSubmissionTextReadable' in changes) { 1088 | config.makeSubmissionTextReadable = changes['makeSubmissionTextReadable'].newValue 1089 | configureCss() 1090 | } 1091 | }) 1092 | //#endregion 1093 | } 1094 | //#endregion 1095 | 1096 | //#region Item list page 1097 | /** 1098 | * Each item on an item list page has the following structure: 1099 | * 1100 | * ```html 1101 | *
1105 | * (item meta info) 1106 | *