The desktop app and an installed PWA had a row across the whole window
with the title and the window controls. That row is gone. The rail,
sidebar, list and reader run to the top of the window in their own
colours, and the controls sit in the top row of whatever is rightmost.
- `lib/window-controls-overlay.ts` finds and marks those rows before
paint, and they reserve the controls' width through
`--titlebar-right-inset`
- the rows and the rail are the drag area, everything clickable is
`no-drag`, and an invisible band lets a page with no row still move
- settings and the admin shell get a row only under the overlay, Pro's
tab strip and Mountain View's bar match the controls' height
- Alt floats the menu over the top left, in `window-menu-bar.tsx`
- skeletons carry the same marks, so nothing shifts when the app loads
An installed app had no title bar of its own, no way to save attachments
except a zip, and "pop out" stayed inside the page. A new window was a
second copy of the app that had to restore the session itself, and on
start-up that evicts any login made without "Remember me" from the list
every window shares.
- Title bar: `display_override` with `window-controls-overlay`, one
`--titlebar-inset` variable at the page roots and skeletons, and a drag
strip with the window title. 0 without an overlay, so nothing moves.
- Save all to a folder: `showDirectoryPicker`, asking the device policy
before the picker and before each file, with the zip as the fallback.
- Pop-out routes `/popout/message/<id>` and `/popout/compose`, on offer in
an installed app or when the desktop bridge has windows. Hidden in Lite.
- A pop-out borrows the main window's session and never evicts or starts a
token timer. Every renewal now takes a Web Lock, so two windows can't
send the same refresh token, which Stalwart 1.0 may treat as a replay.
- A draft moves to its window under the same id. A closing window hands
unsaved text to the main window to save, since its own request wouldn't
survive the close.
- Read state, drafts, theme and language follow across windows, and
`html[data-desktop]` gates the app-like behaviour in `app/desktop.css`.
Review of the calendar store found writes that went missing, went out
wrong, or appeared twice.
- Writes with different scheduling flags stay separate and go out in
order. Merging them collapsed the two steps of an occurrence change, so
attendees who had answered got no mail, and "don't notify" went out to
everyone with a later RSVP.
- An edit made after a create's lost response is sent as an update. An
import of a uid the server has updates what differs.
- `createCalendarEvent`, `batchCreateCalendarEvents` and
`createCalendarTask` choose a uid before the first send, so a lost
answer online doesn't duplicate.
- The client resolves stand-in ids before the network, which covers an
open dialog once the server is back.
- A stale whole `recurrenceOverrides` map no longer erases overrides.
- A 4xx refuses that write and the queue goes on. `serverFail`,
`rateLimit` or an answer with no verdict retry instead of deleting.
- "This and all following" is refused offline with a clear message.
- A full list only drops what the server says it doesn't have. Sync adopts
a stand-in by uid. Reminder keys survive a stand-in becoming real.
Accounts the session no longer lists are forgotten.
Changed tests: the one asserting a sticky scheduling flag is gone, it
locked in the bug. Reminder keys now use the account and uid. Two flush
assertions drop `sent: 1`, because sync reads a lost write back first. The
two-account id test uses a listed account, as unlisted ones are removed.
The installed app only handled mailto: and webcal: launches. The launch
consumer dropped files, and nothing could hand a File to the composer or
the import dialogs, because the pending mailto is JSON in sessionStorage.
- manifest: `shortcuts` named in the browser's language, `file_handlers`
for .eml, .ics and .vcf, and `share_target`, all with the base path and
per-domain branding. Lite's manifest is unchanged.
- `lib/launch-handoff.ts`: the one place files and links wait, in memory
and in the Cache API, so they survive a sign-in that leaves the page.
The desktop bridge can call the same functions, since launchQueue never
fires in Electron. Sign-out clears it.
- launch consumer: routes files by type, and takes a shortcut to an open
window, which focus-existing would otherwise only focus.
- .eml opens in a new host for the preview, also before sign-in, with the
strictest device policy deciding about saving. .ics and .vcf open their
import dialogs through a new `initialFile` prop. A .ics needs the server,
so offline the dialog says so and carries on once back online.
- composer: `initialFiles` goes through `addFiles`, so plugin vetoes and
size limits still apply, also from a Pro compose tab.
- `sw.js` takes the share POST before the offline check and keeps the files
for the page. Without a worker, `/protocol/share` passes text and links
on and the page says the files are lost. `protocol` joins the app
sections and the handler pages join the shell, so a mailto: launch
offline no longer gets a network error.
Browsers without launchQueue or a service worker behave as before.
- The button and the panel of Calendar, Contacts, Files and Settings
show only when the plugin's shouldShow says yes, so a plugin switched
off for these views (admin, user or no service) leaves no button that
opens an empty panel, and no extra rows in Contacts and Settings.
- Files: while the details are open the panel steps aside there only;
the other views keep it as the user left it. With both stored open the
details fold when the page loads.
- Resizing stops where the view keeps its 480 px, so a drag or the arrow
keys no longer fold the panel away (and the handle keeps the focus).
- Settings and Contacts render the same element with or without the
panel, so a form keeps what was typed when the panel starts docking.
- Contacts on a phone: the panel button sits over an open contact too.
- The panel buttons of the reader and the views have one name, "Plugin
panel", with aria-pressed, and name the plugin's panel shortcut in
their title; the open panels are named landmarks.
The reader tests look the button up by its new name and pressed state
(viewer-toolbar-over-side-panel, reader-plugin-surfaces,
viewer-plugin-panel-width), since "Show/Hide plugin panel" is gone.
A frame slot `calendar-task-view` draws the task view below Bulwark's
toolbar in place of its list (props: the toolbar's filter, the selected
calendars); `ui.openCalendar({ view })` opens a calendar view and
`ui.openTask(id)` a task in Bulwark's editor.
Organisation > Devices now sets policy.json `organisation.devices`, each
switch off by default and scoped per domain or group:
- No downloads: attachments and files only preview (reader, conversation
view, Pro tab, list chips, .eml previews, the Files app). Download,
Download all, Open in new tab, Save to Files, drag-out, the browser's
PDF viewer (pdf.js instead) and media download controls go away.
- No export: no .eml export, View source, message drag-out, or folder and
account export; a running export stops and its file is deleted when
the switch turns on. Import stays.
- No printing: no Print buttons in mail, calendar and contacts; Ctrl+P
prints a note; the reader frame prints blank.
- No install prompt: no card, and the browser's own banner is held back.
- Watermark: the user's address and the date over the reader,
previews and every printout (mail, calendar, contacts, plugins).
A message follows its own account's policy; what isn't about one message
follows the strictest signed-in account. Every user path hides its
button, and the anchor and new-tab helpers refuse as a backstop
(lib/download-guard.ts), never the blob readers. The plugin APIs
`ui.downloadFile` and `ui.print` refuse too, and the office editor frame
loses `allow-downloads`.
Two assertions in components/mail/__tests__/blob-routing.test.tsx now
expect `{ kind: 'export' }` as the fifth argument of downloadBlob for an
.eml export: the export says what it is, so the backstop checks "No
export" rather than "No downloads" for it.
Also says how many times it found to screen readers, and the attendee
line for an address the server won't look up reads "Couldn't look up
this address".
The banner only answered on the user's own copy since 877198a4b, so an
invitation in a group mailbox (or a boss's mailbox a delegate reads)
was imported into the user's own calendar and answered there: no
reply reached the organiser and the user got a duplicate. It now uses
the copy in the message's own account and answers as that account, as
the group without SENT-BY, as the person with SENT-BY naming the user.
The user's own answer bar on a calendar shared with them also finds the
same occurrence of their own copy of a series.