Skip to content

Commit

Permalink
Add save to Readwise Reader integration to Hacker News extension (#16749
Browse files Browse the repository at this point in the history
)

* Update hacker-news extension

- Add format script
- Remove force unwrap and unused imports
- Lint
- Add publish script
- Cache saved URLs and show an accessory
- Fix import
- Add documentation
- Only show the save to readwise reader option if the token is set
- Extract readwise logic to new file
- Add action to save article to readwise reader
- Automated add to contributors
- Initial commit

* Add changelog entry

* Remove unnecessary explicit Preferences type

* Restore preferences after merge conflict

* Update CHANGELOG.md

* Update preferences.ts

* Refresh the save icon in the list on save action

* Move UI updates to frontpage, for better separation of concerns

* Make getReadwiseToken optional and handle undefined

* Simplify: check local state only to check if URL is saved

* Handle errors from caching operations

* Set max cache size with FIFO policy

* Linting

* Update CHANGELOG.md and optimise images

---------

Co-authored-by: Per Nielsen Tikær <[email protected]>
Co-authored-by: raycastbot <[email protected]>
  • Loading branch information
3 people authored Mar 3, 2025
1 parent 945bd99 commit 83e2a36
Show file tree
Hide file tree
Showing 7 changed files with 384 additions and 29 deletions.
5 changes: 5 additions & 0 deletions extensions/hacker-news/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Hacker News Changelog

## [🚀 Add Readwise Reader integration] - 2025-03-03

- Add ability to save articles to Readwise Reader
- Show visual indicator for previously saved articles

## [✨ AI Enhancements] - 2025-02-21

- Add AI tools
Expand Down
115 changes: 103 additions & 12 deletions extensions/hacker-news/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 17 additions & 4 deletions extensions/hacker-news/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"GastroGeek",
"sxn",
"itsnwa",
"andreaselia"
"andreaselia",
"russellyeo"
],
"categories": [
"News"
Expand All @@ -30,6 +31,15 @@
"description": "Get the latest stories from Hacker News."
}
],
"preferences": [
{
"name": "readwiseToken",
"type": "password",
"title": "Readwise API Token",
"description": "Your Readwise API token (found in https://readwise.io/access_token)",
"required": false
}
],
"ai": {
"evals": [
{
Expand Down Expand Up @@ -206,7 +216,8 @@
"@raycast/api": "^1.90.0",
"@raycast/utils": "^1.18.1",
"lodash": "^4.17.21",
"rss-parser": "^3.13.0"
"rss-parser": "^3.13.0",
"node-fetch": "^3.3.0"
},
"devDependencies": {
"@raycast/eslint-config": "^1.0.11",
Expand All @@ -220,6 +231,8 @@
"scripts": {
"build": "ray build -e dist",
"dev": "ray develop",
"lint": "ray lint"
"lint": "ray lint",
"format": "prettier 'src/**' '!**/.DS_Store' --write",
"publish": "npx @raycast/api@latest publish"
}
}
}
61 changes: 54 additions & 7 deletions extensions/hacker-news/src/frontpage.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,38 @@
import { Action, ActionPanel, List } from "@raycast/api";
import { Action, ActionPanel, Icon, List, showToast, Toast } from "@raycast/api";
import { startCase } from "lodash";
import { getStories } from "./hackernews";
import { Topic } from "./types";
import { useState } from "react";
import { useState, useEffect } from "react";
import { usePromise } from "@raycast/utils";
import Parser from "rss-parser";
import { getIcon, getAccessories } from "./utils";
import { saveToReadwise, hasReadwiseToken, getSavedUrls } from "./readwise";

export default function Command() {
const [topic, setTopic] = useState<Topic | null>(null);
const [savedUrls, setSavedUrls] = useState<string[]>([]);
const { data, isLoading } = usePromise(getStories, [topic], { execute: !!topic });

// Load saved URLs from cache when component mounts
useEffect(() => {
setSavedUrls(getSavedUrls());
}, []);

// Function to handle saving to Readwise and updating UI
const handleSaveToReadwise = async (url: string) => {
const result = await saveToReadwise(url);
// Show toast notification based on the result
await showToast({
style: result.success ? Toast.Style.Success : Toast.Style.Failure,
title: result.message,
message: result.isRateLimited ? "Please try again later" : result.error,
});
// Reload saved URLs
if (result.success) {
setSavedUrls(getSavedUrls());
}
};

return (
<List
isLoading={isLoading}
Expand All @@ -27,7 +49,16 @@ export default function Command() {
</List.Dropdown>
}
>
{data?.map((item, index) => <StoryListItem key={item.guid} item={item} index={index} topic={topic} />)}
{data?.map((item, index) => (
<StoryListItem
key={item.guid}
item={item}
index={index}
topic={topic}
savedUrls={savedUrls}
onSave={handleSaveToReadwise}
/>
))}
</List>
);
}
Expand All @@ -45,19 +76,28 @@ function setTitle(title: string, topic: Topic) {
return title;
}

function StoryListItem(props: { item: Parser.Item; index: number; topic: Topic | null }) {
function StoryListItem(props: {
item: Parser.Item;
index: number;
topic: Topic | null;
savedUrls: string[];
onSave: (url: string) => Promise<void>;
}) {
// Check if this item is in the savedUrls array
const isSaved = props.item.link ? props.savedUrls.includes(props.item.link) : false;

return (
<List.Item
icon={getIcon(props.index + 1)}
title={setTitle(props.item.title || "No Title", props.topic || Topic.FrontPage)}
subtitle={props.item.creator}
accessories={getAccessories(props.item)}
actions={<Actions item={props.item} />}
accessories={getAccessories(props.item, isSaved)}
actions={<Actions item={props.item} onSave={props.onSave} />}
/>
);
}

function Actions(props: { item: Parser.Item }) {
function Actions(props: { item: Parser.Item; onSave: (url: string) => Promise<void> }) {
return (
<ActionPanel title={props.item.title}>
<ActionPanel.Section>
Expand All @@ -72,6 +112,13 @@ function Actions(props: { item: Parser.Item }) {
shortcut={{ modifiers: ["cmd"], key: "." }}
/>
)}
{props.item.link && hasReadwiseToken() && (
<Action
icon={Icon.SaveDocument}
title="Save to Readwise Reader"
onAction={() => props.onSave(props.item.link || "")}
/>
)}
</ActionPanel.Section>
</ActionPanel>
);
Expand Down
29 changes: 29 additions & 0 deletions extensions/hacker-news/src/preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getPreferenceValues } from "@raycast/api";

/**
* Checks if a Readwise API token is configured in preferences.
*
* @returns true if the token exists and is not empty, false otherwise
*/
export function hasReadwiseToken(): boolean {
try {
const preferences = getPreferenceValues<Preferences>();
return !!preferences.readwiseToken;
} catch {
return false;
}
}

/**
* Gets the Readwise API token from preferences.
*
* @returns The configured Readwise API token, or undefined if not configured
*/
export function getReadwiseToken(): string | undefined {
try {
const preferences = getPreferenceValues<Preferences>();
return preferences.readwiseToken || undefined;
} catch {
return undefined;
}
}
Loading

0 comments on commit 83e2a36

Please sign in to comment.