├── .github
└── FUNDING.yml
├── safari
├── .gitignore
├── Shared (App)
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── appicon128.png
│ │ │ ├── appicon16.png
│ │ │ ├── appicon256.png
│ │ │ ├── appicon32.png
│ │ │ ├── appicon512.png
│ │ │ ├── appicon64.png
│ │ │ ├── appicon1024.png
│ │ │ ├── appicon1024-fullbleed.png
│ │ │ └── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── LargeIcon.imageset
│ │ │ └── Contents.json
│ ├── Resources
│ │ ├── Ad.png
│ │ ├── Icon.png
│ │ ├── Script.js
│ │ └── Style.css
│ ├── Base.lproj
│ │ └── Main.html
│ └── ViewController.swift
├── macOS (Extension)
│ ├── Comments Owl for Hacker News.entitlements
│ └── Info.plist
├── iOS (App)
│ ├── SceneDelegate.swift
│ ├── AppDelegate.swift
│ ├── Info.plist
│ └── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
├── macOS (App)
│ ├── Comments Owl for Hacker News.entitlements
│ ├── AppDelegate.swift
│ └── Base.lproj
│ │ └── Main.storyboard
├── iOS (Extension)
│ └── Info.plist
├── Shared (Extension)
│ ├── SafariWebExtensionHandler.swift
│ └── Resources
│ │ └── manifest.json
└── Comments Owl for Hacker News.xcodeproj
│ └── project.pbxproj
├── scripts
├── copy.js
├── create-browser-action.js
├── build.js
└── release.js
├── icons
├── icon16.png
├── icon48.png
├── icon64.png
├── icon96.png
├── icon128.png
├── icon256.png
├── icon512.png
├── icon600.png
├── toolbar-icon16.png
├── toolbar-icon19.png
├── toolbar-icon32.png
├── toolbar-icon38.png
├── toolbar-icon48.png
├── toolbar-icon72.png
├── chrome-web-store-icon.png
└── icon.svg
├── .gitignore
├── promo
├── app-store.png
├── chrome_small_promo_tile.png
└── draw-the-rest-of-the-owl.gif
├── .vscode
└── extensions.json
├── jsconfig.json
├── PRIVACY_POLICY.md
├── types.d.ts
├── manifest.mv3.json
├── background.js
├── LICENSE
├── manifest.mv2.json
├── NOTICE
├── package.json
├── _locales
└── en
│ └── messages.json
├── README.md
├── options.js
├── options.html
├── options.css
└── content.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: jbscript
2 |
--------------------------------------------------------------------------------
/safari/.gitignore:
--------------------------------------------------------------------------------
1 | xcuserdata/
2 | *.xcworkspace
3 | build/
--------------------------------------------------------------------------------
/scripts/copy.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | fs.copyFileSync(process.argv[2], process.argv[3])
--------------------------------------------------------------------------------
/icons/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/icons/icon16.png
--------------------------------------------------------------------------------
/icons/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/icons/icon48.png
--------------------------------------------------------------------------------
/icons/icon64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/icons/icon64.png
--------------------------------------------------------------------------------
/icons/icon96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/icons/icon96.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /manifest.json
3 | browser_action.html
4 | node_modules/
5 | web-ext-artifacts/
--------------------------------------------------------------------------------
/icons/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/icons/icon128.png
--------------------------------------------------------------------------------
/icons/icon256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/icons/icon256.png
--------------------------------------------------------------------------------
/icons/icon512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/icons/icon512.png
--------------------------------------------------------------------------------
/icons/icon600.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/icons/icon600.png
--------------------------------------------------------------------------------
/promo/app-store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/promo/app-store.png
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "santacodes.santacodes-region-viewer"
4 | ]
5 | }
--------------------------------------------------------------------------------
/icons/toolbar-icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/icons/toolbar-icon16.png
--------------------------------------------------------------------------------
/icons/toolbar-icon19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/icons/toolbar-icon19.png
--------------------------------------------------------------------------------
/icons/toolbar-icon32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/icons/toolbar-icon32.png
--------------------------------------------------------------------------------
/icons/toolbar-icon38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/icons/toolbar-icon38.png
--------------------------------------------------------------------------------
/icons/toolbar-icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/icons/toolbar-icon48.png
--------------------------------------------------------------------------------
/icons/toolbar-icon72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/icons/toolbar-icon72.png
--------------------------------------------------------------------------------
/icons/chrome-web-store-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/icons/chrome-web-store-icon.png
--------------------------------------------------------------------------------
/promo/chrome_small_promo_tile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/promo/chrome_small_promo_tile.png
--------------------------------------------------------------------------------
/promo/draw-the-rest-of-the-owl.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/promo/draw-the-rest-of-the-owl.gif
--------------------------------------------------------------------------------
/safari/Shared (App)/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/safari/Shared (App)/Resources/Ad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/safari/Shared (App)/Resources/Ad.png
--------------------------------------------------------------------------------
/safari/Shared (App)/Resources/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/safari/Shared (App)/Resources/Icon.png
--------------------------------------------------------------------------------
/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/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/HEAD/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/HEAD/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/HEAD/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/HEAD/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/HEAD/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon64.png
--------------------------------------------------------------------------------
/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon1024.png
--------------------------------------------------------------------------------
/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon1024-fullbleed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/comments-owl-for-hacker-news/HEAD/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/appicon1024-fullbleed.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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).
--------------------------------------------------------------------------------
/safari/macOS (Extension)/Comments Owl for Hacker News.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 | )
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 (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/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/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)/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | })
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/_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 | }
--------------------------------------------------------------------------------
/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)/Base.lproj/Main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |

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 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [Comments Owl for Hacker News](https://soitis.dev/comments-owl-for-hacker-news)
2 |
3 | [](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 |
--------------------------------------------------------------------------------
/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">[^<]+, `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}`)
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/icons/icon.svg:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/safari/macOS (App)/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | |
29 |
50 | |
51 |
52 |
53 |
54 |
55 |
65 |
66 | |
67 |
68 |
69 |
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 | *
391 | * (indentation)
392 | * |
393 | * … | (vote up/down controls)
394 | *
395 | *
396 | * (meta bar: user, age and folding control)
397 | * …
398 | * | |