Switching the from currency of the desktop dashboard currency widget quickly
could show the rate of the previous currency: when the older rate request
answered after the newer one, its answer overwrote the newer rates.
Cause: useCurrencyConverter only aborted a request still in flight in its
phone mode (abortStale true); the desktop dashboard passed false and fetched
without any stale guard.
Fix: the hook now always aborts the previous request when a new one starts
and when the widget goes away, and ignores answers of an aborted request. The
abortStale option only existed to keep the desktop without the guard, so it
is removed and both dashboards call useCurrencyConverter().
Red test: FE-COMP-FXHOOK-008 (useCurrencyConverter.test.ts) failed on the
unfixed code with "AssertionError: expected 1.1 to be 1.3".
Changed assertions:
- useCurrencyConverter.test.ts FE-COMP-FXHOOK-002: was "the desktop mode
fetches without a signal, the phone mode with one" expecting the desktop
call to be [url] only; now expects [url, { signal: AbortSignal }].
- FE-COMP-FXHOOK-003 no longer names the phone mode; its assertions are
unchanged. All tests call the hook without the removed option.
With the badge and toast options gone from usePhotoProviderConnections,
the desktop and phone cards called the hook identically and then derived
the same seven per card values (fields, config, values, badge, testing,
whether save and test have a route) line for line, which lint:dup counts
as a copied block of 25 lines in MPhotoProvidersSection.tsx.
The hook now hands each card those values through providerCard, and both
views keep only their markup. Nothing the user sees changes.
New test FE-COMP-PHOTOCONN-011 covers providerCard. No existing assertion
changed.
In the admin's default user settings, the map template dropdown listed
seven tile presets, while every user's own map settings listed nine: the
same seven plus Amap and Amap Satellite. An admin running an instance for
people in mainland China could not pick the one basemap that works well
there as the default, short of pasting its URL by hand.
The Amap presets (57eac32f9) were added to the two user preset copies and
not to the admin's separate copy; nothing in that change or later gives a
reason, and the server stores any template as a default. The later
refactor kept the gap as DEFAULT_MAP_PRESETS beside MAP_PRESETS. There is
now one MAP_PRESETS list, read by the user settings and the admin defaults
on both desktop and phone.
Regression test FE-ADMIN-DUS-029 (DefaultUserSettingsTab.test.tsx) was red
on the old code: "Unable to find role="button" and name "高德地图 (Amap)"".
Changed assertions:
- useMapSettingsForm.test.ts FE-COMP-MAPFORM-001: it expected the admin list
DEFAULT_MAP_PRESETS to hold 7 presets and the user list to add Amap on
top; it now expects the one list to hold 9 presets including both Amap
templates.
On a managed install the phone's Integrations section still showed the
Immich and Synology Photos cards and, with the addon on, the AirTrail
connection form. Each asks for the address of a server the reader runs at
home, which a managed install cannot reach, so saving or testing could only
time out. The desktop has hidden both since the managed mode landed
(#1946, 3a0604b3f), whose message says these "connect to a server the
reader runs at home, and there is no route from here to one".
That change gated the phone's AI section but missed these two, and the
user facing addon list still carries a photo provider that was enabled
before the switch, so the cards did render. The phone now applies the same
!managed gate as the desktop IntegrationsTab.
Regression test FE-MOB-SETINT-008 (MSettingsIntegrations.test.tsx) was red
on the old code: "expected document not to contain element, found <div
data-testid="photo-providers"> instead".
No existing assertion changed.
On the desktop, the Immich (or Synology) card could show "Connected" for a
server that was down. The settings page loads two answers at once: the
saved settings, whose connected flag for Immich only says that a URL and an
API key are stored, and the status route, which actually reaches the
server. Whichever landed last set the badge.
usePhotoProviderConnections had a statusRouteOwnsBadge option that only the
phone turned on. The option is gone: whenever a provider declares a status
route, only that route sets the badge, on both views. A provider without a
status route still takes the badge from its settings read.
Regression test FE-COMP-PHOTOPROVIDERS-028 (PhotoProvidersSection.test.tsx)
was red on the old code: "Unable to find an element with the text: Not
connected" (the late settings read had turned the badge to Connected).
Changed assertions:
- usePhotoProviderConnections.test.ts FE-COMP-PHOTOCONN-005: it expected the
later settings read (connected: false) to override the status route's
connected: true; it now checks that a provider without a status route
takes the badge from the settings read.
- usePhotoProviderConnections.test.ts: render() no longer takes the option,
so every call drops its true or false argument.
On the desktop, a failed photo provider connection test toasted "Could not
connect to Immich : Auth failed" with a stray space before the colon, and
"Could not connect to Immich " with a trailing space when the server gave
no reason. The phone already read "Could not connect to Immich: Auth
failed".
usePhotoProviderConnections took a testErrorGap option that put a space
between the sentence and ": reason", and the desktop passed ' '. The option
is gone, so both views join the reason directly.
Regression test FE-COMP-PHOTOPROVIDERS-027 (PhotoProvidersSection.test.tsx)
was red on the old code: "Unable to find an element with the text: Could
not connect to Immich: Auth failed" (the toast read "Could not connect to
Immich : Auth failed").
Changed assertions:
- usePhotoProviderConnections.test.ts FE-COMP-PHOTOCONN-010: the desktop
toast was 'memories.connectionError(Immich) : bad key', now
'memories.connectionError(Immich): bad key'; it also checks that a
failure without a reason toasts the bare sentence.
When the notification preferences failed to load, the matrix on both the
desktop tab and the phone screen stayed on its loading line for good and
nothing told the user anything went wrong. A failed load of the webhook and
ntfy settings was just as silent and left empty fields that looked like
nothing was saved.
Both loads in useNotificationSettings ended in .catch(() => {}) and had no
cancelled flag. Each failure now shows the common error toast, a failed
preferences load sets loadFailed so both views replace the loading line
with the error text, and both loads ignore a response that arrives after
the screen is gone.
Regression test FE-COMP-NOTIFSETTINGS-010 (useNotificationSettings.test.ts)
was red on the old code: "expected "vi.fn()" to be called 2 times, but got
0 times". FE-COMP-NOTIFSETTINGS-011 covers the cancelled flag and
FE-COMP-NOTIFICATIONS-032 the desktop view.
Changed assertions:
- MSettingsNotifications.test.tsx FE-MOB-SETNOTIF-002: a failed load used
to expect the "Loading..." line to stay; it now expects "Error" and no
loading line.
On the phone, every notification toggle sent the whole preference matrix as
it looked when the switch was tapped. With two toggles in flight the later
request could overwrite the earlier one on the server, and when the first
request failed the screen rolled that switch back while the second request
had already saved its value anyway.
The cause was the sendWholeMatrix option of useNotificationSettings, which
only the phone turned on. The server merges whatever cells it receives, so
the desktop already sent just the toggled cell. The option is gone and both
views send only the cell that changed.
Regression test FE-MOB-SETNOTIF-035 (MSettingsNotifications.test.tsx) was
red on the old code: "expected [ { trip_invite: { inapp: false,
webhook: false } }, { trip_invite: { inapp: false, webhook: true } } ] to
deeply equal [ { trip_invite: { inapp: false } }, { trip_invite:
{ webhook: true } } ]".
Changed assertions:
- useNotificationSettings.test.ts FE-COMP-NOTIFSETTINGS-004: the phone
toggle body was { trip_invite: { inapp: false, email: false } }, now
{ trip_invite: { inapp: false } } (the hook is built with the phone's
remaining option, skipMaskedToken).
- MSettingsNotifications.test.tsx FE-MOB-SETNOTIF-026: the PUT body was
{ trip_invite: { inapp: false, webhook: false } }, now
{ trip_invite: { inapp: false } }.
- MSettingsNotifications.test.tsx FE-MOB-SETNOTIF-032: the mock told the
in-app write apart by body.trip_invite.webhook === false, which only the
whole matrix carried; it now checks for the inapp key.
On the phone storage panel, Test on a mirrored backend sent the mirror
itself to POST /api/admin/storage/test. The server expands a mirror by
looking its primary and replicas up by name in the SAVED storage
config, so the phone tested the saved settings instead of what the
operator had just edited, and a replica added but not saved yet made
the whole test fail with "references unknown backend". The desktop
probes the primary and each replica with their draft options and
merges the results.
Cause: the phone called admin.test with the mirror candidate, while the
desktop called admin.testMirror with mirrorProbeTargets. The choice
lived in each shell's markup.
Fix: useStoragePanel now offers loaded.testRow(row), which tests a
plain row as it is and a mirrored row through admin.testMirror with
mirrorProbeTargets from the draft. Both shells call it.
Red test: FE-MOB-MSTOR-018 (new, MAdminStoragePanel.test.tsx) failed
with "AssertionError: expected [ 'mirror' ] to deeply equal
[ 'backups-local', 'off-box' ]". FE-ADMIN-STOR-PANEL-017 (new,
useStoragePanel.test.ts) covers testRow itself.
No existing assertion changed.
With the appearance set to auto, switching the OS between light and
dark left the TREK wordmark in the phone admin addon header in the old
colour (dark text on a dark page or the reverse) until something else
made the screen render again. The desktop header switches right away.
Cause: the phone worked the flag out from the dark_mode setting and
window.matchMedia during render, and nothing re-renders the screen
when the media query flips. The desktop uses useIsDark, which watches
the .dark class that applyAppearance() writes.
Fix: MAdminAddonManager uses useIsDark like AddonManager.
Red test: FE-MOB-AADD-037 (new, MAdminAddonManager.test.tsx) failed
with "Expected the element to have attribute: src="/text-light.svg""
after the .dark class was added under auto.
Changed assertions in tests/unit/mobile/admin/MAdminAddonManager.test.tsx:
- FE-MOB-AADD-003 (renamed from "dark mode and auto+prefers-dark swap
the wordmark" to "the dark palette swaps the wordmark"): it expected
window.matchMedia to be called with "(prefers-color-scheme: dark)";
that expectation is gone, since the screen now reads the .dark class.
The test now sets that class instead of the dark_mode setting and a
matchMedia stub, and still expects src "/text-light.svg". afterEach
removes the class again.
On the phone admin addon list, the Collab and AI Parsing tiles showed
the generic puzzle piece, while the desktop shows the people and
sparkles icons for them.
Cause: the addons table stores 'Users' for collab and 'Sparkles' for
llm_parsing, and the phone's ICON_MAP had no entry for either, so both
fell back to Puzzle. The desktop map gained them earlier; the phone map
was never updated.
Fix: the phone ICON_MAP now carries Users and Sparkles, like the
desktop one.
Red test: FE-MOB-AADD-036 (new, MAdminAddonManager.test.tsx) failed
with "expect(received).toBeInTheDocument(): received value must be an
HTMLElement or an SVGElement" for svg.lucide-users.
No existing assertion changed.
On the phone plugins panel, "Update all" fired every pending update at
the same time. Only one row can show as busy, so the other rows stayed
clickable while their own install was running, and the list reloaded
once per plugin as each update finished. The desktop runs the updates
one after another, and the hook's own comment calls the parallel way
wrong.
Cause: usePluginsAdmin kept the phone's parallel loop behind the
updateAllInParallel option, which only the phone set.
Fix: the option is gone; updateAll always waits for each update before
starting the next, on both shells.
Red test: FE-ADMIN-PLUGINS-HOOK-009 (usePluginsAdmin.test.ts) failed
with "AssertionError: expected [ [ 'a', undefined ], …(1) ] to deeply
equal [ [ 'a', undefined ] ]".
Changed assertions in src/components/Admin/plugins/usePluginsAdmin.test.ts:
- FE-ADMIN-PLUGINS-HOOK-009 (renamed from "with updateAllInParallel
every update starts at once, without a version" to "Update all starts
only the first update, without a version"): pluginUpdate calls were
[['a', undefined], ['b', undefined]], now [['a', undefined]]. It and
the mounted() helper no longer pass the removed option.
- FE-ADMIN-PLUGINS-HOOK-008: only its name lost "by default".
On the phone, the admin GitHub panel showed "Latest updates from
mauriceboe/TREK" and its releases, bug report, feature request and wiki
links, as well as the same three links in the settings About section,
all went to the old mauriceboe/TREK repository. The desktop panels
already use liketrek/TREK.
Cause: the phone screens kept the repository name from before the move
to the liketrek organisation.
Fix: MAdminGitHubPanel and MSettingsAbout now use liketrek/TREK, like
GitHubPanel and AboutTab. No other user facing mauriceboe/TREK link
is left in the client (the Ko-fi, Buy Me a Coffee and Docker image
names are unchanged and identical on both sides).
Red test: FE-MOB-SET-ABOUT-001 (new, MSettingsAbout.test.tsx) failed
with "Expected the element to have attribute: href=
https://github.com/liketrek/TREK/issues/new?template=bug_report.yml".
Changed assertions in tests/unit/mobile/admin/MAdminGitHubPanel.test.tsx:
- FE-MOB-AGH-001: wiki href was https://github.com/mauriceboe/TREK/wiki,
now https://github.com/liketrek/TREK/wiki
- FE-MOB-AGH-004: subtitle was "Latest updates from mauriceboe/TREK",
now "Latest updates from liketrek/TREK"; GitHub link was
https://github.com/mauriceboe/TREK/releases, now
https://github.com/liketrek/TREK/releases
On the phone, attaching a file to a note, detaching one or deleting a
note with attachments left the trip's Files tab on the old list until a
reload. The desktop panel fires the local collab-files-changed event
that useTripWebSocket listens for, because the server leaves the
writer's own socket out of its broadcast; the phone notes tab never
fired it.
The event now lives in the shared useCollabNotesData hook:
uploadNoteFiles announces it once after a batch, and
announceNoteFilesChanged is what both shells call after deleting a note
or a note file. The desktop no longer fires it by hand after an upload.
Red before the fix: tests/unit/mobile/trip/MCollabNotes.test.tsx
FE-MOB-CNOTE-034 failed with "expected 0 to be 1" on the event count
after detaching a file. FE-COLLAB-NOTESDATA-009 covers the hook.
No existing assertions changed.
poolCounts in the phone places browser model has had no caller since the
phone pool chips stopped showing counts in 8855b5581 (the FilterChip note in
MPlacesBrowser says counts are left out on the phone to save row space). Only
its own tests still ran it, and it had drifted from the desktop counts in
usePlacesSidebar, which count planned places against the day scoped planned
ids. Nothing on the phone should use it, so it goes, with its two tests.
No user facing change, so there is no red test: the helper is dead code.
Removed tests in src/mobile/screens/trip/places/placesBrowserModel.test.ts:
MOBILE-POOL-counts (expected all 3, planned 2, unplanned 1) and
FE-MOB-PMODEL-013 (expected all 2, tracks 1, planned 1, unplanned 1).
On the phone place form, pasting a "lat, lng" pair into the latitude field
filled both coordinates, but a later search pick whose place came without a
position emptied them again whenever an earlier pick had filled them. On the
desktop the pasted position stays.
Cause: the phone's paste handler wrote lat and lng with setForm directly, so
autoFilledRef still listed them as written by the previous pick, and
mergeResult clears such fields when the new pick says nothing about them. The
desktop goes through handleChange, which marks a field as typed by hand.
Fix: the phone's paste handler goes through handleChange for both halves, the
same as the desktop dialog.
Red test: tests/unit/mobile/trip/MPlaceEditSheet.test.tsx FE-MOB-PLEDIT-047
failed on the unfixed code with "expect(element).toHaveValue(35.7101)
Expected the element to have value: 35.7101 Received: (empty)".
No existing assertions changed.
On the phone place form, tapping a search suggestion emptied the address,
the coordinates, the map ids and the website that the previous pick had filled,
before the suggestion's place was even looked up. When that lookup failed, the
form was left with the new name and no position at all. The desktop form only
puts the name in and leaves the rest alone.
Cause: PlPlaceSearch handed the suggestion's name to the sheet as a search
pick (onPick({ name })), and the sheet folds every pick through mergeResult,
which clears each field the previous pick wrote when the new pick says nothing
about it.
Fix: PlPlaceSearch takes an onSuggestionName callback, the same one the
shared usePlaceSearch hook already offers, and MPlaceEditSheet only sets the
name with it, as the desktop dialog does. The resolved place still arrives
as a normal pick.
Red test: tests/unit/mobile/trip/MPlaceEditSheet.test.tsx FE-MOB-PLEDIT-046
failed on the unfixed code with "expect(element).toHaveValue(Taito)
Expected the element to have value: Taito Received: (empty)".
Changed assertions in tests/unit/mobile/trip/PlPlaceSearch.test.tsx:
FE-MOB-PLSRCH-004 expected onPick to be called first with { name: 'Louvre' }
and twice in all; it now expects onSuggestionName to be called with 'Louvre'
and onPick once. FE-MOB-PLSRCH-005, FE-MOB-PLSRCH-005b and FE-MOB-PLSRCH-024
waited for onPick to be called twice; they now wait for one call.
The transport form stays mounted between openings. Editing a flight or a
train after a car kept the car's stops (desktop and phone) and, on the
desktop, the from and to picks of that earlier booking as well. Switching
the type to car in the same dialog showed those stale stops and places,
and saving wrote them onto the booking. The desktop also kept stale
flight and train rows when opening a car, bus or other booking.
seedTransportRoute only reset the rows a type does not show when the
phone asked for it (resetHiddenRoutes), and never reset the car stops for
a flight or a train. It now returns every row for every type, so a seed
always replaces the whole route. The option is removed; both views get
the same clean route.
Red test: FE-PLANNER-TRANSPORTFORM-008 in
client/src/components/Planner/useTransportForm.test.ts failed with
"expected [ { location: { ... }, time: '' } ] to deeply equal []".
Changed assertions (they asserted the stale rows were kept):
- useTransportForm.test.ts FE-PLANNER-TRANSPORTFORM-005: desktop
fromPick toBe(picked) is now toEqual({}) (title updated), and the
resetHiddenRoutes field is dropped from seedOptions and the phone call.
- transportEndpoints.test.ts FE-PLANNER-TRANSPORTEP-005: desktop route
keys toEqual(['waypoints']) is now empty fromPick, toPick, carStops and
trainWaypoints.
- transportEndpoints.test.ts FE-PLANNER-TRANSPORTEP-010: desktop car
waypoints toBeUndefined() is now two empty rows (plus empty train rows),
and the bus route now also carries the two empty row sets.
- transportEndpoints.test.ts: resetHiddenRoutes dropped from DESKTOP and
PHONE.
Editing a transport without metadata of its own (bus, car, taxi, ferry
without a transit itinerary) in the phone sheet wiped the expense price
shown on the booking card. The phone built the payload with
emptyMetadataAsNull and sent metadata: null, which the reservations
service reads as "clear the column", so keepMirroredPrice had nothing to
carry price and priceCurrency into. The desktop and both booking forms
send an empty object, which keeps the price.
The option only existed to keep the phone's null, so it is gone and both
views now send the rebuilt metadata object as is.
Red test: FE-MOB-TRFRM-060 in
client/tests/unit/mobile/trip/MTransportFormSheet.test.tsx failed with
"AssertionError: expected null to deeply equal {}".
RESV-SVC-043 in server/tests/unit/nest/reservations.service.test.ts shows
the server side: {} keeps the stored price, null drops it.
Changed assertions (they asserted the null):
- MTransportFormSheet.test.tsx FE-MOB-TRFRM-006: payload metadata null
is now {}.
- MTransportFormSheet.test.tsx FE-MOB-TRFRM-011: metadata toBeNull is now
toEqual({}).
- MTransportFormSheet.test.tsx FE-MOB-TRFRM-027: metadata toBeNull is now
toEqual({}).
- transportEndpoints.test.ts FE-PLANNER-TRANSPORTEP-014: phone metadata
toBeNull is now toEqual({}) (title updated to match), and the
emptyMetadataAsNull field is dropped from DESKTOP_SAVE and PHONE_SAVE.
- useTransportForm.test.ts FE-PLANNER-TRANSPORTFORM-007: metadata: null
is now metadata: {}.
The invite and the share sheet each carried the same unfolded user list. Both now render MUserPickerList with the users, the picked id and the pick handler from useVacayUserPicker. The markup is unchanged.
The phone storage panel rendered the same test result markup for regular and degenerate mirror backends. Both rows now render MStorageTestResult, with the same elements, classes and text as before.
The desktop and the phone addon manager each carried their own copy of the Immich and Synology SVGs. Both now import them from components/Admin/addons/PhotoProviderIcons, which has its own test. The rendered marks are unchanged.
The transports and bookings tabs drew the same title button, edit and
delete buttons, date and time row with the booking code, file chips and
delete confirmation sheet as two copies. Both cards now render them from
MReservationCardParts, and each tab still decides what a tap opens. The
rendered markup stays the same for both tabs.
The phone booking and transport sheets each carried their own copy of the
booking code and status row, the notes field, the traveler picker with its
avatar, and the transport sheet drew the card of a route stop twice, once
for flights and once for trains and cruises. They now render the same
components from MBookingFields next to the sheets, which take the sheet's
own translate function so every string stays what it was. The rendered
markup is unchanged.
ReservationPluginSlots in tabChrome repeated the plugin store selector
that useReservationDetailPlugins already holds for the desktop panels.
It now calls the hook, which gets its own test.
useMRoadtrip and useMRtCorridor each kept a nowMinutes helper and their
own check whether the selected day is today. Both now use localMinutes
and isLocalToday from utils/localDate.ts, which hold the same rules.
Whether an invite is expired, used up or active, and which status label it shows, was worked out in both user views. It now lives in inviteStatus in the admin model; each view keeps its own row markup.
The All, Unplanned and Planned filter of the tours list was copied into the phone selection list. It now lives in filterTours next to the other tour presentation helpers, and both lists call it.
Both splashes carried their own copy of the four beats, the step time, the still beat and the interval that cycles them. These now live in useTripSplashBeats; each splash keeps its markup and styling. The phone follows the reduced motion setting live while the splash is up, the desktop reads it on each render; that difference stays as the liveReducedMotion option.
The phone trip sheet host wrote out by hand three pieces of planner
wiring the desktop planner already had.
useBookingExpenseEditor now holds the expense editor both planners open
over the trip: the booking request state, the receipt fallback, the base
currency, the current user and the save that reloads the budget.
TripPlannerPage and MTripSheets both read it.
applyTripCoverUpdate patches a new trip cover into the store for the
trip form on both sides.
The transport editor and the transit route change moved into
openTransportEditorWith and changeTransitRouteWith, which take the
planner's setters. usePlannerDialogs builds openTransportEditor and
changeTransitRoute on them, and the phone sheet calls the same helpers
on the planner it receives. There are no differences between the two
views to keep as options.
fileListRules now holds the type category of a file, the filter tab match
and the list of places and bookings a file is linked to (own column plus
extra links, transport or booking). useFileManager and FileManagerRow on
desktop and filesModel on the phone run on it. fileActions gains the field
save with its reload and assign error, which the desktop assign dialog and
the phone note edit both use, and the trash call with its toasts, used by
the desktop delete and the phone menu sheet. The phone sheet no longer
imports filesApi.
Differences that stay, each as an explicit option:
PHONE_FILE_FILTER_RULES sorts pdf, image and doc by type category (videos
count as images, doc is every file that is no PDF and no media, a pass
with a PDF mime type is a doc) and counts a note id of 0 as collab, while
desktop reads the mime type (images only, doc is Word, Excel and text).
DESKTOP_FILE_LINK_LIST_RULES skips an own place or booking column of 0,
the phone only a null one. PHONE_FILE_UPDATE_RULES waits for the reload
after a save, so a failed reload shows the assign error there; desktop
starts it and moves on. DESKTOP_FILE_TRASH_TOAST_RULES falls back to
English when the trash toast translation is empty.
The holiday countries, the calendars split by type, the weekend day toggle, dissolving a fusion, the leave year save and window label, the region options and the add check now live in useVacaySettings. The phone sheet still loads countries only while open.
The read only share reuses useVacayUserPicker for picking and sending, and useVacayShareActions holds the hide toggle and the removal with their error toasts for the sidebar card, the phone sheet and the phone chips.
Loading the users that can be invited, the picked user and sending the invite with its toasts now live in useVacayUserPicker. The dialog still resets and closes on success, the sheet still closes and resets on the next open and empties the list when loading fails.
The log mode and its half and comp modifiers, the entry, shared and company holiday maps, the trip dots, the day click rules, the default person and the day count format lived in both the desktop calendar and the phone screen. They now live in useVacayCalendarLogic and vacayFormat; the phone keeps reloading trip dots only on a new window shape.
The currency converter, the world clocks, the collection lists, the plugin widgets and card badges and the card date format each existed once per dashboard. They now live in useCurrencyConverter, useDashboardTimezones, useCollectionLists, useDashboardPlugins and dashboardModel; the phone keeps aborting a pending rate request through an option.
The dialog and the phone sheet each carried the cover upload, the Unsplash search, the staged cover for a new trip and the title and range checks. They now live in useTripCoverPicker and tripFormModel; the blob url release keeps each view's timing behind an option.
The centre button resolved its target in two copies of the same hook. useCreateAction now holds it once, with the phone only branches (gallery upload, atlas search, collections) behind a flag.
Both presenters ran their own queue behind the window toast bus. The queue, the auto dismiss and the exit delay now live in useToastQueue; each presenter keeps its markup, its exit timing and its unmount handover.
useJourneyCarouselSync keeps the map on the card nearest the centre once
scrolling settles and brings a card to the centre for a marker, a day or a
tap, for both the phone journey screen and the narrow timeline of the desktop
and public pages. useGalleryUpload normalises and uploads gallery files for the
desktop gallery and the phone screen. The phone still moves the map on a tap
of another card, the desktop gallery still counts the files up while they go.
useJourneyEntryForm holds the fields, the photo queue that only goes out on
save, the gallery links, the location search and "use my location", the
verdict rows, quick capture and the save for both the editor dialog and the
entry sheet. journeyWeatherCategory moves to components/Journey/journeyWeather
so the dialog no longer reaches into mobile/. Where the two differed they still
do: the dialog starts with a blank verdict row, counts a moved point as a
change, toasts a failed save or location and fills the weather from the
forecast; the sheet edits tags and shows a refused location under the field.
useJourneySettings holds the name, cover, archive, the switches saved on the
spot, delete, unlinking a trip and removing a contributor for both the settings
dialog and the settings sheet. useJourneyTripLinking loads the linkable trips
and links one for the add trip dialog and the sheet, and useJourneyShareLink
manages the public share link for the share section and the sheet. The dialog
still closes after a new cover or an unlinked trip, the sheet still stays open.
useAtlasCountryActions marks a country or a region, unmarks a region, adds the
country or region to the bucket list and takes the country off it again for
both the desktop dialog and the phone sheet. The desktop dialog moves into its
own component in AtlasPage so the page body keeps calling useAtlas only. The
phone sheet keeps storing a marked region as visited and its own month input;
the desktop keeps its month and year picker in useAtlas.
useCollectionPlaceForm reseeds the edit form when a different place opens,
fetches a cover photo for a place without one, uploads and removes the custom
cover and saves the edits for both the detail sheet and the phone sheet. The
detail keeps editing the coordinates and keeps a late photo across an update
of the same place; the sheet holds the last place through its exit and drops
a late photo once the place object changes, as before.
useListEditor seeds the form from the target, handles the cover upload and the
Unsplash search, cleans the links and creates or updates the list for both the
dialog and the sheet. The sheet keeps holding the last list through its exit
and still hands over a list whose cover upload failed when it closes.
useTripCopyPicker loads the trips on every opening, filters them by the search,
formats the date range and turns the copy result into toasts for both the
dialog and the sheet. The dialog keeps its guard against an empty selection.
useCollectionSharing holds the invitable users lookup, the roster order, invite,
role, cancel, remove and leave for both the share dialog and the share sheet.
The dialog keeps one busy member per kind of row action, the sheet one for all.
Answering a question once, following a link after marking it read and the compact timestamp live in useNotificationItemActions. The desktop item keeps closing its panel after a navigation through the optional onClose.
The live preview, the debounced save with its flush on leave, the colour mode save and the widget toggles live in one useAppearanceEditor hook. The contrast helpers and widget groups move to appearanceModel.