1083 Commits

Author SHA1 Message Date
DIYgod 8c9df6e961 fix(store): find a feed by the RSSHub address it was looked up by
A Discover result for an RSSHub instance address such as
http://localhost:1200/test/sort kept offering Follow after the follow, as
the server keeps the feed as rsshub://test/sort. The store now remembers
the URL an address was looked up as, and finding a feed by URL follows it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 03:39:49 +08:00
DIYgod 7463bc3d4f fix(store): follow a feed the server has not stored by its URL
A new feed address was previewed under a random nonce id that mobile sent
as feedId, so following it failed with 422 "Invalid Snowflake ID". The
nonce is no longer sent and the feed is followed by its URL; the feed that
arrives for that URL then replaces the preview.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 03:35:35 +08:00
DIYgod 515bc47ae3 fix(mobile): check profile edits as the server does and explain refusals
Edit Profile sent blank names, values too long for their columns and handles
with spaces, then toasted the server's English text or a bare 500. Fields are
now checked as the server checks them, its update-user codes show translated
messages under their fields, and after a bare 500 a taken handle is looked up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 01:33:40 +08:00
DIYgod 55690ce2d8 fix(mobile): keep Spotlight highlights readable in dark mode
Highlighted words kept the theme's white or gray text on the pale
highlight colors, about 2:1 contrast in dark mode, in the timeline, entry
titles and entry text. Highlights now set black text, or white on a dark
color.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:47:47 +08:00
DIYgod e31d01ba22 fix(store): save a cleared profile handle as no handle
updateProfile sent a cleared or space-only handle as "", which the unique
index on handles refuses with a bare 500 once another user has one, and
the updated server stores a blank handle as none. It now sends and keeps null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:45:25 +08:00
DIYgod 87cbc67e05 fix(store): stop profile saves from clearing the social links
updateProfile sent socialLinks: null unless the caller passed links, so a
mobile avatar change or a name-only save wiped them. It now sends them only
when given, and trims the name and handle as the server saves them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:13:04 +08:00
DIYgod a5f01e9f3e fix(store): unfollow every list of a selection the server can handle
Unfollowing feeds and lists together sent one DELETE with feedIdList and
listId; the server only handles feedIdList, so the lists stayed followed
(and of several lists only the first was sent). Feeds now go in one
request and each list in its own; a failed request restores only its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:57:06 +08:00
DIYgod df73e5e31f test(store): cover a failed unfollow and a delta behind the cursor
The tests for confirmed follows and unfollows did not check that a failed
unfollow confirms nothing, or that a replica answering with an older sync
id leaves the cursor and the confirmed unfollow in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:34:39 +08:00
DIYgod ce29910ba9 fix(store): keep a confirmed follow or unfollow over older sync rows
Right after unfollowing a feed or list, a refresh could put it back for up
to a minute: /sync/delta lags behind the DELETE and still returned the
follow before it, and an undo vanished the same way. Confirmed follows and
unfollows now keep transaction-queue overlays until the engine applies
their sync id; older delta rows and lagging subscription lists yield.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:44:18 +08:00
DIYgod 52532ad8d5 fix(desktop): reach the AI model picker from the keyboard
The model picker was a plain div that could not take focus, and Tab in
the composer indented text instead of leaving it, so keyboard users could
not change the model. The picker is now a button named "Model: <name>",
Tab moves on from the composer, and the title that opens the chat history
is named "Chat history: <title>" instead of repeating "New Chat".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:52:46 +08:00
DIYgod d1386331eb fix(desktop): use the Folo name in exported files and 2FA
Export Database saved follow.db, Export Settings saved
follow-integration-settings-<date>.json, the OPML link asked for
follow.opml, and authenticator apps listed the account under "Follow".
They now say Folo; the 2FA setup passes "Folo" as the TOTP issuer. The
local database keeps its follow.db name so existing data stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:09:59 +08:00
DIYgod 7f5b016205 fix(desktop): stop requesting feed analytics with no feed ids
Every time the Feeds tab opened, it first sent POST /feeds/analytics
with an empty id list, before the table had measured its rows, and the
server answered 422. The analytics query now waits for ids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:01:23 +08:00
DIYgod cd889e36ff fix(desktop): say once why a Spotlight regex is invalid
An invalid regex read "Invalid regex: Invalid regular expression:
Invalid regular expression: /[abc/: Unterminated character class", and
an empty pattern switched to Regex was called an invalid regex. The
validator now returns only the engine's explanation, and an empty
pattern shows no error. Mobile uses the same validator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:00:27 +08:00
DIYgod 5e9fb049e6 fix(desktop): count the saved AI personalization prompt
Coming back to the AI tab showed the saved prompt with "0/500", because
the counter only moved after an edit. The editor's length plugin never
called its callback; it now reports the opening length and each change,
which also brings back the AI task prompt counter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:13:41 +08:00
DIYgod c51c066b5a fix(desktop): bundle the SN Pro font files in production builds
Choosing SN Pro as the global or content font did nothing on the web:
the production Tailwind pass rewrites the whole stylesheet and drops the
font CSS's relative url()s, so no font file was emitted and each request
got index.html. The font CSS is now imported from JS on its own, in the
renderer and the SSR client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:07:41 +08:00
DIYgod 95f5b9db40 fix(store): import exported action rules that have an empty name
The editor saves rules with an empty name and shows "Action N" for them,
but the importer rejected an empty name as missing, so such an export or
clipboard copy could not be imported again. Only reject a missing name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:52:59 +08:00
DIYgod 1076922a87 fix(desktop): keep existing action rules when they fail to load
When GET /actions failed on a fresh device, the Actions page showed the
empty state, and saving a new rule there replaced every rule on the server.
Show a load error with Retry instead, and make saveRules refuse to send
rules before the server's rules were loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:38:53 +08:00
DIYgod 423faf59c1 fix(desktop): enable the mark-all-read shortcut when the app starts in the sidebar
The subscription column focuses its sidebar in a mount effect, which
writes the focus scope in the same commit that mounts the timeline
header. jotai 3 subscribes after that commit, so the header kept the
empty scope and never bound Shift+Cmd+A until focus moved elsewhere.
Read focus scopes with useAtomValueRawSync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:49:52 +08:00
DIYgod ebc18454d7 fix(store): let a fresh feed fetch update follower and update counts
Upserting a feed kept the stored subscriber count, updates per week and
latest entry date whenever one was set, in memory and in SQLite, so the
follow screen and search cards kept showing the first count ever seen.
Keep the stored values only when the incoming feed has none, as feeds
from subscriptions carry no analytics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:36:58 +08:00
DIYgod 6c2b46693e feat(mobile): prompt to update from the store when a newer binary is out
The OTA provider fetched the store policy but never used it. Fetch it
once per launch, from the env profile's OTA URL, and show an alert when
the App Store or Google Play has a newer version. Later puts that
version off, a prompt shows at most once per launch, and a blocked
version asks every time the app returns to the foreground. APKs from
outside Google Play are not prompted, the alert waits until the app is
in front, and while it is open the review prompt stays away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:12:40 +08:00
DIYgod aa5c70b1c6 fix(store): treat an account without action rules as an empty rule set
GET /actions answers without data when the account has no rules, and
fetchRules then left the store alone. Unsaved edits could not be
discarded for such accounts, since there was no confirmed copy to go
back to, and rules deleted on another device stayed in the store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:20:57 +08:00
DIYgod 15d83c367f fix: drop unsaved action edits when the editor closes
Leaving the Actions screen after choosing to discard the changes only
navigated away. The edits stayed in the store marked dirty, so the next
visit showed them again, remote updates from the sync engine were held
back, and the next save sent them to the server.

The store now restores the confirmed rules when the editor unmounts. On
mobile a swipe back is also disabled while there are unsaved changes, so
leaving goes through the prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:43:08 +08:00
DIYgod 120c6b1a2d fix(mobile): remove unchecked feeds when saving a list
Saving the manage-list screen only sent every selected feed to the add
endpoint, so feeds the user unchecked stayed in the list and the local
copy collected duplicate ids. It now adds only the new feeds and removes
the unchecked ones. The selection also started empty when the list was
still loading; it now starts from the list's feeds once they arrive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:39:05 +08:00
DIYgod af5b4e535b fix(components): let the segment control be used from the keyboard
The segment control's group could take focus, but its segments couldn't
be reached and nothing handled keys, so keyboard users couldn't change
a selection in the settings, Discover, action rules or view toggles,
and screen readers weren't told which segment was selected. Move the
selection with the arrow, Home and End keys, and expose the selected
segment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:21:10 +08:00
DIYgod a83e387569 fix(components): disable the narrow-screen responsive select
Below 1024px the responsive select renders a button over a native
select, and neither received disabled, so a phone or a narrow window
could still change the fields of a disabled action rule or the language
while it was loading. Disable both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:20:05 +08:00
DIYgod 88f114844a perf(store): store streamed entry contents once per network chunk
Prefetching the contents of up to 30 entries updated the entry store
once per streamed entry, and every update re-ran the timeline selectors
that filter and sort every entry of the session. Store the entries that
arrive together in one update, and report a failed stream instead of
leaving the rejection unhandled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:50:45 +08:00
DIYgod ebb769a3e9 fix(store): update the mobile subscription list when unread counts change
The mobile subscription list sorted by count and hid read subscriptions
through subscription store selectors that read the unread counts from
the unread store, which never ran them again, so a feed read to zero
stayed visible and the order froze until a subscription changed. The
folder sort also walked every subscription twice per comparison.

Run the selectors again when the counts they use change, and add up
each folder's unread count once per sort from the ids already grouped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:49:19 +08:00
DIYgod bdf991b9ec perf(store): apply new-entry counters of the change log together
Every new-entry action in the change log updated the dirty feed set,
copied and published the whole unread map and wrote the database on
its own. After a night away that is thousands of sequential writes and
store updates, re-rendering every badge and sidebar count each time.
Add up consecutive new-entry actions and apply them in one update,
before any other action so counts set outright still apply in order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:47:28 +08:00
DIYgod 8acaece7a5 fix(atoms): keep paid settings while the role is loading
Paid settings fell back to their defaults whenever the user's role was
unknown, which it is at every launch until the session loads and for
the whole session offline, because the role isn't stored. The setting
hooks also never re-rendered once the role arrived. Keep a signed-in
user's saved values while the role is unknown, and refresh the setting
hooks when it changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:45:54 +08:00
DIYgod b0ed03e260 fix(database): clear unfollowed entries and read only kept ones at launch
Launch hydration read every stored entry with its content, kept the 20
newest per subscription and deleted the rest, but entries of feeds no
longer followed, such as previewed feeds or opened links, were neither
kept nor deleted, so the table and the launch time grew without bound.
An entry shared by a list and a full feed was also hydrated twice and
deleted at the same time.

Decide from the ids and sources only, delete every entry not kept for a
followed source, then read the kept entries in batches below SQLite's
bound variable limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:43:18 +08:00
DIYgod e10e28bd24 fix(store): allow for clock skew before skipping a new entries refresh
New entries skipped refreshing a list that was fetched after the newest
entry was logged, but compared the server's log time with this device's
fetch time. With a clock a few minutes fast, a list fetched before the
entries looked newer and never showed them until it went stale. Only
skip lists fetched clearly after the entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:41:11 +08:00
DIYgod 35ac2c9e62 fix(store): keep other views' subscriptions when refetching one view
Refetching a single view's subscriptions, the fallback when the sync
engine isn't running such as a pull to refresh on mobile, emptied the
set of subscribed ids for every view but refilled it only for that
view, so feeds in other views looked unsubscribed. It also kept the
subscriptions the server had dropped. Reset only the refetched view's
subscriptions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:39:12 +08:00
DIYgod 3081bfffdd fix(store): clear unread counts when a list is unfollowed
Unfollowing a list only cleared the unread counts of feeds unfollowed
directly, so feeds that had been followed only through the list kept
counting toward the app badge until the next hourly recount, and a list
deleted on another device never cleared them at all. Clear the counts of
feeds no longer followed after a local unfollow, and recount after a
synced list delete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:38:33 +08:00
DIYgod 0435abcd29 fix(database): update hide-from-timeline when a subscription is upserted
The subscription upsert listed the columns to update on conflict before
hide from timeline existed, so re-saving an existing subscription kept
the old value in the database while the store had the new one, and it
came back after a restart. Update every column except the id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:36:57 +08:00
DIYgod 5b3c7943c3 perf(store): drop the summary cleanup that never removed anything
After every summary update the store sorted all summaries in memory to
keep the ten most recent, but it deleted them by language code instead
of entry id, so nothing was ever removed and every update paid for the
sort. Hydration loads every stored summary anyway, and evicting them
would only refetch summaries still shown in the timeline, so remove the
cleanup together with an unused getter that wrote to the store on read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:36:29 +08:00
DIYgod 5c973e7506 fix(store): move an edited list to its new view
Editing a subscribed list's view only updated its record, so the
sidebar kept listing it under the old view and view-wide unread counts
and mark-as-read used the wrong view until a restart. The later sync
patch couldn't repair it because the record already had the new view.
Move the list between views like feeds, and refresh both views.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:35:42 +08:00
DIYgod c497f725be fix(store): keep the sidebar's unread order current
Sorting feeds by unread count sorted the caller's array in place and
returned the same reference, so the store's equality check skipped the
update and the order inside a folder went stale; folders and lists also
sorted that shared array in opposite directions. The folder sort built
new tuples on every unread change, which re-rendered every folder.

Sort a copy, keep the folder tuples stable, and pass them through
unchanged so an unchanged order doesn't re-render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:34:51 +08:00
DIYgod 3cdcb2a1b8 fix(database): clear stored unread counts for an empty snapshot
Replacing the unread counts returned early for an empty snapshot before
deleting the stored rows. When everything was read, memory showed zero
but the database kept the old counts, which came back as badges on the
next launch and were never repaired by later recounts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:32:54 +08:00
DIYgod 373caeeed1 fix(store): apply a removed category to the local subscriptions
Taking a feed out of its category sends a null category, but the batch
update only wrote truthy categories to the store, so the feed stayed in
its folder until the next full sync even though the server had moved
it. Apply null as well, drop categories the move leaves empty, and
restore them on rollback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:30:48 +08:00
DIYgod 2dda3df123 fix(desktop): act on every feed of a merged folder in the All view
The All view merges same-named folders from every view, but folder
actions used the view of the folder's first feed. Renaming or
ungrouping changed only the feeds of that view and split the folder,
unsubscribing it left the other feeds, its view switcher hid that view,
and dropping a feed on it or adding a feed to a category moved the feed
into that view.

Run folder actions in the All view across all of its feeds, update the
categories of each view they touch, and keep every feed's own view when
it is dropped on a folder or added to a category.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:04:08 +08:00
DIYgod ab8050f732 perf(desktop): fetch AI-cited entries only when they aren't loaded
Every entry link in an AI answer fetched the entry detail with its full
content as soon as it rendered, even when the entry was already in the
store, so an answer citing a whole timeline sent a request per entry.
Only fetch the entries the hover card can't show yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:48:47 +08:00
DIYgod 67aa847285 fix(store): roll back profile and email changes the auth server rejects
updateProfile, updateEmail and sendVerificationEmail awaited the
better-auth client, which resolves with an error instead of throwing.
On mobile a rejected handle or email kept the new value in the local
user and database and showed a success toast, and a failed verification
email reported as sent. They now throw on an auth error, so the
optimistic change rolls back and callers show the error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 01:43:10 +08:00
DIYgod 3f29a8f57f fix(actions): reset the operator and value when a condition changes field type
Changing a rule condition's field only merged the new field, so a
"title contains foo" condition switched to "Subscription view" kept the
text operator and value. The operator select showed nothing and the rule
saved an invalid condition. Desktop and mobile now go through
changeConditionField, which keeps the operator and value only when the
new field has the same type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 00:37:05 +08:00
DIYgod 47bcdf2298 fix(timeline): keep route params when the timeline mounts
Since jotai 3, useAtomValue subscribes in a passive effect without
re-reading the atom. StableRouterProvider writes the route atom in a
layout effect, so components that mount in the same commit (a full page
load in dev, or opening a timeline from Discover, AI and other layouts)
kept the previous params: feedId and entryId stayed undefined, the view
fell back to Articles, the header and title were empty and scroll
mark-read never fired.

Read the route atom with useAtomValueRawSync, which re-checks the
snapshot after subscribing, and cover both cases with a regression test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:18:40 +08:00
DIYgod 2c246b61eb fix(timeline): mark the last entries read when scrolling to the end
The picture masonry marked entries up to the smallest index among the
items an IntersectionObserver callback reported as gone. Several items
leave in the same frame, so later callbacks only caught up mid-list and
the last entries stayed unread. Use the first laid out item still in view
as the boundary instead and reuse the list's anchor range logic.

The list and grid virtualizers memoized onChange without onRangeChange. A
list mounted while a scroll reset was pending kept a handler that always
returned early, leaving only onScroll, which runs before the range update,
so the range of the last scroll event was never marked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:21:08 +08:00
DIYgod 9603533ed4 fix(i18n): translate hard-coded UI strings and complete every locale
Move the hard-coded English left in the desktop renderer, the Electron
main process, the mobile app and the shared packages to i18next, with
en, zh-CN, zh-TW, ja and fr-FR translations. SSR share pages are
unchanged.

- Fill every namespace in every language, delete keys no code reads
  (server error codes stay) and update translations left behind by
  later English changes.
- Stop HTML-escaping interpolated values in the desktop renderer.
  React already escapes text, so names with & or quotes showed entities.
- Render error boundary fallbacks as components so they can use hooks.
- Localize zod validation messages with the app language.
- Throw ClipboardError from the clipboard helpers and show translated
  toasts. formatEstimatedMins takes a formatter so callers localize the
  units.
- Use one glossary term per concept in ja, fr-FR and zh-TW.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:50:23 +08:00
DIYgod b7df9ad01e fix(i18n): stop title-casing non-English titles and labels
Entry titles and settings labels went through title-case unconditionally,
but it applies English capitalization rules, so with a French UI and French
feeds titles rendered as "Pourquoi Votre Café Est Une Arme De Défense" and
settings labels as "Afficher Le Nombre De Non Lus". CJK titles were also
touched ("iOS 27 beta" became "IOS 27 Beta").

titleCaseIfEnglish in @follow/utils only applies title case when the
language is English (en, en-US, en_GB, or franc's eng). On desktop, entry
titles use the entry language and translated titles the translation
language, falling back to the UI language when unknown; settings labels
and section titles use the UI language. Mobile grouped list cells use the
UI language too. AI chat tool names keep title case since they are English
identifiers.

Mobile no longer imports title-case directly, so the dependency moves to
@follow/utils.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:46:44 +08:00
DIYgod 59fda3cee7 fix(shared): point the dev environment at the folo.is domains
api.dev.follow.is no longer resolves to a working API, so the dev env
profile (the default in mobile development builds) could not load
anything. Use the current api.dev.folo.is and dev.folo.is hosts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:04:33 +08:00
DIYgod ce7210f064 fix(components): unwrap the CommonJS marquee export for Vite 8
Shared pages built with Vite 8 received the whole react-fast-marquee module object for its default import and crashed with React error #130 when a title marquee rendered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:04:33 +08:00
DIYgod 681de464cb fix(store): include every list in the All view
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:04:32 +08:00