The "add to trip" dropdown in the admin create-invite dialog was a native
<select>; swap it for the shared CustomSelect so it matches the rest of the UI
(searchable once there are many trips).
Adds two filter dropdowns next to the Search Expenses field (height-matched
to it): one filters by expense category, the other narrows to a single day.
Selecting a day shows a prominent summary banner with that day's total, and
hides the now-redundant per-day header. Both filters work on the desktop and
mobile layouts and compose with the existing search + all/mine/owed filters.
New i18n keys (costs.filter.allCategories / allDays, costs.expensesCount)
across every locale.
The page container held useState/useEffect directly, tripping the CI
page-pattern check. Move the token preview + accept logic into a co-located
useJoinTrip() hook; the page is now a thin presentational shell.
Trip invite links (#1143): each trip can have one rotating invite link in its
Share panel. An existing, logged-in user who opens /join/<token> is added to
the trip as a member; an anonymous visitor is sent to the login page and
returned to the invite afterwards — there is no registration from this link.
Reading, rotating or disabling the link all require the share_manage permission.
Admin invite trip binding (#1402): the admin create-invite dialog can now bind
a registration invite to a trip. Someone who registers via that link is
auto-added to the trip as a member (password and OIDC paths), inside the same
atomic step that consumes the invite.
Adds a trip_invite_tokens table and a nullable invite_tokens.trip_id, a shared
owner-safe/idempotent add-by-id helper, the manage + join endpoints, the
JoinTripPage and Share-panel section, the admin trip picker, and the new i18n
keys across every locale. Wiki updated.
Discussion #1143
When a user signs in via OIDC and hasn't uploaded a custom avatar, their
`picture` claim is now used as their avatar. The users.avatar column holds
either an uploaded file name or an absolute https URL from the claim, and a
single resolver on each side (server avatarUrl, client avatarSrc) renders
both. An uploaded avatar always wins and is never overwritten; the picture
refreshes on each login otherwise. Only https URLs are stored, matching the
image CSP.
All the scattered /uploads/avatars/ builders now go through the resolvers,
which also fixes collection member avatars that were rendering a bare file
name.
Discussion #1399
Seeing the driving/walking distances between a day's places on mobile
meant tapping the day (which closes the plan sheet), reopening it, then
tapping Route. Now the per-day Route button in the mobile footer toggles
that day's leg distances in place, so the sheet stays open and you get the
distances between places without selecting the day first.
The leg computation runs for every route-toggled day instead of only the
selected one, and the leg/hotel-bookend maps are nested per day so several
toggled days can't overwrite each other's segments. Desktop is unchanged.
Discussion #1374
Next to the existing "Open in Google Maps" action, add an OpenStreetMap button that
opens the place on openstreetmap.org (a marker at its coordinates, or a name search
when it has none) — the same map source TREK already renders, and a jumping-off point
for OSM-based apps like OrganicMaps / CoMaps. Requested in discussion #880.
Strings across all 22 locales; a unit test for the URL builder.
- #1394 planner: two transports on one day no longer draw a phantom airport→airport
road route between them (a run is only a drive when it holds a real place); mirrored
in the map hook and the sidebar's leg list, with a regression test.
- #1392 planner: the per-day Route button now points the selection at the tapped day
before toggling, so on mobile it computes that day's route instead of the previously
selected one, and only the selected day's button reads as active.
- #1372 planner: the "open in Google Maps" route now includes the day's hotel bookends,
matching the drawn map route.
- #1375 planner: a multi-day accommodation no longer thrashes the plan scroll — the
auto-scroll lock keys on the selection identity, not the per-day row.
- #1377 planner: the reset-orientation compass is now shown on small screens too.
- #1382 budget: settlement nets in the trip's canonical currency and converts to the
display currency once, so balances no longer drift with live FX and no phantom
third-party micro-flows appear (identity, hence unchanged, when they're the same).
- #1366 atlas: countries reached only by a transport booking (no lodging/place) now
count as visited, on both the dashboard and the Atlas page.
- #1383 bookings: a hotel linked to an accommodation shows only its day-range, not a
duplicate stamped date row, and the range stays correct after an edit.
- #1353 bookings: any non-hotel reservation can now link an existing trip place/activity.
- #1390 i18n: fix the Polish word for "buddies" (Towarzysze → Współpodróżnicy).
- #1265 planner: drag-and-drop of places now works on touch devices via a polyfill.
Port the three main-only fixes onto dev's (post-rewrite) architecture:
- fix(backups): prevent recursion when the backup path sits inside the backed-up dir
- fix(share): convert budget items to the viewer's base currency instead of a flat EUR
- fix(files): surface the descriptive server error for unsupported upload types (#1363)
Cherry-picked from 819aa793 on main; the SharedTripPage and useTripPlanner
conflicts were resolved to keep dev's font-scaling and full import set while
taking the fixes' currency conversion and translateApiError wiring.
- Replace fixed-order date parser with locale-aware implementation
using Intl.DateTimeFormat.formatToParts to detect field order;
adds swap fallback for unambiguous inputs (day > 12) to handle
locale mismatches gracefully
- Pre-fill keyboard input with locale-formatted numeric date
(e.g. 14.06.2026) instead of raw ISO value
- Replace all hardcoded English aria-labels and titles with t()
calls; add new keys under common.datepicker.* namespace across
all locale files
- Update FE-COMP-DATEPICKER-013 to use unambiguous day value (> 12)
to avoid locale-dependent test failures
- Add three-level calendar view (days → months → years) via clickable
header label, allowing fast navigation to distant dates without
repeated arrow clicks
- Replace double-click text input affordance with a visible keyboard
icon button; compact/borderless variants show the icon in the
calendar footer
- Pre-fill text input with locale-aware numeric date (DD.MM.YYYY)
when a value is already selected
- Add aria-label and aria-pressed to all interactive calendar elements
for screen reader support
- Update existing tests to reflect new two-button trigger layout
- Add FE-COMP-DATEPICKER-018 through 027 covering drill-down
view transitions, prev/next behaviour per view, aria-pressed
state, and keyboard icon trigger
- Add client tests for the new collections UI (80 tests): collectionsModel (incl.
the undefined-entry guards that fix the white-screen regression), StatusBadge,
CollectionFilterBar, CollectionList, CollectionPlaceDetail (permission gating),
MoveToListModal.
- Offer the select toggle in "All saved" too (server enforces per-place rights).
- Drop the now-duplicate select button from the map controls (it lives in the
filter row).
Swap the two native <select> role pickers in the share modal (invite + per-member)
for the app's CustomSelect (portal dropdown, size sm) so they match the rest of
the UI instead of the browser's native control.
Add a "Save to collection" action to the trip place list's selection bar (next to
bulk category + delete): it opens a list picker and copies every selected place
into the chosen list in one request, instead of one-by-one from each place.
- Server: saveFromTripPlaces (one access check + one WS notify), POST
places/from-trip-many; dedups by name/coords, skips missing ids, honours force.
- Client: saveFromTripMany api + SaveTripPlacesToListModal; the selection-bar
button is gated on the collections addon being enabled.
- Copy count / skipped-duplicates toast; strings in all 22 locales.
- Service + controller tests for the bulk path.
The owner now assigns each member a role — viewer (read + copy-to-trip only),
editor (default: add + edit places) or admin (full incl. delete). The owner is
always full. Existing members default to editor via migration 152, so nothing
regresses.
- Server: role column on collection_members (migration 152 + schema); roleOf +
assertCanEdit (save/update/status/list-meta) + assertCanDelete (delete) layered
on assertAccess; sendInvite takes a role; new setMemberRole (owner-only) +
POST members/role; members payload carries each role.
- Client: Share modal gains a role picker on invite and a per-member role select
for the owner (read-only role badge for others); the page hides add / edit /
status / move / delete for roles that can't perform them (server still enforces).
- Roles in all 22 locales; service + controller tests for the new gating.
From a mobile UX audit of the collections page:
- Detail sheet: the read-mode footer no longer clips "Remove from list" (it wraps,
drops the growing spacer) and clears the home indicator (safe-area padding,
84dvh instead of 84vh).
- Bigger touch targets on phones (≥40px): view toggle, filter dropdowns, select-bar
buttons, detail close/actions, drawer rail rows, and the interactive status badge
(enlarged tap area via a pseudo-element, look unchanged).
- Select action bar breaks its bulk actions onto their own line instead of
stranding them behind a growing spacer.
- Lists drawer honours device safe-areas and gets an explicit close button.
- Page honours the top safe-area and goes full-width on phones (drop the 95vw cap);
filter popovers cap their width so long category names don't overflow.
- Add-place: Cancel/Add pinned in the modal footer (reachable without scrolling),
status pills wrap.
- Drop dead hero mobile CSS left over from the hero refactor.
- Trip "Saved places" picker: drop the fixed 360px cap so the list fills the
panel instead of stopping half-way, and add list + status filter dropdowns
(filter by which collection the place is saved in).
- "All saved" showed nothing on first open: setActive(ALL_SAVED) unioned the
lists from the store, but on first load those aren't fetched yet (loadAll still
running). Load them first when empty so the union isn't blank.
The .trek-dash scope (dashboard, collections, vacay, atlas) hardcoded an orange
accent, ignoring the appearance theme. Drop the override so --accent inherits the
theme tokens (index.css): monochrome black/white by default, coloured per
data-scheme / custom accent. --accent-ink/-soft now map onto --accent-on/-subtle,
and accent-filled elements use --accent-text for legible text on any scheme.
Category colours are set explicitly per element and stay untouched.
- The select toggle now sits at the right of the filter row (same height as the
status/category dropdowns) instead of the top toolbar.
- Select mode gains a "select all / deselect all" toggle and shows even with
nothing selected yet.
- Selected places can be moved or duplicated into another of your lists via a
target-list picker (move re-points collection_id; duplicate re-saves the place
data, carrying description / category / notes / etc.).
- New strings across all 22 locales.
updatePlace wrote `res.place` into the places list, but the endpoint returns the
updated place directly (not wrapped in { place }, unlike savePlace) — so an
`undefined` slipped into the list and the category-filter's presentCategories()
crashed on `undefined.category_id`, blanking the whole page. The WS echo used to
mask it by refetching; excluding the editor's own socket exposed it.
- Read the updated place directly and guard against a falsy response.
- Fix the api return types to match (updatePlace/setStatus return the place).
- Harden filterPlaces / statusCounts / presentCategories / mappablePlaces against
a stray undefined entry so a single bad row can never white-screen the page.
The little map pin in front of a place's address sits in a flex row with the
address text but had no flex-shrink guard, so a long address squeezed the icon
smaller. Pin the SVG to its size.
- Copy-to-trip modal showed blank rows: trips are keyed by `title`, not `name`,
so nothing rendered. Read `title` and add the trip's date range under it.
- List editor gains an Unsplash cover search (same source as trip creation) next
to the upload button; picking a photo sets it as the list cover.
- Add-place result rows: pin keeps a hard min width so a long address can't
squeeze it.
- Editing a place (category, status, …) no longer reloads the view: the mutating
client's own socket is now excluded from the WS broadcast (x-socket-id threaded
through save/update/status/delete + list update/cover), so the optimistic update
stands on its own instead of being chased by an echoed refetch.
- Detail sheet pulls a higher-res cover photo from the maps provider when the
place has no image of its own (the avatar thumbnail was too low-res).
- Hero: Share moved onto the title row (no more empty top band) with an Edit
button beside it; editing/deleting a list now happens there. The list rail drops
its per-row kebab entirely (and with it the janky open animation).
- The list editor can delete the collection from its footer (owner only).
- Wider, screen-relative page (max-width min(2100px, 95vw)).
- List rows: the place avatar no longer shrinks when the address is long.
- Map: markers no longer rebuild on every unrelated re-render (memoised the
mappable list + only update the hero size when it really changes), the floating
controls bar is click-through except its buttons, and the collection map runs
with the hover tooltip off. Together these stop a marker click from landing on
a mid-rebuild element / the tooltip so the pick actually registers.
- Filters moved out of the hero into a compact status + category dropdown row
above the places (custom dropdowns); the hero no longer carries the stat chips.
- Add-place is a single popup now: search fills the location, and name / status /
category / description / links are all editable together before saving.
- Category shown as a badge top-left on the detail cover and next to the status
in each list row (divided by a hairline).
- Slimmer hero: shorter, tighter spacing, links tucked into the eyebrow row
instead of their own line.
- Rebuild the place detail as a clean, opaque, sectioned sheet (cover → meta →
status segment → description → links) with a proper footer action bar — the
loose "white lower half" is gone.
- Assign a place to a central (admin-defined) category, both in the detail edit
and when adding a place; categories are fetched once for the page.
- Add-place now sets category + description (markdown) + links + status in the
same step, closer to the trip's place form.
- Switching to the full-map view now closes the (list-docked) detail.
- Fix the selected-row highlight: it was clipped by the column's overflow — use
an inset ring and only clip during the map-collapse animation; a picked row
now scrolls into view above the detail sheet.
- New category strings across all 22 locales.
- Editing a place (status, description, title, …) no longer reloads the view or
closes the detail: the WS echo now refreshes via loadCollection, which keeps
the current selection + select-mode instead of setActive resetting them.
- Place detail: docks over the list column on the desktop split (measured rect)
instead of centred over the map, and the card is now opaque (was too see-through).
- Map: click a marker in full-map view to drop back to the split; picking a place
scrolls its list row into view; the select toggle is disabled in full-map view;
the floating controls are one non-overlapping top bar and less transparent.
- Hero: drop the New-list button (it's already in the rail).
- Rail: the kebab is always visible (easy to hit); menu is Edit + Delete only
(colour moved into the editor); "Rename" → "Edit".
- Add-place: pick a result, then set description (markdown) / links / status
before saving, all in one step.
- Share modal: member roster as cards with clearer role badges + a count.
- Block the list-cover upload in demo mode (mirror the trips cover endpoint).
- Restrict list/place links to http(s) (schema) and normalise scheme-less URLs
to https:// on save, so a bare "booking.com" no longer resolves as a relative
SPA route (and javascript:/data: hrefs are rejected).
- Place detail: surface save errors with a toast instead of silently swallowing
a 400 and leaving the sheet stuck in edit mode.
- List editor: don't create a duplicate list when a retry follows a cover-upload
failure (reuse the created id); revoke the cover preview object URL.
- Map controls: one top bar (left toggle/select, right add/search) so they can't
overlap on a narrow split map — the search shrinks instead.
- Dashboard list badge: full-opacity colour wash so the name stays legible over
bright covers.
Dashboard widget (B): the collections tool now shows the user's LISTS as
compact colour-washed badges (cover image tinted with the list colour, or a
gradient) that jump to the list — one list() call, no N+1.
List details (C): lists gain a description, a custom cover image (uploaded to
/uploads/covers, tinted with the list colour in the hero) and links. A shared
ListEditorModal handles both create and edit; the hero shows the description +
link chips. New `links` JSON column on collections + collection_places
(migration 151) with parse/serialize in the service; a POST :id/cover upload
endpoint mirroring trips; cover-file cleanup path-confined locally.
Place detail (D): clicking a place opens a bottom sheet (no backdrop, so the
map stays visible) — status cycle, copy-to-trip, remove, and an edit mode with
a markdown description + links editor (collectionsApi.updatePlace, now wired
via a store action). A "+" next to the search adds a place to the list via the
maps search.
Fusion + fixes (E): the owner can now remove an accepted member (kick) — new
removeMember service/route/store + a button in ShareCollectionModal, with a
collections:removed WS bounce. findMembership no longer matches on name alone
(coordinate proximity required, killing "Starbucks everywhere" false positives).
loadCollection swallows a 403/404 after a leave/remove so the URL sync can't
throw uncaught. Grid remnants gone; the map select toggle moved onto the map.
New strings across all 22 locales; i18n parity strict passes.
- Drop the grid/tile view. The list view is now the default and, on wide
screens, a list + persistent map split; a top-left control on the map
collapses the list to a full-width map (and back), animating smoothly (the
map stays mounted and is nudged to re-layout during the transition). The
place search moves onto the map (top-right); mobile keeps a list/map toggle.
- Let a place be deselected again: clicking it once more, clicking the map
background, or picking another all toggle the selection (collections map now
wires onMapClick).
- Fix the stuck hover tooltip: selecting a place swaps its marker's DOM node so
the browser never fires mouseout/mouseleave, orphaning the fixed-position
tooltip (it hung on screen and drifted with scroll). Both map stacks now clear
the hover on selection change and on scroll.
- Remove CollectionGrid + PlaceCover; add hero eyebrow + map control strings.
- List view splits into a scrollable list + a sticky map on wide screens;
clicking a place pans/highlights it on the map (single selectedPlaceId, no
inspector over the map). Narrow screens keep the single-column list.
- Keep the list rail at least as tall as the hero (measure the hero via a
small useElementSize hook and feed its height as the rail's min-height).
- List row kebab menu: portal the menu/colour popover to the body so the
rail's overflow + backdrop-filter can't clip it ("renders only in the
module"), and fade the place count on hover so the kebab stops overlapping it.
Rebuilds the /collections page from the functional placeholder into the
dashboard's glass visual language (light + dark):
- A colour-washed hero per list: eyebrow + member avatars, big title, and
stat chips (All / Idea / Want / Visited) that double as the status filter.
- A sticky glass list rail (owned + shared + invites) with a mobile drawer.
- Gradient/photo cover place cards modelled on the trip cards, via a new
rectangular PlaceCover (photoService-backed, gradient fallback) + a shared
gradients util. List and map views restyled to match.
- Status pill rendered as a role=button span so it survives the .trek-dash
button reset and can nest inside the card; the share member-count badge is
now owner-only.
- New hero eyebrow strings across all locales.
- ShareCollectionModal: the owner manages a list's members and invites users
(available-users picker → invite, cancel pending); a member can leave a shared
list. The incoming accept/decline surface stays in the lists rail. A Share
button is added to the collections header for owners (and members, to reach
Leave).
- CollectionsWidget: a dashboard glass card after the currency widget showing the
saved count and the most recent saved places, double-gated by the admin addon
and a new per-user appearance flag.
- Appearance: a 'collections' dashboard-widget flag (desktop + mobile defaults)
wired into the appearance settings, surviving normalize.
- Sharing + settings strings across all 22 locales (parity strict passes).
Adds the client side of the Collections addon:
- A distinct /collections page (Atlas pattern, page/hook split) gated behind the
addon: a multi-list rail, a Grid (default) / List / Map view switch, the
idea/want/visited status with a one-tap badge, search and status filters, and
considered empty states. Store + hook + model + websocket wiring; the place
detail reuses the trip place inspector via a mode guard.
- Entry points: a "Save to Collection" button next to Open-in-Google-Maps in the
place inspector (and the two sidebar context menus), a "Copy to trip" modal,
and a desktop-only two-column add-place picker (mobile keeps the single-column
form).
- The collection namespace and the new keys across all 22 locales.
Adds the Collections addon backend: a server-wide-per-user library of saved
places, independent of any trip, with multiple named lists, an idea/want/visited
status, and Vacay-style fusion invitations to share a list with other users.
- Data: collection / collection_members / collection_places / collection_place_tags
tables (+ migration and baseline schema). Saved places carry the owner plus a
nullable saved_by so a member deleting their account can't drop shared content.
- Service: list + place CRUD with owner-or-accepted-member visibility, dedup,
status, save-from-trip and copy-to-trip (reusing the trip copy column list),
and the full fusion-invitation state machine mirrored from vacay (send / accept
/ decline / cancel / leave) with a websocket broadcast and an invite
notification. Deleting a list snapshots its members and notifies them.
- NestJS module + addon guard (404 before auth), registered in the app module.
- Widens the place photo cache reference check to count collection places so the
nightly sweep no longer evicts photos a saved place still uses.
- collection_invite notification wired across all 22 locales.
The MapLibre/Mapbox hover showed an anchored popup with a large photo
thumbnail, completely unlike the Leaflet map's slim, cursor-following
name/category/address card. Drop the anchored photo popup for places and
render the same cursor-following overlay the Leaflet map uses (no photo,
matching fonts/padding/shadow), so the two maps hover identically.
MapLibre/Mapbox showed every place as its own rich HTML marker with no
grouping when zoomed out, unlike the Leaflet map. Feed the place points
through a clustered GeoJSON source: clustered points render as a dark
count bubble (click to zoom in and expand) while the rich HTML photo
markers are only drawn for the points the source reports as unclustered.
Always on, matching the Leaflet MarkerClusterGroup.
Rework the private-packing flag into a full sharing model. Every item is now
Common (the group pool — where all existing items live, so nothing breaks),
Personal (private to its owner) or Shared with specific people (it shows up on
those travelers' own lists, marked "by <bringer>"). is_private discriminates
restricted from common; a new packing_item_recipients table holds who a shared
item covers, and packing_item_contributors records "I can bring that too"
pledges on Common items.
The panel gains a Gemeinsam / Meine Liste view switch, each item a sharing
control (owner sets the tier + the people it covers), and Common items can be
co-brought or cloned onto your personal list. Visibility is enforced server-side
in listItems and the WebSocket broadcasts are scoped to exactly who can see an
item across every tier transition. All 22 locales stay in parity.
Add "guest" trip participants — people without a Trek account who can still be
assigned to costs, packing, to-dos and day-plan activities. A guest is a
credential-less users row (is_guest=1) joined into trip_members, so it is
assignable everywhere a real member is, with the cost-splitting, settlement,
packing and assignment paths working unchanged.
Guests are firewalled from everything account-related: they can never sign in
(password, OIDC and reset lookups skip them), never appear in the global user
directory, the member-add picker or admin user management, are never resolved as
notification recipients, can't be invited to another trip, and can't be made
owner. The trip owner manages guests from the share dialog in a dedicated,
clearly-labelled section (add / rename / remove), and guests carry a "Guest"
badge wherever members are picked. All 22 locales stay in parity.
The onDragOver guard `drag.isDragging || true` is a constant condition (eslint
no-constant-condition). The handler is already gated by canDrag, so run the
drag-over logic directly, matching the to-do row.
Add POST /api/trips/:id/transfer so the owner can hand a trip to one of its
existing members. The swap runs in a transaction: the new owner takes
trips.user_id and the former owner is kept on as a regular member, so nobody
loses access. The endpoint is owner-only, writes a trip.transfer_ownership
audit entry and broadcasts the refreshed trip. The members modal gains a
"Make owner" action, shown only to the current owner.
Add drag-to-reorder to the packing and to-do lists, mirroring the budget
panel's native HTML5 drag pattern. A drag within a filtered/grouped view is
mapped back onto the global order so untouched items keep their place, and the
order persists optimistically via the existing reorder endpoints.
Packing items can now be marked private (#858): a private item is visible only
to its owner. createItem/bulkImport stamp the owner, listItems filters by the
viewer, and the WebSocket broadcasts are scoped to the owner so a private item
never reaches another member's screen — including the public/private toggle
transitions. Owners get a lock toggle and a private indicator on their items.
Markdown (.md/.markdown) is now an allowed upload type and opens in a rendered preview in the file manager instead of just downloading. Reuses the existing react-markdown stack with rehype-sanitize (these are untrusted uploads, so output is sanitized) and detects markdown by extension first since browsers send unreliable MIME for .md.
Bookings get a first-class url column (migration) instead of users pasting links into notes. It's editable in the booking modal and rendered as a clickable link on the reservation card. The reservation request schemas are open passthroughs, so only the entity schema + service SQL enumerate it.
The gallery upload input now accepts image/*,video/* — update the two JourneyDetailPage selectors that matched the old value. The files/journey e2e suites mock fileService and were missing the new MAX_VIDEO_SIZE / isVideoExtension / isVideoMime exports, which broke module load.
Security: the gallery-video poster is now always stored as .jpg instead of the client-supplied extension, so a poster declared image/* but named x.html / x.js can't be written with that extension and served inline same-origin; local gallery files are also served with X-Content-Type-Options: nosniff.
Robustness: rejected/unauthorised uploads no longer orphan their bytes on disk (the gallery-video and file-manager handlers unlink before throwing); the file-manager per-type size cap is keyed on the extension like the filter, so a real video labelled application/octet-stream isn't wrongly rejected. UX: the file-manager thumbnail strip shows a play placeholder for video instead of a broken image; shared (public) journeys now return media_type and play videos with a play badge; and a poster-less video shows a neutral tile instead of a broken thumbnail.
The file manager (which already attaches files to a place/activity) now accepts video uploads up to the larger video cap — other types stay at the document limit — and the lightbox plays them with the Plyr player over the plain same-origin download URL, so cookie auth and HTTP Range both work. Videos are excluded from the offline blob prefetch so one clip can't evict a trip's documents.
Immich timeline and album listings no longer filter out videos; each asset now carries its media type, which the provider picker forwards when linking. A linked video streams through Immich's transcoded /video/playback endpoint, and the asset proxy forwards the viewer's Range header (and passes 206/Content-Range back) so the player can seek. Synology video stays excluded until its stream API is verified.
Adds media_type/media_types to the provider-photos request contract.