EAS generates android/ with `expo prebuild`, which sets NODE_ENV to
"development" when it is unset. The app config picked the 0.0.0-dev
runtime from NODE_ENV, so every Android binary since the React Native
0.79 upgrade embedded expo_runtime_version 0.0.0-dev, and ota.folo.is
rejects its update checks. iOS kept the real runtime because eas-cli
syncs its committed Expo.plist outside of prebuild.
Decide the development runtime from the EAS build profile and fall back
to NODE_ENV only without one (expo start, local prebuild). The
eas-build-post-install hook now fails any build whose native runtime
differs from the expected one, and a test evaluates every eas.json
profile the way prebuild does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lightbox placeholder was the original image URL instead of the
proxied thumbnail the card had loaded, so it never came from the cache,
and expo-image on Android decodes placeholders at full size. Every
lightbox page also got the tapped image as its placeholder, so the other
pages showed the wrong picture until their own loaded.
Hand the lightbox the card's proxied thumbnail for the tapped page only,
and let the carousel and the lightbox page through the same deduplicated
media list so an index means the same image in both. The Social Media
view had the same placeholder bug.
Cards in the Pictures view asked the proxy for 1200 px tall images. Ask
for one side sized from the measured card instead, rounded up to 50 dp
so the URL stays stable, and the card's height for images relatively
wider than the card so cover fit stays sharp. Asking for both sides
would make the proxy crop the image.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entries in the Pictures view can carry dozens of images (cosplaytele
posts have 25 to 110), and MediaCarousel rendered every page of its
ScrollView, so each mounted row loaded all of them. Until onLayout the
carousel was 0 px tall, so FlashList measured rows at footer height,
mounted every row of the page and kept them in its recycle pool. The
lightbox likewise mounted the original of every page.
Load images only for the visible page and its neighbors in both, and
size the carousel by its aspect ratio before its width is measured.
On an Android emulator with 20 cosplaytele entries (1142 images),
opening the Pictures view took the native heap from 96 MB to 2.1 GB
before this change and to about 170 MB after it.
Refs #5108
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Marking entries read on scroll stopped after the first entry. Once the
scrolled-out entries were handed off, the list kept for them was stored
again as a new array, an empty one when all of them were handed off.
That ran the marking effect again, and since marking goes through the
transaction queue, marking nothing resolves at once, so the effect ran
every few milliseconds without end. It kept the JS thread busy: no
later entry was reported as scrolled out, and the queue's flush timer
never fired, so even the first entry only reached the server on the
next launch.
Store the same list when nothing changed, and none when nothing is
left.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discover search results for feeds the server has not stored yet all have
an empty id, so their cards shared the key "" and React warned about
duplicate keys. Fall back to the feed URL when the id is empty.
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>
The Handle field kept the text defaults, so keyboards capitalized its first
letter and offered corrections that can rewrite it or add a space, which the
new check then refuses. It now turns both off, as the Website field does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit Profile refuses only spaces and invisible characters in a new handle,
as the server does, yet said "Use only letters, numbers and underscores."
from desktop's stricter check, although dots, dashes and CJK handles save.
It now says what it refuses, in every locale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The paid lock missed this Appearance switch, which desktop also marks paid,
so on a free or trial plan it flipped back off without a reason. It now
shows the paid badge and offers the upgrade, and a test checks that each
settings screen locks the paid settings it changes.
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>
A free or trial account could turn on AI Translation, its mode, Hide private
or Hide Recent Readers, which then did nothing without saying why. These now
read as off, show the paid badge and offer an upgrade when pressed; a value
saved before applies again once the plan includes them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Forgot Password and Edit Email hints waited for the field to lose focus.
Android keeps it focused when Back closes the keyboard, as does tapping the
Edit Email header, so Continue or Submit stayed disabled without a reason.
The hint now also shows a second after typing stops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Earlier exports left a full copy of the database named follow.db in the
app's documents. With exports now named folo.db nothing replaced that copy
any more, so it stayed next to the new one. Exporting now removes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Android kept the custom colors of a disabled switch, so a list's locked
Private Follow and Hide from Timeline switches looked usable to free users
and did nothing when tapped. A disabled switch is now dimmed on Android, as
iOS already does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a 411dp Android phone the Application Updates tile read only
"Application": Android fits the font to the text view's width with its
padding included, so it kept the size and hid the second word. The name is
now indented with a margin and shrinks to fit, as on iOS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In Discover's Popular list, sites with route tags outside the translated
categories, such as other or sport, showed chips reading
"discover.category.other". Those tags now show their own name, as on desktop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Export Database shared a file named follow.db. It now uses the Folo name; the
database the app reads stays follow.db, so existing data is kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A long site name in a Discover category, such as Association for Computing
Machinery (ACM), pushed its category tag past the right edge. The name is now
truncated to one line, and a site with many tags wraps them as desktop does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Long category names such as Programming and Application Updates ran under or
over the emoji on their tiles. The emoji now sits above the name and shrinks
on small tiles, and a name too long for one line shrinks to fit it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Feeds that publish their description as HTML showed tags such as <em> and
<br> as text in Discover search results and the follow form. The summary now
shows the description's text on one line, with character references decoded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no trending feeds, Discover showed the Trending header over a blank
gap. It now says there are no trending feeds yet, as desktop does. The
filter buttons by Trending and Categories had no accessible name and are
now announced as Discover Filters buttons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Free users could turn on Private Follow and Hide from Timeline for a list,
which their plan does not include. Both switches now show the paid badge,
which offers an upgrade, and are disabled for them, as on desktop. A list
that already has one on can still turn it off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fields and switches of the follow and list forms had no accessible name,
so screen readers announced only "edit box" or "switch". They now take
their label or the label of their grouped cell. Action bar items are
buttons, and toggles such as Show Readability report when they are on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 24 pt initials avatar padded its text into 8 pt, so "SW" wrapped and
showed only "S". On Android it also turned square after sign-in, as the
unfilled signed-out view was flattened and recreated without its corner
radius. Drop the padding, keep the initials on one line, never flatten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The follow-list form called its title "Custom title for this Feed". Edit
Email opened a text keyboard with autocorrect and kept Submit disabled for
an invalid address without a reason; it now checks the address like the
server does and explains it under the field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit Profile placed "Your unique identifier." under the Email row, which
had been inserted between the Handle field and its description.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On iOS an aligned field word-wrapped its value when not focused, so Edit
Email showed only "sweep-mobile-" and profile fields such as Website were
cut at a hyphen the same way. Single-line fields now end in an ellipsis
like other iOS text fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Save stayed disabled while the confirmation differed, so tapping it did
nothing and the mismatch message was only reachable from the keyboard.
Save is now enabled once every field is filled and reports the mismatch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Continue sent any text to the server, which answered with the raw
"[body.email] Invalid email address" toast in every language. Check the
address first, keep Continue disabled until it is valid and say why
under the field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a free plan "Show Translation" got checked but nothing was translated and
nothing said why, and the item was hidden whenever the AI Translation setting
was on. Free plans now always see the item, and it opens the upgrade dialog
that names the Basic plan, as desktop's paid badge does, instead of toggling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The player bars and the full player were blank for episodes without their
own image, or whose image failed to load, while the list showed the feed icon.
Playback now starts with the artwork the row shows, and the player loads it
through the image proxy and falls back to a letter avatar if it can't load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The header of a feed screen renders outside the screen, so its Mark all as
read button marked the whole view selected on Home as read instead of the
feed, list or category on screen. The header actions now get the screen's
entry list context again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The header measured its buttons once to center the title, so after signing
in or rebuilding the database, when the Home actions appeared later, the
title sat off center until the tab was switched. It now re-measures them on
every layout change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a language switch the native iOS tab bar kept its old labels, since
only the first title reached the tab bar item, and times on screen mixed
languages ("2 years前"). Later titles now reach the item, the dayjs locale
switches before the language, and relative times re-format on the switch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a feed or list loaded, failed or was missing, the follow screen showed
only a spinner or an error, with no header to close it, and the feed error
said the list failed to load. These states now keep a Follow header with its
close button, and the feed error names the feed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The profile's header with its close button stayed invisible until scrolled,
so on Android, where the profile opens full screen, the top showed only an
unlabeled share icon. The banner now shows a close and a labeled share
button, in the banner's colors, until the header fades in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a screen or dialog with a focused field closed on Android, focus moved
to the hidden TTS WebView, which kept the keyboard up with nowhere to type.
The WebView now sits in a native view that blocks focus (needs a new Android
build; older binaries fall back to a plain view).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The feed screen used the view selected on Home, so with Pictures selected
an articles feed opened from a profile showed no entries, and its mark as
read requests named the wrong view. It now uses the subscription's view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no rules, Spotlight showed "Add rule" as gray text that did nothing,
and the footer repeated "Add rule" even when rules existed. The empty list
now has an "Add rule" button, and the footer says what Spotlight
highlights and which rule wins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Tapping + saved an empty rule before the editor opened, so closing the
editor with X, a swipe or Back left a blank "Pattern" rule in the list
each time. Closing the editor now removes its rule while the pattern is
still empty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>