76 Commits
Author SHA1 Message Date
Paul Lützner 518ea401e1 reordered php files 2026-07-30 04:45:16 +02:00
Paul LütznerandClaude Opus 5 346f496374 Add search to every file store, and stop flooding the console and the wire
Four fixes, all from using the pickers against real servers.

Nextcloud had no search at all. It does now, via WebDAV's SEARCH method rather
than the OCS unified-search endpoint: OCS answers with app URLs like
/apps/files/?dir=…&openfile=123, which are not WebDAV paths, and our entry ids
are paths. SEARCH returns ordinary multistatus hrefs, so the existing parsing
handles the response unchanged and every hit is immediately addressable. Drive,
OneDrive and Dropbox gained search too, so the only provider without it is
Google Photos, whose Library API offers no text search over media.

The scope href in that request has to be relative to the DAV root —
"/files/alice", not "/remote.php/dav/files/alice". Nextcloud cannot resolve the
absolute form and answers 404, which surfaced to the user as "could not find
that folder or file" when the folder was fine and the request was not.

The thumbnail route answered 404 whenever a service had no preview. Legitimate
HTTP, but plenty of things legitimately have no preview, so the console filled
with failed requests that looked like breakage and buried real errors. It now
serves a neutral inline SVG with 200, which also makes three copies of an inline
onerror handler dead code, so they are gone.

Previews in the file list were costing far more than they were worth: a search
returning 200 rows fired 200 proxied requests, each making the service render a
document preview — 4.5 MB and 34 seconds for a list of invoices, where every
preview resolved to the same red PDF square. Previews are now asked for only
where they tell you something, images and video, and everything else gets a
typed icon. The Nextcloud search cap dropped from 200 to 60 at the same time:
basicsearch has no cursor, so that number is the whole result set, and a picker
is for finding one file.

Dropdowns were see-through enough to read toolbar icons through their labels.
The design system had pane and pane-flat, both alpha surfaces, and nothing for
something that floats over content — so there is now a popover utility with an
opaque surface, used by the compose service menu and the save-to menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 03:31:35 +02:00
Paul LütznerandClaude Opus 5 6a563761b3 Modernise the modal chrome and let the picker use the screen
The dialog read as a three-tone sandwich: bg-sunken filled both the header and
the footer while the body sat on the pane surface. That is the opposite of what
the rest of the app does — a pane is one surface and divides with hairlines,
keeping bg-sunken for genuinely inset areas. The header also rounded with
rounded-t-2xl against a panel using rounded-pane, so the corners disagreed at
every density setting.

Now: one continuous surface, no fills, and no internal rules at all. The
hairlines under the header, above the footer and beside the scrubber read as
seams across a single sheet rather than structure, so spacing carries the
separation instead. The panel clips, which is what keeps the corners honest.

Width is per-trigger. A label form wants to stay narrow and a photo grid wants
the screen, so a trigger asks for a preset and the controller applies it —
every preset removed before the chosen one is added, since the shell is shared
and a wide picker would otherwise leave the next form stretched. The map lives
in the template, not the controller: a width named only in JavaScript risks
being purged from the Tailwind build. Media libraries open at the widest preset,
file stores one step down.

The frame no longer scrolls; it is a flex column and the loaded content decides
what moves inside it. That is what lets the picker hold its search, trail and
footer still while only the results scroll, and it replaces the picker's fixed
55vh slice of the viewport with "whatever the panel has spare", floored so a
short listing still looks deliberate.

Note for anyone hitting a stretched, footer-less dialog after pulling this:
Tailwind compiles on demand, and this adds new arbitrary values. Run
bin/console tailwind:build — without the rebuild the panel has no height cap and
pushes its own footer off screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 03:06:22 +02:00
Paul LütznerandClaude Opus 5 ee0e26ac02 Add compose file picker: attach from a connected service
The compose action bar had a dead Google Drive button with no handler. It
becomes an entry point sized to what the user actually has: nothing at all
with no connections, the provider's own icon with one, a menu only once
there is a choice to make.

Per file the user picks copy or link. Copy fetches the bytes into a real
MessagePart, so the mail is self-contained and the recipient needs nothing
from the service; it obeys the same 25 MB cap a local upload does. Link
asks the driver for a public URL and drops an anchor in the body — the
only option above the cap.

Both constraints are enforced where they are known rather than discovered
late. A file over the cap cannot be copied, a service that cannot share a
single file cannot link, and a file that is both is shown as unattachable
instead of offering a choice that would fail. An unknown size counts as
possibly-too-big: Immich omits it until EXIF extraction runs, and a copy
that dies at the cap after downloading wastes the round trip.

Bytes are fetched eagerly rather than deferred behind an integration://
storage scheme. The cap bounds the request, and it leaves AttachmentResolver
— which every send and download already goes through — untouched.

The response is JSON plus one window event rather than a Turbo Stream.
Compose lives in the dock or thread frame and the picker in the modal
frame, so a stream would have to reach across two nested frames and fight
the modal's close-on-submit. The event carries the draft id so compose
windows other than the one that opened the picker ignore it.

Thumbnails are proxied through our own route: every service puts previews
behind the same credential as the originals, so a direct URL would either
leak the credential into markup or 401 in the browser.

Adds a small generic dropdown controller — compose's from-picker has
bespoke open/close logic written before there was a second menu to justify
sharing anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:22:05 +02:00
Paul Lützner e7b9ac8ee7 fix drafts and file uploads 2026-07-28 21:34:58 +02:00
Paul Lützner b8a0f4b290 update 2026-07-28 18:31:41 +02:00
Paul Lützner 4ef4616ee8 more thread ui and jmap start 2026-07-26 18:06:50 +02:00
Paul Lützner a1da945283 thread view and compose fixes 2026-07-26 17:53:59 +02:00
Paul Lützner 9b9846633f compose inline 2026-07-26 17:06:30 +02:00
Paul Lützner a947c4aaac theming done for now 2026-07-23 16:09:03 +02:00
Paul Lützner 6a4adb3274 update theme 2026-07-23 15:00:05 +02:00
Paul Lützner 7c8d0de52d update 2026-07-23 14:34:56 +02:00
Paul Lützner efad90b3de theme update 2026-07-23 14:28:06 +02:00
Paul Lützner 7a1170b643 update theme 2026-07-23 14:17:49 +02:00
Paul Lützner 1f836f1042 update 2026-07-19 21:11:26 +02:00
Paul Lützner 27f1f6bdb4 update compose window 2026-07-16 15:43:32 +02:00
Paul Lützner 76faf22342 progress on compose, title upgrade 2026-07-16 10:56:53 +02:00
Paul Lützner 3ca64ee0d1 autocomplete shenanigans 2026-07-15 17:10:22 +02:00
Paul Lützner ec7abf6dba update 2026-07-15 14:48:35 +02:00
Paul Lützner 985a7b3018 tomselect start 2026-07-14 12:26:08 +02:00
Paul Lützner 329956d6ef compose window fixes 2026-07-13 14:10:41 +02:00
Paul Lützner ba8dad9060 update compose window 2026-07-13 13:56:33 +02:00
Paul Lützner 1ff18f0c82 fix read/unread function 2026-07-09 12:08:08 +02:00
Paul Lützner 100c2ae6e7 fix from field 2026-07-07 18:14:42 +02:00
Paul Lützner 3a383f50ff progress on sending emails 2026-07-05 20:07:31 +02:00
Paul Lützner 678b33e0cb start compose feature 2026-07-04 20:29:51 +02:00