The submit handler searched the text from the last render, so text
pasted or typed in the same frame as the search key was ignored and
nothing was searched. Read the text from the submit event.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The result count, follower count and entries per week always used the
plural strings, so a single match read "Found 1 results" and
"1 followers". Pass the count to the base keys and let i18next pick the
form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The article renderer is loaded from a local file, so an embedded YouTube
player gets no referrer and only shows "Video player configuration
error 153". Render YouTube video embeds as their thumbnail linking to
the watch page, which opens in the in-app browser or the YouTube app.
Other iframes and playlists keep rendering as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jotai 3 subscribes after the mount commit, so a value written in between
is missed until the next write. The device type arrives from an async
init while the first screens mount, the onboarding check reads routes
that screens push from mount effects, and every mounted player control
writes the pause flag and playback rate in its own mount effect, so a
sibling mounting in the same commit kept stale values. Read them with
useAtomValueRawSync.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The store version comes from the US storefront, so devices in China
mainland, where Folo was pulled from the App Store, would be asked to
install every new release from a page that says the app is unavailable.
Use the request country as a stand-in for the storefront and answer
none for iOS there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The list, list follow and RSSHub sheets turned the dismiss gesture off
while dirty, so a swipe silently did nothing, and every one of these
forms, follow feed included, closed from the header without a word and
dropped the edits. They now set preventNativeDismiss and
promptBeforeLeave, so both paths ask first. The RSSHub header read
isDirty from a memoized child through form.formState, which never
updated; it now subscribes with useFormState. The header prompt's Exit
is destructive like the swipe prompt's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed trending request left an empty section on Discover and the
onboarding interests step with no way to try again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The readable-width wrapper only gets a style on tablets, so on phones it
shrank to its content inside the centered scroll view and the w-full
preference cards collapsed to a narrow column with text wrapping per
letter. Give the wrapper full width and keep its children centered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The iOS app config ships e2eEnvProfile and e2eLanguage as {} instead of
null, so the bundle env fallback never applied and isE2EEnabled() was
true in store builds. Only accept non-empty strings and drop the unused
isE2EEnabled helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The restore waited for the window's show event, but on macOS the main
window shown with show() after the renderer is ready never emits it, so
the window always came back unmaximized. WindowManager.showMainWindow now
shows the main window and maximizes it the first time when it was
maximized at quit, and every place that shows the main window uses it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tab screens register themselves, and screens set their options, from
effects in the same commit that mounts the components reading those
atoms. With jotai 3, useAtomValue keeps the value from before its
subscription. The JS tab bar only showed its tabs because an unrelated
re-render wrote the list again, and options a screen sets on mount were
dropped until they changed. These readers now use useAtomValueRawSync.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plan screen's header button forced canDismiss to false, so when the
plans were presented as their own sheet, as settings still does for the
legacy trial role, the header had no button at all. It now closes the
sheet when there is no screen to go back to. The header button prop
types also list the canDismiss and modal props the header passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The list sheet's header read isValid from form.formState inside a
memoized component that no form change re-renders. Editing worked
because the title was valid from the start, but creating a list kept
the submit button disabled after a title was typed, so no list could be
created. The header now subscribes with useFormState.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Screens like the create-list sheet put their header into the screen's
slot atom from a layout effect, in the same commit that mounts the
component rendering the slot. Since jotai 3, useAtomValue keeps the
value from before its subscription, so the header stayed empty: no
title, no close and no submit button until something re-rendered the
screen. The slot is now read with useAtomValueRawSync.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The create-list and add-spotlight-rule buttons and the back button of
the discover category page were icon-only without an accessible name,
so screen readers skipped or could not describe them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With payments enabled, tapping Lists, Actions or Feeds in settings sent
Free users to the plan screen instead, although the Free plan includes a
list and an action rule, as that plan screen itself says, and desktop
opens these pages for everyone. The server enforces the limits and the
app already shows the upgrade prompt when one is hit. Only the legacy
trial role is still sent to the plans.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The settings tab stays focused while another screen is pushed over it,
so the light status bar for the banner also applied to screens such as
Data Control, leaving white text on a light header. The banner status
bar now also requires that no screen is pushed or presented over its
own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The profile opens full screen on Android, with the status bar over the
user banner, and it had the same dark-on-dark text as the settings tab.
The banner-aware status bar is now a shared component that the profile
uses on Android; iOS shows the profile in a sheet below the status bar.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Android the Edit button's blur fallback is an opaque system
background, so its white label disappeared on a dark banner; it now
uses a translucent fill there. The status bar also kept dark text over a
dark banner on both platforms. While the banner is behind it, the status
bar now follows the banner colors, and returns to the default once the
header fades in or another tab shows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Searching for a feed URL shows a card built from the URL, without a feed
id. Its follow state looked up the subscription by that missing id, so
the card still offered Follow after the feed was followed. It now finds
the feed by URL as well.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backup of the onboarding-finished flag used the SecureStore key
"isOnboardingFinished:persistent". expo-secure-store only accepts
alphanumerics, ".", "-" and "_" in keys and throws otherwise, so
hasFinishedOnboarding rejected for every user who had not finished it
yet, and the onboarding never opened. The key now uses a dot, and a
failing backup read or write no longer blocks the onboarding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the first page failed and nothing was stored for the view yet, the
timeline said "No results found.", as if the view had no entries. It
now shows why loading failed with a retry button, and pull to refresh
still works.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Switches in grouped settings cells had no accessible name, so TalkBack
and VoiceOver only announced "switch" without saying which setting it
was. They now take the label of their cell. Radio rows, used for the
choices in the action rule editor, report their role and selection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enabling Show as Badge asks for the notification permission. When the
user denied it, the switch stayed on although no badge could show. It
now switches back off. The badge helper only checks the permission
now, as the setting is the one place that asks for it.
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>
The feed rows on the manage list screen only showed a check mark, so
screen readers could not tell which feeds were in the list. They are now
checkboxes with their checked state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the network or the API was down, error views and toasts showed the
raw native reason from expo/fetch, such as "fetch failed:
java.net.ConnectException: Failed to connect to …". Those errors now
read "Unable to connect to the server. Please check your network
connection.", the message the app already uses for an unreachable API.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The back/close button on every screen, the check mark submit button and
the profile share button had no accessible name or role, so TalkBack
and VoiceOver announced them as unlabeled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The back button went past an open dialog and navigated the screen under
it, leaving the dialog on top of a different screen. It now closes the
topmost dialog first, like its cancel button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The RootSiblingParent lived inside App, which renders as the content of
the root screen. On Android every pushed or modal screen covers the root
screen, so toasts, dialogs and loaders opened from those screens drew
underneath them, e.g. the Link Copied toast in an entry and the mark all
as read dialog in a feed. iOS was unaffected because it uses native
toasts and FullWindowOverlay. The sibling host now wraps the screen
stack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
playEntryTts now registers a teardown with AudioPlayer so another audio
source stops the TTS. The test's AudioPlayer mock lacked the setter, so
two tests failed. The mock has it now and the first test checks the
teardown is registered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The effect that tracks uiRenderInit and logs "App is ready" also ran on
the first render, while the skeleton was still showing, so every launch
sent the event twice and the first time was too early.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing called waitAppReady, so the queue App drained when the app was
ready was always empty. Its delay handling was also inverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
copyToClipboard already shows its own error toast before it throws, and
the link context menu showed a second one on top.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cmd+F filled the find field with the clipboard text and selected it, so
anything copied before, like a password, showed up on screen and was one
Enter away from being searched. Like browser find bars, the field now
starts with the previous search.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cmd+N filled the URL field with whatever text was on the clipboard, so a
copied password or private message showed up on screen. It now takes
the clipboard only when it holds a single http, https or rsshub URL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A paragraph with a lone image or a video rendered the block media's
<div>s inside a <p>, which React reports as invalid nesting and browsers
may restructure. Such paragraphs now render as a <div>, keeping the
paragraph renderer's other behavior.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The play state selectors in the list and All views compared the player
source with the src of the first render, because the selector hook keeps
its callback until the deps change and none were passed. When an entry's
audio attachment arrived later, the button kept showing the wrong state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sign-in screen pops to the root 300 ms after the session arrives.
When the new-user check answered within that window, the onboarding was
presented and then closed by that pop, and it was not shown again for
the rest of the session. It now waits until no screen is open over the
tabs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
linkSocial returns a redirect, so the auth client's Expo plugin already
runs the browser session through the authorization proxy before the
call resolves. The account screen then opened the raw authorization URL
a second time. The plugin also had no callback URL to return to the
app with. The screen now passes the app callback and only refreshes the
account data afterwards.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The webhook and rewrite rule editors could only add entries. An empty
webhook added by mistake could not be removed and made the server reject
the whole save. Webhooks can now be swiped to delete like conditions,
and each rewrite rule has a delete button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
user-profile-modal.constants.ts is an identical copy of
user-profile-modal/constants.ts and nothing imports it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Profiles store the account name for each social platform, but the
profile header passed it to Linking.openURL as is, so tapping a social
icon failed. A website saved without https:// failed the same way, and
both left an unhandled rejection. The header now builds the profile URL
for each platform as desktop does and adds a scheme when it is missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The button imported deleteAsync from the expo-file-system root, which
since SDK 54 only exports stubs of the legacy API that throw. A user
stuck on the migration error screen could not reset the database. It
now uses the legacy module like the other database deletes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deleting a list removes it optimistically and restores it when the
request fails, but the error was neither caught nor shown. The sheet
stayed open with the list back in place and nothing explained why.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed feed or list search rendered an empty page with no message,
and submitting the same keyword again did not search again because the
query key did not change. Failed searches now show the error with a
retry button, and submitting the same keyword refetches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When TTS finished or failed while the full screen player was open, the
player rendered nothing but its transparent modal stayed in place. The
app underneath was visible but did not take touches, and iOS had no
gesture to leave. The screen now dismisses itself once nothing is
playing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An entry without content switches to readability automatically, and the
spinner stayed until readability content arrived. When the entry had no
URL, the request failed (offline or a blocked site) or it returned
nothing, the spinner never went away, and the failed request was an
unhandled rejection.
The screen now tracks the readability request, and once nothing is
pending a missing body shows a message with an Open in Browser button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
follow://add?url=... without a type opened the follow sheet for a feed
with no id, and feed?url=... ignored the URL, so both kept spinning.
Links without an id now look the feed up by URL as the url flow does,
and list links need an id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>