Commit Graph

1424 Commits

Author SHA1 Message Date
DIYgod 9949ddeabb fix(desktop): give each autocomplete input its own accessible name
The shared Autocomplete always set aria-label "Select Category", which
was wrong for the feed search in the list settings. Callers now pass a
label that matches their purpose. The list search reuses
lists.select_feeds, whose zh-CN copy drops a stray period and zh-TW
copy uses the Taiwan word for "current".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:52:12 +08:00
DIYgod 5cddfa4edd fix(discover): show validation and search errors
- The OPML import form rendered no FormMessage, so a file over 500KB or
  with another extension failed silently on Import.
- UnifiedDiscoverForm checked RSSHub routes inside the search mutation
  and never rendered the error. The check now lives in the form schema,
  so the message shows under the input and Preview stays disabled. RSS
  and RSSHub previews open from onSubmit, which drops an RSS check that
  could never fail and the "No feeds found" line shown after a preview.
- Failed searches on the discover page and in the sidebar discover
  dialog now show a toast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:51:58 +08:00
DIYgod 9e51f06c69 refactor(desktop): remove dead integration, discover and AI settings code
Nothing imports CustomIntegrationValidator, DiscoverForm (replaced by
UnifiedDiscoverForm), TimelinePromptSection (taken off the AI settings
page in 691307925) or AiOnboardingModal. URLSchemeHandler.getExamples,
the CustomIntegrationManager validate helpers, formatFileSize and
withDisplayStateHandler have no callers. Remove them along with the
locale keys only they used.

The aiTimelinePrompt setting stays because the server still reads it
when it scores the AI timeline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:51:16 +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 da7b60ab15 fix(desktop): bundle the zh-CN and zh-TW AI locales in Electron
Electron never loads locales at runtime: every language is bundled in
default-resource.electron.ts. The ai namespace of zh-CN and zh-TW still
pointed at the English placeholder added before those translations
existed, so the AI settings and chat panel stayed in English.

- Import the real ai locale files, and test the Electron map against
  locales/ for every supported language and namespace.
- Skip the localStorage locale cache in Electron and start i18next in the
  user's language. Only builds from before June 2025 wrote that cache and
  nothing refreshes it, so it could only shadow the bundled resources.
- Translate the zh-TW plan settings titles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:20:36 +08:00
DIYgod 0eb2fd46fa fix(settings): sync every setting again except the device-local ones
Until 1.6.0 the settings sync sent every appearance, general and AI setting
except the device-local keys each domain lists: interface language, voice,
launch at startup, anonymous data, fonts and the like. 93e4a3b48 read those
lists as the keys to send and swapped omit for pick, on desktop and mobile,
so since then only the device-local keys synced across devices and nothing
else did, the action language and the AI settings included. Its e2e test
checked that the interface language syncs.

The lists name the device-local keys again and are renamed to say so. The
appearance domain also leaves the spotlight keys in its tab to the spotlight
domain, which still syncs only those. actionLanguage and personalizePrompt,
which b0f555266 added to the lists, are out of them again, so they sync.

The server kept what the clients sent before 1.6.0, so once per account
each device first queues the settings it changed from their defaults, before
it takes any from the server. While a local change to a tab waits,
applyRemoteSettings now leaves that tab alone, as the change log already
did; settings still at their defaults take the account's values afterwards.

The e2e test now syncs the action language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:17:23 +08:00
DIYgod b0f5552661 fix(settings): sync the action language and AI personalize prompt
The server adds the general actionLanguage and the AI personalizePrompt
settings to the AI chat system prompt, but neither was in the server sync
whitelist, so both stayed on the device and AI chat ignored them: a French
action language still got English answers. Desktop now syncs both, and
mobile syncs actionLanguage, the only one of the two it has.

A value set before this change reaches the server the next time it is
changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:20:49 +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 f0945110e1 chore(desktop): declare the Markdown strikethrough dependency
The Markdown export imports mdast-util-gfm-strikethrough directly; declare it instead of relying on the hoisted copy from remark-gfm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:04:33 +08:00
DIYgod 469412a1d9 fix(desktop): update push-receiver to stop a parser stack overflow
push-receiver 4.3.1 parsed MCS messages recursively and could crash the main process with "Maximum call stack size exceeded" (#4457); 4.4.0 parses them in a loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:04:33 +08:00
DIYgod ec0e193f77 fix(desktop): keep the bundle Dock icon in packaged builds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:04:33 +08:00
DIYgod 07ea633f5e fix(desktop): keep the media preview header clear of the Windows title bar
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:04:33 +08:00
DIYgod 655e3051b1 fix(desktop): keep strikethrough when exporting entries to Markdown
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:04:32 +08:00
DIYgod 86b21acbf4 fix(desktop): stop saving toolbar order on every drag over
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:04:32 +08:00
DIYgod 167a6a1519 fix(desktop): show custom subscription titles for social posts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:04:32 +08:00
DIYgod d5ffb82f4c fix(desktop): start in the tray when launched at login
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:04:32 +08:00
DIYgod 506137f22a fix(desktop): link the upgrade toast to the desktop release tag
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:04:32 +08:00
DIYgod 034f5bc558 fix(desktop): open issue forms for in-app feedback
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:04:32 +08:00
Sangeeth Thilakarathna 489019f62e docs: add regex help for action conditions (#5036)
Co-authored-by: sanmaxdev <sanmaxdev@users.noreply.github.com>
2026-09-26 22:22:44 +08:00
DIYgod 2f392594d5 chore(deps): refresh dependencies and verify affected app flows 2026-09-22 16:20:40 +08:00
DIYgod 141567e928 fix(store): pair unread snapshots with the sync id they reflect
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>
2026-09-19 10:29:35 +08:00
DIYgod d8835acd02 fix(desktop): route the remaining main-process requests through Chromium and keep the CLI token out of logs
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>
2026-09-18 17:40:26 +08:00
DIYgod ca0c85b20d fix(desktop): send API requests through Chromium's network stack so they follow the proxy
The main process sent the renderer's API requests with Node's `fetch`, which knows
nothing about the app's proxy settings: with only a system proxy configured, every API
request went out directly while images and pages went through the proxy. On networks
that need the proxy the API was unreachable.

`net.fetch` uses Chromium's network stack and therefore the session's proxy: the system
proxy, PAC files and SOCKS alike. Cookies stay attached by hand from the managed auth
cookies, so Chromium's own cookie handling is switched off for these requests. Verified
in a real main process: the proxy is resolved and used, a manual `Cookie` header is sent,
`Set-Cookie` is exposed through `getSetCookie()`, and `credentials: "omit"` leaves the
cookie store untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 16:47:43 +08:00
DIYgod c75533cbd2 fix: report the API as unreachable only when a probe confirms it
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>
2026-09-18 16:18:30 +08:00
DIYgod 9be883a63a fix(desktop): accept bodiless responses from the main process and catch lists up on return
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>
2026-09-18 15:50:01 +08:00
DIYgod b6c50296a1 feat(store): stop requesting the session, actions, settings, wallet and push token on every launch
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>
2026-09-17 21:32:04 +08:00
DIYgod 97e92a8ac2 feat: show a prompt when the API cannot be reached
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>
2026-09-17 21:08:05 +08:00
DIYgod b09af5f5cd feat(store): rely on the delta feed instead of full snapshots once a sync cursor exists
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>
2026-09-17 19:53:09 +08:00
DIYgod b71dba08d4 feat(desktop): add a settings entry for unauthenticated users
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>
2026-09-17 19:29:15 +08:00
DIYgod 0e59bd2ddd feat(store): settle transactions by sync id and sync lists and inboxes
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>
2026-09-17 18:16:25 +08:00
DIYgod 9b890144e8 chore(deps): upgrade client sdk to 0.3.96 and use its sync module
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>
2026-09-17 14:36:00 +08:00
DIYgod 247a9294ea feat(store): add an incremental sync engine driven by server sync ids
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>
2026-09-17 13:53:47 +08:00
Komh 44f0e5df06 fix(tray): Linux tray icon duplicates and dead menus on minimize-to-tray toggle (#5086)
On Linux, `Tray.destroy()` does not remove the icon from StatusNotifierItem
hosts (waybar, KDE Plasma), so toggling "Minimize to tray" off and on stacked a
dead icon every time. Keep the single Tray instance for the app's lifetime
instead; the window close handler reads `getTrayConfig()` live, so a tray icon
that outlives a disabled setting is inert.

The icon cannot be removed from a running process, so `setTrayConfig` reports
when a restart is needed and the renderer offers one via a new `app.relaunch`
IPC. That relaunch runs from `$APPIMAGE` where set (AppImage's `execPath` points
into a mount that is gone by then), drops `--start-in-tray` so it never comes
back hidden, and uses `app.quit()` so `before-quit` still persists the window
bounds and flushes cookies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 18:10:14 +08:00
DIYgod 8b097abc52 fix(reader): show missing cover images above entry content 2026-09-09 13:13:09 +08:00
DIYgod 41d009a87e fix(desktop): reset masonry layout when item order changes 2026-09-06 11:07:04 +08:00
DIYgod 9787707ca3 feat(timeline): support oldest-first unread sorting 2026-09-01 20:26:14 +08:00
DIYgod f82a5dad81 fix(desktop): restore inbox secret copying 2026-09-01 20:11:40 +08:00
DIYgod 7c220c69a8 fix(desktop): allow custom URL schemes 2026-08-15 14:51:34 +08:00
DIYgod dd2c933936 chore(ci): format integration URL scheme test 2026-07-28 13:52:45 +08:00
Sebastion bd91b015ee fix(desktop/ipc): allowlist URL scheme protocols in openURLScheme (#5056)
The 'integration.openURLScheme' IPC method invokes 'shell.openExternal'
with a renderer-supplied string after only checking that it contains
'://'. Electron's documentation explicitly warns that passing untrusted
URLs to 'shell.openExternal' is unsafe: schemes such as 'file://',
'smb://', 'ms-msdt:', 'search-ms:', 'jar:', 'res:', 'javascript:',
'data:' and 'vbscript:' have well-known abuse chains (local file
disclosure, NTLM credential theft over SMB on Windows, MSDT/Follina-style
RCE, etc.). Because the renderer process can also reach this IPC via any
XSS sink in untrusted RSS feed content, the previous validation was not
sufficient.

Replace the substring check with strict URL parsing plus an allowlist of
protocols that match the integration use-cases documented in the UI
(Obsidian, Bear, Drafts, Things, Notion, DEVONthink) plus generic
http/https/mailto. All other protocols are rejected with a clear error.

Adds vitest cases for representative dangerous schemes (verifying that
'shell.openExternal' is never invoked) and for every scheme shipped as a
built-in example, so future regressions on either side are caught.
2026-07-28 13:40:13 +08:00
DIYgod 52f587dacb fix(desktop): dismiss share popover after actions 2026-07-25 20:23:44 +08:00
DIYgod 3d65478fb6 fix(subscription): recover past-due Stripe checkout 2026-07-25 20:22:55 +08:00
DIYgod 22c8a667e6 fix(desktop): restore category chevron rotation 2026-07-25 20:18:50 +08:00
Tony c3e1c26194 fix(desktop): set referrerpolicy on youtube embed iframes (#5039) 2026-07-12 18:07:09 +08:00
DIYgod 1158281f59 chore(deps): update frontend dependencies (#5038)
* chore(deps): update frontend dependencies

* fix(ci): format OTA verification commands
2026-07-10 22:36:12 +08:00
DIYgod 72e289bb3e fix(tracker): sample PostHog events at five percent 2026-07-03 16:46:26 +08:00
DIYgod 43fb8f23a7 fix(desktop): avoid idle Spline AI indicator render 2026-06-22 11:38:31 +08:00
DIYgod 84f692ad35 fix(desktop): keep reading mode content when translated
Refs RSSNext/Folo#5023.
2026-06-22 10:43:34 +08:00
DIYgod 4ab06f42b4 fix(desktop): stabilize streaming tts scheduling 2026-06-22 10:41:54 +08:00
Tony c98f572cf8 fix: dedupe code blocks with nested line divs
fix: dedupe code blocks with nested line divs
2026-06-05 19:44:07 +08:00