The call-ended toast gets a Details button (and stays 10 s) that opens a
card with who was in the call and the peak, connection verdict and
reconnects, how long your / each person's / everyone's connection was
poor (likely the server when everyone), RTT, packet loss and jitter, and
your talk, mute, deafen and screenshare time, in technical wording.
Deafened time is tracked by CallEmbed; the rest comes from the fork's new
io.lotus.call_summary `details` (lotus.29). Settings → Calls gets
"Summary After Calls" (on by default).
Fixes#253
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
Each widget in the Widgets panel (and the open widget's header) gets an
"Open in a new window" button, so a pad or board can sit beside the chat.
It opens the same filled-in URL the frame uses (widgetCompleteUrl, now
shared) with noopener,noreferrer, standalone as in Element. Only
cross-origin https URLs get the button: room state can carry anything,
e.g. a script URL from another client.
Refs #205
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
Long-press opens the message action sheet (#166), but on iOS the system
takes a long-press for itself: the link/image callout or the text-selection
loupe races the 450 ms press. Message rows on touch screens now carry
data-touch-actions (no -webkit-touch-callout), and the pressed row carries
data-pressing from touch-down until the finger lifts, which turns text
selection off for that moment only. Copy Text stays in the sheet.
Refs #199
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
- Developer tools (send event, state event editor, room account data) and
setMarkedUnread go through the typed helpers in utils/room and
utils/accountData (new setRoomAccountData) instead of casting at the
call site; sendStateEvent accepts a developer-chosen type string.
- SecretStorage passes the key description as the SDK's
SecretStorageKeyDescription, CallEmbed replies with an IWidgetApiRequest,
and fillMissingPowers writes through a Record instead of `as any`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
#265: in the compact layout (phones, narrow windows) the control bar
stacked into four rows (~330px, 40% of a portrait phone). It is now one
row: mic, deafen, camera, chat, soundboard, More and an icon-only End.
Screenshare, its audio mute and fullscreen move into More; Share Screen
is only offered where getDisplayMedia exists. (The self-view comes from
the fork, lotus.27.)
#277: "Mute Screenshare Audio" showed whenever screensharing was allowed,
as a speaker icon that did nothing. It now shows while the fork reports
remote screenshare audio (io.lotus.screenshare_audio_state), or while it
is muted, so a hidden mute cannot silence the next share.
Fixes#265Fixes#277
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
- Modal500 takes a label: Room/Space Settings ("<name> settings") and
Settings dialogs have accessible names.
- aria-selected on plain buttons (inbox filter chips, date/time picker
chips, verification/usage/notification menu items) -> aria-pressed; the
Permissions power chips open a menu -> aria-haspopup + aria-expanded.
- Labels tied to the room pack name input and the Export From/To dates.
- UserAvatar defaults to alt="" (decorative next to a name / in a
labelled button) unless a caller passes one.
- Composer placeholder: Slate's inline opacity 0.333 overrode the #222
AA-contrast class (2.66:1); it is dropped so the class (0.75) applies.
- e2e/a11y.spec.ts also audits Room Settings → Permissions, Emojis &
Stickers and Export, checks the dialog name, and audits the inbox.
Fixes#278
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
#279: Room Settings → Soundboard said clips are "inherited by every room
under this space" for plain rooms too; the wording now follows
room.isSpaceRoom(). The Push to Deafen description only mentions Escape
while rebinding. "can pin a messages" → "can pin a message".
#280: the tracking stripper removed Reddit's context=, which is what
makes a comment permalink show its parent comments. Only share_id and
rdt are stripped now.
Fixes#279Fixes#280
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
Settings → About only showed cinny's package version, the same on web
and desktop and unchanged across many deploys a day. vite.config.js now
stamps the short commit (git, or LOTUS_BUILD_COMMIT; "unknown" without
git) and build time into the bundle, and About shows "Web build <commit>
· built <date>", linking the commit. On desktop it also shows "Desktop
<version>" from Tauri's built-in plugin:app|version (allowed by
core:default).
Fixes#276
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
Production Synapse caps delayed events at 24h, but the dialog let any
later time be picked and then showed the raw server error. The date
input now has min/max (today .. now+24h), a too-late time shows "You can
schedule up to 24 hours ahead." as soon as it is picked (and blocks
Schedule), and ORG.MATRIX.MSC4140_DELAY_TOO_LARGE maps to the same text.
Fixes#275
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
The strip showed for PREPARED and for the first SYNCING after it (or after
RECONNECTING), i.e. until the next /sync returned: ~30 s of long-poll on a
quiet account while the app already worked. A SYNCING means a sync just
succeeded, so the strip now shows only during CATCHUP; failures still show
as Reconnecting / Connection lost.
Fixes#274
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
#269 read-receipt pill sits at the message row's right edge, not where
the text ends (outside the shrink-to-fit content column; grows in
Compact). Bubble layout unchanged.
#270 Most Active Members: the bar's % is of its own track, so every row
shows its count.
#271 Noise Suppression description at T200 like the rest.
#272 decoration preview shows your own avatar under the decoration; the
empty state is a visible dashed circle.
#273 grouped settings get a divider between tiles (SettingTileGroup, via
globalStyle; SettingTile roots carry data-setting-tile), and on phones
the Sync tile's buttons sit under its description.
Fixes#269Fixes#270Fixes#271Fixes#272Fixes#273
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
OutboxFeature loaded lotus_outbox_v1 once and then saved its own
in-memory list over the key on every change, so a second tab's save
dropped the first tab's unsent messages. persist() now re-reads the
stored list and applies the change (add/remove by txnId) to it.
Fixes#268
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
#266: remembered widget approvals (lotus.widgetConsent.*) survived logout,
so they applied silently to the next account on the device and listed the
previous account's rooms; clearPlaintextCaches now sweeps them. Space
structure, call membership and Lotus room-policy state join
PROTECTED_STATE_TYPES, so no widget can be granted them.
#267: any website could auto-submit a form to /share-target and land the
user in the share picker with attacker-chosen text and files. The worker
now refuses a share whose Origin is another site. (Sec-Fetch-Site, as the
issue suggested, never reaches a service worker: checked in Chromium.)
Origin null/absent, as the OS share sheet sends, stays accepted, and
/share now says the content came from another app or website. Both logout
paths await clearSharedPayload(); the server-forced one also clears the
soundboard clip cache, as #57 intended.
Fixes#266Fixes#267
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
Members and bookmarks were 266px, the gallery 320, the threads list 340,
a thread and widgets 360. Swapping panels resized the room view, so the
header icons slid sideways (74px for members -> threads) and a second
click on the same spot opened Widgets instead of closing the panel. All
six now use SIDE_PANEL_WIDTH (340px, styles/sidePanel.ts).
Fixes#263
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
The topic editor's hand-rolled markdown treated any single _ or * as
emphasis, inside URLs and #alias:server / @user:server too, and built the
plain topic by stripping tags, so the underscores were lost for every
client. It also left " unescaped in link targets.
buildTopicContent() now uses the composer's inline markdown parser (links,
bare URLs and HTML escaping as for messages) and treats _ between two
letters/digits as literal (CommonMark), except inside code, link targets
and URLs. The plain topic drops the markers but keeps links as
"text (url)" and decodes entities.
Fixes#262
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
The Server ACL editor treated an absent allow_ip_literals as false, so the
first save in a room with no ACL (or one omitting the field) silently banned
IP-literal servers. The spec default is true. normalizeServerAcl() fills the
spec defaults once and feeds the initial state, the dirty check and the save.
It also stops showing an existing ACL with no allow list as allow-all: per
spec that allows no servers, and the /acl command already keeps it as-is.
The checkbox is driven by onClick (folds has no onChange), now readOnly so
React stops warning about a checked input without onChange.
Fixes#261
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
DND was sent as presence unavailable with the literal status text
"dnd": it replaced your status, and other clients showed "dnd". Where
the server supports MSC3026 (busy_presence), DND now sends presence
org.matrix.msc3026.busy and leaves the status alone (Synapse keeps
busy across /sync set_presence=online). Busy renders as red "Do Not
Disturb" with the real status line; the legacy unavailable + "dnd"
encoding is still recognised and still used on servers without it.
Status saves / clears no longer downgrade busy. The SDK rejects
non-spec presences, so busy goes through a direct authed PUT.
Fixes#283
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5