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>
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>
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>
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>
The Thumb's `before:` pseudo-element was meant to enlarge its hit target,
but its negative offsets plus `-translate-x-full -translate-y-full` place
the box 44px to the left of the scrollbar and half a thumb height above
it. Tailwind 3 dropped the unitless `min-w-[44]`, so the box was inert;
Tailwind 4 compiles it to `min-width: 44px`, turning it into an invisible
strip in the scrollbar layer that steals hover and swallows clicks on the
right edge of timeline entries (and the right edge of wide views).
Radix already handles pointer down on the whole track, so drop the
pseudo-element instead of repositioning it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A `/reads` recount taken while new-entry hints were still hidden behind a
stale KV head already counted those entries; when the hints arrived with
the next pull their increments were added on top, and the reads that
followed left the hint values behind as phantom unread counts.
The engine now keeps the sync id an unread snapshot reflects
(`unreadSnapshotSyncId`, from the `/reads` answer or `/sync/state` read
just before) and applies the counter part of `N`, `U` and `inbox_entry`
`D` actions only above it. A refresh the user asks for recounts through
the engine, and the list-versus-counter check also fires when a fully
loaded unread-only list shows fewer unread entries than the counter.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
After the API bridge moved to `net.fetch`, several main-process requests still used
Node's `fetch`, which ignores the app's proxy settings: the update manifest and policy,
the main-process API client, file downloads, text-to-speech, page fetching for the
reader, and the Eagle, qBittorrent and custom integration requests. They now go through
`net.fetch` as well. The readability package takes a `fetch` implementation for that;
downloads hand it to ky. Requests that manage cookies by hand pass
`credentials: "omit"`, so Chromium does not keep them too.
A failed CLI login sync used to be logged with the whole command line, token included.
The command is now described with the token redacted, and the command's output is
scrubbed the same way.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One request failing without a response proved little: an endpoint may be slow or
briefly broken while the API itself is fine, and the prompt appeared on every such
hiccup.
- a failed request now only schedules a probe, a cheap `/status/configs` request two
seconds later, repeated once; the API counts as unreachable when every probe fails
and no other request got an answer in the meantime
- while unreachable the probe repeats every fifteen seconds so the prompt goes as soon
as the API answers; any ordinary answer lifts it as well
- the desktop probe travels the same path as its requests, through the main process;
the mobile probe uses the app's fetch
- without a probe, three failures in a row are needed instead of one
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The staging desktop build showed "Unable to connect to the server" and a timeline that
never caught up.
- the main process hands API responses over as text, so a `204` such as the analytics
endpoint's arrived as an empty string; `Response` refuses a body for those statuses,
the throw looked like a failed connection, and the new prompt reported it
- a desktop window keeps its visibility when the user switches apps, so the sync engine
never saw a "return"; window focus now counts as one, and duplicate signals within a
few seconds share one pull
- views that received new entries during background pulls are remembered and their
lists brought up to date on the next return, even when that pull finds nothing new
- oldest-first lists receive new entries at their end: the empty page that marked the
end is dropped and the page after the last entry is requested again
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The server no longer repeats entry ids in `N` actions, answers up-to-date delta polls
from KV and drops `N` rows after three days. The client never read the entry ids, so
only its type and the design document follow.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A launch of the web app made twelve API requests. Half of them fetched data the
sync engine can keep current, or fetched the same thing more than once.
- session: start-up fetches it through the query cache and `useSession` reuses
it for a minute, instead of three separate requests
- action rules and settings become sync models: they are loaded in full once per
account, stored locally, and afterwards follow the change log; unsaved rule
edits and queued setting changes are never replaced
- sync models outside the engine register through `sync/model-registry`, with a
one-time bootstrap and an apply step; a model whose actions went by unhandled
is bootstrapped again
- wallet: requested when the profile menu opens, which is the only place that
uses the answer
- web push: the token is registered again only when it changed or the change
log shows another browser took over the channel
- the new-user guide remembers accounts that already have settings, so the
entry list no longer requests `/settings`
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Requests that fail without any response (DNS failure, refused or reset
connection, offline device, timeout) previously failed silently. Track
them through the SDK interceptors and surface a single prompt per outage:
a persistent toast on desktop that is dismissed once the API answers
again, and a transient toast on mobile.
The connection state lives in an atom because the first requests can fail
before the toaster is mounted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
whoami() told connection failures apart from a missing session by looking
for "Failed to fetch" in the error message. Electron's IPC fetch, React
Native's fetch and SDK timeouts use different messages, so an unreachable
API cleared the local user, and on mobile redirected to the login screen.
Only a 401 (or an empty session) now clears the user. Every other failure
is rethrown so callers keep the cached user.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The sync engine added an incremental channel, but the app kept fetching full
snapshots next to it: both prefetch hooks, a recount after every pull, every
loaded page of the entry lists, and whole-list refetches after unsubscribing or
importing.
- unread counters keep confirmed counts under the local transactions; the change
log moves them by the flips and new unread rows the server reports, and an
acknowledged transaction drops its prediction once the engine reached its
sync id
- usePrefetchSubscription, usePrefetchUnread, pull to refresh, the refresh
button, OPML import, unsubscribe and the mobile background task go through the
engine and only fall back to the full requests when it cannot help
- snapshots remain as calibration: unread hourly, subscriptions daily, plus a
throttled recount when the list on screen disagrees after a delta pull
- new entries fetch only the first page of the lists on screen and merge it in
front of the loaded pages; a refresh trims the infinite query to one page
- mobile refreshes subscriptions through the delta feed after an OPML import
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Show a preferences button next to the login button when logged out, so
guests can open the settings modal without signing in. The modal already
supports guest mode and gates account-bound tabs behind login.
Also fix KbdCombined splitting the comma key in shortcuts like "$mod+,"
as if it were a separator between multiple shortcuts.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Follow up on the sync engine:
- Transaction kinds report the `lastSyncId` their mutation returned. The
queue releases an acknowledged transaction's overlays as soon as the sync
engine has applied that id, instead of always waiting out the 30 s window,
which stays as the upper bound for servers without sync ids.
- The engine applies the new `list`, `inbox` and `inbox_entry` actions and
inbox `N` hints, so list edits, inbox changes and new mail made elsewhere
show up without a full refetch.
- While the engine is active the unread mismatch heuristic stands down and
the desktop blanket invalidation skips the queries the delta feed already
keeps fresh. Both remain as the fallback for servers without `/sync`.
Also fix the Android build: `setup-android` installs the obsolete `tools`
package by default, which the SDK repository no longer serves.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bump `@follow-app/client-sdk` to 0.3.96 (and `@folo-services/drizzle` to
0.1.48) and hand `followApi.sync` to the store instead of requesting the
`/sync` endpoints through `followClient.request`.
The new SDK no longer declares the frozen wallet and RSS3 writes. Keep the
existing screens and their runtime behaviour unchanged through a small
compatibility module: legacy withdraw fields and the `IS_RSS3_TESTNET`
flag get local types, and the removed `wallets.post()` is requested at the
same path with `followClient.request`. Drop an over-wide tuple annotation
in the mobile plan screen now that `PaymentFeature` keys are precise.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add `sync/sync-engine.ts`, which keeps the last applied sync id in a new
`sync_meta` SQLite table (migration 0039), bootstraps from `/sync/state`
plus the existing snapshot endpoints, and then pages through `/sync/delta`
on launch, foreground, reconnect, a 60 s interval and after the transaction
queue receives an acknowledgement. Subscription, collection and timeline
actions are applied to the stores and SQLite, pending local marks keep
precedence through the transaction queue overlays, and unread counts are
refreshed once per pull. A 404 from the sync endpoints keeps today's full
refetch behaviour for the session.
Both apps expose the endpoints through `followClient.request` until the
client SDK release with the `sync` module lands.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- add a Reading settings screen (content font, line height, text color,
inline style, code theme) reachable from Appearance and from the entry
"more" menu
- add a System / Light / Dark theme setting applied through
Appearance.setColorScheme so the reader WebView follows it as well
- optionally slide the entry header away while scrolling down and keep a
blurred status bar strip so the clock stays readable
- forward reader typography to the html renderer through a new
setReaderStyle bridge call
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Introduce a Linear-style transaction queue for user mutations. Read marks,
unread marks, mark-all and star/unstar are applied optimistically, written to
a new `sync_transactions` outbox table, batched and sent in order, retried
while offline and rolled back only when the server rejects them. Pending and
just-acknowledged transactions expose overlays so server snapshots are rebased
on top of the local intent, which replaces the 30 s read-protection window.
Also add the sync engine design spec that covers the server change log and
delta endpoints planned for the next phase.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>