The batched unfollow removed UnfollowRequest but left its doc comment, which now sat on top of
ConfirmedSubscriptionChange's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server now also removes the timeline rows only that list brought, for its owner and its
followers, but the change log says nothing about unread counts. A synced list deletion and a
feed removed from a list on this device now recount them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server now refuses views outside FeedViewType and migrates the rows it had, but one such
row, stored or synced, threw in every store update and emptied the app. The store now skips it,
keeps a synced change of view to one, and adds entries only to views it knows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /profiles/batch now reads at most 1000 ids. The batcher sent every profile asked for within
100 ms in one request, as many as a long list or timeline needs; it now sends 100 at a time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign-up, sign-in links, password resets, verification and email change emails now answer 429
TOO_MANY_REQUESTS. Desktop, mobile and the web login pages show it translated, Electron sign-up
shows its refusals at all, and mobile's reset mail from settings no longer claims it was sent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server now sends the approval link to the current address; the new one gets its link after
that. Desktop and mobile say so for verified accounts, and the store no longer shows the new
address before the change is done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DELETE /subscriptions now takes listIdList next to feedIdList and unfollows both together, so a
selection no longer needs a request per list. A failed request puts the whole selection back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PATCH /categories now takes {from, to, view}. A folder whose feeds are all filed under its name
is renamed that way, which also renames a feed another device moved in after the pull. Feeds an
auto-grouped folder holds by their site have no category on the server, so those are still named.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SDK types lastSyncId on the read, collection, subscription and messaging responses, so
readLastSyncId and its unknown-typed lookups go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0.3.98 needs @folo-services/exceptions 0.1.33, whose ESM build is back, so the 0.1.31 pin goes.
Only the @folo-services subtree changes in the lockfile; every peer suffix stays as locked.
The new versions are exempt from minimumReleaseAge, and code 16008 gets its translations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A launch before the session check answered, as on a fresh profile, pulled
nothing, and the engine stayed without a cursor until the next focus or the
60 s interval. A category rename then could not pull first and split it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TanStack folds a next page asked for while a list refetches its first
page into that refetch. A refresh trims a short list to its first page,
so the list asked for its end page meanwhile and got nothing, and the
mobile list never asks again when the same entries come back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
expo/fetch rejects an aborted request with a "fetch failed" error, so a timeout
read as a lost connection and was retried 3 times: 4 minutes for a slow feed. It
now fails once as a timeout with its own message, and looking up a feed by URL
outwaits the server's 70 s, so the server's own error arrives.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When two accounts follow the same new feed at once, the server can answer without
either, having followed nothing. The model built from that had no id, so saving it
failed as "Invalid subscription" with an unhandled rejection; it now asks to retry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app asks for the session while the local data is still loading, so on a
launch with another account's session that account claimed the data before its
owner was read: no reset, and the queued changes were dropped or sent with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mark acknowledged during the first sync before the unread snapshot was read
was counted by the snapshot and taken off it again until the next pull, up to
a minute. The snapshot's sync id now tells the queue which marks it holds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Patches from the change log only ever added categories, so after a rename the
old, empty name stayed in "Move to Category" on both clients. Patches and edits
now drop a name no feed uses, and the mobile menu follows the store.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rename sent only the feeds this device knew to be in the category, so a feed
another device had just moved in kept the old name and the category split. The
rename now pulls first (for at most 2 s) and names the feeds in the category then.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edits sent the whole subscription from the local copy, so a field changed on
another device since the last pull was written back with its old value. The
PATCH and the local update now carry only the fields that really changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mark replayed from the outbox while the engine bootstrapped its cursor was
counted by its own prediction and again by the first delta, so the unread
count stayed one below the server's. A bootstrap now counts as delivering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rejected star, read mark or mark-all was rolled back without a word. The
queue now reports each rejected batch once, and both apps show a toast with
the server's reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rollback re-marked the entries unread but left the feed's count at the
optimistic zero, because the recount found the confirmed counts unchanged.
It now derives the displayed counts again once the transaction is dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Writes queued while a session was dead went out with the next account's cookie.
Outbox rows record their account; the queue sends them only once the session
endpoint confirms that account, resumes after re-login, and a sign-in to another
account wipes the local data on both apps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The queue gave up after eight attempts (about two minutes), rolled changes back
and made every later change fail on its first try. Transport errors, timeouts,
408/425/429 and 5xx now retry with a back-off that resets on resume and focus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Discover card for a followed RSSHub instance address showed Follow again after a restart,
as the address-to-feed-URL map lived only in memory. Keep the 100 latest entries
in the account's sync metadata table, which sign-out clears.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A save now sends the version of the rules it started from as expectedUpdatedAt,
so the server answers 409 instead of replacing another device's changes.
Desktop and mobile then offer to load the latest rules, keep editing, or save over them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A stored pattern RegExp rejects, such as a legacy (?i)foo, blocked every later save.
Like the server, check only conditions that differ from the rules as loaded,
and accept a leading (?i), which the server stores as (?i:...).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>