Skip to content

News feed of links shared across the fediverse#310

Merged
dahlia merged 31 commits into
hackers-pub:mainfrom
dahlia:news
May 31, 2026
Merged

News feed of links shared across the fediverse#310
dahlia merged 31 commits into
hackers-pub:mainfrom
dahlia:news

Conversation

@dahlia

@dahlia dahlia commented May 30, 2026

Copy link
Copy Markdown
Member

Hackers' Pub already extracts the first external link from each post and stores it in post_link. This branch uses that data for News: a feed of links shared across the fediverse, ranked by a time-and-engagement score instead of votes. The discussion for a link is the federated conversation that already formed around the posts sharing it, and each link has a stable page for reading and joining that conversation.

How links are ranked

A link's score is log10(max(1, weightedMass)) + recency, where the recency term measures the link's latest-activity time against a fixed epoch, 2024-01-01, instead of now(). This follows the same basic idea as Reddit's older ranking formula: the score is a pure function of the stored posts and engagement, so recomputing it is idempotent. It changes only when the underlying data changes.

weightedMass sums, over each public non-boost post that shares the link, a source weight, an account-reputation weight based on the sharer's follower/following ratio, and weighted engagement from public quotes, replies, and reactions. Local Hackers' Pub posts count most, generic remote instances count less, and Bluesky-bridged accounts count least. Bot-only links, from ActivityPub Service or Application actors, are excluded from the feed. Repeated shares from the same account get a reduced, gap-dependent weight after the first share, and rapid repeats do not refresh recency, so one account cannot keep the same link pinned by reposting it.

The feed and the discussion pages

The feed at /news has three orders: popular, newest, and all-time. Popular uses the full score, newest uses first-shared time, and all-time uses recency-independent mass. Each card shows the link, a short description, the latest activity time, and a discussion-count link. The per-link page at /news/[link_id] shows the sharing posts as roots, with replies and quotes loaded as a depth-limited tree. It supports #post-<uuid> anchors, deep-link expansion, and a compose box for adding a note from the page. If the note does not already contain the link URL, the URL is appended so the note joins the link's discussion. Signed-out visitors who hit the site root are redirected to /news.

Moderation

Moderators get /admin/news and an inline feed-card control. They can exclude links by domain or URL pattern using the Web-standard URLPattern; matching links are hidden from every feed order, while direct links to their discussion pages still work. Moderators can also apply a per-link score penalty, demote or bury, from each feed card. Penalized links are listed on the admin page so they can be found and cleared later.

Data model and freshness

The scores live on post_link itself: a few double precision and timestamp columns, plus partial indexes for the feed orders. I kept them there instead of adding a separate table because scoring runs off the post hot path, and feed reads need link metadata and score together. The scoring code is in models/news.ts. Scores refresh incrementally when a link is shared, on a five-minute Deno.cron sweep in the GraphQL server, and through the manual recompute button. Because recompute is idempotent, the sweep and manual button are safe to rerun, including after adding exclusion patterns that should apply to already-scored links.

dahlia added 18 commits May 30, 2026 14:18
Introduce the storage for the News feature's popularity scoring: seven
columns on `post_link` (score, weighted_mass, recency_component,
post_count, first_shared_at, latest_activity_at, score_updated) populated
later by an idempotent batch job, plus three partial indexes for the feed
sorts (popular/newest/all-time).

Each index carries an `id DESC` tiebreaker so it fully covers the
`(sortKey, id)` keyset pagination order, and all three share the partial
predicate `latest_activity_at IS NOT NULL` (the canonical "has a public
share" flag) so scraped-but-never-publicly-shared links stay out of every
feed query.

Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.5
Implement the idempotent batch that ranks links shared across the
fediverse for the News feature.  `recomputeNewsScores` computes, per
link, a Reddit/Hacker-News-style additive-log score from its public
sharing posts: a weighted engagement mass (quotes > replies > reactions,
scaled by source weight — local > generic remote > Bluesky bridge — and
the sharer's follower/following reputation) plus a recency term anchored
to a fixed epoch.  Anchoring to a constant (not `now()`) makes the score
a pure function of the underlying posts and engagement, so it is
idempotent and never goes stale as the clock advances.

The recompute runs as one set-based SQL aggregation and supports three
scopes: a full rebuild, an explicit `linkIds` set (for the incremental
single-link refresh on write), and `activeSince` (for the periodic
sweep, which derives its target set from source timestamps so a fresh
reaction on an older story is still picked up).  `getNewsStories`
paginates the feed by keyset over the partial indexes, and
`getNewsScoreStatus` backs the admin page.

Adds test fixtures (insertPostLink, insertReaction, seedInstance, link
and count options) and a TDD suite that cross-checks the SQL against an
independent re-implementation of the formula.

Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.5
Wire the post-persist paths to re-score a link the moment it is shared
(or unshared), so a new story appears in the news feed without waiting
for the periodic sweep.  `syncPostFromNoteSource` (local notes) and
`persistPost` (federated posts, top level only) now call
`refreshNewsScores(db, [post.linkId, existingPost?.linkId])` — the new
link plus the previous one, so an edit that removes a link drops the old
story out.

`refreshNewsScores` runs the recompute in its own savepoint and swallows
(logs) any failure, so a scoring error can never poison the caller's
transaction or block the post from being saved.  Engagement-driven
re-ranking (new replies, quotes, reactions on a story) is intentionally
left to the periodic sweep, which derives its target set from source
timestamps and therefore reads correct, settled counts.

Also fixes a latent bug surfaced by this change: a removed link in a
local note left `link_id` set (drizzle drops `undefined` from the update
set), which would have kept a dropped story in the feed; the no-link
case now uses explicit `null`, matching `persistPost`.

Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.5
Add the read side of the News feature.  A new root `newsStories`
connection ranks links shared across the fediverse, with a `NewsOrder`
argument (POPULAR by default, plus NEWEST and ALL_TIME).  Pagination is
forward-only keyset over the partial feed indexes; `first_shared_at` is
truncated to milliseconds on recompute so the JS-Date NEWEST cursor
cannot skip sub-millisecond-close links.

The existing `PostLink` node gains the scoring fields (`score`,
`weightedMass`, `postCount`, `firstSharedAt`, `latestActivityAt`), a
`sharingPosts` connection (the visibility-filtered roots of the link's
discussion), and a batched `sourceBreakdown` (local / remote / Bluesky-
bridge share counts).  The Bluesky-bridge classification is factored into
one boolean-total SQL condition reused by the recompute and the
breakdown, so a NULL-software remote instance is counted as remote in
both places rather than dropped.

Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.5
Give moderators a way to rebuild news scores and watch their state, and
keep engagement-driven rankings fresh automatically.

`recomputeNewsScores` is a moderator-only mutation that runs a full,
idempotent rebuild (the authoritative reconciler and dev backstop);
`newsScoreStatus` exposes the scored-link count and last-recompute time,
returning null for non-moderators.  A `Deno.cron` in the GraphQL server
entry point sweeps recently-active links every five minutes so a new
reply, quote, or reaction on an existing story re-ranks it without a
manual trigger.  The sweep's active set now also keys off `post.updated`,
so a federated Update that revises a remote share's engagement totals is
picked up even when it creates no local engagement row.

Also fixes `getNewsScoreStatus`: it read the last-recompute time via
`max()`, which handed back a raw Postgres timestamp string the GraphQL
`DateTime` scalar rejected; it now reads the column directly so drizzle
maps it to a `Date`.

Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.5
Surface the News experience in web-next.  A new `/news` route lists the
links circulating across the fediverse, ranked by the popularity score,
with Popular / Newest / All-time sort pills driven by `?sort=`.  Each
story is a border-b row showing the link (opening in a new tab), its
host, the share count and first-shared time, a "Discussion" link to the
per-link permalink, and a "Share this link" button that opens the
composer pre-filled with the URL (so the published note shares the same
link and is attributed back to the story).

Plumbing this needed: the logged-out `/` redirect now goes to `/news`; a
"News" item sits at the top of the sidebar's Timeline group for everyone;
and the note composer gained an `openWithContent` path so a fresh compose
can be pre-filled (the reply-target effect no longer wipes a deliberate
pre-fill).  On the GraphQL side, `PostLink` gains a `uuid` for the
`/news/{uuid}` permalink and a `newsStory(id:)` lookup backs the
discussion route that follows.

Translations for the new strings are filled for ja, ko, zh-CN, and zh-TW.

Assisted-by: Claude Code:claude-opus-4-8
Add the stable discussion permalink `/news/{uuid}`.  The page header is
the link itself (image, title opening in a new tab, host, description,
share count, first-shared time, and a local/remote/Bluesky source
breakdown), with a "Share this link" button.  Below it the federated
conversation renders as a tree: each public post that shares the link is
a root, and its replies and quotes nest beneath it.

The tree auto-expands a few levels deep and loads deeper threads only on
request, so a busy discussion does not fetch everything up front.  Every
post is anchored as `#post-<uuid>`; opening such a deep link expands the
thread down to that post and scrolls it into view.  Each post carries the
usual inline reply / quote / share / react controls.

Posts are de-duplicated discussion-wide (a post can be both a sharing
post and a reply elsewhere), ancestor cycles are broken, and the async
child loads are owner-scoped and disposed cleanly.  Translations for the
new strings are filled for ja, ko, zh-CN, and zh-TW.

Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.5
Add /admin/news, a moderator-only page that shows how many links are in
the news feed and when scores were last recomputed, with a button that
triggers a full, idempotent rebuild via the recomputeNewsScores
mutation.  Scores stay fresh on their own (incrementally on share plus
the periodic sweep), so this is mainly a manual backstop and a
development aid.  A "News scores" entry is added to the Admin sidebar
group, and the page redirects non-moderators away.

Translations for the new strings are filled for ja, ko, zh-CN, and
zh-TW.

Assisted-by: Claude Code:claude-opus-4-8
Two correctness fixes found reviewing the whole feature.

Keep private discussion out of public scores: the weighted mass used the
denormalized `replies_count`/`quotes_count`, which include followers-only
and direct posts, so a public link's score (and `weightedMass`) could be
driven by, and leak, non-public discussion volume.  The mass now counts
only `public`/`unlisted` replies and quotes, matching the rule already
used for `latestActivityAt`.  Reactions keep the denormalized count
(reactions have no visibility).

Refresh scores on destructive paths: the periodic sweep derives its work
from surviving source rows, so it can't see a deletion or a reaction
undo.  Post delete (local cascade and federated), quote revoke, and
reaction undo (local and federated) now re-score the affected links, so a
deleted share drops out of the feed and a removed reply/quote/reaction no
longer leaves a link's mass inflated until a manual recompute.

Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.5
Links shared only by bot accounts (fediverse actors whose type is
`Service` or `Application`, e.g. automated link feeds) no longer surface
as news. The exclusion is applied wherever a "sharing post" or its source
is defined: the `shares` CTE in `recomputeAggregate`, the sweep's
`activeLinkIdsSubquery`, the `zeroStaleLinks` qualifying-share check,
`getNewsSourceBreakdowns`, and the GraphQL `sharingPosts` discussion
roots. `Person`, `Group`, and `Organization` actors stay eligible.

Only the sharing post's author type is filtered; replies, quotes, and
reactions still count regardless of who made them. A single
`nonBotSharerCondition` SQL fragment and the exported
`NEWS_BOT_ACTOR_TYPES` constant keep the SQL and the Relay filter in
lockstep.

When a federated actor crosses the bot/non-bot boundary (e.g. a Mastodon
user toggles the bot flag, changing its type between `Person` and
`Service`), `persistActor` now re-scores the links that actor shares via
`refreshNewsScoresForActor`, since the periodic sweep cannot detect such
a transition on its own.

Already-scored bot-only links from before this change are cleared by a
full recompute (the moderator recompute button / `recomputeNewsScores`
mutation with no `activeSince`), which scans every scored link.

Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.5
When the same account re-shared the same link, each share previously
added another full base contribution to the link's weighted mass and
refreshed its freshness timestamp, letting an account inflate or pin a
link by re-posting it.

A sharing post now carries the gap since that account's previous share
of the same link, via a window over `(actor_id, link_id)` ordered by
`published`. The base `W_SHARE` term of a repeat is scaled by
`NEWS_REPEAT_CAP * (1 - exp(-gap / NEWS_REPEAT_RECOVERY_TAU_SECONDS))`,
so a rapid repeat adds almost nothing and a long-gap re-share recovers
toward the cap but never reaches a first share's weight. The first share
of each account keeps full weight, and different accounts are
independent.

The discount also covers recency: a repeat refreshes the link's
freshness only if it is a first share or its gap is at least
`NEWS_REPEAT_FRESH_MIN_SECONDS`, so rapid re-sharing cannot keep a link
pinned at the top. Replies, quotes, and reactions are never discounted
and still refresh freshness, so genuine engagement on a repeat post
still counts. `postCount` still counts every share.

The new factors depend only on fixed `published` timestamps, so the
recompute stays idempotent and time-stable.

Assisted-by: Claude Code:claude-opus-4-8
Adds a `discussionCount` field to the `PostLink` GraphQL type: the size of
a link's federated discussion, namely its non-bot public sharing posts
plus their direct public replies and quotes, deduplicated. Unlike
`postCount` (sharing posts only) this reflects the whole conversation, so
the feed can invite readers in with a comment-style count rather than a
bare "N shares".

`getNewsDiscussionCounts` (models/news.ts) is a batched query patterned on
`getNewsSourceBreakdowns`: a `shares` CTE of the non-bot public sharing
posts, then a `union` of `(link_id, post_id)` pairs over those shares and
their direct public replies and quotes, counted per link. `union` (not
`union all`) deduplicates, so a post that is both a reply and a quote of
the link's shares is counted once, matching the deduplicated discussion
tree. Exposed as a batched `t.loadable` Int field, mirroring
`sourceBreakdown`. Counts direct children only (deeper nesting is not
traversed) and is viewer-independent (public posts only).

Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.5
Reworks each News feed card to be compact and discussion-led so more
stories fit per screen and the conversation is the invitation. Layout:

- A prominent left column shows a chat icon over the link's
  `discussionCount` and is itself the link into the discussion at
  `/news/{uuid}` (merging the old "Discussion" button and the count, with
  an "{n} opinions" accessible label), instead of a separate "N shares"
  stat and button.
- The middle column keeps the external title link, adds the link
  description as a teaser (when present), and shows "Last active" (the
  freshest share/reply/quote/reaction) rather than the first-shared time.
- A small thumbnail sits on the right (decorative, mouse-only; the title
  link already opens the same URL), hidden on mobile.

The discussion-page header gains the same opinions count and last-active
time alongside first-shared and the source breakdown. Styling stays
black-and-white and uses the post-card row idiom; the count is a quiet
achromatic figure (the raw recency-dominated score is not human-facing).
New strings ("# opinion/# opinions", "Last active") are translated for
ja-JP, ko-KR, zh-CN, and zh-TW.

Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.5
Puts the short-form note composer at the top of `/news/[link_id]` so a
reader can post an opinion in place, Reddit/HN style, instead of reaching
for the floating composer or the share modal.

A new `NewsDiscussionComposer` reuses `NoteComposer` (Markdown editor with
its edit/preview tabs and the usual controls). When signed in it renders
the form; otherwise a "Sign in to post" prompt linking to
`/sign?next=…`. On submit the discussed link's URL is appended to the
bottom of the content unless the author already included it, so the note
becomes a public sharing post (a new root) of this link's discussion. The
append-if-absent rule is a small pure helper, `ensureLinkInContent`, with
unit tests; `NoteComposer` gained an optional `ensureLinkUrl` prop that
applies it to new notes only (edits/quotes/replies are unaffected).

`NewsDiscussion` exposes the link `url` on its fragment, renders the
composer above the roots list, and on a successful post refetches the
sharing-posts connection with `network-only` so the new opinion appears
immediately (an unchanged-variables refetch would otherwise resolve from
the store). New strings are translated for ja-JP, ko-KR, zh-CN, zh-TW.

Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.5
Adds the persisted state for two upcoming moderation tools:

- `post_link.score_penalty` (double, default 0): a moderator penalty
  subtracted from a link's `score` to demote it in the feed; persisted so
  it survives recomputes.
- `post_link.excluded_from_news` (boolean, default false): set when the
  link's URL matches a moderator pattern, to hide it from the feed list.
- `news_excluded_pattern`: moderator-managed URLPattern strings (with an
  optional note and creator) that drive the exclusion flag.

Additive migration (NOT NULL columns have defaults), no behavior change
yet; the model and GraphQL layers follow.

Assisted-by: Claude Code:claude-opus-4-8
Implements the moderation logic over the columns/table added previously:

- Score penalty: `recomputeAggregate` now subtracts the persisted
  `pl.score_penalty` from a link's `score` (the POPULAR order), so a
  penalty survives every recompute. `setNewsScorePenalty` updates the
  column and recomputes the link in one transaction; it rejects negative
  or non-finite penalties (which would boost or poison ranking). Presets:
  `NEWS_PENALTY_DEMOTE` (push well down) and `NEWS_PENALTY_BURY` (sink to
  the bottom). `getNewsPenalizedStories` lists penalized links for review.

- URL exclusions: `applyNewsExclusions` matches link URLs against the
  stored patterns with the Web-standard `URLPattern` (invalid patterns
  skipped + logged) and sets `excluded_from_news` in one no-op-skipping
  update; it runs inside every recompute for the scoped links so new links
  get flagged. `addNewsExcludedPattern` (validates the pattern, idempotent)
  / `removeNewsExcludedPattern` / `getNewsExcludedPatterns` manage the set,
  each re-applying exclusions in a transaction. `getNewsStories` now also
  filters `excluded_from_news = false` in every sort order; the per-link
  discussion page is unaffected.

Tests cover penalty demotion/clear, exclusion across all orders + restore,
exclusion of newly-shared matching links, invalid-pattern rejection, and
the penalized-links list.

Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.5
Adds the moderator-only GraphQL surface for the penalty + exclusion model
functions:

- `NewsPenalty` enum (`NONE`/`DEMOTE`/`BURY`) and `PostLink.penalty`
  (moderator-only; `null` for everyone else) derived from the stored
  `scorePenalty`.
- `setNewsScorePenalty(id, penalty)` mutation returning the updated
  `PostLink`, and `newsPenalizedStories` listing penalized links for
  review.
- `addNewsExcludedPattern(pattern, note)` (maps an invalid `URLPattern` to
  `InvalidInputError`), `removeNewsExcludedPattern(id)`, and
  `newsExcludedPatterns` (the `NewsExcludedPattern` list).

All guard `ctx.session`/`ctx.account.moderator` like the existing
`recomputeNewsScores`/`newsScoreStatus`, returning `null` (queries/fields)
or `NotAuthenticatedError`/`NotAuthorizedError` (mutations). Tests cover
the auth matrix, demotion reordering the popular feed, and a pattern
hiding then restoring a link; schema regenerated.

Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.5
Renames the "News scores" admin page to "News" and turns it into the
moderation hub, and adds an inline penalty control to the feed.

Admin `/admin/news` gains two sections beside the existing recompute:
- Excluded URL patterns: an add form (a URLPattern field + optional note)
  and a list with Remove buttons, wired to add/removeNewsExcludedPattern.
- Penalized links: lists `newsPenalizedStories` with a Clear button each
  (a buried link leaves the feed top, so this is where it is reviewed).

The feed (`/news`) now threads `viewer.moderator` through `NewsList` into
each `NewsStoryCard`; for moderators only, a "Moderate" dropdown offers
Demote / Bury / Clear via `setNewsScorePenalty`, shows the current
state, and refetches the connection (`network-only`) so the reordering
shows. Non-moderators see nothing new.

The sidebar label is updated, and 26 new strings are translated for
ja-JP, ko-KR, zh-CN, and zh-TW (using the `moderator` glossary term).

Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.5
@dahlia dahlia self-assigned this May 30, 2026
@dahlia dahlia added web-next The new web frontend (SolidStart) graphql GraphQL API server labels May 30, 2026
@dahlia

dahlia commented May 30, 2026

Copy link
Copy Markdown
Member Author

@codex review

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7fc4ca0e-42d9-4b09-80fb-8e74ed50ac59

📥 Commits

Reviewing files that changed from the base of the PR and between cc3b706 and 247c1c9.

📒 Files selected for processing (1)
  • web-next/src/components/NewsDiscussionThread.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • web-next/src/components/NewsDiscussionThread.tsx

📝 Walkthrough

Walkthrough

Adds a ranked News feed: DB migrations and indexes, scoring engine with incremental/periodic recompute, GraphQL API and moderator controls, model refresh hooks, frontend feed/discussion/admin UI, and comprehensive tests.

Changes

News Feed: Backend & Frontend

Layer / File(s) Summary
Database schema and migrations
drizzle/20260530050716_overrated_captain_cross/migration.sql, drizzle/20260530145328_pretty_blacklash/migration.sql, models/schema.ts
Scoring/activity columns and partial indexes added to post_link; new news_excluded_pattern table; scorePenalty & excludedFromNews columns and FK/index.
Core scoring implementation
models/news.ts
Implements recomputeNewsScores, incremental refresh helpers, aggregation/update SQL, zeroing stale links, source breakdowns, discussion counts, exclusion pattern application, and penalty handling.
Model integrations and hooks
models/post.ts, models/actor.ts, models/reaction.ts, federation/inbox/subscribe.ts
Hooked score refreshes into post sync/persist/delete flows, reaction undo, federation reaction-undos, and actor bot-status transitions; added prior-type lookup for actor updates.
GraphQL schema definitions & resolvers
graphql/schema.graphql, graphql/news.ts, graphql/mod.ts
Adds NewsOrder, NewsPenalty, NewsScoreStatus, NewsSourceBreakdown, new PostLink feed fields and connections, forward-only newsStories pagination, moderator admin mutations/queries, and resolvers wired to model functions.
Periodic recomputation job
graphql/main.ts, graphql/deno.json
Adds a cron job (every 5 minutes) that runs recomputeNewsScores for the recent active window; updates Deno task flags to include --unstable-cron.
Tests and test helpers
models/news.test.ts, graphql/news.test.ts, test/postgres.ts
Large model and GraphQL test suites validate scoring math, pagination, bot/exclusion behavior, repeat damping, moderation controls; added DB seeding helpers for links/reactions/actors/posts.
Frontend utilities and composer context
web-next/src/lib/composerLink.ts, web-next/src/lib/useNewsSort.ts, web-next/src/contexts/NoteComposeContext.tsx, web-next/src/components/NoteComposer.tsx, web-next/src/components/NoteComposeModal.tsx
Adds ensureLinkInContent, tests, useNewsSort hook, initialContent and openWithContent support for prefilled composer usage.
Frontend feed list and story cards
web-next/src/components/NewsList.tsx, web-next/src/components/NewsStoryCard.tsx, web-next/src/components/NewsStoryHeader.tsx
NewsList paginated feed with sort pills; NewsStoryCard displays story metadata, discussion count, share action, and moderator penalty controls; NewsStoryHeader shows timestamps and source breakdown.
Frontend discussion and composer
web-next/src/components/NewsDiscussionThread.tsx, web-next/src/components/NewsDiscussion.tsx, web-next/src/components/NewsDiscussionComposer.tsx
Recursive discussion thread loader with lazy pagination and auto-expand/scroll-to-target; sharing-posts pagination and composer that pre-fills story URL or shows sign-in CTA.
Frontend admin page & routes
web-next/src/routes/(root)/admin/news.tsx, web-next/src/routes/(root)/news/index.tsx, web-next/src/routes/(root)/news/[link_id]/index.tsx, web-next/src/components/AppSidebar.tsx, web-next/src/routes/(root)/index.tsx
Admin UI for recompute, exclusion patterns, and clearing penalties; /news feed and /news/[link_id] discussion pages; sidebar and home redirect updates.
Module wiring
graphql/post.ts, graphql/mod.ts, models/deno.json
Exports PostLink from GraphQL post module, imports graphql/news.ts into GraphQL module aggregation, and exposes ./news subpath in models/deno.json.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GraphQLServer
  participant NewsResolver
  participant ModelsNews as models/news.ts
  participant Database

  Client->>GraphQLServer: Query newsStories(first, after, order)
  GraphQLServer->>NewsResolver: resolve newsStories -> getNewsStories(options)
  NewsResolver->>ModelsNews: call getNewsStories(db, options)
  ModelsNews->>Database: SELECT post_link WHERE latest_activity_at IS NOT NULL ORDER BY score/ts LIMIT
  Database-->>ModelsNews: rows
  ModelsNews-->>GraphQLServer: PostLink nodes with loaders (sourceBreakdown, discussionCount)
  GraphQLServer-->>Client: paginated newsStories response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested labels

enhancement, federation

Suggested reviewers

  • Nebu1eto

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements a comprehensive 'News' feature, introducing a ranked feed of shared links across the fediverse. It includes database schema updates, core scoring models (with recency damping and bot exclusions), GraphQL queries/mutations for fetching and moderating stories (penalties and URL pattern exclusions), a periodic cron sweep, and a SolidJS frontend with discussion threads and moderator admin tools. The feedback highlights a critical issue in deletePost where parent post links are not re-scored during bulk deletions, leading to stale discussion counts. Additionally, minor import ordering violations against the repository style guide were identified in graphql/news.ts and graphql/main.ts.

Comment thread models/post.ts
Comment thread graphql/news.ts
Comment thread graphql/main.ts

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9eaa9e589d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread graphql/main.ts

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
models/post.ts (1)

581-590: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Refresh the parent story link for local replies and quotes too.

This only rescored the note's own current/previous linkId. A public reply or quote changes the parent sharing post's score immediately, and an edit can change both the old and new parent story. Please include the parent links from replyTargetId/quotedPostId here as well, or reuse refreshNewsScoresForPostLinks for both the new and previous post state.

Also applies to: 751-753

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@models/post.ts` around lines 581 - 590, The query that loads existingPost via
db.query.postTable.findFirst only gathers the note's own linkId and misses
parent story links for replies/quotes; update the logic that prepares links for
rescoring to also include the parent linking fields from replyTargetId and
quotedPostId (both current and previous states) or simply call
refreshNewsScoresForPostLinks for the new and previous post objects so parent
story linkIds are refreshed as well; locate the code using existingPost (from
db.query.postTable.findFirst) and ensure it collects replyTargetId/quotedPostId
and feeds those parent linkIds into the same rescoring path (or reuse
refreshNewsScoresForPostLinks) for both old and new states.
🧹 Nitpick comments (2)
graphql/news.test.ts (1)

1-18: 💤 Low value

Consider grouping external imports before internal imports.

The imports mix external libraries (@std, drizzle-orm, graphql) with internal modules (@hackerspub/models/*). Per coding guidelines, external imports should come first, followed by internal imports (alphabetically within groups).

Suggested order:

 import { assert } from "`@std/assert/assert`";
 import { assertEquals } from "`@std/assert/equals`";
-import { recomputeNewsScores } from "`@hackerspub/models/news`";
-import { accountTable } from "`@hackerspub/models/schema`";
 import { eq } from "drizzle-orm";
 import { execute, parse } from "graphql";
+
+import { recomputeNewsScores } from "`@hackerspub/models/news`";
+import { accountTable } from "`@hackerspub/models/schema`";
 import type { Transaction } from "`@hackerspub/models/db`";
 import { schema } from "./mod.ts";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graphql/news.test.ts` around lines 1 - 18, Reorder the import statements in
graphql/news.test.ts so external packages come first and internal project
modules after: group imports from "`@std/assert/assert`", "`@std/assert/equals`",
"drizzle-orm", and "graphql" before any "`@hackerspub/`..." imports; within each
group sort alphabetically (e.g., "`@std/`*" then "drizzle-orm" then "graphql"),
and then list internal imports like "`@hackerspub/models/news`",
"`@hackerspub/models/schema`", "`@hackerspub/models/db`", and the test helpers from
"../test/postgres.ts" alphabetically so identifiers like recomputeNewsScores,
accountTable, eq, execute/parse, Transaction, schema, and
insertAccountWithActor/insertNotePost/etc. are grouped per the guideline.
graphql/news.ts (1)

176-192: ⚡ Quick win

Describe the generated news connection and result types.

These additions publish new schema types like PostLinkSharingPostsConnection, QueryNewsStoriesConnection, and the news mutation result unions, but they still land in the emitted schema without descriptions. Please add descriptions for those generated public types too, otherwise this new API surface is only partially documented.

As per coding guidelines graphql/**/*.ts: Every element in the GraphQL schema (types, interfaces, unions, enums, enum values, fields, arguments, and mutations) must have a description.

Also applies to: 293-344, 425-630

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graphql/news.ts` around lines 176 - 192, The new GraphQL connection and
result types (e.g., PostLinkSharingPostsConnection, QueryNewsStoriesConnection,
and the news mutation result unions) lack descriptions and must be documented;
locate where those types are declared/generated (look for the relatedConnection
call creating sharingPosts and nearby code that defines the news
queries/mutations and their result unions) and add descriptive description
strings to each generated public type, union, and any connection/result types
emitted by the new code (including enum values, fields, and arguments mentioned
in the additions around lines 293-344 and 425-630) to satisfy the
graphql/**/*.ts schema-description guideline.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@graphql/news.ts`:
- Around line 346-364: The newsStory resolver currently returns any post_link
row by UUID (newsStory, ctx.db.query.postLinkTable.findFirst) which can leak
non-public link metadata; after validateUuid(args.id) update the database query
to require that the found post_link has an associated qualifying public share
(the same “qualifying public share exists” predicate used by your story-scoring
logic) before returning it — e.g., replace the simple where: { id: args.id }
with a compound where that enforces id: args.id AND an exists/join condition for
a related post/share record matching the scoring predicate (reuse the existing
predicate function or expression used for scoring so both behaviors stay in
sync).

In `@models/post.ts`:
- Around line 975-982: The federated create/update path currently only rescoring
the post's own linkId and misses refreshing the parent story's link when the
incoming remote post is a public reply or quote; update the handling around
db.query.postTable.findFirst / existingPost to detect when existingPost.linkId
changes or when existingPost.quotedPostId/linkId refers to a parent story and
call the same link-rescore/refresh routine for that parent story (and similarly
patch the same logic in the other block around lines referenced 1304-1308) so
the parent story's link score is updated immediately on federated create/update.
- Around line 1957-1965: When deleting a post that is a quote, we also need to
decrement the quoted post's denormalized quotesCount; add a branch similar to
the reply handling that checks deletedPost.quotedPostId, loads the quoted post
via db.query.postTable.findFirst({ where: { id: deletedPost.quotedPostId } }),
and then decrement its quotes count (e.g., call a new or existing helper like
updateQuotesCount(db, quotedPost, -1) or perform a db.update to decrement
quotesCount) before calling refreshNewsScoresForPostLinks so the quoted post's
visible quote count stays in sync.

---

Outside diff comments:
In `@models/post.ts`:
- Around line 581-590: The query that loads existingPost via
db.query.postTable.findFirst only gathers the note's own linkId and misses
parent story links for replies/quotes; update the logic that prepares links for
rescoring to also include the parent linking fields from replyTargetId and
quotedPostId (both current and previous states) or simply call
refreshNewsScoresForPostLinks for the new and previous post objects so parent
story linkIds are refreshed as well; locate the code using existingPost (from
db.query.postTable.findFirst) and ensure it collects replyTargetId/quotedPostId
and feeds those parent linkIds into the same rescoring path (or reuse
refreshNewsScoresForPostLinks) for both old and new states.

---

Nitpick comments:
In `@graphql/news.test.ts`:
- Around line 1-18: Reorder the import statements in graphql/news.test.ts so
external packages come first and internal project modules after: group imports
from "`@std/assert/assert`", "`@std/assert/equals`", "drizzle-orm", and "graphql"
before any "`@hackerspub/`..." imports; within each group sort alphabetically
(e.g., "`@std/`*" then "drizzle-orm" then "graphql"), and then list internal
imports like "`@hackerspub/models/news`", "`@hackerspub/models/schema`",
"`@hackerspub/models/db`", and the test helpers from "../test/postgres.ts"
alphabetically so identifiers like recomputeNewsScores, accountTable, eq,
execute/parse, Transaction, schema, and
insertAccountWithActor/insertNotePost/etc. are grouped per the guideline.

In `@graphql/news.ts`:
- Around line 176-192: The new GraphQL connection and result types (e.g.,
PostLinkSharingPostsConnection, QueryNewsStoriesConnection, and the news
mutation result unions) lack descriptions and must be documented; locate where
those types are declared/generated (look for the relatedConnection call creating
sharingPosts and nearby code that defines the news queries/mutations and their
result unions) and add descriptive description strings to each generated public
type, union, and any connection/result types emitted by the new code (including
enum values, fields, and arguments mentioned in the additions around lines
293-344 and 425-630) to satisfy the graphql/**/*.ts schema-description
guideline.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0d282ba-b339-404b-8a1f-9795ffdff4f7

📥 Commits

Reviewing files that changed from the base of the PR and between 4fa8274 and 9eaa9e5.

📒 Files selected for processing (41)
  • drizzle/20260530050716_overrated_captain_cross/migration.sql
  • drizzle/20260530050716_overrated_captain_cross/snapshot.json
  • drizzle/20260530145328_pretty_blacklash/migration.sql
  • drizzle/20260530145328_pretty_blacklash/snapshot.json
  • federation/inbox/subscribe.ts
  • graphql/main.ts
  • graphql/mod.ts
  • graphql/news.test.ts
  • graphql/news.ts
  • graphql/post.ts
  • graphql/schema.graphql
  • models/actor.ts
  • models/deno.json
  • models/news.test.ts
  • models/news.ts
  • models/post.ts
  • models/reaction.ts
  • models/schema.ts
  • test/postgres.ts
  • web-next/src/components/AppSidebar.tsx
  • web-next/src/components/NewsDiscussion.tsx
  • web-next/src/components/NewsDiscussionComposer.tsx
  • web-next/src/components/NewsDiscussionThread.tsx
  • web-next/src/components/NewsList.tsx
  • web-next/src/components/NewsStoryCard.tsx
  • web-next/src/components/NewsStoryHeader.tsx
  • web-next/src/components/NoteComposeModal.tsx
  • web-next/src/components/NoteComposer.tsx
  • web-next/src/contexts/NoteComposeContext.tsx
  • web-next/src/lib/composerLink.test.ts
  • web-next/src/lib/composerLink.ts
  • web-next/src/lib/useNewsSort.ts
  • web-next/src/locales/en-US/messages.po
  • web-next/src/locales/ja-JP/messages.po
  • web-next/src/locales/ko-KR/messages.po
  • web-next/src/locales/zh-CN/messages.po
  • web-next/src/locales/zh-TW/messages.po
  • web-next/src/routes/(root)/admin/news.tsx
  • web-next/src/routes/(root)/index.tsx
  • web-next/src/routes/(root)/news/[link_id]/index.tsx
  • web-next/src/routes/(root)/news/index.tsx

Comment thread graphql/news.ts
Comment thread models/news.ts Outdated
Comment thread models/post.ts
Comment thread models/post.ts
dahlia added 4 commits May 31, 2026 02:10
deletePost collected only the deleted and original posts' own link ids,
so a bulk-deleted reply or quote that pointed at a different story (for
example a post quoting the deleted one while also replying to another
story) left that story's news score stale. The periodic sweep cannot
recover it because a deletion leaves no fresh source timestamp. Collect
the bulk-deleted interactions' reply and quote parents and refresh their
links too.

hackers-pub#310 (comment)

Assisted-by: Claude Code:claude-opus-4-8
deletePersistedPost decremented the reply target's repliesCount but
never the quoted post's quotesCount, so deleting a remote quote left the
quoted post's visible quote count inflated. Mirror the reply branch for
deletedPost.quotedPostId.

hackers-pub#310 (comment)

Assisted-by: Claude Code:claude-opus-4-8
Per the repository style guide, external imports come before internal
ones, alphabetically within each group. graphql-yoga sat after the
@hackerspub model imports in news.ts, and main.ts interleaved the
@hongminhee and @LogTape externals with a model import. Reorder both.

hackers-pub#310 (comment)
hackers-pub#310 (comment)

Assisted-by: Claude Code:claude-opus-4-8
newsStory returned any post_link row by UUID, so anyone holding a link's
id could read its scraped metadata even when the link had only ever
appeared in followers-only or direct posts, breaking the news feature's
public-only contract. Require a qualifying public share (latest_activity_at
is not null), the same predicate that admits a link to scoring. Links
excluded from the feed keep their activity timestamp, so the discussion
permalink still resolves for them.

hackers-pub#310 (comment)

Assisted-by: Claude Code:claude-opus-4-8
dahlia added 2 commits May 31, 2026 02:13
main.ts registers the periodic news-score sweep with Deno.cron at module
load, but Deno.cron is unstable and aborts unless the feature is enabled.
The dev and start tasks ran main.ts without it, so the GraphQL server
crashed on startup. Add --unstable-cron to both (codegen runs mod.ts and
needs no cron).

hackers-pub#310 (comment)

Assisted-by: Claude Code:claude-opus-4-8
recomputeNewsScores applied exclusions to every scored link on every run
whenever the scope was not an explicit id list, so the five-minute
activeSince sweep re-read and URLPattern-tested the whole feed each time,
growing O(all scored links). Pass the active link set through for the
activeSince scope so the sweep stays O(recently active links); a full
recompute still re-evaluates everything.

hackers-pub#310 (comment)

Assisted-by: Claude Code:claude-opus-4-8
@dahlia

dahlia commented May 30, 2026

Copy link
Copy Markdown
Member Author

@codex review

@dahlia

dahlia commented May 30, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive federated 'News' feature, including database migrations for link scoring and URL exclusion patterns, a periodic scoring cron job, GraphQL schema additions, backend scoring and moderation models, and frontend UI components for the news feed and discussion threads. The review feedback highlights a critical reactivity bug in the Solid.js discussion thread component that breaks post deduplication, a severe recursive fetching issue when following deep links, a potential unhandled server error with non-positive pagination limits, and several style guide violations regarding missing GraphQL descriptions and unsorted imports.

Comment thread web-next/src/components/NewsDiscussionThread.tsx
Comment thread web-next/src/components/NewsDiscussionThread.tsx
Comment thread web-next/src/components/NewsDiscussionThread.tsx
Comment thread web-next/src/components/NewsDiscussionThread.tsx Outdated
Comment thread graphql/news.ts Outdated
Comment thread graphql/news.ts
Comment thread web-next/src/routes/(root)/news/[link_id]/index.tsx
Comment thread graphql/news.ts

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05e58b2bbe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread web-next/src/components/NewsDiscussionThread.tsx Outdated
dahlia added 4 commits May 31, 2026 06:27
Every GraphQL argument needs a description per the schema-docs rule; the
newsStory id argument had none. Document it and regenerate the schema.

hackers-pub#310 (comment)

Assisted-by: Claude Code:claude-opus-4-8
A negative or zero first reached the query as a negative SQL LIMIT,
which Postgres rejects with an unhandled error. Validate it the same way
the upper bound is, returning a PAGINATION_ERROR instead.

hackers-pub#310 (comment)

Assisted-by: Claude Code:claude-opus-4-8
The @hackerspub/models/uuid import was wedged between the @solidjs
imports. Move it to the front of the external group, alphabetically
ahead of the @solidjs/relay/solid packages.

hackers-pub#310 (comment)

Assisted-by: Claude Code:claude-opus-4-8
The discussion thread fetched quotes once with first:20 and no
load-more, so quote hackers-pub#21 and beyond never appeared even though the
discussion count included them. Quotes now paginate with their own
cursor and a "Load more quotes" control, alongside replies.

Following a #hash link also passed targetUuid to every node and
auto-paginated all of each node's replies, so a deep link could fan out
into fetching the entire tree. Auto-expansion for a target is now capped
to a maximum depth and a maximum number of reply pages per node; deeper
or later matches stay reachable through the manual load-more controls.

hackers-pub#310 (comment)
hackers-pub#310 (comment)

Assisted-by: Claude Code:claude-opus-4-8
@dahlia

dahlia commented May 30, 2026

Copy link
Copy Markdown
Member Author

@codex review

@dahlia

dahlia commented May 30, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web-next/src/components/NewsDiscussionThread.tsx`:
- Around line 214-216: The current auto-expansion/pagination logic (autoPaginate
calculation around autoPaginate, props.targetUuid,
NEWS_DISCUSSION_TARGET_MAX_DEPTH, autoPages, NEWS_DISCUSSION_TARGET_MAX_PAGES)
blindly expands every mounted node when a targetUuid exists; change it to only
auto-expand/paginate when the branch is known or likely to contain the target by
adding a positive signal (e.g. pass in and check ancestry ids from the
route/query like props.targetAncestryIds or a propagated boolean such as
node.containsTargetInSubtree) and only allow expansion past
NEWS_DISCUSSION_AUTO_DEPTH (or pagination) if that signal is true; update the
same guard usage at the other occurrences you flagged (lines around 227-230 and
245-249) to use the new check (e.g. hasTargetInSubtree(node) ||
noTargetSpecified) so unrelated branches do not auto-load.
- Around line 39-53: The pagination call is refetching the untouched connection
because when invoking loadChildren you reset the other cursor to null; update
the loadChildren invocation (and/or its implementation) so it always supplies
both GraphQL variables ($cursor and $quoteCursor) and preserves the current
endCursor for the untouched connection instead of nulling it. Specifically, when
loading "replies" pass cursor = repliesPageInfo.endCursor (or next cursor) and
quoteCursor = current quotesPageInfo.endCursor; when loading "quotes" pass
quoteCursor = quotesPageInfo.endCursor and cursor = current
repliesPageInfo.endCursor, so the NewsDiscussionThreadChildrenQuery for replies
and quotes no longer requests the other connection unnecessarily.
- Around line 159-166: When loadChildren() runs with mode === "initial" it
resets seen, autoPages, reply/quote children and cursors but does not reset the
pagination flags replyHasMore and quoteHasMore; update the initial block in
loadChildren (the branch checking mode === "initial") to also clear those flags
(e.g., call the setters that control replyHasMore and quoteHasMore) so the UI
won't show stale "Load more" buttons after a failed refresh; locate the block
using symbols mode, loadChildren, seen, autoPages, setReplyChildren,
setQuoteChildren, setReplyCursor and setQuoteCursor to add the missing
setReplyHasMore(...) and setQuoteHasMore(...) calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1399a208-4692-468c-ab3c-ebc30a71bc07

📥 Commits

Reviewing files that changed from the base of the PR and between 05e58b2 and f2a5006.

📒 Files selected for processing (9)
  • graphql/news.ts
  • graphql/schema.graphql
  • web-next/src/components/NewsDiscussionThread.tsx
  • web-next/src/locales/en-US/messages.po
  • web-next/src/locales/ja-JP/messages.po
  • web-next/src/locales/ko-KR/messages.po
  • web-next/src/locales/zh-CN/messages.po
  • web-next/src/locales/zh-TW/messages.po
  • web-next/src/routes/(root)/news/[link_id]/index.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • web-next/src/routes/(root)/news/[link_id]/index.tsx
  • graphql/news.ts
  • graphql/schema.graphql

Comment thread web-next/src/components/NewsDiscussionThread.tsx
Comment thread web-next/src/components/NewsDiscussionThread.tsx
Comment thread web-next/src/components/NewsDiscussionThread.tsx Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive "News" feature that aggregates, ranks, and moderates shared links circulating across the fediverse. It includes database migrations to support scoring and exclusion patterns, standalone backend logic for idempotent popularity calculations, GraphQL schema updates, and a complete frontend SolidStart implementation for viewing and managing stories. The review comments are highly valuable, pointing out critical issues such as Drizzle relational query builder limitations, strict TypeScript template literal type mismatches with Relay-generated strings, memory leaks from uncleaned subscriptions, non-reactive signal reads in SolidJS, and unsafe URL parsing that could cause runtime crashes.

Comment thread models/post.ts
Comment thread web-next/src/components/NewsStoryCard.tsx
Comment thread web-next/src/routes/(root)/admin/news.tsx
Comment thread web-next/src/routes/(root)/admin/news.tsx
Comment thread web-next/src/components/NewsDiscussionThread.tsx
Comment thread web-next/src/components/NewsDiscussionThread.tsx
Comment thread web-next/src/components/NewsDiscussionThread.tsx
Comment thread web-next/src/components/NewsDiscussionThread.tsx Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f2a5006bbf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread graphql/main.ts
dahlia added 2 commits May 31, 2026 06:50
Loading more replies refetched quotes (and the reverse) because the
unused cursor was reset to null while the query still requested both
connections. Gate each connection with @include so a pagination click or
deep-link auto-pagination fetches only the connection it advances. Also
reset replyHasMore/quoteHasMore on a fresh load, so a reload that errors
does not leave stale "Load more" buttons against an empty thread.

hackers-pub#310 (comment)
hackers-pub#310 (comment)

Assisted-by: Claude Code:claude-opus-4-8
engagementBase passed a post URL straight to new URL(), which throws on a
malformed or relative value. Wrap it in try/catch and fall back to null,
matching how the other news components parse external URLs.

hackers-pub#310 (comment)

Assisted-by: Claude Code:claude-opus-4-8
@dahlia

dahlia commented May 31, 2026

Copy link
Copy Markdown
Member Author

@codex review

@dahlia

dahlia commented May 31, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive federated 'News' feature, adding database schema migrations, backend scoring models with repeated-share damping and bot exclusions, GraphQL APIs, and Solid.js/Relay frontend components for the news feed, discussion threads, and moderator admin views. The reviewer feedback identifies a critical non-reactive setup in the Solid.js discussion thread component that could cause duplicate post rendering, as well as overly restrictive template literal types for UUID parameters in several frontend files.

Comment thread web-next/src/components/NewsDiscussionThread.tsx
Comment thread web-next/src/components/NewsStoryCard.tsx
Comment thread web-next/src/routes/(root)/admin/news.tsx
Comment thread web-next/src/routes/(root)/admin/news.tsx
Comment thread web-next/src/components/NewsDiscussionThread.tsx

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cc3b706096

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread web-next/src/components/NoteComposer.tsx
Comment thread web-next/src/components/NewsDiscussionThread.tsx Outdated
The bounded deep-link auto-pagination only advanced the replies cursor,
so a target that is a quote past the first quote page of an ancestor was
never fetched or scrolled to. Drive it from a single maybeAutoPaginate
that walks replies then quotes, each capped per connection and by depth.

hackers-pub#310 (comment)

Assisted-by: Claude Code:claude-opus-4-8
@dahlia

dahlia commented May 31, 2026

Copy link
Copy Markdown
Member Author

@codex review

@dahlia

dahlia commented May 31, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive "News" feature that ranks and displays shared links across the fediverse. It includes database migrations to track scores, engagement mass, and moderation penalties; a scoring model in models/news.ts that handles bot exclusions, repeated-share damping, and recency; a periodic cron job to recompute scores; GraphQL schema additions; and a complete Solid.js frontend with news feeds, discussion threads, and moderator admin controls. The review feedback highlights two important issues: a potential false positive in the link-appending logic (ensureLinkInContent) when checking for URL existence via a simple substring match, and a Solid.js anti-pattern in NewsDiscussionThread where a reactive signal (post()) is accessed directly in the component setup function, which could bypass duplicate checks during hydration.

Comment thread web-next/src/lib/composerLink.ts
Comment thread web-next/src/components/NewsDiscussionThread.tsx

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 247c1c94ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread drizzle/20260530050716_overrated_captain_cross/migration.sql
@dahlia dahlia merged commit dddb32f into hackers-pub:main May 31, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

graphql GraphQL API server web-next The new web frontend (SolidStart)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant