├── .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 | [](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 |
29 |
|
51 | |||
55 |
66 | |
67 |
391 | * ![]() |
393 | * … | (vote up/down controls) 394 | *
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 … | (spacer)
1104 | *
1105 | * … (item meta info)
1106 | * |
1107 | *
12 |
19 | ![]() 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 |
20 |
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 |
21 |
24 | Ad
22 | ![]()
25 |
33 |
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 | |