Skip to content

Models, fetch-based API, settings context (migration 2/8) - #690

Open
charityquinn-cognition wants to merge 1 commit into
devin/react-migration-1-scaffoldfrom
devin/react-migration-2-core-logic
Open

Models, fetch-based API, settings context (migration 2/8)#690
charityquinn-cognition wants to merge 1 commit into
devin/react-migration-1-scaffoldfrom
devin/react-migration-2-core-logic

Conversation

@charityquinn-cognition

@charityquinn-cognition charityquinn-cognition commented Aug 25, 2026

Copy link
Copy Markdown

Summary

Stacked on #689. Ports the framework-agnostic layer: models, the HN API service, the settings service, and the comment pipe. No UI yet, so nothing is wired up — PR3 onward consumes this.

Models (src-react/shared/models/) become plain interfaces/types, keeping the API's wire names verbatim (time_ago, comments_count, poll_votes_count, and the misspelled crated_time) so no mapping layer is needed. Story.type keeps the item-kind union 'poll' | 'story' | 'job'; the feed names are a separate union, since feed-type.type.ts was overloading one type for both concerns:

export type FeedName = 'news' | 'newest' | 'show' | 'ask' | 'jobs';

API (shared/api/hackernewsApi.ts) drops RxJS for async/await over fetch, same baseUrl and endpoints. Two behavioral notes:

  • fetchJson now throws on non-2xx; the Angular version resolved with the error body, so a 500 could reach the UI as a "story".
  • the poll loop is awaited to completion before returning, instead of Angular's fire-and-forget subscriptions that mutated story.poll after the caller had already received it:
const pollResults = await Promise.all(story.poll.map((_, i) => fetchPollContent(story.id + i + 1)));
story.poll = pollResults;
story.poll_votes_count = pollResults.reduce((total, r) => total + r.points, 0);

Settings (shared/settings/SettingsContext.tsx) becomes context + provider + useSettings(), preserving the localStorage keys (openLinkInNewTab, theme, titleFontSize, listSpacing), the prefers-color-scheme: dark default with a live matchMedia listener (cleaned up on unmount), and the same mutators (toggleSettings, toggleOpenLinksInNewTab, setTheme, setFont, setSpacing). useSettings throws outside the provider rather than returning undefined.

comment.pipe.tsformatComments(count) utility, same output strings.

Verified: yarn react:build passes; GET /news?page=1 against the live API returns 30 stories through the new module.

Devin-Org: engineering

Link to Devin session: https://app.devin.ai/sessions/1ff25c6cf2f949458f82cc596fc79c65
Requested by: @charityquinn-cognition


Devin Review

Status Commit
⚪ Not started

Run Devin Review

Devin Review (Staging)
Open in Devin Review

Co-Authored-By: Charity Quinn <charity.quinn@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines +17 to +29
function getInitialSettings(): Settings {
const openLinkInNewTab = localStorage.getItem('openLinkInNewTab');
const titleFontSize = localStorage.getItem('titleFontSize');
const listSpacing = localStorage.getItem('listSpacing');

return {
showSettings: false,
openLinkInNewTab: openLinkInNewTab ? JSON.parse(openLinkInNewTab) : false,
theme: 'default',
titleFontSize: titleFontSize || '16',
listSpacing: listSpacing || '0',
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Saved theme applied only after first paint

getInitialSettings hardcodes theme: 'default' and reads the persisted theme only inside a later useEffect (SettingsContext.tsx). The Angular service applied it in the constructor before render, so users with a saved night theme can see a brief default-theme flash.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accurate. getInitialSettings deliberately doesn't read theme synchronously because the effect that applies it also subscribes to prefers-color-scheme, and the saved value has to win over the media query — doing it in one place avoided duplicating that precedence logic. The flash is real though; the fix is to seed theme from localStorage in the initializer and let the effect only handle the media-query default when nothing is saved. Flagging to the requester rather than folding a behavior tweak into this PR unprompted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant