Logout left lotus-per-user-volume (Element Call's per-person volume map:
whom the previous user called and turned up/down; same origin on the web)
and settings-sync-meta:<userId> (a key naming the account) in
localStorage. clearPlaintextCaches now removes both; device preferences
(`settings`) are kept as before.
Verified: after joining/leaving a call and logging out via Settings, both
keys are gone (unit test added).
Fixes#347
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
Lotus encodes DND as status_msg "dnd". After switching back to Auto, the
heartbeat read the server's status, adopted "dnd" as the user's own and
kept re-sending it, so the custom status was lost and other clients kept
showing "dnd"; the Settings status field showed "dnd" too.
pickHeartbeatStatus (unit-tested) never adopts the "dnd" sentinel and
falls back to the device's real status; Profile neither mirrors it into
the local status nor shows it in the field.
Verified on the local Synapse: "🎮 Gaming" → DND (unavailable/dnd) → Auto
→ online/"🎮 Gaming" (before: online/"dnd").
Still open in #283: while in DND other clients show the literal "dnd";
that needs MSC3026 busy presence on the homeserver (matrix#19).
Refs #283
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
usePresenceUpdater sent a one-off PUT /presence, but the SDK kept
long-polling /sync with the default set_presence=online and Synapse treats
every sync as "online", so Invisible / Hide Online Status / Idle / DND
flipped back within seconds — a privacy setting that silently didn't work.
The presence helpers now also call mx.setSyncPresence(): offline for
Invisible / Hide Online Status, unavailable for Idle / DND (and auto-mode
idle / hidden tab), online otherwise. The PUT stays for the immediate
change.
Verified on the local Synapse, another account polling every 5 s for 60 s
after a reload in each mode: invisible → offline ×12, hidePresence →
offline ×12, idle → unavailable ×12, dnd → unavailable ×12, online →
online (currently_active) ×12; /sync carries set_presence=offline /
unavailable / online accordingly (before: online* throughout).
Fixes#282
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
The composer's location button sent the exact position (6 decimals) from a
single click once the browser permission had been granted, discarding the
fix's accuracy, so a ±3.5 km Wi-Fi/IP fix was shown as exact.
- ShareLocationDialog: "Share your location?" with the coordinates and
±accuracy, a "Check on OpenStreetMap" link, and Cancel / Send
approximate (~1 km) / Send. Focus starts on Cancel; Esc cancels.
- utils/geoShare.ts (unit-tested): RFC 5870 geo URI with `;u=` accuracy;
approximate shares round to 2 decimals and never claim better than 1 km.
- Location cards show the ± when the event carries `u=`.
- The toolbar icon is a pin instead of the globe.
Verified headless (fix 40.748817,-73.985428 ±3500 m): the click opens the
dialog and sends nothing; Cancel/Esc send nothing; Send approximate →
geo:40.75,-73.99;u=3500; Send → geo:40.748817,-73.985428;u=3500; the card
reads "40.74882, -73.98543 (±3.5 km)".
Fixes#340
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
ReminderMonitor pushed an ordinary toast (auto-dismissed after 4 s) and
deleted the reminder at once, with no OS notification — one that came due
while the app was in the background or the user was away was lost.
Now the reminder toast is sticky; clicking it opens the message and
dismissing it closes it, and only then is the reminder deleted (an unseen
one returns on the next start). When the window isn't focused it also
raises an OS notification (respecting "show notifications" and quiet
hours; no decrypted E2EE text, per N106).
Verified headless (notifications granted, window unfocused): due at
t+24 s, the toast stayed up for the remaining 51 s, one OS notification
"Reminder :: call the dentist before 5", the reminder stayed in
io.lotus.reminders until dismissed, then was removed.
Fixes#338
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
useVoiceChannelFull compared the number of MatrixRTC memberships (one per
device) with io.lotus.voice_limit, so one person on desktop + phone, or a
crashed tab whose membership hadn't expired, filled two slots and locked
others out, while the server-side guard (which counts distinct users)
would have admitted them. utils/voiceChannelFull.ts (unit-tested) counts
distinct senders; the header/sidebar counter shows people too.
Verified on the local stack: limit 2, @aud_main in the call on two
devices; @aud_bob's prescreen now shows 1/2 with Join enabled (was
"Channel Full (2/2)"), matching the guard's 200.
Fixes#342
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
The pagehide keepalive write sent { presence: 'offline' } with no
status_msg, which Synapse treats as clearing it, so closing or reloading
any Lotus tab wiped the user's status for everyone (intermittently, as
the 10 s presence rate limit sometimes 429'd the write). It now sends the
current status along with the offline state.
Verified against the local Synapse: with a status set from another
device, two reloads and a tab close keep it (server shows
offline/"Gone fishing" after the close; before, the first reload wiped it).
Fixes#339
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
`npm audit --audit-level=high --omit=dev` (Build & Quality Checks) started
failing on every branch after new advisories were published:
- source-map-js 1.2.1 → 1.2.2 (high, GHSA-68fv-2mgg-jv7q; transitive via
sanitize-html → postcss, in range — lockfile only)
- i18next-http-backend 4.0.0 → 4.0.2 (low, GHSA-xvq9-wjp8-hwqf)
- katex 0.16.47 → 0.18.11 (low, GHSA-238p-pmpm-9mq7; fixed in 0.18.2).
0.17/0.18's breaking changes are an internal __defineFunction API and
prefixed internal CSS classes; Lotus uses neither (KaTeX.tsx calls
renderToString and imports the package's own stylesheet).
Only these four lockfile entries change (plus katex's own CLI dependency
commander 8 → 15). npm audit --omit=dev: 0 vulnerabilities.
Verified headless against Vite with a fresh dep cache (katex 0.18.11
served): inline and display math render (\frac, \sqrt, \sum with limits),
KaTeX fonts load, no page errors; translations still load (en strings,
no raw keys). tsc clean, 1319 unit tests pass, eslint 0 errors (36
warnings, unchanged), prettier clean, production build OK.
Fixes#345
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
Giphy and Tenor link previews forced every GIF into the same box — full
card width, max 200px tall, object-fit: cover — whatever its shape: a
square GIF lost half its height, a portrait one three quarters (only the
middle band showed, so captions and faces at the top/bottom were cut), and
a 100×80 GIF was blown up 4×.
utils/gifPreviewSize.ts (unit-tested) fits the GIF inside 400×320 from
og:image:width/height, keeping its aspect ratio and enlarging small GIFs
at most 2×. The card centres the GIF on the surface colour with
object-fit: contain, reserves the box before it loads, and the GIF badge
sits on the GIF's corner. Without dimensions it shows at natural size,
capped by the card. The >10 MB thumbnail fallback is requested at 2× the
box (800×640) so it isn't upscaled.
Verified headless (fixture previews, desktop / Pixel 7 / 320px phone):
480×200 → 398×166, 300×300 → 320×320 (292×292 on 320px), 240×480 →
160×320, 100×80 → 200×160, all 100% visible; the no-dimension fallback
and narrow cards keep the ratio. tsc clean, 1325 unit tests pass, eslint
0 errors (36 warnings, unchanged), prettier clean.
Fixes#341
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
One "security" strip, in the same top slot and style as the status banner
(#124), for this device, in priority order:
- "Verify this device" — cross-signing exists but this device isn't
verified (can't unlock backed-up history, untrusted to others). First,
because verifying with the recovery key also connects the backup.
- "Connect this device to your key backup" — a backup exists, this device
isn't using it.
- "Set up key backup" — no backup at all (includes accounts without
cross-signing; the setup flow does both).
The button opens Settings → Devices (existing flows); "Not now" snoozes.
Rules: nothing in a device's first 24 h; "Not now" snoozes that nudge 7
days; 3 dismissals stop it; never on a healthy device; waits until sync
has settled (never under "Connecting…"); outage/maintenance/connection
strips win. Per-device record in localStorage, wiped on logout. Mounted
inside the Matrix client context and an error boundary (an early version
outside the context crashed the app on load — caught before pushing).
Also: the status strip wraps its text on phones instead of truncating it
when there's no Details button (benefits #124's strips too).
Design approved on #123 (real-client screenshots there). Tests: 4 unit
(the #123 state table, loading, timing, stored record); e2e: nothing during
the grace period, "Set up key backup" → Settings → Devices, "Not now"
holds across a reload. Unit 1319, Playwright 29 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
`sudo pacman -U <url>` also fetches `<url>.sig` and failed (404) on
CachyOS: remote packages fall under RemoteFileSigLevel (signature
required) and we don't sign the package. A downloaded file installs under
LocalFileSigLevel (optional): `curl -LO <url> && sudo pacman -U ./…`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
npm audit --omit=dev started failing every PR: brace-expansion <=1.1.20
(via @eslint/eslintrc → minimatch 3) has three high-severity DoS
advisories. Lockfile-only patch bump; nothing else changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
Reported on CachyOS: "Check for Updates → The update downloaded but
couldn't be installed … Permission denied (os error 13) at path
/usr/bin/tauri_current_app…". The app was installed from the Arch package;
Tauri's Linux updater can only replace an AppImage.
With cinny-desktop's new update_install_kind command:
- pacman / deb installs: the toast says the update is available and opens
Settings → General → App Updates, which shows the package-manager command
(`sudo pacman -U …pkg.tar.zst`, or the .deb + `sudo apt install`) with
Copy command and Download package — no Install & Restart that can't work.
"Copied" only when the clipboard write actually succeeded.
- other distros: a link to the downloads page.
- Windows, AppImage, and desktop builds without the command: unchanged
in-app update.
- a native "package-managed" refusal shows the same help.
Tests: unit (kinds, commands, refusal detection); in the real client with a
simulated desktop bridge: pacman → toast + Settings command/buttons,
install never attempted; older desktop → in-app flow as before. Unit 1321,
Playwright 26 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
When the homeserver, voice calls or sign-in break, or maintenance is under
way, say so in the app — driven by the Kuma status page
(isitup.lotusguild.org/status/matrix), managed from Kuma's UI. The client
asks Kuma directly (not via our servers) so it still hears "the server is
down" when our servers can't tell it.
- config.json `statusPages`, keyed by homeserver: users of other servers
never contact Kuma.
- utils/kumaStatus.ts (pure, unit-tested): parse Kuma 2.x's public JSON;
a group is down when any monitor fails two checks in a row (down+down or
pending+down); maintenance = windows under way; announcements = incidents.
One strip at a time: server down (connection lost AND Kuma confirms) >
server having problems > maintenance > calls down > announcement;
sign-in problems on the login screen only.
- Wording about the user's own connection: "Connection lost … our status
checks say the server is up, so it may be your internet connection" ONLY
when Kuma checked the server after this client's connection dropped and
it passed; a stale "up" (Kuma needs a minute or two to notice an outage)
keeps the plain "Connection Lost!".
- useServerStatus: polls only while visible; 5 min, 60 s while something is
wrong or the connection is lost, at once when it drops; backoff; any
failure = no banner (Kuma being unreachable never looks like Matrix
being down); GET only, no cookies.
- UI in the existing banner slot and style (ContainerColor/Line like the
sync and clock banners); Details expands; dismiss for calls-down and
announcements (an edited announcement comes back); calls-down note above
Join; phone: one line + Details.
Needs the CSP connect-src to allow https://isitup.lotusguild.org (matrix
repo) before it can fetch in production; until then it fails quiet.
Tests: 15 unit tests (live page layout, two-check rule, any-monitor rule,
unknown/garbage, UTC beat times, stale-vs-fresh "up", priorities, login vs
client, maintenance, announcements + dismiss/edit); e2e (fixtures for Kuma):
calls-down strip + dismiss across reload, other homeservers make no
requests, Kuma 500 → nothing, lost connection + Kuma down → critical strip
instead of "Connection Lost", + fresh "up" → "may be your connection",
+ stale "up" → plain "Connection Lost". Unit 1315, Playwright 26 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
Step 1 of moving the desktop app's login out of the webview's plaintext
localStorage: keep a verified copy of the tokens in the OS keychain
(Windows Credential Manager, via cinny-desktop's new secure_session_*
commands). The session is still read from localStorage exactly as before,
so nothing about login changes and a keychain problem can't log anyone out.
Step 2 (a later release, once this has run on real installs) switches reads
to the keychain and drops the tokens from localStorage.
- sessions.ts: onSessionPersisted — listeners told about every session
write (login, token rotation) and removal (logout); a throwing listener
can't break the write.
- keychainMirror.ts: desktop only. Mirrors userId/deviceId/accessToken/
refreshToken (not the rest of the session); reads first and writes only
when the copy differs, then verifies by reading back; serialized, 5 s
timeouts; no session → clear (also covers a logout whose reload beat the
clear). Every failure is a status, never an exception. A desktop build
without the commands reads as "unsupported", so this can ship before the
desktop side.
- Settings → General (desktop): "Login in the system keychain" status.
Tested: unit tests (fake keychain: store, no rewrite when current, rotation,
clear, unsupported, missing commands, denied write, read-back mismatch,
timeout); a simulated desktop app with a fake keychain, 14/14 (login
mirrors only the secrets, Settings status, reload verifies without
rewriting, logout clears, an existing session is mirrored after upgrade,
Linux/denied show an honest status and stay logged in); the real Linux
desktop binary (commands answer "unsupported", login unaffected, Settings
says so). Unit 1295 pass, Playwright 20 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
The desktop app loads the bundled Element Call page from its own origin
(http://localhost:<port>), so the call frame can read the app's storage
(login token) and DOM — the hole #43 closed on the web by moving the page
to call.chat.lotusguild.org.
The desktop's local server can also answer on http://127.0.0.1:<port>: the
same server and bundle, but a different origin (and still a secure
context). resolveDesktopCallPageUrl loads the bundled page from there when
the desktop config sets `desktopCallOrigin`:
- only a loopback http origin on the SAME port as the app, no path, query
or credentials;
- only when the app itself runs on http://localhost (release builds; debug
builds on tauri:// keep the same-origin page);
- unset (every desktop build until cinny-desktop opts in, together with the
server bind, CSP and permission changes it needs): unchanged.
The web app is unchanged (elementCallUrl as before).
Tested in a simulated desktop app (Tauri bridge stub + the desktop
config.json, served on localhost and 127.0.0.1) against a local Synapse +
LiveKit, two users: call page from http://127.0.0.1:<port>, parentUrl =
the app origin; the frame gets SecurityError on parent.localStorage and
parent.document (same-origin control: readable); join, speaking indicator,
mic off/on, screenshare start/stop, layout switch and hang-up all work, no
page errors — 12/12 in 5 of 6 runs, like the same-origin control (3 of 4;
the misses on both sides were the local LiveKit connection).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
"Ahead" is now reported only once it has held for a minute of fresh
samples (a stalled server delivers late and reads as ahead). The test sends
its ticks, checks nothing is shown yet, fast-forwards the page clock past a
minute and sends two more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
Incident 2026-09-29: the homeserver's host ran out of memory and stalled for
~2 minutes. The /sync that finally went out carried events whose `age` was
computed ~30 s before it arrived, so every client showed "Your computer's
clock is 30 seconds ahead of the server" while the real problem was the
server (all host clocks were within 0.25 s the whole evening).
The skew estimate was the median of the last 5 samples, and a sample is
local skew + delivery delay, so one late /sync with a handful of events
tripped it.
- Estimate = the LOWEST sample of the last 5 minutes: delay only ever adds,
so the fastest-delivered event is the truest.
- "Behind" (which a delay can't cause) is reported as soon as there are 3
samples, like before. "Ahead" must hold across samples received at least
a minute apart, so a single late burst never trips it.
- Samples are aged on the monotonic clock, and a change of the local clock
(someone fixing it) resets the measurement, so the warning clears at once.
- Only events stamped by our own homeserver are sampled: a federated event's
origin_server_ts is the other server's clock.
- Wording: "This device's clock is … Voice calls and encrypted messages can
fail until it's corrected." / call bar "Device clock … : calls may fail"
(was "will fail").
Unit tests: the incident (late burst after normal traffic, and a fresh
client whose first samples are all late), mixed slow/fast deliveries,
ahead only after a minute, behind at once, hysteresis, clock fixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
Until now a send that failed (offline, homeserver down, a blip) went
straight to "Failed to send": nothing retried it, and a reload dropped it
without trace (chronological pending ordering keeps local echoes in memory
only).
- Outbox (utils/outbox.ts + features/outbox/OutboxFeature): own message
sends (text, stickers, reactions, polls; not call signalling or
redactions) are mirrored to localStorage from their first local echo until
the server confirms them or the user cancels.
- After a reload they come back as local echoes via room.addPendingEvent,
same shape as the SDK's own. Recent ones (< 1 h) are sent again with the
same txnId; older ones come back as "Failed to send" for the user to
retry or cancel. Ones the server already has (transaction id seen in
/sync) are dropped, so no duplicates.
- Retries: network failures (ConnectionError, 408/429/5xx) are re-sent when
the connection returns (sync recovers or the browser goes back online),
and after a blip while online (5 s, backing off, max 10 per message).
Oldest first, in order per room. 4xx / consent / encryption failures are
left to the user.
- UI: a network failure while offline shows a clock, "Queued. Will send
when you're back online" (thread view too), not the red ✕. The ✕ is now
a button: click to retry.
- Logout wipes the outbox with the other plaintext caches (the content is
decrypted, like drafts).
Tested end to end against a local Synapse (Chromium): offline → queued →
sent once on reconnect; homeserver unreachable → queued → sent once; failed
send → reload → sent once and shown once; server accepted but response lost
→ reload → no duplicate; 2 h old entry → failed, not sent, click ✕ → sent;
cancel → gone after reload; encrypted room → restored message goes out as
m.room.encrypted with no plaintext and decrypts; one-off failure retried by
itself in ~5 s; no page errors. Unit tests for the pure parts; Playwright
20 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
Lotus permalinks (#130) were built from window.location.origin. In the
desktop app that's the local tauri-plugin-localhost server (hash-routed), so
"Copy Lotus Link" copied e.g. http://localhost:…/#/home/!room…, which works
for nobody else. And the link recogniser only knew that local base, so a real
https://chat.lotusguild.org/home/… link in a message opened the browser
instead of the room.
- useLotusShareBase: in the desktop app, links for other people use config
`webAppUrl` (https only, set by cinny-desktop #23) in web path-routing
form; otherwise the origin, as before. Used by "Copy Lotus Link" on
messages, the space menu and space tabs.
- The recogniser accepts several bases: the origin, plus `webAppUrl` in the
desktop app.
- The web app is unchanged.
Verified with a simulated desktop (Tauri bridge + webAppUrl) against a local
Synapse. Copied links are https://chat.lotusguild.org/home/<room>/<event> and
https://chat.lotusguild.org/<space>. A public link in a message renders as
the room pill and clicking it opens the room in-app, with nothing sent to the
system browser. The web app still copies origin links. Unit tests for the
base selection; Playwright 20 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
Rendering only; the vote logic (tally, optimistic votes, end poll, keyboard
radiogroup, voters) is unchanged.
- Every row shows "N votes · P%". Multiple choice looked broken as
"100% / 100%" with no counts; the footer now says "voters" there.
- Results are a thin progress bar under each answer: accent for your pick,
success for the winner, neutral otherwise. The old full-row fill was the
same grey as the row, so a 100% answer just looked disabled.
- Radio and checkbox indicators are 18px with a 2px border in a colour mixed
from the theme's text colour. Primary.ContainerLine was nearly invisible,
especially in dark themes.
- The winner shows a star and "Winner" instead of a second check mark.
- Bordered card, 460px wide (max 100%, same width for every poll). The header
is now "Poll · Single choice | Pick up to N | Results hidden until the end |
Final results", replacing the letter-spaced "◉ POLL · …" line.
- Footer: plain muted text that wraps, and chips ("Who voted", "End poll")
that never wrap. Previously the chip label broke onto two lines inside a
one-line chip and spilled out of it. Reproduced before/after in Lotus
Terminal at 150% zoom, a 360px phone, and dark at 125%.
- Lotus Terminal theme: data-winner gets its own green border rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
The Linux desktop app runs on WebKitGTK, which ships without WebRTC (2.52
has no RTCPeerConnection; 2.54 disables it outright pending a libwebrtc
backend around 2.56), so calls can't work there. Until now the call button
just disappeared, the call room said "Your browser does not support WebRTC"
with Join disabled, and an incoming call couldn't be answered.
In the desktop app (isTauri) without WebRTC:
- call rooms: "Calls aren't available in the desktop app on Linux yet: its
web engine has no WebRTC" + an "Open in browser" button;
- incoming-call overlay: the same, with "Answer in browser";
- room header: the call button stays, and opens the room in the browser.
The link is the room in the web app (config.json `webAppUrl`, https only,
new key); the user presses Join there. Deliberately not an auto-join link:
a crafted URL must not be able to join a call and open someone's mic. It
opens through the desktop's new-window handler (web/mail schemes only → the
system browser). Without `webAppUrl` the explanation shows with no button;
browsers without WebRTC keep the old message.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
lotus.22 draws the "Share your screen?" prompt inside the call frame on
request and drops the corner button. Verified against the published
package: no-delegation (Firefox path) cross- and same-origin — bar button,
in-frame prompt, Cancel, Share → screenshare tracks, bar Stop; Chromium
unchanged; picture-in-picture prompt fits and Share works; room policy
hides the button and refuses the prompt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
With a fork that reports `screensharePrompt` (element-call lotus-screenshare-
prompt), the call bar and status bar keep their screenshare button on every
engine. Where the click can't be delegated, starting asks the fork to show
"Share your screen?" inside the call frame (io.lotus.prompt_screenshare)
instead of our own confirm; its Share click starts the share. Stopping works
from the bar as before (no click needed in the frame). Chromium is unchanged.
- useScreenshareMode: hidden (older fork's corner button) | prompt | direct.
- The room's call policy is also pushed to the fork in prompt mode, so the
prompt never opens where sharing is forbidden.
- Picture-in-picture: the "Return to call" overlay covers the frame; while
the fork reports the prompt open it lets clicks through to it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
lotus.21 checks widget messages against the host's origin (works same- and
cross-origin) and accepts soundboard clip bytes. Same-origin behaviour is
unchanged; verified against the published package: join, both screenshare
paths, PTT/deafen in the frame, layout/reactions/settings, speaking and mic
level, soundboard, avatars, muted-speech warning, per-person volume, and the
foreign-frame spoof stays blocked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA