Snapshot of every feature that has landed on main. Last updated 2026-09-27. 2009 C++ + 747 Rust tests.
Calls: camera failures detected and explained (2026-09-26, unreleased). A missing, busy or blocked camera is reported by
tk::VideoCaptureand explained in calls, the lobby and/selfie; the camera track stays muted until a real frame arrives, and turning video on retries the device. Qt6 build + ctest 1993/1993; user-verified live. Windows/macOS unbuilt.
Member moderation: kick, ban, unban (2026-09-24, unreleased). Right-clicking a member row in
RoomInfoPanelBodyopens aPopupMenuwith Show profile / Kick user… / Ban user…. Kick/Ban are enabled per member through aMemberActionsProviderbacked by the newClient::can_kick_user/can_ban_user(ruma's target-awareuser_can_kick_user/user_can_ban_user, room-v12 creators included);RoomView::confirm_and_moderate_member_confirms viaConfirmDialog's new optional reason field. Room Settings gains a Moderation tab (RoomModerationSection, index 4) listing banned users fromClient::get_banned_members(member sync with a 10 s cap, then the store), each with Unban; a successful unban removes its row locally, since the store keeps the ban until sync delivers the change. All three actions go throughShellBase::moderate_member_(ModerationActionKick/Ban/Unban, failures on the status line). Kick/ban reasons also show on timeline membership lines (with_membership_reason) and in history export (label slot 32). Qt6 build + ctest 1960/1960, cargo 715/715; user-tested live, unban row-removal fix unverified. GTK4/Windows/macOS share the code, unbuilt.
Mention pills: initials disc when there's no avatar (2026-09-24, v0.8.25). A pill's reserved avatar slot used to stay blank until an avatar loaded, and forever if the user or room had none.
tk::paint_pill_leading_visualnow draws an initials disc there (display-name initials; "@" for@room) in the theme'savatar_initials_bg/avatar_initials_text, carried onPillSpec::initials_bg/initials_fg. Composers get the colors throughNativeTextArea::set_mention_colors(const tk::MentionColors&), which replaces the old(bg, fg)overload on all four backends. Windows build + pill/mention ctest 63/63; user-verified live. Qt6/GTK4/macOS unbuilt.
Command-line options (2026-09-23, v0.8.25). A shared parser and startup pipeline for all shells:
--help,--version,--profile(isolated profiles),--hidden,--log-level/--verbose,--logoutall, and--open-*on every platform. See docs/CLI.md.
Space room management (2026-09-21, v0.8.25).
SpaceRootViewgains an add/remove-rooms section: a searchable candidates list on the left, the space's current children (joined + unjoined) as a grid on the right. Drag a room either direction, or use Enter/Delete; optimistic with revert-on-failure via the existingpending_room_actions_correlation. Hidden entirely when the user lacksm.space.childsend power. New Rust/FFI/Client mutation API (can_edit_space_children/add_room_to_space_async/remove_room_from_space_async) reusingresolve_route_viaand the existingon_room_action_completecallback — no new IEventHandler hook. Also fixed a gap where the room list never refreshed when a room's space membership changed, sincem.space.childis a state event on the space's room, not the child's —room_list_fingerprintnow tracks aspace_children_summaryper room. The view's top row was redone as avatar+alias on the left and a scrollable, linkified topic on the right (was overlapping the new section for long topics). First real consumer of the in-app drag-and-drop framework below. Linux (Qt6 + GTK4) build + full ctest 1906/1906, cargo 711 (+7); user-verified live over several rounds of interactive fixes (grid cell shape, stale/unknown room filtering, scrollbar-vs-drag conflicts, tooltip offset, a sidebar-resize-grip regression). No automated tests yet for the two new widgets themselves. Windows/macOS share the code, unbuilt.
In-app drag-and-drop framework for
tesseract_tk(2026-09-21, v0.8.25). New synthetic, in-process widget-to-widget drag-and-drop primitive:Host::begin_drag()(a kind-taggedDragPayload+ a fully customizable pre-renderedDragVisualthat floats above everything) hands pointer routing to a claim-bubble drop-target search (on_drag_enter/on_drag_over/on_drag_leave_target/on_drop), with Escape/leave-surface cancel and a sharedDragGestureTrackerclick-vs-drag threshold helper. No OS drag-source APIs are touched — this doesn't interoperate with dragging content to other apps. The existing OS-inbound file-drop hover API was renamed (on_drag_hover→on_native_drag_hover, etc.) so the two systems can never be confused. Framework only, no product call site yet — first consumer will be dragging rooms onto Spaces inRoomListView, which also needs a still-missing client API to mutatem.space.child. Linux (Qt6 + GTK4) build + full ctest 1906/1906 (+8); Windows/macOS share the code, unbuilt this session.
Call rooms (MSC3417) get room-list UI and full call lifecycle (2026-09-21, v0.8.25). Call rooms now show in their own "Call Rooms" room-list section (after Rooms, ranked below Favorites/Unread/Inactive; reappears per-Space when drilled in), and Tesseract auto-joins the call on first switch into one. Switching away floats the call instead of hanging up; switching back restores the user's saved (non-floating) mode; switching directly between two call rooms leaves the old call and joins the new one. Docked/ DockedExpanded mode controls now hide while the call's room isn't the one being viewed. Linux (Qt6 + GTK4) build + full ctest 1898/1898 (+15); unverified live this session. Windows/macOS share the code, unbuilt. Since 2026-09-24, call rooms can also be created from the Create Room dialog's split button ("Create Call Room"); the new room opens its pre-call lobby once sync delivers it. Windows build + ctest 1935/1935; user-verified live.
Custom emoji get BigEmoji sizing too (2026-09-20, v0.8.24). A body made entirely of custom emoticons (MSC2545), alone or mixed with native emoji, now gets the same 2x BigEmoji size a native-emoji-only message did; a custom emoji mid-sentence also renders larger than the surrounding text (
FontRole::InlineCustomEmoji, native inline emoji unchanged). Linux Qt6 build + full ctest 1883/1883 (+6); user-verified live. GTK4/Windows/macOS share the code, unbuilt this session.
Call banner is a room state (2026-09-19, v0.8.24). The banner stays while the room has live call members and you aren't in the call: member avatars and one Join button, no dismiss or timeout. Driven by
RoomInfo.call_membersviaShellBase::refresh_call_banners_(). Qt6 build + ctest 1877/1877; unverified live.
Calls: multi-SFU / cross-homeserver (2026-09-19, v0.8.24). Calls speak Element Call's
multi_sfumode: we publish on our SFU and subscribe to every member's SFU (rtc/members.rs,rtc/sfu_set.rs), with per-member key delivery, MSC4140 delayed leave and hardened key-event checks. User-verified against Element Web; stickymatrix_2_0memberships not supported yet.
Activity Monitor window (2026-09-18, v0.8.24). Settings → Advanced → "Open Activity Monitor" opens a separate window listing every background job grouped by area (Sync, Backfill, Media, Search, Calls, Export, Workers, UI housekeeping): state, last run, run count and last error, refreshed once a second while open (no cost when closed). Rust jobs register in a process-wide registry (
sdk/src/client/activity.rs, pulled viaactivity_snapshot()); C++ jobs and the three worker pools register inActivityRegistry, withrun_async_(label, fn)for labeled tasks. Hosted by a new genericAuxWindowBasesecondary window. Linux Qt6 build + full ctest 1866/1866, +4 Rust tests; unverified live. GTK4/Windows/macOS windows written by analogy, unbuilt; not all jobs are instrumented yet (one-shot FFI calls, OS listeners).
Image metadata stripped on upload; Compressed photos keep orientation (2026-09-18, v0.8.24). Uploaded JPEG/PNG/WebP images now have EXIF/GPS, XMP, IPTC and text chunks removed losslessly in the Rust SDK (
strip_meta.rs, built onimg-parts+little_exif), covering Original quality, animated WebP, file sends and avatar uploads; JPEG/WebP keep only the Orientation tag. GIF is not stripped (unsupported byimg-parts). Separately, Compressed mode re-encoded photos without applying EXIF orientation, so portrait shots arrived sideways; every platform'sencode_for_sendnow bakes it in. Linux Qt6 build + full ctest 1856/1856, +5 Rust tests; unverified live. GTK4/Windows/macOS unbuilt this session.
In-memory cache size accounting fixed (2026-09-18, v0.8.24). Settings → About could show an absurd in-memory cache size (tens of billions of GB) instead of the real figure.
AnimImageCachetracked frame bytes as a running total nudged incrementally on add/remove, which could underflow if a resident frame'smemory_bytes()grew after being cached — e.g. Qt memoizing extra pre-scaled copies of the same image as it's painted at different sizes elsewhere. Now computed by summing live frame sizes on demand instead of trusting a running total. Windows build
- full ctest, 1823/1823; unverified live, no clean repro. Qt6/GTK4/macOS share the fix, unbuilt.
Timeline link tooltips show the real target URL (2026-09-17, v0.8.24). Hovering a linkified URL or a markdown-style
[text](url)link in the message timeline now shows a tooltip with the real target URL, reusing the existing generictk::Host::show_tooltip/hide_tooltipmachinery (the same one already used for action-pill and emoji-shortcode tooltips) rather than adding new UI. Skipped for autolinked plain URLs, where the display text already is the URL, and for@mention/#roompills. Linux (Qt6 + GTK4) build + full ctest, 1855/1855; user-verified live. Windows/macOS share the code, unbuilt.
Room info panel: real scrollbar for the member list (2026-09-17, v0.8.23). The member list previously had no scrollbar at all — mouse-wheel only, no visual indicator, no drag — making a long expanded list unusable.
RoomInfoPanelis now a thin outer chrome widget (backdrop + fixed header) wrapping a newRoomInfoPanelBodythat genuinely inheritstk::ScrollableBase, mirroringImagePackEditorView's existing fixed-header-plus-scrollable-body composition. Public API unchanged — no call-site changes inRoomView.cppor any platform shell. Linux (Qt6 + GTK4) build + full ctest, 1850/1850; unverified live this session. Windows/macOS share the code, unbuilt.
Room info panel: members sorted by power level, scroll speed fixed (2026-09-17, v0.8.23). The member list now sorts admins/mods first (alphabetical within each tier) instead of matrix-sdk's arbitrary local store order, via a new
RoomMember::power_level()threaded through the FFI (ruma'sRoomPowerLevels::for_user, so room v12+ privileged creators sort correctly). Separately, the list's scroll speed was ~20x faster than every other scrollable view — a stray* 20.0fmultiplier on top of a wheel delta the platform host already scales to pixels — now matchestk::ScrollableBase's plain 1:1 delta. Linux (Qt6 + GTK4) build + full ctest, 1850/1850; cargo test 667/667; unverified live this session. Windows/macOS share the code, unbuilt.
Fixed
/selfiefreezing the app on Windows (2026-09-17, v0.8.23). Opening the camera overlay via/selfiefroze the app instead of showing it — the shared modal gatearrange()uses to force-hide the compose bar's native text area didn't know about the camera/screen-picker overlays, so Windows' own post-arrange hide foughtComposeBar::arrange()'s unconditional re-show every pass, a self-sustaining relayout storm that starvedWM_PAINT. Both overlays are now included in that gate, matching every other modal. Windows build + full ctest, 1824/1824; user-verified live. Qt6/GTK4/macOS share the fix, unbuilt this session (macOS had the identical latent gap, not yet reported there).
"System" accent color now works on Qt6, GTK4, and macOS (2026-09-17, v0.8.23). Previously Windows-only; the other three platforms rendered "System" identically to Blue. All four platforms now also live-update on an OS accent-color change without an app restart. Linux (Qt6 + GTK4) build + full ctest, 1855/1855 (+5); user-verified live on Qt6 (KDE Plasma). Windows/macOS unbuilt this session.
@roommention pills show the room's own avatar (2026-09-16, v0.8.23).@roompills previously showed no image, just the pill label. They now resolve the current room's own avatar —MessageListViewgained aRoomAvatarProvider(mirrors the existing per-userMentionAvatarProvider), wired inRoomPaneto a newroom_self_avatar_()cache-peek that mirrorsmention_avatar_for_user_'s shape (fetch-on-miss viaShellBase::ensure_room_avatar_, the same call the room header uses). Applies to both spellings of a self-mention (plain text; the spec-invalid<a href="https://matrix.to/#/@room">link some senders emit, which now also has itsurlcleared to match) — a Room-kind pill that's an actual permalink to a different room keeps itsurland gets no avatar, keeping the avatar-reservation rule in the four per-platform canvas backends Matrix-agnostic (pill_kind==Room && url.empty(), no matrix.to string matching outsidehtml_spans.cpp). Typing@roomin the composer and accepting it from the mention popup shows the same avatar in the inserted pill (MentionController::Hooks' newresolve_room_avatarcallback, also wired through compose-draft restore); all four composer backends now always reserve the pill's leading-avatar slot for a room mention, since a composer-inserted@roomis always a genuine self-mention — noPillKind::Roomambiguity there. Root cause of it not showing up at all: GTK4/Win32/macOS's main windows builtMentionController::Hooksinline instead of viaRoomPane::wire_mention_hooks_(only pop-out windows and Qt6's main window used that shared helper), soresolve_room_avatarnever reached 3 of the 4 main-window composers regardless of cache state — fixed by consolidating all three ontowire_mention_hooks_. Separately, since a composer pill is a baked bitmap (unlike the timeline's mention pills, which just re-evaluate on the next repaint), an avatar that lands after insertion needs an explicit patch:RoomPane:: notify_avatar_media_ready_()— called from each shell's existingon_media_bytes_ready_, the same event-driven signal every other avatar consumer in the app already relies on — now does that via a newTextArea::refresh_room_mention_avatar()(all four composer backends), replacing an earlier timer-based poll loop (and its pre-existing per-user analogue,pending_mention_avatars_) that didn't match how media arrival is handled anywhere else in the codebase. Linux (Qt6 + GTK4) build + full ctest, 1834/1834; unverified live this session. Windows/macOS share the code, unbuilt.
Plain-text
@roommentions now render as pills (2026-09-16, v0.8.23). A plainm.text@roommessage (noformatted_body, as some other Matrix clients send it) rendered as literal text instead of a pill — the bare-text@roomscanner (split_room_mentions()) was only ever wired into the HTML-parsing path. Exposed and called from the plain-text fallback branches inMessageListView'sbody_layout_for()/assemble_emote_spans_()too, without ever parsingm.bodyas HTML. Linux (Qt6 + GTK4) build + full ctest, 1834/1834 (+3); unverified live this session. Windows/macOS share the code, unbuilt.
Global mentions/@room/keywords notification controls (2026-09-16, v0.8.23). New "Mentions & Keywords" group in the Notifications settings tab, on top of matrix-sdk's push-rule API: separate on/off switches for @-mentions (
.m.rule.is_user_mention), @room (.m.rule.is_room_mention), and general messages (the default underride mode across all encrypted × one-to-one room categories), plus a master "Notify on keywords" switch (a derived read/bulk-write over every non-default Content-kind rule — there's no dedicated push rule for it) and an editable keyword list rendered as a wrapping flow of removable pills. Every toggle is optimistic-then-confirmed, reverting on a failed server write. Keyword add/remove deliberately doesn't re-fetch the server list afterward — reading matrix-sdk's push-rules cache right after a mutation races with a concurrent/syncresponse that can clobber it with a stale pre-change snapshot, the same self-echo class of bug that previously hit thread read receipts. Linux (Qt6 + GTK4) build + full ctest, 1831/1831; cargo test, 667/667 (+3); user-verified live on Windows. macOS unbuilt this session.
Room directory browser (2026-09-15, v0.8.23). New "Browse" tab in Add Room: a searchable, paginated public-room directory (own homeserver or another via federation), with join-rule badges and a Join/Go action, backed by matrix-sdk's
RoomDirectorySearch. Linux (Qt6) build + full ctest, 1824/1824 C++ + 664 Rust; user-verified live on Qt6. GTK4 blocked by a pre-existing, unrelated build break; Windows/macOS unbuilt.
Pinned-messages banner now reflects edits (2026-09-11, v0.8.23). The banner showed a pinned message's original text forever after it was edited —
resolve_pinned_eventnever consultedm.replacerelations, and edits weren't a notable-update reason either, so nothing re-ran it. Now resolves the latest sender-authored edit and re-triggers viaEventCache's generic per-room update stream, filtered to rooms with active pins. 664 (+6) Rust / 1781 C++ tests; user-verified live.
URL preview cards: aspect-correct sizing (2026-09-11, v0.8.23). Every preview card stretched its image to a fixed 56×56 square regardless of aspect ratio, distorting non-square previews — the event's own
og:image:width/heightwere already parsed and threaded through the FFI intoUrlPreviewData::image_w/image_h, butUrlPreviewCardDisplaynever read them. Legacy homeserver-fetched previews now contain-fit their thumbnail within that same 56×56 slot (fit_media) instead of stretching. MSC4095 sender-bundled previews get a new layout (paint_one_bundled_): image on top, capped at the same size an inline timeline image uses (kMaxInlineImageWidth/Height, 320×200), aspect-fitted and horizontally centered, with title/description/url text below. Card height is now content-dependent for bundled previews; a sharedbundled_card_size_()helper keepsstack_height()'s row-layout reservation in sync with what paint actually draws, mirroring the existingsticker_fit_box_()measure/paint pattern.image_w/image_hare finally used, as the pre-decode sizing fallback. Linux (Qt6 + GTK4) build + full ctest, 1781/1781; user-verified live on Qt6. Windows/macOS share the code, unbuilt.
Resizable / collapsible room-list sidebar (2026-09-10, v0.8.22). The
RoomListView/ chat-pane separator is a drag handle (MainAppWidget::RootLayoutWidget; width math inviews/sidebar_metrics.h), capped atmin(½ window ≥ 260, widest on-screen room row)and only re-clamped when the window narrows. Dragging to the 68 px minimum gives an icon-only mode (RoomListView::set_icon_only+UserInfo::set_icon_only): avatars only, hover shows the full row as a flyout (RoomListView::paint_overlay). A grip on the separator toggles collapse;Settings::sidebar_width/sidebar_collapsedpersist it; 3-stateon_sidebar_cursorwired through all four shells (new Win32Cursor::SizeWE). Linux (Qt6 + GTK4) build + full ctest, 1780/1780 (+14), user-verified Qt6; Windows/macOS unbuilt.
MSC4095 bundled URL previews — receiving side (2026-09-10, v0.8.22). Link-preview metadata carried inline on a message (
com.beeper.linkpreviews/m.url_previews, rumaunstable-msc4095) is parsed insdk/src/client/timeline_convert.rs(map_bundled_url_previews, filtered so each entry'smatched_urlappears in the body), carried across the FFI asTimelineEvent::bundled_url_previews+ a_presentflag, and rendered byUrlPreviewCardDisplay(now one card per entry, keyed offMessageRowData::bundled_previews). Encrypted thumbnails (beeper:image:encryption) flow through the existing encrypted-media fetch path. The homeserver/preview_urlfetch stays the fallback only when the event carries no bundled field; an empty array suppresses previews. Sending side not implemented. Linux (Qt6 + GTK4) build + full ctest, 1781/1781 (+1) C++ + 658 (+6) Rust; user-verified on Qt6 against mautrix-bridged rooms. Windows/macOS unbuilt.
MSC4426 user status (2026-09-08, v0.8.22). A self-set emoji + short text status, editable in Settings › Account (new shared
StatusEditorwidget; the emoji is chosen via the sharedEmojiPicker, hosted bySettingsViewas a Host popup — Unicode-only there for now, no Client wired), shown as a row on the user profile card, and as a third line on the sidebar account strip (<emoji> <text>or a "Click to set status" placeholder → opens Settings › Account; strip 48→64 px). Read/write for the status and call fields now go through matrix-sdk's typed extended-profile-field API (Account::set_profile_field/delete_profile_field/fetch_user_profile_of), which also replaced the hand-rolled reqwest GET/PUT/DELETE that the MSC4133 pronouns/timezone/ biography path used. Joining a MatrixRTC call publishesm.call(call_joined_ts); leaving clears it — surfaced as an "In a call" line on the profile card. Linux (Qt6 + GTK4) build + full ctest, 1757/1757 (+4) C++ + 652 Rust; the SDK migration and them.callhook still need live verification.
MSC3030 completion-side restore now shared with pop-outs (2026-09-08, v0.8.22). A pop-out that jumps to a date/permalink previously began a focused subscription (moved onto
RoomPane2026-09-07) but never re-armed the historical-mode gate or re-applied the scroll-to-focus-event once the timeline reset landed — that restore logic lived only inShellBase::handle_timeline_reset_ui_'s main-window-only branch. Moved ontoRoomPane::on_timeline_reset, keyed by the pane's own room id via the sharedpagination_map, so any pop-out gets the same begin-focused-gate/historical-mode/scroll-to-event/return-to-live behavior the main window always had;ShellBase's own copy shrank to just the tabs_ saved-scroll-offset restore, which has no pop-out equivalent. Linux (Qt6 + GTK4) build + full ctest, 1753/1753 (+3).
Pop-out windows: old (backward-paginated) message thumbnails now load (2026-09-07, v0.8.22).
fetch_media_pipeline_'s delivery gate only ever recognized the main window's current room or an open gallery as "live" — a pop-out showing any other room had its ordinary timeline media silently dropped as stale. Already-cached thumbnails (e.g. from an earlier main-window visit) still rendered, which is why only scrolling up into never-before-fetched history exposed it. Linux (Qt6 + GTK4) build + full ctest, 1750/1750 (+2); user-verified live.
RoomPane consolidation: reply-quotes, pinned banner, compose drafts, MSC3030 initiation (2026-09-07, v0.8.22). Four pieces of
ShellBaselogic that duplicated, or were missing from,RoomPane— the per-room-view collaborator shared between the main window and pop-outs — now live there instead: reply-quote resolution, pinned-events-banner refresh, compose-draft save/restore, and MSC3030 focused-timeline/date-jump initiation. Surfaced and fixed two latent bugs along the way:build_rows_resolved reply-quotes against the wrong room for a pinned-open gallery or a differently-shown pop-out, and compose-draft restore existed nowhere onShellBaseitself, only duplicated across all four platform shells. No user-facing feature change beyond those two fixes. Linux (Qt6 + GTK4) build + full ctest, 1748/1748; user-verified live. Windows/macOS share the code (all four shells touched by the compose-draft and MSC3030 wiring cleanup), unbuilt.
Recovery-key dialog: field grabs focus on open (2026-09-07, v0.8.21). The key-verification dialog's recovery-key field now takes keyboard focus as soon as its "Enter your recovery key" step opens, instead of requiring a click first. Windows build-verified.
Account picker: all accounts shown, scrolls past 8 (2026-09-26). The picker no longer keeps the row count from its first open, so accounts added or removed later show up correctly; it shows at most 8 rows and scrolls the rest (
AccountPicker::kMaxVisibleRows). Qt6 build + ctest 1987/1987, user-verified live; GTK4/Windows/macOS share the code, unbuilt.
Account picker: in-place row updates keep their click handler (2026-09-07, v0.8.21). Logging out of one account and into a different one, with the total account count unchanged, left the switcher unresponsive for the new account — the in-place row-update path refreshed each row's name/avatar but left its click handler bound to whichever account previously occupied that row. Windows build-verified.
Account picker: active-account row gets the selected-row treatment (2026-09-07, v0.8.21). The active account now gets the same tinted background + left accent bar
RoomListViewgives the active room, replacing a small accent dot that read too similarly to the new per-account unread-notification dot on the avatar corner. Windows build-verified.
Account picker + sidebar: unread-notification dot for background accounts (2026-09-07, v0.8.21). The sidebar avatar and each row of the account picker now carry a small unread-notification dot — the sidebar one flags that some other signed-in account has unread messages, each picker row flags that account specifically. Reuses
per_account_rooms_andRoomListView's presence-dot visual pattern. macOS build + full ctest, 1748/1748 (2 pre-existing unrelated failures reproduced against baseline); GTK4/Qt6/Windows share the code, unbuilt.
Spaces: "Leave Space" button, and left rooms no longer stay listed as joined (2026-09-06, v0.8.21). The space summary view gains a "Leave Space" button, reusing the existing leave-room confirm/command chain, tagged so a successful leave also steps back out to the parent space (or room list root). Alongside it, a room left while a child of a Space no longer vanishes from that space's "available to join" list —
space_children()'s membership filter checked only that the room was known to the local store, not that it was still joined. Linux (Qt6 + GTK4) build + full ctest, 1748/1748; macOS/Windows share the code, unbuilt.
macOS: "System" theme restyles immediately after an explicit choice (2026-09-06, v0.8.21). Switching the theme setting to "System" right after an explicit Light/Dark choice now restyles the whole app immediately instead of only the titlebar.
os_color_scheme_()readNSApp.effectiveAppearancewhile it was still pinned to the prior explicit choice; it's now cleared first so the read reflects the live OS appearance. macOS build-verified, user-verified live.
Hover action pill: one position, opaque (2026-09-06, v0.8.21). The action pill now sits flush above the row for every message layout and row shape, instead of a different bottom/centred/top-band anchor per case, and is opaque so a read-receipt cluster it overflows onto can't show through it. Full ctest; user-verified live. +5/-1 C++ tests.
Media prefetch: single-flight guard no longer blocks the real fetch (2026-09-06, v0.8.21). The pre-paint prefetch below shared its single-flight dedup set with the real network fetch path, so a cold disk cache marked media "in flight" and silently suppressed its actual download — room-list avatars never loaded until clicked. Prefetch now keeps its own dedup set. Full ctest; user-verified live on Linux. +2 C++ tests.
Media prefetch: dedup against the real fetch pipeline (2026-09-06, v0.8.21). The pre-paint prefetch above never deduped its dispatched keys against the real fetch pipeline's in-flight guard, so a key that couldn't resolve within one frame got redispatched on every subsequent paint, flooding the shared decode pool and starving real fetches — visible on macOS as the pending-fetch count climbing while nothing rendered. Now guarded the same way
ensure_media_image_/ensure_room_avatar_already are. User-verified live on macOS.
Media: pre-paint disk-cache warming (2026-09-06, v0.8.21). Images, stickers, reactions, and avatars already on disk now decode before each paint instead of on first draw, bounded by a 2ms deadline so a slow decode can't stall a frame. Linux (Qt6 + GTK4) build + full ctest; user-verified live on Linux, Win32/macOS unverified. +17 C++ tests.
Bubble layout: furniture stays inside a narrow panel (2026-09-05, v0.8.21). Own-message hover pill, read receipts, and pending indicator no longer spill past the panel edge when a long message hugs full width in a narrow panel (e.g. the thread side panel). Full ctest. +1 C++ test.
Thread panel: backfills automatically until the panel is filled (2026-09-06, v0.8.21). Opening a thread no longer waits for the user to scroll up before pulling more history — if
subscribe_thread's initial batch doesn't cover the panel, it now keeps backfilling on its own, same as the thread-list panel already did. Linux (Qt6 + GTK4) build + full ctest. +5 C++ tests.
Per-room media-preview override survives a restart (2026-09-05, v0.8.21). A per-room media-preview override no longer reverts to "Use global default" after restarting the app. The write always reached the server, but sliding sync only delivers a room's account data once its timeline subscription round-trips, so the read right after a fresh room switch raced ahead of it; the write is now blocking and the read is verified directly against the server in the background, correcting a stale value once the round trip lands. Linux (Qt6 + GTK4) build + full ctest; user-verified live across a restart.
Thread panel: find-bar no longer steals focus (2026-09-05, v0.8.21). Opening a thread no longer moves keyboard focus into its always-visible find-in-thread bar. Linux (Qt6 + GTK4) build + full ctest; user-verified live.
Thread panel: "Reply in thread" shown on replies (2026-09-05, v0.8.21). The action now shows on messages that are themselves replies, not just root-level ones. Linux (Qt6 + GTK4) build + full ctest.
Thread panel: reply-quote previews resolve (2026-09-05, v0.8.21). A reply row inside an open thread panel now resolves its "replying to" preview instead of permanently showing "unavailable". Linux (Qt6 + GTK4) build + full ctest; user-verified live.
Thread panel: media sends route into the thread (2026-09-05, v0.8.21). Pasting/dropping an image, video, audio file, or document while a thread is open now sends it into the thread instead of the room. Linux (Qt6 + GTK4) build + full ctest.
Thread panel: own message backfill (2026-09-05, v0.8.21). An open thread now paginates its own older messages on scroll-to-top instead of only ever showing its newest batch. Linux (Qt6 + GTK4) build + full ctest; user-verified live.
Bubble layout reserves room for read receipts (2026-09-05, v0.8.21). A wide message from another user could hug out under the read-receipt avatars painted at the row's right edge; bubble shaping now reserves the same receipt-cluster width Classic/IRC layouts already did. Linux (Qt6 + GTK4) build + full ctest.
Compose box: text area re-measures height on width change (2026-09-05, v0.8.21). The compose box's text area could get stuck at roughly double height under HiDPI scaling until the first edit —
set_rect()applied a new width to the native control but never re-queried its natural height against it. Now re-measures whenever the width actually changes.
Thread-reply preview chip sizes to row width (2026-09-05, v0.8.21). Bubble layout's thread-reply preview chip capped itself to the bubble's own hugged content width, truncating short messages with long thread previews almost immediately. It now sizes off the full row width like Classic/IRC, hugs its own measured text instead of always stretching, and own (right-anchored) bubbles anchor/grow the chip from the right instead of the left. 1719 C++ ctest pass; user-confirmed live on Linux.
Thread panel: incoming stickers now show up in the thread view (2026-09-05, v0.8.21). A sticker sent into a thread by someone else didn't show up there — stickers converted through a path that never extracted MSC3440 thread metadata, so incoming stickers always got an empty thread root.
RoomPaneis now the single source of truth for thread-panel state across the main window and pop-outs. User-verified live against Synapse + element-web.
Thread panel: sending a sticker into an open thread stays in the thread (2026-09-04, v0.8.21). Sending a sticker into an open thread no longer lands it in the room. The main window read a stale, separate copy of thread-panel state;
RoomPaneis now the single source of truth for both the main window and pop-outs. Linux debug + release build, full ctest + cargo test; user-verified live.
Disabled widgets are opaque to input (2026-09-04, v0.8.20). A disabled
tk::Widgetnow stops pointer, key, and drop dispatch (plus hit-test and Tab traversal) at itself instead of relying on its own handler to reject the event — clicks/hover/drops no longer fall through to whatever is behind or inside a disabled control, and clicking one no longer clears keyboard focus. Linux (Qt6 + GTK4) build + full ctest; macOS/Windows share the code, unbuilt.
macOS: application menu covers every keyboard-shortcut action (2026-09-04, v0.8.20). The menu bar now has an entry for Settings, Add Room, Find/Search Your Messages, Quick Switcher, and room navigation — previously only reachable via an invisible
NSEventmonitor with no menu presence. macOS build-verified, user-confirmed working.
Win32: accessibility bridge no longer crashes on shutdown (2026-09-04, v0.8.20).
UiaDisconnectProvidercould pump a reentrantWM_GETOBJECTinto the still-live window, reaching back into the bridge mid-teardown and invalidating its provider map. The registry entry is now dropped beforedetach()runs, anddetach()iterates a moved-out copy of the map as a second guard. Windows build-verified; the original crash was an intermittent teardown race, not reliably reproducible on demand — monitoring post-release rather than blocking on a forced repro.
Message-layout live preview (2026-09-03, v0.8.20). Appearance → Layout shows a live preview beside the message-layout combo box: two fake messages ("Hello my friend!" / "Hi! How are you?") rendered through the real Classic/Bubbles/IRC row renderer, updating immediately as the combo box changes. A new
MessageLayoutPreviewwidget hosts a real but inertMessageListView— no live room, no avatar/image providers — fed two hand-built rows in a fixed-size swatch. Windows-verified (win32 shell); user-verified on Qt6, GTK4, and macOS too.
Unread-thread indicator (2026-09-03, v0.8.20). A dot on the room-header threads button (accent-coloured when an unread reply pings you, neutral otherwise) whenever any thread in the room has replies you haven't read, plus a matching per-row dot in the thread-list panel. Read state is derived from MSC3771 threaded read receipts, which Tesseract now also sends — the open thread panel marks its thread read as it's viewed (new
send_thread_read_receiptFFI), and an optimistic local marker clears the dot before the receipt echoes back. The receipt targets the exact reply the user scrolled to (the acked event id is threaded through the FFI). Because Synapse's sliding-sync receipts extension does not echo the user's own threaded receipts back on the live path (element-hq/synapse#17247), the local marker is persisted to a dedicated per-accountthread_read_state.dbso the dot stays cleared across a restart; it survives "Clear all caches" and is cleared only by logout. The thread-list panel header also carries a "mark all threads as read" button (Lucidelist-checks, left of the close button, greyed when nothing is unread) — a batchedmark_all_threads_readFFI sends threaded receipts for every unread thread at once. v1 limitations: the ping check reads only the latest reply'sm.mentions(no push-rule / display-name / keyword evaluation), and a read on another device only clears the dot on that device once a new reply arrives.
Low power mode (2026-09-03, v0.8.20). Settings → General → Power:
Auto(default) /On/Off. When active the client stops all proactive background work and does nothing but keep the sliding-sync long-poll running — background timeline backfill, unread/favorite prefetch, bridge-status checks and the 2 s decoded-image GC timer halt (C++), and the SDK pauses the search-index crawl and per-room warm-check auto-pagination (one newset_low_power_modeFFI flag). Message sync, encryption sync and user-driven scroll-up pagination are never touched.Autoactivates when the machine is on battery or the OS energy-saver profile is on, with a ~20 s debounce against flapping; a subtle battery glyph appears in the status bar while active. On a desktop / mini PC with no battery the whole feature is hidden and inert (the Settings group isn't shown; a persistedOnis ignored).IPowerMonitoris a per-platform OS probe (UPower + power-profiles-daemon on Linux Qt/GTK,GetSystemPowerStatus+RegisterPowerSettingNotificationon Win32,NSProcessInfo+ IOKitIOPowerSourceson macOS), mirroring theIScreenLockDI pattern;PowerPolicyis the tested pref+signals resolver. Linux (Qt6 + GTK4) build + tests; user-verified on Windows and macOS too. (2026-09-15) Active low power mode now also freezes animated images/stickers/emoji at their current frame everywhere they play (timeline, emoji/sticker pickers) and only resumes playback while the pointer hovers that specific item; a newAnimImageCache::set_paused()gate backs this, gated per-item by hover so unrelated on-screen animations stay frozen. The image/video viewers are exempt and always play. Linux (Qt6 + GTK4) build + full ctest, 1829/1829 (+5); user-verified live. Windows/macOS share the code, unbuilt this session.
Joins route through via servers (2026-09-03, v0.8.20). Joining a room listed in a Space — or reached by a
matrix.to/matrix:permalink — that lives on another homeserver now works instead of failing with "join failed or cancelled".resolve_route_via()gathers routing servers from local state only (no network), in priority order: a permalink's?via=, the Space'sm.space.childvia list, then the room-id/alias domain. The list is threaded through join / knock / room summary;matrix_uri.rskeeps?via=instead of stripping it, and a failed join now surfaces the homeserver's real error. The Join button is also disabled while a join or knock is in flight, so a double click can't fire a duplicate request. User-verified end-to-end.
Roster sweep no longer freezes the client (2026-09-03, v0.8.20). The known-users roster build now reads members from the local store only (
members_no_sync()); previously it triggered an untimedGET /rooms/{id}/membersper freshly joined room, and after joining a Space one stalled request could sit forever inblock_onholding the shared FFI lock — libc++'s writer-preferringshared_mutexthen starved the UI thread.poll_presence_nowalso drops to a shared lock to remove the writer-priority starvation entirely.
ComboBoxmouse-wheel cycling (2026-09-03, v0.8.20). Scrolling the mouse wheel over a closed combobox cycles the selected option, matching native Win32/Qt/GTK combobox behavior; a no-op while expanded, disabled, or empty. Windows build-verified, user-confirmed working; user-confirmed on Qt6, GTK4, and macOS too.
Reply-quote inline formatting (2026-09-01, v0.8.20). Reply previews render the replied-to message's inline formatting (bold, italic, code, links, strikethrough) instead of its raw markdown-ish plain body; thread-preview snippets are unchanged for now. Linux (Qt6 + GTK4) verified; user-verified on macOS/Windows too.
Window minimum-size enforcement extended to pop-outs and call windows (2026-09-01, v0.8.20). The main window, room pop-outs, and call windows now all enforce a minimum size (previously only the main window did, width-only), so none of them can be resized down to unusable dimensions. Windows-verified; user-verified on macOS/Qt6/GTK4.
Selectable message layout (2026-09-01, v0.8.20). Appearance → Layout combobox —
Classic(default),Bubbles, orIRC. Bubbles right-aligns your own messages in a subtle rounded bubble (avatar dropped), hugging content up to 520px, and gives other messages a faint bubble too. IRC is a monospaced mIRC look-alike: one flat left-aligned column, every line[HH:MM] <nick>with a colour per person,* nickactions,-nick-notices, IRC-styled join/part + separator lines, and replies shown as a dim<sender> original messagescrollback line above the reply. Each layout is aMessageRowRendererstrategy; the shared row helpers take their layout-specific behaviour from that interface, not a global. Applied live, no restart. Linux (Qt6 + GTK4) verified; user-verified on macOS/Windows too.
Markdown tables render as an aligned grid (2026-08-31, v0.8.20). Tables draw as a column-aligned grid — cell borders, a tinted and ruled header row, per-column width sizing, and honored column alignment (
|:--|,|:-:|,|--:|). Wide tables shrink, then wrap, then clip to the message width. Text is selectable inside a cell and across cells (a rectangular block); copy yields plain text or tab/newline TSV. The HTML sanitizer passestext-alignthrough on<td>/<th>(canonicalized, no other CSS) so alignment survives from other clients. Linux (Qt6 + GTK4) verified; user-verified on macOS/Windows too.
jemalloc is the Linux process allocator (2026-08-30, v0.8.19). The Rust SDK links jemalloc as a
#[global_allocator], builtunprefixedso it also interposesmalloc/freefor the C++ side (Qt/GTK, bundled SQLite, GStreamer), and tuned (background_thread:true, 5 s decay) to return freed pages to the OS. Replaces stock glibc malloc, whose per-arena retention under this app's thread count let RSS climb to ~1 GB while the live heap stayed ~266 MB.TESSERACT_ENABLE_JEMALLOC, default on, Linux only. Build + test suite verified; RSS confirmed being released back to the OS as expected in long, media-heavy sessions.
Room media gallery backed by a persistent index (2026-08-30, v0.8.19). The full-screen room media view no longer re-scans the SDK timeline every time it opens. A per-room
room_mediatable inapp_cache.dbis seeded once (lazily, on first open) from the SDK's own event-cache store — no network — and kept current from the same timeline diff stream the search index uses. The gallery paints its newest page instantly from SQLite and pages older history from the index, only falling through to network back-pagination once the index is drained. The "Media (N)" badge now counts all synced history. Image + video only. Linux-verified; user-verified on macOS/Windows too.
Decoded-image caches garbage-collected to the visible set (2026-08-30, v0.8.19). Avatars and inline thumbnails are kept decoded only while on screen (plus a short grace), reclaimed by a generational mark-and-sweep that freezes whenever the user is idle — replacing a fixed TTL plus per-row pinning that let the decoded-bitmap heap grow unbounded (~200 MB over 80 s in a Qt6 heaptrack). A new in-RAM compressed-bytes tier keeps scroll-back re-decode instant. Qt6-verified; user-verified on GTK4, macOS, and Windows too.
"Clear all caches" actually clears everything now (2026-08-30, v0.8.19). Wipes the account's state, event-cache and media stores, closes tabs and pop-out windows, and re-fetches the room list from a fresh sync — keeping the crypto store and session, so no re-login or re-verification. Refuses while a call or device-verification is in progress. Room-list re-fetch after a wipe never worked before (the sliding-sync cursor lives in the crypto store, not the state store);
SyncService::expire_sessions()now resets it. User-verified on Linux, GTK4, macOS, and Windows.
Full-screen mode for the image and video viewers (2026-08-29, v0.8.20). A top-right toggle takes the whole window into OS full-screen with the media edge-to-edge (no caption; video controls auto-hide); ESC and click-outside still close the viewer. Wired for the main window and pop-outs on all four shells. Qt6/GTK4 verified; user-verified on Win32/macOS too.
Ctrl+Tab / Ctrl+Shift+Tab MRU room switcher (2026-08-27, v0.8.18). Alt-Tab-style recent-room cycling: hold Ctrl, tap Tab/Shift+Tab to move through recent rooms, release to jump, Escape to cancel. User-verified on Windows, Qt6, GTK4, and macOS.
DPI / HiDPI display-scale awareness across all four platforms (2026-08-24, v0.8.18). The UI now detects live DPI/backing-scale changes on all four platforms and rescales accordingly — native text-field image captures, avatar/thumbnail fetch size, popups, and fonts all track the new scale instead of going stale or blurry. Linux verified against the full test suite; Win32/macOS written to the same pattern but unverified.
Read-receipt timestamps and overflow grid popup (2026-08-24, v0.8.18). Read receipts now show a per-reader timestamp on hover, and a "+N" pill opens a scrollable grid popup listing every reader beyond the inline avatar cluster. Also fixes layout overlap between the receipt cluster and nearby hover-toolbar/pending-send/message-body UI.
Idle-TTL eviction for warm room/thread timelines (2026-08-07, v0.8.18). A 30-minute idle timer now evicts any room/thread timeline that isn't actually on-screen, even if it's a tab, favorite, or pinned pop-out — bounding memory that the warm-subscription LRU previously exempted indefinitely.
Custom Windows 11-style title bar (2026-08-26, v0.8.18). A self-drawn extended title bar on
MainWindowand pop-outRoomWindows matching the app's Mica/dark-caption theming, with custom caption buttons, Snap Layouts, and right-click system menu all preserved. Title text renders through BetterText for proper color-emoji support, and the window title now reflects the active room on all four platforms (later unified behind a single sharedShellBase::compose_window_title_()). User-verified on Qt6, GTK4, and macOS.
DirectComposition presentation (2026-08-26, v0.8.18). Windows surfaces now present via DirectComposition instead of a plain DXGI swap chain, fixing a visible "stretch" artifact during interactive window resize.
Progressive video streaming (2026-08-26, v0.8.18). Fast-start MP4/MOV videos now begin playing while still downloading, on all four platforms, with a draggable buffered-range scrub bar and disk caching of completed downloads. Also fixes the video/image lightbox not cancelling its in-flight fetch on close (audio could keep playing after close), a GTK4 pipeline bug that broke all video playback, and a macOS streaming loader that stalled forever waiting on content length. User-verified on GTK4 and macOS.
In-thread search (2026-08-25, v0.8.18). Adds find/search scoped to a single thread, plus a persistent filter field on the thread list, and dims the main timeline whenever the thread-list panel is open.
Room knocking (MSC2403) (2026-08-15, v0.8.18). Lets a user request to join a knock-restricted room, track and cancel a pending request, and lets moderators review and accept/deny/deny-and-ban requests — wired uniformly across all four platform shells. Also fixes federated knocks failing silently due to a missing server routing hint.
Full room-history export (2026-08-16, v0.8.18). Exports a room's complete message history to plain text or HTML (optionally with images, optionally zipped), with resumable checkpointing and progress UI. A later pass added a time-range selector and a "Stop & save" action, and restyled the HTML export to match the live timeline's appearance. User-verified on Windows, Qt6, and macOS; GTK4 still unverified.
Optional local crash handler (2026-08-13, v0.8.18). An opt-in, off-by-default crash handler (Settings → Advanced → Diagnostics) writes a plain-text native/Rust-panic stack trace to disk, with nothing transmitted anywhere.
Per-room compose draft persistence (2026-08-14, v0.8.18). Unsent compose text, a staged attachment, and the caret position now persist per room instead of being cleared on every room switch.
Windows D2D rendering reliability: flip-model presentation and device-loss/hang recovery (2026-08-20 through 2026-08-24, v0.8.18). Fixes a class of bugs where the Win32 window would silently stop repainting with no crash or error, by switching the main window to flip-model swap-chain presentation and correctly detecting and recovering from device loss/hang.
Use MSC4491 atomically when the homeserver advertises support (2026-08-06, v0.8.18). When a homeserver advertises MSC4491, room/DM creation now attaches the invite reason atomically inside the
createRoomcall itself, instead of the two-step create-then-invite fallback.
Optional invite reasons: room creation,
/invite, quick-switcher DMs, and display on receipt (2026-08-06, v0.8.18). Room creation,/invite, and the quick switcher's DM flow now accept an optional invite reason, sent as plain text even in encrypted rooms; a received reason also renders on the invite card. Implemented via the stable per-invite reason field, since no homeserver yet supported MSC4491 at the time.
Native text controls render into the canvas instead of overlaying it (2026-08-05, v0.8.18).
NativeTextField/NativeTextAreanow render into the canvas via an offscreen image capture instead of floating on top of it, so they participate in clipping, opacity, and paint order like any other canvas content. Text input is still backed by real native controls for IME/selection; only painting moved into the canvas.
Windows: MSIX packaging (Store + direct) alongside NSIS, and taskbar shell integration (2026-08-01 – 2026-08-05, v0.8.18). Adds MSIX packaging for both Microsoft Store and direct distribution alongside the existing NSIS installer, plus deep taskbar integration: unread/mention overlay icons, thumbnail-toolbar controls, upload progress, and Jump Lists across all top-level windows.
Linux: MPRIS, GNOME Shell search provider, and KRunner integration; macOS: Now Playing and Spotlight search (2026-08-02 – 2026-08-04, v0.8.18). Voice/audio messages now expose MPRIS media controls on Linux and Now Playing controls on macOS. Room/contact search is also surfaced through GNOME Shell's search provider, a KRunner plugin, and macOS Spotlight.
Linux: Flatpak/Flathub manifest and AUR packaging hardening (2026-07-31 – 2026-08-03, v0.8.18). Adds a Flathub-ready Flatpak manifest (offline-vendored Rust deps) and hardens the AUR
tesseract-matrix/tesseract-matrix-gitPKGBUILDs (license, arch, LTO, dependency fixes), both validated with real local builds.
Linux: GTK4 rendering fixes; single-instance guard shared with Qt6 (2026-07-31, v0.8.18). Fixes several GTK4 rendering bugs — stalled sticker/emoji animations, a large avatar-scaling CPU cost, missing room-list text ellipsis, a stuck pagination spinner, and a lingering placeholder — and adds a shared single-instance guard so launching one build while the other is already running gets forwarded instead of opening a second instance against the same store.
Windows: hide
main_app_surface_until session restore completes (2026-08-04, v0.8.18). Fixes the Windows shell briefly showing the room-list sidebar alongside the branding/login page before session restore completes, matching the other three platforms' behavior.
Per-image scaled-surface LRU cap raised from 4 to 8 (2026-08-03, v0.8.18). Raises the per-image pre-scaled-surface cache size on the GTK/Cairo and Qt backends, fixing repaint thrashing for avatars drawn at many different sizes across the UI.
Floating date badge in the message timeline (2026-07-30, v0.8.17-unreleased). A small pill fixed to the top-center of the timeline shows the date of whatever day is currently scrolled to, appearing and disappearing together with the scroll-to-bottom pill.
Redundant network fetches on room switch fixed (2026-07-30, v0.8.17-unreleased). Revisiting an already-open room no longer re-fetches a fresh page of history or re-runs other per-switch work on every visit — each now runs once per warm subscription.
Desktop notification quick-reply, across all four shells (2026-07-30, v0.8.17-unreleased). Desktop notifications now support replying inline without opening the app, on Windows (toast), macOS, and Linux (KDE inline-reply and the portal's standard reply action). Replies send as proper threaded replies. Also fixes a concurrency bug where Linux notifications could permanently wedge after the first one.
Launch-at-login support, across all four shells (2026-07-30, v0.8.17-unreleased). A new Settings → General toggle (default off) registers the app with each OS's native login-item mechanism; a launch via autostart starts hidden to the tray only when a saved session restores silently.
PopupMenumigrated to a native popup surface (2026-07-29, v0.8.17-unreleased).PopupMenunow renders as a genuine OS popup window instead of a canvas overlay, fixing it never being able to z-order above native text controls. Also fixes a menu not closing on outside click, a frozen entrance animation, and a menu left floating after alt-tab.
matrix-sdkinit hardened; new sessions' local store encrypted (2026-07-29, v0.8.17-unreleased). Bounded request retries/timeout and auto-enabled key backup alongside cross-signing bootstrap. New logins now encrypt the local SQLite store with a per-session key held in the platform secret store; existing sessions stay unencrypted since matrix-sdk has no in-place migration path.
Two pop-out room window bugs fixed (2026-07-29, v0.8.17-unreleased). Popping a room out no longer leaves it appearing open in the main window at the same time, and closing the only open tab now deselects to the empty state instead of refusing to close. Also fixes the pop-out compose bar's native text field showing through overlays that should cover it.
Right-click context menu on room list rows (2026-07-29, v0.8.17-unreleased). Open in tab / Open in window / Leave room (with confirmation), with the open-in items disabled when the room is already open in that context.
Image-pack editor scroll/field bugs fixed (2026-07-29, v0.8.17-unreleased). Fixes three bugs in the sticker/emoji image-pack editors: the personal pack editor's shortcode field never appeared at all, the room/space editor's shortcode field didn't track list scrolling, and beginning an edit could visibly jump-scroll the pack list.
Per-class lifetime guards unified onto
tk::EnableWeakSelf<T>(2026-07-29, v0.8.17-unreleased). Replaces ~16 independent ad hoc shared_ptr/weak_ptr lifetime-guard patterns across the codebase with a single shared mixin, and fixes three confirmed unguarded async use-after-free gaps found along the way.
Room-list rebuild skipped on unchanged presence (2026-07-28, v0.8.17-unreleased). The room list no longer does a full rebuild on every presence poll tick when the polled state hasn't actually changed.
Flash-highlight on quote/jump scroll (2026-07-28, v0.8.17-unreleased). Reply-quote clicks and other jump-to-message scrolls (search results, thread reveal, pinned banners) now flash-highlight the destination row so it's clear which message you jumped to.
Fixed the startup splash freezing during account restore (2026-07-28, v0.8.17-unreleased). The animated splash screen could freeze solid throughout account restore on all four platforms. Also moves a synchronous image-pack cache rebuild and session restore/
start_sync()off the UI thread, and caps each restored account's Tokio runtime at 2 worker threads instead of defaulting to the core count.
Gallery pagination ported to
RoomPanefor pop-out windows (2026-07-28, v0.8.17-unreleased). Pop-out room windows now share the same media-gallery backward-pagination implementation as the main window instead of lacking gallery pagination entirely.
macOS: fixed a GIF-strip color-channel swap (2026-07-28, v0.8.17-unreleased). Fixes an intermittent red/blue channel swap during GIF-strip playback on macOS, caused by a sample buffer aliasing pooled storage.
RoomPane: main-window and pop-out room wiring unified (2026-07-27/28, v0.8.17-unreleased). Consolidates near-identical per-room display/composer/send-edit-react-pin wiring that was duplicated across the main window and pop-out windows on all four shells into a sharedRoomPaneclass. Fixes several real bugs surfaced by the consolidation: pop-out composer popups not auto-dismissing, blocking media sends, silent failures on send/edit/topic-save, and a stale leave-room handler on Win32, among others.
Client-side video-thumbnail generation hardened (2026-07-26, v0.8.16). Fixes videos with no server-supplied thumbnail getting stuck showing only a play-button placeholder, and generated thumbnails not persisting to the disk cache. Adds a real Windows first-frame generation path and an authenticated Range-GET prefix fetch so all four platforms can generate a preview without a full download.
Startup account restore no longer blocks the UI thread (2026-07-26, v0.8.16). Restoring saved accounts at startup no longer freezes the window; it now runs off the UI thread with a live "Restoring session…" status shown during the process.
BrandViewhypercube wireframe background (2026-07-26, v0.8.16). Adds a faint, continuously-rotating 4D hypercube wireframe behind the icon/name/version stack on the splash/branding screen, on all four backends.
MSC4391 in-room bot commands (2026-07-25, v0.8.16). Bot commands declared via
m.bot.command_descriptionnow appear in/commandautocomplete alongside built-in commands, with a guided argument-entry flow that sends structured invocations. Qt6/GTK4 fully verified; AppKit's pop-out window has parity but its main window doesn't yet share it.
Adaptive narrow-window layout (2026-07-24, v0.8.16). Below a 600px breakpoint, the room-list/room-view split collapses into a single pane with a back button (and Escape) to return to the list. Also adds a real minimum window width derived from the compose bar's own footprint, and an overflow menu for room-header actions that don't fit.
Native context menus now follow the app theme; multi-language pronoun tooltip (2026-07-23, v0.8.16). Native context menus (user-info, message Copy, sticker save) now follow the in-app theme selection instead of the OS theme, on Qt6 and Windows. The Pronouns row now shows a tooltip listing every configured language/pronoun pair. Also hardens Linux shutdown (a blocking self-pipe socket, an uninstalled GTK4 shutdown handler) and makes blocking SDK sends cancellable so Ctrl+C mid-send doesn't wait out the network timeout.
Multi-language pronoun editor; searchable timezone picker (2026-07-22, v0.8.16). The pronouns field becomes a repeatable per-language editor matching MSC4247's multi-entry shape, and the free-text timezone field is replaced by a searchable picker generated from real tzdata. Membership-narration text now resolves the acting user's declared pronoun. Also fixes a profile-field save silently reverting a sibling field, and gates the room-header call button on actual power level.
Combined Join/Create "Add Room" dialog (2026-07-21, v0.8.16). Replaces the separate per-platform Join Room dialog with a shared overlay and adds a previously-missing Create Room flow, combined into one tabbed
AddRoomViewmodal. Also fixes an avatar picker showing initials instead of the picked image until reopened, and a few popup-focus/resize glitches.
Trackpad momentum (kinetic) scrolling, all four platforms (2026-07-20, v0.8.16). Scrollable views now ease to a stop with momentum after a trackpad gesture instead of jumping by discrete wheel deltas, on all four platforms. Also includes a visual polish pass: stronger active-room highlight, removed row separators, square compose button icons, and in-window (non-native-popup) emoji/sticker pickers.
Media-caption editing; livekit build fix; emoji font packaging (2026-07-19, v0.8.16). Editing Image/File/Video captions now preserves the original media instead of rebuilding the event as plain text. Separately fixes an intermittent livekit build failure and declares an emoji font as a runtime dependency on Linux packaging.
"Developer mode" setting; global toast system (2026-07-17, v0.8.16). Adds an off-by-default "Developer mode" setting that enables a "Copy event source" message-menu item. Also consolidates three near-duplicate toast-notification implementations into one shared
tk::Hostmechanism.
MatrixRTC calls always-on (2026-07-17, v0.8.16). MatrixRTC voice/video calls are now a permanent part of every build on all four platforms instead of an opt-in CMake flag, removing all the associated
#ifdefgates. Also fixes a link-order bug that broke the Windows/Linux/macOS calls-only libraries once they became unconditional.
Tab traversal now follows widget geometry, not insertion order (2026-07-17, v0.8.16). Tab/Shift-Tab order is now computed from actual on-screen position (reading order) instead of child-insertion order, so grids, pickers, and reordered widgets now tab in visual order.
Fixed unfocused widgets reacting to stray keys; theme-picker gained real keyboard access (2026-07-17, v0.8.16). Several widgets (
ComboBox,Button,CheckButton,SwitchButton,ListView,GridView,TabBar) could react to a keypress while unfocused elsewhere in the tree; all are now gated on actual focus. The Appearance settings' theme picker also gained real keyboard focus and arrow-key cycling.
macOS: fixed the compose box losing focus to any click elsewhere in the window (2026-07-17, v0.8.16). Clicking the room list, user info panel, or timeline on macOS silently stole keyboard focus away from the compose box before the app's own click handling ever ran. Also refocuses the compose box after declining an incoming call banner.
Fixed the recovery-key/passphrase fields becoming unfocusable on every repaint (2026-07-17, v0.8.16). On Qt, the recovery-key/passphrase fields during Recover-mode login could become permanently unfocusable because a hide-then-reshow on every repaint silently cleared focus.
Linux: shut down gracefully on SIGINT/SIGTERM (2026-07-17, v0.8.16). Ctrl+C previously killed the process outright, skipping the destructor that flushes session/token state — which could corrupt a just-refreshed OAuth token and wipe the local account on next launch. Signals now route through the normal graceful-quit path.
Configured a default media-retention policy on every client build (2026-07-17, v0.8.16). The media cache previously never shrank under "Clear all caches" because no retention policy was configured. Now defaults to a sensible cache size/per-file/expiry policy on every account.
Stickers now support replies (2026-07-16, v0.8.16). Selecting a sticker while the compose bar has an active reply now attaches the reply relation like any other message. Also fixes sticker timeline rows never rendering their replied-to quote block.
Tab traversal scoped to MainAppWidget-level overlays (2026-07-16, v0.8.16). Fixes Tab/Shift-Tab cycling through background widgets while a top-level overlay (image/video viewer, confirm dialog, quick switcher, search, forward picker, encryption setup, QR grant) is open, instead of staying scoped to the overlay.
Every
tk::Widgetnow constructed through a Host-aware factory (2026-07-16, v0.8.16). Widget construction now goes through a factory that makes the owningHostavailable from the first line of any constructor, replacing hand-threadedHost*parameters throughout. Fixes a real release-build-only crash caused by undefined behavior in the previous approach.
Real keyboard-focus system; native text fields now live directly in the widget tree (2026-07-16, v0.8.16). Adds Tab/Shift-Tab traversal and a keyboard-only focus ring, and migrates every native text-entry surface across the app from shell-polled overlays to self-positioning widgets. Adds a default-focus policy: the compose box is now focused whenever nothing else needs attention, replacing ~40 scattered manual refocus call sites. Also fixes Tab/Shift-Tab leaking into background controls while a room modal is open.
Reaction chips redesigned; mixed text/emoji reaction keys (2026-07-14, v0.8.16). Reaction pills get a smaller, tighter shape of their own instead of sharing geometry with other pill UI, and now render mixed text/emoji reaction keys (MSC4027) correctly, each run sized and aligned appropriately.
/locationslash command; three-bug GeoClue2 fix along the way (2026-07-17, v0.8.16)./locationsends a one-shot device location fix as anm.locationevent with no confirmation step, on all four platforms including pop-outs. Along the way, fixes three real bugs in the Linux GeoClue2 backend that had shipped unwired and never worked: wrong D-Bus bus, a broken in-flight-request check, and a dangling cancellable.
Windows: fixed a use-after-free in native text field/area destruction (2026-07-14, v0.8.15). A repeatedly opened/closed transient text control (search bar, quick switcher, etc.) left a dangling registry entry that could cause a use-after-free on the next theme change. Windows-only; unverified in this environment pending an on-platform build.
Six unbounded caches found in a memory-usage audit, bounded or pruned (2026-07-14, v0.8.15). A memory audit found and fixed six independent caches (Rust media-fetch hints, two SQLite backoff tables, five
ShellBasemaps, a receipt map, and animated-image frame decoding on three backends) that grew unbounded for the life of a session. Verified on Qt6/GTK4; Windows/macOS changes mirror the same pattern but weren't build-verified here.
Widget removal hardened against reentrant/self-destroying callbacks (2026-07-14, v0.8.15). Closes two subtle use-after-free hazards where a widget's own callback could destroy itself or a sibling mid-invocation:
Host's tracked widget references are now weak, and actual subtree destruction is deferred to the next event-loop turn. Verified on Qt6/GTK4; Win32/macOS mirror the same pattern but weren't build-verified here.
Legacy username/password login for non-OIDC homeservers (2026-07-13, v0.8.15). Adds
m.login.passwordas a fallback login path for self-hosted homeservers with no OIDC/MAS provider, auto-detected from the homeserver's advertised login flows, behind a default-on build flag. Verified end-to-end against a real self-hosted Synapse with no OIDC/MAS, on all platforms.
File drop and drag-hover dispatch through the widget tree (2026-07-13, v0.8.15). File drop and drag-hover now dispatch through the normal widget tree instead of requiring a hand-rolled handler per drop target, so each target (compose bar, pack editors) claims its own drop and paints its own localized highlight. Also fixes the native compose text field swallowing file drags as pasted text on Qt6/macOS/GTK4. Confirmed working on-platform on all four platforms.
Bigger emoji in the composer and room-list preview (2026-07-14, v0.8.15). Emoji in the compose bar and room-list message preview now render at message-body size instead of plain body size, matching the timeline, resizing live as-you-type on all four platforms. Also fixes a one-keystroke lag on Qt/Win32 and adds missing single-line-ellipsis truncation to the rich-text renderer.
macOS: fixed composer inline-emoji resize corrupting glyph layout (2026-07-14, v0.8.15). Fixes a macOS-only bug where the bigger-emoji feature above could corrupt a just-typed emoji's glyph layout, leaving it invisible until a later edit.
macOS: room-list preview text no longer drifts when it contains emoji (2026-07-14, v0.8.15). Fixes a macOS-only bug where the bigger-emoji feature above could make the room-list preview's text baseline drift downward whenever the preview contained emoji.
Slash-command popup: full list on no match, plus scrolling (2026-07-14, v0.8.15). The
/commandpopup no longer hides itself when the typed prefix matches nothing — it falls back to the full command list — and now scrolls instead of hard-capping at 8 visible rows.
Room header: topic-link clicks no longer leak onto the room name/avatar (2026-07-14, v0.8.15). Fixes clicking the room name or avatar occasionally opening the room topic's link instead, on Qt6 and macOS.
Room Settings → Permissions: aligned combo boxes across groups (2026-07-14, v0.8.15). Combo boxes across the Permissions tab's four groups (Default Role, Messages, Membership, Advanced) now align to a shared label-column width instead of each group sizing independently.
Login: homeserver field's drawn border no longer lingers in the password form (2026-07-14, v0.8.15). Fixes the homeserver field's rounded-rect border staying painted after switching to the username/password login form.
Sticker right-click save menu no longer leaks through room overlays (2026-07-12, v0.8.14). Fixes right-clicking over Room Settings/Room Info/User Profile popping the sticker-save context menu from stale timeline content underneath.
Inline custom-emoji shortcode tooltips in the timeline (2026-07-12, v0.8.14). Hovering a custom MSC2545 emoji in a message timeline now shows its
:shortcode:tooltip, matching the emoji/sticker picker grids.
Personal image-pack editor: drag-drop wired (2026-07-12, v0.8.14). Dropping an image onto the personal image-pack editor in Settings now works — the Settings window's own surface previously wasn't wired for file drop at all.
Fixed: the Emoji/Sticker picker's shortcode tooltip froze every animation in the app on Windows while visible (2026-07-12, v0.8.14). A self-sustaining repaint loop in the shortcode tooltip starved the Win32 timer that drives animation frame advance app-wide while the tooltip was shown. Windows-specific; unverified live in this environment.
Custom MSC2545 emoji now render inline in the timeline on macOS (2026-07-12, v0.8.14). Custom emoji previously rendered fine in the composer and pickers but not in the message timeline itself on macOS. macOS-only fix; unverified in this environment pending a build check.
Emoji/sticker pickers and the shortcode popup now surface packs from any Space the current room belongs to (2026-07-12, v0.8.14). Extends the existing personal/current-room/subscribed-room pack scopes with a fourth: packs from every Space (direct or nested) that contains the current room. Verified on Qt6/GTK4; Win32/macOS mirror the same pattern but weren't build-verified here.
Native-field theming now traverses the widget tree instead of a hand-maintained per-shell field list (2026-07-12, v0.8.14). Every native text field now re-themes itself automatically via a new
Widget::apply_theme()tree walk instead of relying on each shell to remember to push color updates to every field by hand. Fixed several fields that the old manual lists had missed entirely (Qt6's Settings/ Join-Room dialogs, macOS's join-room dialog permanently stuck in light mode). Verified on Qt6/GTK4; Win32/macOS mirror the same pattern but weren't build-verified here.
Generic
tk::Hosttooltip system replaces 8 hand-rolled hover/tooltip implementations and 4 duplicate per-platform native tooltip codepaths (2026-07-12, v0.8.14). Every tooltip in the app is now driven by one sharedtk::Hosttooltip mechanism with a consistent 500ms show-delay, replacing 8 independently hand-rolled implementations (two of which, macOS and GTK4, weren't even using a real native tooltip API). Verified on Qt6/GTK4; Win32/macOS mirror the same pattern but weren't build-verified here.
Native text fields no longer go stale on a theme change; forward-picker close wired on all four shells (2026-07-12, v0.8.14). Fixes 11 of Qt6's 13 native text fields (including the quick switcher) keeping unreadable colors after a theme change, and fixes Escape/outside-click never closing the forward-message picker's field on any of the four shells. Verified on Qt6/GTK4; Win32/macOS mirror the same pattern but weren't build-verified here.
Media lightbox pagination leak + gallery backpressure fixed (2026-07-12, v0.8.14). Fixes several bugs where opening the room media viewer left background work running after close or room switch: the video lightbox not swallowing wheel input (driving background pagination), an unabortable pagination task that could block shutdown, and unthrottled pagination in media-sparse rooms outrunning the row renderer.
Image pack editor: multi-pack room/space editor + global settings tab, fully wired end to end (2026-07-11, v0.8.14). A new Room Settings "Emojis & Stickers" tab edits every MSC2545 sticker/emoji pack in a room or space at once (rename, usage toggle, per-image add/remove/shortcode, drag-drop and paste), gated behind the room's actual power levels. A matching global Settings tab covers the account-wide personal pack and the list of subscribed room packs. Pack discovery and reads now combine both the stable and legacy unstable MSC2545 event names so partially migrated rooms/accounts don't lose images. Qt6 build-verified end to end; GTK4 user-verified; Win32/macOS verified by static review only. Also fixes several smaller bugs found along the way (animated stickers not rendering in editor tiles, shortcode-field/paste edge cases, an i18n-pseudo generator bug).
Edited plain-text messages no longer render as a bare
*(2026-07-11, v0.8.14). Fixes edited messages with no formatting sometimes rendering as a literal*instead of the edited text.
Windows: clipboard image paste restored in the BetterText composer (2026-07-11, v0.8.14). Fixes Ctrl+V no longer pasting a clipboard image into the composer after it moved to the BetterText control.
Fixed a runaway pagination loop in the room media gallery (2026-07-10, v0.8.14). Closing the room media gallery didn't actually stop its backward-pagination retries, producing an unbounded pagination loop that could delay message send confirmation and even hold up app shutdown. Shared code, so all four platforms get the fix.
Pop-out window feature-parity audit (2026-07-10, v0.8.14). An audit found and fixed 13 places where a feature worked in the main window's room view but not in pop-out room windows (attachment save dialogs, jump-to-message, pin/unpin, edit-last-message, retry/abort send, inline autoplay, forward picker, media gallery, and more).
MSC2545 image packs now combine stable + unstable event names (2026-07-10, v0.8.14). Pack loading now reads both the stable and legacy unstable MSC2545 event names and merges them, instead of stopping at whichever it found first — fixing images silently disappearing on partially migrated rooms/accounts.
Linux OS dark/light mode detection fixed on Qt6 and GTK4 (2026-07-10, v0.8.14). Fixes dark mode never being detected via Qt6's D-Bus fallback path, and GTK4 not picking up live theme changes on Wayland — both now query the same XDG desktop-settings portal.
Windows composer mention pills render as real inline chips (2026-07-10, v0.8.14). @mentions typed in the Windows composer now render as a colored inline chip instead of plain
@Nametext, matching the other three platforms.
Pinned-events room-list fingerprint fix (2026-07-10, v0.8.14). Fixes the pinned-messages banner and the Pin/Unpin menu label going stale after a pin/unpin that didn't also touch some other room field.
Faster local echo under background load (2026-07-09, v0.8.14). Fixes a just-sent message's local echo sometimes taking seconds to appear under load, caused by tokio async-worker starvation in the SDK. Moving the room-timeline-build work onto tokio's blocking pool frees the async workers for the latency-sensitive send/diff tasks. Verified on Qt6 and GTK4; Win32/macOS mirror the same pattern pending an on-platform build.
Custom MSC2545 emoji inline, end to end (2026-07-06, v0.8.14). Picking a custom emoji from the picker or shortcode autocomplete now inserts a real inline image in the composer and sends it as a proper
<img data-mx-emoticon>tag that renders inline in the timeline, on all platforms. Also fixes an XSS where an attacker-controlled shortcode was interpolated unescaped intoalt/title.
BetterText: D2D/DirectWrite text backend on Windows (2026-07-09, v0.8.14). Vendors a new from-scratch D2D/DirectWrite text control for Windows native text fields, adding inline IME composition, placeholder/password rendering, and real inline bitmap rendering — so custom emoji show inline in Windows compose fields like everywhere else.
Copy image to clipboard from the lightbox (2026-07-07, v0.8.14). The full-window image viewer gains a "copy" button that copies the displayed image to the system clipboard, on all four platforms, with a confirming toast. Verified on Qt6.
Room Permissions self-lockout warning (2026-07-06, v0.8.13). The Permissions tab now warns and disables Accept if a staged change would lock the current user out of ever editing room permissions again.
Idle-CPU and animation-repaint performance fixes (2026-07-04, v0.8.12-unreleased). Reduces idle CPU usage by disabling an unneeded store-lock lease renewal, fixing an animated sticker/GIF forcing a full-UI repaint on every frame instead of just its own region, and avoiding re-establishing a hardware video decode session every time an already-seen video is revisited.
Voice message auto-advance (2026-07-01, v0.8.12-unreleased). A voice message that finishes playing on its own now automatically starts the next voice message from the same sender in the room, if any.
Scroll-position stability during pagination (2026-07-01, v0.8.12-unreleased). Loading more history — backward while scrolled to the top, or forward while browsing old messages — no longer shifts what the user is looking at. Auto-scroll-to-bottom is now correctly limited to a live message arriving while already pinned to the tail.
Room join/leave timeline events (2026-07-02, v0.8.12-unreleased). An opt-in setting (default off) surfaces join/leave/kick/ban/invite/knock membership transitions in the message timeline. Consecutive same-action events collapse into one summary line, expandable into individual lines on click.
Room settings — edit name/topic/avatar (2026-07-02, v0.8.12-unreleased). A wrench icon in the room-info panel opens a full-panel view for staging edits to the room's avatar, display name, and topic, gated per-field by power level. Nothing is sent until Accept.
Screen-share picker thumbnails + Linux stability (2026-07-03, v0.8.12-unreleased). The screen-share picker now shows real per-source thumbnails instead of placeholder tiles. Also fixes a black-tile bug on Linux, a UI freeze on stopping a stalled capture, leaked portal sessions, solid-black Windows capture of GPU-composited apps, and a macOS thumbnail deadlock.
Location map click-through (2026-07-04, v0.8.12-unreleased). Clicking (not panning) a location message's embedded map opens it on openstreetmap.org, centred on the pin.
Media caption linkify (2026-07-01, v0.8.12-unreleased). Captions on image/file/video messages now render through the same rich-text pipeline as regular message bodies, so bare URLs in a caption are clickable links instead of plain text.
Room-switch viewport auto-backfill (2026-07-01, v0.8.12-unreleased). Switching rooms now proactively fetches more history if the first page doesn't fill the viewport, instead of waiting for a manual scroll gesture. Benefits both the room timeline and the thread panel.
MatrixRTC voice/video calls (2026-06-25, v0.8.12-unreleased). Native LiveKit-based MatrixRTC calls (MSC4143), interoperating with Element X and Element Call. The call overlay supports docked, floating, and pop-out-window modes with mute/video/hang-up controls, and incoming calls surface a ring notification. Hidden when the server doesn't advertise LiveKit support, or for bridged rooms.
/selfieslash command (2026-06-25, v0.8.12-unreleased). Typing/selfiein the composer opens a full-surface camera overlay with a 3-second countdown; the captured still is inserted as a compose-bar attachment. Disabled while a call is active.
Audio/video device selection (2026-06-25, v0.8.12-unreleased). Settings → Media gained microphone, speaker, and camera selection dropdowns, applied at the next session start.
Bridged-room detection (2026-06-27, v0.8.12-unreleased). Rooms bridged via a third-party network (MSC2346) suppress the call button and threads panel and show a Bridged badge in the room-info panel.
Space root view (2026-06-28, v0.8.12-unreleased). Selecting a joined space itself (rather than drilling into a child room) now shows a centred summary panel with avatar, name, topic, and joined/unjoined child counts.
Phone icon for active calls (2026-06-27, v0.8.12-unreleased). The room list shows a phone icon on any room with an active call.
Room-switch media fetching overhaul (2026-06-30, v0.8.11). Fixes media requests appearing to freeze in rooms that trigger many at once, and the in-flight indicator lingering after leaving a media-heavy room. The main cause was every room switch eagerly fetching an avatar for the entire membership list up front; avatars now fetch on demand instead, on all four shells.
macOS thread-reset stack-overflow fix (2026-06-30, v0.8.11). Fixes a macOS-only crash when a thread's timeline reset while the message list was mid-layout, caused by a synchronous relayout call recursing into itself.
Group unread rooms (2026-06-24, v0.8.9). An optional "Group unread rooms" toggle adds an Unread section above Favorites in the room list, collecting every room with a visible unread indicator — including rooms nested inside spaces, previously invisible at the root list. Rooms leave the section automatically when read.
Colored sender display names (2026-06-24, v0.8.9). Sender names in the message timeline are tinted using a hash of the user's Matrix user ID, mapped into an 8-hue palette tuned for contrast in both light and dark mode. The color stays stable across display-name changes.
Space topic preview in room list (2026-06-24, v0.8.9). Space entries in the Spaces section now show the space's topic as the one-line preview instead of a last-message snippet, falling back to name-only when the topic is absent.
Forward message (2026-06-19, v0.8.8). A "Forward message" item in the message menu opens a room picker to resend the message's content, with all msgtypes preserved, to one or more other rooms.
macOS dock badge + dock-click unread navigation (2026-06-17, v0.8.6). The macOS dock icon shows the total notification count as a badge across all signed-in accounts. Clicking it raises the window and navigates to the highest-priority unread room, matching tray-click behavior on the other platforms.
Win32 body font raised 1 pt above the OS default (2026-06-17, v0.8.6). Raises the Windows UI's base font size by 1pt above the OS default, since the stock size reads noticeably small next to modern chat clients; every font role scales accordingly.
Async space-summary and server-info FFI (2026-06-17, v0.8.5). Space-summary and server-info fetches are now asynchronous instead of blocking a C++ worker thread for the full HTTP round-trip, preventing concurrent fetches from saturating the worker pool and making the client unresponsive.
System font scaling across all four backends (2026-06-15/16, v0.8.5). Tesseract now reads the OS body font size at startup on all four platforms and derives every UI font role as an offset from it, so the UI scales naturally with the user's OS accessibility font-size setting.
Inline emoji scaling (2026-06-16, v0.8.5). Unicode emoji in message bodies render at ~125% of body font size; emoji-only captions beneath media render at 2× body size, matching standalone emoji messages.
Automatic update checker (2026-06-16, v0.8.5; AUR RPC variant 2026-09-01, v0.8.20). A background checker queries GitHub releases at startup and periodically, showing an in-app banner when a newer version is available. Controlled by a Settings → Privacy toggle. Builds packaged for the AUR (
-DTESSERACT_AUR_PACKAGE=<pkgname>, set by thetesseract-matrixPKGBUILD) query the AUR RPC API instead of GitHub, so the check tracks the AUR package version.
MSC4133 extended user profiles (2026-06-14, unreleased). Adds Pronouns, Timezone, and Biography fields to account settings and the user-profile panel, backed by MSC4133 account data with stable/unstable key fallback. Wired on all four shells.
Unjoined space-children section +
RoomPreviewView(2026-06-14, unreleased). Navigating into a space now shows a collapsible "Not joined" section listing child rooms the user hasn't joined; clicking one opens a preview panel with room details and a Join button, without changing the active room.
Block-level Markdown rendering (2026-06-13, unreleased). Headings, lists (including nested), blockquotes, and tables now render visually in the message timeline across all four canvas backends, complementing the existing inline styles (bold, italic, code, strikethrough, links).
Full-text message search, incl. encrypted rooms (2026-06-11/13). A global search overlay (Ctrl+Shift+F / ⌘⇧F) searches your message history, including encrypted rooms, via a local opt-in SQLite FTS5 index of decrypted message bodies (off by default, since it stores decrypted text at rest). Results show room, sender, and snippet, and clicking jumps to the message. Verified on Qt6.
In-room find-in-conversation search bar (2026-06-13). Ctrl+F / ⌘F opens a search bar anchored below the room header that highlights matching rows in the timeline and navigates between hits, auto-paginating older history when needed to find more matches.
Shared jump-to-date picker (2026-06-13, unreleased). Replaces the four platforms' separate native date pickers with one shared calendar widget for jumping to a specific date in a room's history.
Room-switch performance overhaul (2026-06-11, unreleased). Switching rooms is now instant and flicker-free: the old room's rows clear immediately, a loading spinner appears only if the load outlasts ~500ms, and a still-live timeline is reused instead of rebuilt (bounded by a new warm-subscription LRU). Verified on Qt6, GTK4, Win32, and macOS.
Multi-window: one window per account. Ctrl+click an account in the picker opens it in its own window; clicking an account that already has a window raises it instead of switching in place. Each account's SDK event bridge can follow whichever window is showing it, and closing a secondary window no longer stops sync for shared accounts. Verified on Qt6.
Unread-room message prefetch. Rooms that quietly accumulate unread messages (unread but not muted) now have their recent messages warmed into the SDK cache ahead of time, so opening them renders instantly instead of fetching on click. Default-on.
Pre-launch hardening + decomposition (2026-06-09, unreleased). A full-tree code review drove a large correctness/safety/dedup pass and the start of a god-object decomposition: shells routed through shared
ShellBasehandlers, a multi-window/logout use-after-free and an FFI aliasing bug closed, and ~1,250 lines of cross-shell duplication hoisted into shared code. No user-facing feature change.
Unified Lucide icon set. The composer, message hover-action bar, media viewers, and other UI chrome now render from monochrome Lucide SVG icons instead of Unicode glyphs or hand-drawn shapes, tinted to the active theme and crisp on HiDPI.
matrix.toandmatrix:URI navigation (MSC2312). Clicking amatrix.toormatrix:link in a message body navigates within the app instead of opening the browser: a joined room navigates directly, an unknown room opens the join dialog pre-filled, user links open the profile panel, and event links scroll to the target event. All four platforms register as the OS handler for thematrix:URI scheme.
Sticky, collapsible section headers in the room list. Section headers (Favorites, DMs, Rooms, Spaces, Inactive) stick to the top of the room list while scrolling their section, and stay fully interactive — click to collapse/expand, hover highlight — while pinned. Rooms with unread messages render their title semibold.
Auto-scroll the room list to unread rooms. When a room receives new messages, the room list scrolls the most-recent unread room into view instead of leaving it hidden below the fold. The scroll is minimal — already-visible rooms aren't disturbed — and only genuinely new activity re-triggers it. Optional via an Appearance setting, default on.
Faster room switching & message bursts. The message list no longer rebuilds and re-shapes every row when messages arrive or a room is opened — text layouts are shaped once and cached, and a single inserted/updated/removed message re-measures only its neighbourhood instead of the whole room. A sync burst now triggers one layout pass instead of one per message. Shared code; verified on Qt6.
Pop-out room windows. Ctrl/⌘+click a room tab to pop the room out into its own native window. A pop-out is a full room view — timeline, compose, pickers, reactions, mentions, animated media, and a room info panel — on all four platforms. Verified on Win32; the other shells mirror the same shared logic.
GIF picker (
/gif). Type/gif <query>in the composer to search and send GIFs. Results appear in an animated horizontal strip above the compose bar; chosen GIFs send as autoplaying video, encrypted like any other media in E2EE rooms. Wired on all four shells.
Room navigation history (Alt+Left / Alt+Right; Cmd+[ / Cmd+] on macOS). Back and forward navigation through the session's room visit history, like browser navigation. Shortcuts are application-scoped and fire even while the compose box holds focus.
Quick switcher (Ctrl/⌘+K). A centered command-palette overlay for jumping between rooms, with a "Recent" strip and a full name-filtered room list. Typing a leading
@flips it into user mode to start a DM with anyone, including a previously-unseen Matrix ID, resolving and confirming the profile before offering the row.
One encryption dialog. Every encryption interaction happens in a single shared dialog: creating a recovery key (generated by default, with an optional passphrase), unlocking a new device (another device, the recovery key, or resetting encryption if both are lost), and answering verification requests with the emoji comparison. A slim reminder strip reopens it for users who skipped setup and can be snoozed for three days. The verification logic is shared by all four shells.
In-flight request indicator (animated spinning ring). An animated ring in the status bar shows the number of currently in-flight Matrix API requests — green, amber, or red depending on count — with a tooltip showing the exact number. Wired on all four platforms; macOS gained a status bar in the same pass.
Non-blocking media downloads. Media fetches (avatars, thumbnails, full-size images, sticker/emoji images, map tiles, URL previews, voice/audio) now run as async tasks instead of blocking calls that each pinned a worker thread, so a slow or dead server can no longer starve the media the user is actually waiting on. Switching rooms cancels the previous room's still-pending downloads.
Pinned events. A banner above the message list cycles through pinned messages with a counter and jump-to-message; a Pin/Unpin action appears in the hover menu, gated by the user's power level.
Room tags (favourite / low priority). The room info panel gains Favourite and Low Priority toggle switches, mutually exclusive both in the UI and on the server.
Hover action pill. The per-message hover affordances (reply / thread / react / edit) consolidate into a single rounded pill anchored to each row, with destructive/moderator actions tucked behind an overflow menu to keep the pill tidy.
Win32 windowless RichEdit compose bar. The Windows compose bar is now driven by a windowless native rich-edit control rendered directly into the surface, with correct color-emoji rendering and all prior text-area behavior (mention pills, clipboard image paste, slash popups, IME) preserved.
Win32 full HiDPI fix. Fixes a systematic coordinate-space mismatch on Win32 (physical pixels vs. D2D's DIPs) affecting pointer dispatch, widget bounds, and popup positioning. Emoji/sticker pickers and the rich-edit compose area also now honour dark mode.
Tab session restore. The full set of open room tabs is now persisted across restarts, restored in a single pass with no inter-tab flickering. Wired in all four shells.
Matrix threads UI. A "threads" button toggles a right-side panel with three states: closed, a list of every thread in the room, or one thread's own reply timeline with its own compose bar. While a thread is open the main timeline dims and in-thread replies are hidden from it. Wired in all four shells.
Privacy settings tab — presence toggle and room key export/import. A new Privacy settings tab lets the user disable outgoing/incoming presence, and export or import encryption room keys via a passphrase-protected file, with native dialogs on all four platforms.
Storage size display and cache-clear in About settings. The About settings tab shows local cache and SDK store sizes, and a destructive "Clear all caches" button that wipes them without touching credentials or active sessions. Wired on all four shells.
@mentions. Typing
@in the composer opens an autocomplete that filters room members as you type, with an@roomentry pinned on top. Selecting one inserts an inline pill (plain@Nametext on Win32 for now); received mentions render as clickable pills that open the user's profile. Verified on Qt6; GTK4 builds clean; macOS/Win32 written but unverified in this environment.
Account registration (OIDC
prompt=create). The login screen offers a "Create an account" link that reuses the existing OAuth flow to reach the homeserver's own signup page, shown only when the homeserver advertises registration support.
Group inactive rooms. An Appearance setting adds a default-collapsed "Inactive" section holding DMs and rooms with no activity past a configurable threshold (default one month); favorites and spaces are never grouped, and a room reclassifies out automatically on new activity.
Outgoing Matrix presence. The app now publishes its own presence, not just receives it, via an idle-decay state machine: Online while engaged with the app, Unavailable after 5 minutes of no input/focus, Offline on logout.
Code-block syntax highlighting and tinted backgrounds. Fenced code blocks in messages now render with syntax-highlighted colors and a tinted background panel, across all four canvas backends; inline code gets a tight per-run tint. Unknown or absent languages fall back to plain monospace.
For build instructions, architectural overview, and the open-roadmap items, see CLAUDE.md. For tracked open issues / known gaps, see the "Known gaps" section at the bottom of CLAUDE.md.
| Suite | Count |
|---|---|
Rust unit tests (cargo test -p tesseract-sdk-ffi) |
630 |
| C++ Catch2 tests via ctest (Qt6 preset) | 1707 |
| Shell | UI toolkit | Canvas backend | Status |
|---|---|---|---|
| Linux | Qt6 Widgets | QPainter | primary dev target — verified end-to-end |
| Linux | GTK4 | Cairo + Pango | verified end-to-end |
| macOS | AppKit (NSWindowController, NSView) |
CoreGraphics + CoreText | verified on macOS 15; opus playback requires macOS 14+ |
| Windows | Win32 + COM | Direct2D + DirectWrite + WIC | MSVC verified; MinGW cross-compile verified; audio via IMFMediaEngine |
- OAuth 2.0 (RFC 8252) loopback flow — two-phase
begin_oauth/await_oauthAPI, ephemeral loopback HTTP server, mDNS-safe redirect URI. Dynamic client registration advertises the loopbackredirect_urisentry without a port (§7.3 — required by some authorization servers, e.g. continuwuity), while the local listener and the actual login request keep the real port.await_oauth/cancel_oauthare cancel-safe: both hold only the shared FFI lock so a Cancel click can interrupt the wait instead of deadlocking behind it (mirrors theqr_granthandle pattern); the newly-authenticatedClientis committed in a separate, fast step. - Legacy
m.login.passwordfallback — for self-hosted homeservers without an OIDC/MAS provider, gated behindTESSERACT_ENABLE_LEGACY_LOGIN(defaultON).LoginViewauto-detects support via a homeserver capability probe and shows a "Sign in with password" screen alongside the OAuth button. Session storage is a taggedSessionEnvelope{OAuth, Native}sorestore_session/export_session/logoutshare one code path regardless of auth mechanism. - Secure token storage — per-platform
SecretStorebackend: Windows Credential Manager (CredWriteW/CredReadW), macOS Keychain (SecItemAdd/SecItemCopyMatching), Linuxlibsecret(probed at build time; plaintext stub fallback when absent).SessionStoremigrates transparently from the legacy plaintextsession.jsonon first load, writing a{"v":2}sentinel on success so subsequent starts bypass the migration path. - Session restore on startup —
SessionStorepersists the fullPersistedSessionJSON on every token refresh and reloads it at launch. All open room tabs and the active account are also restored: theim.gnomos.tesseractaccount-data event carries anopen_roomsarray so the full tab workspace survives a restart. - XDG data/config split — account data (per-account
accounts/<uid>/tree withsession.json+ the matrix-sdk SQLite store, plus theaccounts.jsonindex) lives underdata_dir():~/.local/share/tesseract/on Linux,%APPDATA%/Tesseract/on Windows,~/Library/Application Support/Tesseract/on macOS. Onlyapp_settings.jsonstays inconfig_dir()(~/.config/tesseract/on Linux);data_dir()equalsconfig_dir()on Windows/macOS.migrate_legacy_layout()runs on startup and handles both the pre-multi-account single-account layout and a multi-accountaccounts/tree left underconfig_dir()by older builds (Linux), moving each intodata_dir()crash-safely. logout— wipes Rust session, C++ wrapper state, and the SQLite store; surfaces back through the FFI.- Soft logout —
SessionChange::UnknownTokenthreaded throughon_errorwith asoft_logoutflag so the UI can retry restore without clearing the store. - Recovery key / device verification (Step 6) —
needs_recovery,recover(key_or_passphrase),backup_stateFFI;on_backup_progresscallback; per-platformRecoveryBanner(in-toolkit; not a modal dialog). - Server capabilities on login —
tesseract::ServerInfostruct captures homeserver URL, Matrix spec versions, MSC3030 (Jump-to-Date) support flag, capability bits (can_change_password,can_set_displayname,can_set_avatar), and default room version; fetched concurrently via/_matrix/client/versions(no-auth) +/_matrix/client/v3/capabilities(Bearer) afterRoomListState::Running; stored inShellBase::server_info_for feature-gating across all four shells; Settings "Server" tab shows the homeserver URL. - Shutdown stability — background workers are drained before the tokio runtime tears down, preventing use-after-free when a worker posts back to the UI thread after the EventHandler is destroyed; a separate guard prevents a double-callback segfault when
stop_syncis called re-entrantly. - Identity strip in sidebar — circular avatar + display name + right-click "Log Out" on every platform.
- Single-instance enforcement — a per-user OS lock prevents two app instances from running concurrently (
QLockFileon Qt6,GApplicationuniqueness on GTK4, a named mutex on Win32,NSRunningApplicationcheck on macOS); the second launch exits with a notice. - Duplicate account guard — after OAuth completes the shell checks existing
accounts_for a matchinguser_idbefore committing to disk; re-adding the same account discards the temp store and returns to the last active account without side effects. - Startup restore error dialog — when
restore_session()fails at launch (network outage, transient server error), the login view displays a modalAlertDialogoverlay ("Connection Error") with Retry and Sign In buttons instead of silently showing a blank login form. The session files are left untouched so Retry can re-attempt restore once connectivity returns;SessionStore::clear_account()is called only byhandle_auth_error()on a confirmedsync_auth_errorresponse. All four shells wired. - Hardened
matrix-sdkinit — bounded request retries/timeout and auto-enabled key backup alongside cross-signing bootstrap (matching Element X Android's client config). New sessions' local SQLite store is encrypted with a randomly generated per-session key, persisted via the platform secret store; sessions created before this shipped remain unencrypted permanently (matrix-sdk has no in-place store-migration API). - Descriptive device display name — reports the actual Linux distro, macOS OS version, or a normalized "Windows 11/10 " string (via
os_info) instead of a bare "Windows"/"macOS"/"Linux", sanitized before reuse in the User-Agent's"(name; os)"token, the OAuthdevice_display_nameparam, andrename_device; MAS's session list derives its device label from the first token of that parenthetical.
- Sliding sync via matrix-sdk-ui —
SyncService+RoomListServicereplace the legacysync_onceloop. - Initial-sync progress in the status bar —
RoomListService::stateexposed via a newon_room_list_stateFFI callback; each shell paints "Syncing rooms…" (debounced 300 ms) / "Reconnecting…" / "Downloading encryption keys (N)…" until both sliding-sync and key-backfill settle, then clears to "Connected". Wired on Qt6, GTK4, and Win32; macOS deferred (no status-bar surface). - Per-room
Timelinehandles —HashMap<OwnedRoomId, TimelineHandle>keyed by room; subscribed lazily. - Timeline FFI —
subscribe_room,unsubscribe_room,paginate_back,paginate_back_with_status(reportsreached_start); position-alignedon_timeline_reset/on_message_inserted/on_message_updated/on_message_removedcallbacks mirror matrix-sdk-ui'sVectorDiffsemantics. - Back-pagination on scroll-to-top — UI fires
paginate_backwhen the user reaches the top; in-place insertion preserves the visual scroll position. Scroll preservation is row-anchored (ListView::ScrollAnchor+ListAdapter::row_key): the row under the cursor (or the top-of-viewport row) is pinned to its screen position across prepends and async row-height growth (images, URL previews, voice waveforms decoding in/above the viewport), with the hover highlight re-resolved to the same message after the relayout. Keyless lists (room/thread) fall back to the legacy total-height delta. - Background backfill —
start_background_backfillwalks every joined room not currently subscribed and warms the persistent event cache with bounded concurrency. - Async room actions — text sends, reactions, pagination, room join/leave/invite-accept/decline, and file uploads converted from blocking C++ worker-thread calls to fire-and-forget
rt.spawn()tokio tasks delivering results viaIEventHandlercallbacks (paginate_back_async,accept_invite_async,send_image_async, etc.). Blocking wrappers removed. - Kind-aware last-message preview — each room row's preview uses
formatted_body's first plain line for text/notice/emote, shows "<sender> sent an image/video/file/voice message" for media kinds, and draws an inline ~28 px thumbnail for sticker last-messages (RoomListViewsticker_provider_backed by the shells' shared image cache; wired on all four platforms). - Unread highlighting — a room with unread messages is bolded and badged by severity: a mention shows an accent count pill, a notifying room a neutral count pill, and a room with unread messages that don't notify (e.g. set to "mentions only") a bold name + small neutral dot — so quiet-but-unread rooms are no longer invisible. Muted rooms are excluded (silenced on purpose). The decision is one pure
unread_style_for(notification, highlight, unread, muted)helper (views/roomlist_unread.h) consumed by both the row and the collapsed section-header rendering;RoomInfocarriesunread_count(Room::num_unread_messages()) andmuted(cached_user_defined_notification_mode), and the room-list update-dedup fingerprint includes the quiet-unread state so the dot appears and clears live. - Tombstoned (upgraded) rooms hidden from the room list.
- Runtime offline banner — when sync loses connectivity (
sync_offline/sync_error), a 32 px amber "No internet connection — reconnecting…" strip appears at the top of the chat panel; it auto-hides whenRoomListStatereturns toRunning.ShellBase::offline_tracks the flag;EventHandlerBasewires both transitions;MainAppWidget::set_offline(bool)drives the banner. All four shells benefit with no per-shell changes. - Graceful shutdown —
DroponClientFficallsstop_sync(). - Non-blocking FFI lock (room-switch freeze fix) — the C++
Clientno longer serialises every FFI call behind one coarsestd::mutexheld across blockingblock_ons. The read + dispatch bridge methods are now&ClientFfi(interior-mutable Rust state:thread_lists/thread_timelinesmoved behindparking_lot::RwLock), guarded by astd::shared_mutextaken in shared mode; only ~15 genuine writers (start_sync,restore_session,logout, …) take the exclusive lock. The UI thread's cheap room-switch reads (list_room_threads,subscribe_room_threads) now run concurrently with a worker mid-subscribe_roomtimeline build instead of freezing behind it. - Low-power CPU optimisations — the sync worker no longer fans out into matrix-sdk SQLite queries on every notable update. The room-info watcher coalesces
RoomInfoNotableUpdatebursts in a 150 ms window and folds their reasons, skipping the image-pack/prefs rebuild when only read-receipt / latest-event / recency bits are set.sync_room_subscriptionsis diff-aware — a re-selection of the already-open room or a thread toggle that lands in an already-subscribed room is a no-op. The presence polling loop reads a cached DM-counterpart set (refreshed fromRoomInfo.dm_counterpart_user_idafter every room-list rebuild) instead of walking every joined room with adm_other_userlookup per tick, the tick interval is raised from 30 s to 60 s, and the loop is suspended entirely while the window is hidden/minimized/unfocused (re-enabled with an immediate one-shot kick on focus regain viaClient::poll_presence_now). On low-end laptops these collapse a previously dominantchunk_large_query_overhotspot. - One-time initial history fill per subscription — revisiting an already-subscribed room no longer re-runs a 100-event
paginate_back_with_statusfetch on every visit; a one-timeinitial_fill_doneflag gates it so the fill runs once per warm subscription and later revisits are free.reply_details_requested_clearing and the room-layout account-dataPUTalso moved from every switch to once (the layout write to window close). - Right-click context menu on room list rows — Open in tab / Open in window / Leave room (with confirmation), via the shared
PopupMenuwidget; the open-in items disable when the room is already open in that context.
is_space: boolonRoomInfo; spaces shown at the bottom of the room list with#prefix on Qt6 / GTK4 (top-row dedicated bar on macOS).space_children(space_id)FFI returning joined direct children;space_children_all(space_id)returning all direct children (joined + unjoined).- Stack-based drill-in navigation — selecting a space replaces the room list with its children;
←back button + space name label at the top of the sidebar; recursive sub-spaces; auto-pop to "All rooms" when the stack is empty. - Space children hidden from the root room list — they appear only when navigating into the parent.
- Unjoined space children — a collapsible "Not joined" section below the joined-rooms list shows every child room the user hasn't joined. Clicking opens
RoomPreviewView(name, avatar, topic, member count, Join button) without leaving the current room. Summaries fetched concurrently via MSC3266 with generation-based cancellation.
tk::Canvas— abstract 2D backend with four concrete impls (canvas_d2d,canvas_qpainter,canvas_cairo,canvas_cg). Color / Rect / Point / Image / TextLayout primitives; rounded-rect, stroke, push/pop clip; circle-cropped image draw; initials disc helper.tk::Widget— measure / arrange / paint + pointer / wheel dispatch withdispatch_pointer_down+world_to_localcapture semantics. Every subclass is constructed exclusively throughtk::create_widget()/create_root_widget()(a Host-aware factory backed by a thread-local pending-Host*stack), never directly — constructors areprotectedand friend the factory viaTK_WIDGET_FACTORY_FRIEND, sohost()is valid from the first line of any constructor with no manual parameter plumbing.tk::Host— per-platform integration surface (repaint scheduling, post-to-UI, native edit overlays).request_repaint,post_to_ui,make_text_field,make_text_area,make_audio_player,make_audio_capture,encode_for_send.- Keyboard focus — real Tab/Shift-Tab traversal (
Host::advance_focus/request_focus/clear_focus) with a:focus-visible-style ring shown only after keyboard navigation, not a mouse click. Traversal order follows each widget's ownbounds()in reading order (top-to-bottom rows, left-to-right within a row via a row-overlap comparator), notadd_child()insertion order, soStack/rect-positioned widgets (grids, pickers) traverse sensibly too.Host::set_focus_scope()/clear_focus_scope()lets an open modal (Room Settings, an overlay, ...) scope Tab traversal to its own subtree. The compose box is focused by default whenever nothing else needs attention. Native text fields (tk::TextField/tk::TextArea) participate directly as self-positioning widgets in the tree rather than shell-polled overlays. - Native text overlays —
NativeTextField(QLineEdit/GtkEntry/ Win32 EDIT /NSTextField) andNativeTextArea(QTextEdit/GtkTextView/ multi-line EDIT /NSTextView) for IME-friendly input.set_placeholderis implemented on all four platforms (GTK4 uses adim-labelGtkLabeloverlay child sinceGtkTextViewhas no native placeholder API). - Shared views —
LoginView,RoomListView,MessageListView,EmojiPicker,StickerPicker,RecoveryBanner,ComposeBarmounted identically on every platform. AlertDialog— modal overlay widget (not backdrop-dismissible) with a title, body, and up to two configurable action buttons (open(Options, primary_cb, secondary_cb)/close()/is_open()). Used byLoginViewto surface startup restore errors; available for other blocking error prompts.- Drag-and-drop ingest (OS-inbound files) —
tk::Widgetvirtuals (on_file_drop/dispatch_file_drop,on_native_drag_hover/dispatch_native_drag_hover) mirror the existing pointer-event dispatch shape, so each drop target (ComposeBar,RoomView,ImagePackEditorView,UserPackEditor) claims its own drop and paints its own localized hover highlight instead of one whole-surface overlay; image-data MIME types route to the compose bar's image preview, generic files route to the file chip. Distinct from the in-app drag-and-drop framework below. - In-app drag-and-drop framework —
Host::begin_drag(DragPayload, DragVisual, Point)starts a synthetic, in-process widget-to-widget drag (no OS drag-source APIs), with a claim-bubble drop-target search (Widget::on_drag_enter/on_drag_over/on_drag_leave_target/on_drop,dispatch_drag_enter), a Host-owned floating visual painted above everything, and a sharedDragGestureTrackerclick-vs-drag threshold helper (ui/shared/tk/drag_gesture.h). Cancels on Escape or the pointer leaving the surface. PopupMenu— renders throughtk::PopupSurfaceHandle(Host::make_popup_surface(), the same primitivetk::ComboBox's dropdown uses) rather than a canvas overlay, so it's a genuine OS popup window that z-orders correctly above everything, including native controls; row drawing/hit-testing lives in a nestedMenuListwidget. Supports separator and disabled items. Dismisses on any outside click — including a click that lands in a native text field and never reaches canvas hit-testing — and on the window losing activation (alt-tab), viaHost::dismiss_active_popup()(also benefitsComboBox/DatePickerView).PopupSurfaceHandle::on_dismiss_requestedoutside-click auto-dismiss (Mention/Slash/Shortcode/Gif popups) works on all four shells, not just Qt.
- Send text / image / file / sticker —
send_message,send_image,send_file,send_stickerFFI; matrix-sdk handles E2EE transparently. Text sends usetimeline.send()local echo so the message appears immediately with a ◷ indicator; transitions to ✓ on delivery, ⚠ + Retry on recoverable failure, ⚠ + ✕ on unrecoverable failure.retry_send(re-enables SDK send queue) andabort_send(timeline.redactfor local echoes) exposed through FFI and C++ client API;RoomPane's registration of these handlers is the single source of truth (a redundant overwrite fromwire_main_app_widget_()was removed), and a retry/abort failure now surfaces as a status toast instead of the button silently doing nothing. - MSC2530 captions —
image_filenamedistinct frombodyround-tripped; UI shows the body beneath the image only when the sender supplied an explicitfilename. - Redactions —
redact_event(room_id, event_id, reason);MsgLikeKind::Redactedsurfaces asmsg_type: "m.redacted"tombstone placeholder in the timeline. - Reactions —
send_reaction(toggle) FFI; aggregated reaction chips (24px, fixed 6px corner radius) under each message with sender-name tooltips and a hover-only "+" add button. Reaction keys aren't always emoji (MSC4027 plain-text reactions) — the glyph is segmented into emoji vs. text runs and drawn at different sizes (text at 4/5 the emoji size), vertically centred against the emoji box. - Replies (
m.in_reply_to) —in_reply_to_id/in_reply_to_sender_name/in_reply_to_bodyextracted intimeline_item_to_ffi; quote block rendered above the message body inMessageListView; hover "↩ Reply" button fireson_reply_requested;ComposeBargrows a reply-preview banner (kReplyBandH = 44 px) above the text input with a "×" cancel;send_replyFFI sends anm.textwithRelation::Reply; reply relation threaded through image/file/sticker sends viaAttachmentConfig::reply/send_sticker_; click on a quote block scrolls to the original message in-list or fireson_scroll_to_originalwhen not loaded; all 4 shells wired. - Message editing —
send_editFFI wrapsroom.make_edit_event()+send_queue().send();is_editedfield inTimelineEventset frommsg_content.is_edited();(edited)badge appended after the body inMessageListView; hover "✏" button on own text messages fireson_edit_requested;ComposeBargrows an edit-mode banner (kEditBandH = 44 px) above the text input with a "×" cancel andon_send_editcallback; edit mode and reply mode are mutually exclusive (set_editingclears reply state); all 4 shells wired. - Location messages (
m.location/ MSC3488) receive — location events render as interactive 240 px inline maps; OSM tiles fetched fromtile.openstreetmap.organd composited with a disk cache; pan by drag, zoom by scroll wheel (one notch = one zoom level); attribution overlay; red-circle pin at event coordinates; location description shown as a hover tooltip.on_tile_neededwired in all four primary shellMainWindowfiles. Send:send_locationFFI builds and sends them.locationevent, triggered either by pasting a recognized Google Maps/OpenStreetMap link (opt-in via Settings) or by the/locationslash command, which fetches the device's current OS location viatk::LocationProvider(CoreLocation/WinRTGeolocator/GeoClue2) and sends it immediately with no confirmation step. - Read receipts —
EventTimelineItem::read_receipts()aggregated via acollect_read_receiptshelper;MessageListViewpaints up to 5 mini-avatar discs (16 px) with a+Noverflow pill at the row's bottom-right. - Hover-only
HH:MMtimestamp — paints under the sender avatar when the row is hovered; no always-visible time column. - MSC2545 sticker decryption — encrypted-sticker support via direct
ruma = { features = ["compat-encrypted-stickers"] }; sticker timeline events emit JSON-encodedMediaSourcefor the encrypted variant. - Block-level Markdown rendering — headings (
#through######), unordered and ordered lists (including nested), blockquotes, and tables render visually inMessageListViewacross all four canvas backends. Headings useFontRole::UiSemibold; list items indent with correct bullet / ordinal; blockquotes get an accent left-border stripe; tables use fixed-width columns. Complements the existing inline styles and code-block syntax highlighting. - Floating date badge — a rounded pill fixed to the top-center of the timeline viewport while scrolled away from the live tail, naming the day of whatever row is at the top (reuses the inline day-separator's
format_day_label()). Modeled onRoomListView's sticky-header pattern; pushes up and blends into the real inlineDaySeparatorrow as it scrolls into place; shown regardless of how many distinct days are loaded. - Drag-select + copy text — click-drag (or double/triple-click for word/line) selects plain text across message bodies in
MessageListView; right-click shows a native "Copy" context menu on all four platforms, and Ctrl+C/Cmd+C at the window level copies the selection too. Starting a real selection now moves OS keyboard focus off the composer (Host::release_focus_to_canvas(), fired viaMessageListView::on_selection_started) so the composer's still-focused native text field doesn't swallow the Ctrl+C first; deselecting (a later click) returns focus to the composer viaon_selection_cleared.
fetch_media_bytes(mxc)/fetch_source_bytes(source_json)— synchronous wrappers around matrix-sdk's media cache; the latter handles plain mxc + encryptedEncryptedFiletransparently.- Avatars — sender avatars (24 px per row) + room avatars (36 px); circular crop via
draw_circle_image; initials-disc fallback when bytes aren't yet cached. Rooms without a custom avatar fall back to the other participant's avatar in 1:1 chats (RoomInfo::dm_avatar_url, populated in Rust by inspectingm.directfirst and then filtering joined members byservice_membersper MSC4171); render sites read via the inlineeffective_avatar_url()accessor andShellBase::ensure_room_avatar_routes the DM-fallback fetch throughfetch_media_bytesso the cache key naturally dedupes with the user's avatar elsewhere. - Lazy room-list avatars — room-list avatars are requested only when a row is first painted (
RoomListView::on_room_avatar_neededfires frompaint_rowon a cache miss, wired toensure_room_avatar_inShellBase::wire_main_app_widget_), so rooms in collapsed or off-screen sections fetch nothing until scrolled into view. The former per-shell "fetch every room" loops are gone. - Visible-first download priority — the per-lane FIFO
tokio::Semaphoreis replaced by aPriorityGateover a pureMediaQueue(priority desc, then FIFO seq). The timeline still eagerly enqueues every row's media atNormal, but aMessageListView::on_visible_range_changedcallback (frame-coalesced, de-duped; re-exposed viaRoomView, bound once inwire_main_app_widget_) callsprioritize_media(group, ids)so the media for the rows currently on screen jumps ahead of the off-screen backlog — and re-prioritizes as the user scrolls. Covers all four shells + the thread panel. - Stuck-download reclamation — matrix-sdk media is a single opaque await with no progress hook, so a stalled fetch would otherwise hold its lane slot until the 30/120 s timeout and freeze the queue. A slot held past an 8 s stall deadline stops counting against the lane limit (the gate grants the next, highest-priority waiter while the stuck download keeps draining in the background), and a hard ceiling (2× the lane) bounds total concurrent connections. Healthy downloads still behave exactly like the old semaphore.
- Bounded fetches — every media download runs under a per-request timeout (30 s thumbnails/avatars, 120 s full files), so a stalled or endlessly-retrying request can't hang a read-pool worker thread or pin the in-flight indicator.
- HTTP/2 multiplexing — the reqwest media client uses HTTP/2 prior knowledge so parallel MXC downloads share connections;
MEDIA_BULK_PERMITSis 10 concurrent fetches to take advantage of the extra bandwidth. - Failed-fetch backoff — a fetch that returns empty (network error / 5xx / timeout) is recorded in a per-key exponential-backoff cache (30 s → 30 min); the
ensure_*avatar/media paths skip a key still in cooldown, so an unreachable avatar (e.g. a forgotten DM on a dead homeserver) stops being re-requested on every sync tick. The backoff state is persisted toapp_cache.dbacross sessions so it survives a restart. Cleared on success and on cache-wipe. - Inline images — thumbnail to max 320 × 200, MSC2530 caption rule applied, rounded-rect chrome. Bytes are decoded off the UI thread on all four shells (
QImageReaderon Qt6, WIC on Win32,CGImageSourceon macOS,GdkPixbufon GTK4) and posted back viapost_to_ui_so large images never stall paint or input. - Media-preview gating (MSC4278) — a global
media_previewssetting (Off/Private/On, defaultOn) backed by them.media_preview_configaccount-data event controls whether inline image/sticker/video thumbnails auto-load. Suppressed media renders a BlurHash (MSC2448) placeholder behind a click-to-load pill and is not fetched until revealed;Privatemode suppresses only in public rooms (resolved against each room's cachedjoin_rule, with the per-roomm.media_preview_configoverride applied on top). The decision is a single pure function (app/media_preview_policy.h::media_allowed) consulted at both the receive-time fetch gate and the paint-time placeholder predicate, so a revealed/allowed item is fetched exactly when it is shown. The user's own media is exempt from public-room suppression inPrivatemode (you already have it locally and it is never a privacy/safety concern to you), butOffstill suppresses everything including your own uploads. Wired once inShellBase, so all four shells share it. - File cards — fixed 56-px-tall rounded card with filename (ellipsis-trimmed) + human-readable size.
- Inline stickers — borderless 256 × 256 thumbnail; right-click context menu offers "Add to Saved Stickers" (Qt6 / GTK4 / macOS).
- Animated images — GIF / APNG / animated WebP frame-by-frame decoding on Qt6 (
QImageReader), GTK4 (GdkPixbufAnimationIter), Win32 (IWICBitmapDecoder+ per-frame metadata), macOS (CGImageSource). 60 Hz frame tick repaints when any frame advances; delays clamped ≥ 20 ms. - Homeserver upload limit —
media_upload_limit()cached per session. - Clipboard image paste + drag-drop in the compose bar; image data re-encoded to JPEG ≤ 1600 × 1200 when sent via
encode_for_send(compress=true). - Media-viewer chrome as real widgets — close/save/copy on both lightbox overlays, plus the video overlay's play/pause and speed-pill, are
tk::Buttonchildren (not hand-rolled rects with manual hit-testing), so they get hover/press/keyboard activation for free. Each button supplies its own fixed, backdrop-tuned colors via a new opt-intk::Button::FillOverride(rest/hover/pressed, unset by default for every other button in the app) instead of the theme's normal low-alphasubtle_hover/subtle_pressed, since the app's light/dark theme palette wasn't designed to read against the overlay's permanently near-black scrim — without the override, hover/press were nearly invisible.
- Receive path —
MessageType::Audioarm intimeline_item_to_ffi(gated onunstable-msc3245-v1-compat); voice events surface asmsg_type = "m.voice"carryingaudio_source_json,audio_duration_ms,audio_waveform(MSC1767, 0..=1024),audio_mime. Plainm.audio(no voice marker) folds through the file-card path. - C++
VoiceEvent+EventType::Voice. - Voice card UI — 280 × 48 rounded card with play/pause circle, waveform strip (flat placeholder bars when waveform is omitted), mm:ss remaining-time label.
- Scrubbable waveform — click or drag anywhere on the bars to seek; clicking on a non-active row starts playback at the chosen position.
- Speed pill —
1×/1.5×/2×on the active row; cycles the global playback rate. - Background prefetch — each shell kicks off a worker thread when a Voice row is first seen, warming the SDK media cache so the first play tap is instant.
- Per-platform
tk::AudioPlayerbackend — Qt6QMediaPlayer+QAudioOutput; GTK4 GStreamer pipeline (giostreamsrc!decodebin!audioconvert!autoaudiosink); macOSAVAudioPlayer; Win32tk::audio_win32.cppusingIMFMediaEngine— in-memoryIStreamavoids disk spillage; 60 ms timer-pool tick drives progress; callbacks marshalled back to the UI thread viapost_to_ui. - Send path — mic button in
ComposeBarstarts/stops recording; cancelled via a dedicated cancel button. OGG/Opus encoding in Rust (audiopus+oggcrates) at 48 kHz mono; MSC1767 waveform sampled every ~100 ms of PCM (up to 256 samples, normalised [0, 1000]). Live waveform strip in the compose bar animates during recording. Per-platformtk::AudioCapturebackend: Qt6QAudioSource, GTK4 GStreamerpulsesrcpipeline, Win32 WASAPI (IAudioCaptureClient), macOSAVAudioEngine(async permission request to avoid main-thread deadlock).send_voiceFFI +Client::send_voiceC++ API. Mic button hidden automatically when no capture device is available (factory returnsnullptr). Voice recording wired in all four main shells viaShellBase::wire_voice_capture_(); pop-out secondary windows hide the mic button — recording is a singleton interaction owned by the main window. Room ID is captured at the moment recording starts so room switches during a long recording send to the correct room.
- Emoji picker — Unicode-category tabs + per-pack custom tabs; search; virtualised grid via
tk::GridView. Hovering a cell shows an inline:shortcode:tooltip (centred above the cell, flipped below near the top edge). - Sticker picker — Favorites tab + per-pack tabs; search; virtualised grid. Floating panel on every platform (Qt6
QFrame, GTK4GtkPopover, macOSNSPanel, Win32WS_POPUPHWND). Same:shortcode:hover tooltip as emoji picker. - GridView hover tracking —
GridView::on_pointer_move/on_pointer_leaveupdatehovered_index_and exposehovered_index()+rect_at()accessors; cell highlight on hover now works correctly (was silently broken). - Recent emoji (MSC4356) —
m.recent_emoji+io.github.johennes.msc4356.recent_emojiaccount-data, dual-written on every bump; reads stable → unstable → legacyio.element.recent_emojiso existing Element users keep their picker rank. 100-entry cap, move-to-front-and-increment semantics, count-desc top-N for the Frequents tab. - Add to Saved Stickers — right-click on an inline sticker offers
save_sticker_to_user_pack(all four platforms: Qt6 / GTK4 / macOS / Win32 viaWM_RBUTTONUP+TrackPopupMenu). All platforms now pass the realImageInfoJSON instead of"{}", so width/height/mimetype/size are preserved in the saved pack entry. - Toggle favourite —
toggle_favorite_stickerflips theim.tesseract.favoriteflag on user-pack entries. - Async sticker image fetch — Win32 + Qt6 + macOS + GTK4 all run a worker thread + decode + post-to-UI + cache + repaint per pending sticker. GTK4 also wires the same async path for
EmojiPickercustom emoticon tabs (ensure_emoji_image_async, deduped viaemoji_fetches_in_flight_). - Unified async picker image cache —
EmojiPickerandStickerPickernow share the message list'stk_images_/anim_cache_on all four shells (Qt6 dropped its private per-picker caches), so picker artwork and inline-message artwork are decoded once and reused. Images route throughmedia_disk_cache_, so custom emoticons and stickers survive an app restart, and decode runs off the UI thread (Qt6QImageReader, GTK4GdkPixbuf+ cairo, macOSCGImageSource, Win32 WIC) so the first paint of a large pack no longer stalls the UI.
sdk/src/image_packs.rsaggregator — user pack (im.ponies.user_emotes/m.image_pack), enabled-rooms list (im.ponies.emote_rooms/m.image_pack.rooms), per-room state events (im.ponies.room_emotes/m.room.image_pack). Reads combine the stable + unstable event names (merge_pack_contents) at every read site instead of stopping at the first hit. 16 unit tests.- Spec-correct usage semantics — missing/empty
usage→ both sticker + emoticon allowed; per-imageusageoverrides pack-level. - Per-room discovery — a shared full-state fetch (
RoomStateCache, triggered on room switch) rather than a single guessedstate_key, since packs can use non-empty state keys and sliding sync doesn't deliver customm.room.image_packstate; cached in a lazily-builtroom_image_pack_cache. - Picker/popup scoping — emoji/sticker picker tabs and the inline
:shortcode:popup filter to the personal pack, the currently-open room, and explicitly subscribed rooms; each pop-out window computes its own filtered list. - FFI surface (reads) —
list_image_packs,list_known_room_packs,list_pack_images,list_favorite_stickers,user_pack_has_sticker. - FFI surface (writes) —
send_sticker,save_sticker_to_user_pack,toggle_favorite_sticker,remove_user_pack_image,rename_user_pack_image,set_pack_room_subscribed(dual-writes stable + unstable event types, forces a synchronous rebuild sois_subscribedis correct before returning),save_room_pack(wholesale-replaces a room/space pack's images, matching the editor's full-snapshot staging model),remove_room_pack(empties a pack — Matrix has no true state-event delete; discovery skips zero-image packs),can_set_room_image_packs(power-level gate, mirrorscan_set_room_name). IEventHandler::on_image_packs_updated— fires whenever the cache is rebuilt; pickers refresh in place.- UI —
ImagePackEditorView(multi-pack room/space editor,RoomSettingsView's "Emojis & Stickers" tab) andImagePacksSection(UserPackEditor+KnownPacksList, globalSettingsViewtab) share tile-grid logic viaImagePackTileGridBase.
- Shared
tesseract::views::ComposeBaron every platform viatk::*::Surface. - Multi-line expanding input via
tk::NativeTextArea(auto-grows 56 → 160 px, clamped). - Send-on-Enter, Shift+Enter inserts a newline.
- Emoji + sticker + send buttons painted by the toolkit.
- Send button gates on trimmed non-empty content.
- Clipboard image paste; clipboard file-list paste (files copied in a file manager, not dragged — Qt6/GTK4 shipped, macOS/Windows unbuilt); file drag-drop; pending-image / pending-file preview chip with clear button.
- Reply-mode banner (
kReplyBandH = 44 px) with sender + body snippet and "×" cancel; edit-mode banner (kEditBandH = 44 px) with "×" cancel; both modes mutually exclusive. - Slash commands —
SlashCommandEngine/SlashCommandPopupautocomplete (typing/opens the popup);dispatch_compose_sendroutes recognised commands:/me+/slap→m.emote,/shrugappends¯\_(ツ)_/¯,/spoiler [(reason)] <text>→m.textwith adata-mx-spoilerspan (MSC2010; content rendered through inline markdown). Unknown/foois sent verbatim.
- One mechanism on every platform:
tk::tr/trn/trflook strings up in gettext.mocatalogs compiled fromi18n/*.po(English, Spanish, French, plus a pseudo-locale for QA). Shared views and all four shells use it; macOS wraps it asTkTr(). Qt'sQObject::trand GTK'sgettextare not used. - Language picked in Settings → Language (Auto follows the OS); "Restart now" relaunches into it.
tk::N_marks literal tables translated at display time;tk::format_dateformats dates from a translatable strftime-style pattern (so locales can reorder day and month) with catalog month/weekday names;tk::format_sizegives translated byte units.- The
i18n_catalogs_completectest (i18n/check_i18n.py) fails when a marked string is missing from any.po, or when a shell calls a baretr()/_().
ThemePreference— persisted user preference (Light/Dark/System);set_theme()added to every platformSurface;apply_current_theme_()inShellBaseapplies the selected palette.- OS appearance detection — each shell overrides
os_color_scheme_()to returnThemeMode::DarkorThemeMode::Light:- Win32 —
WM_SETTINGCHANGEwith"ImmersiveColorSet"parameter. - macOS —
effectiveAppearancechecked on theme-change notification. - GTK4 —
GtkSettings::gtk-application-prefer-dark-themeproperty. - Qt6 —
QStyleHints::colorSchemeChangedsignal; falls back to the XDG Desktop Portal (org.freedesktop.portal.Settings, namespaceorg.freedesktop.appearance, keycolor-scheme) whenQStyleHints::colorScheme()returnsUnknown(GNOME without QGnomePlatform or Qt < 6.6). The portal value is read at startup and kept current via theSettingChangedD-Bus signal.
- Win32 —
- Live updates — all four shells re-apply the theme whenever the OS switches, provided
ThemePreference::Systemis active. User-pinned Light or Dark is never overridden by OS changes. - Native text field color sync (Qt6) —
QLineEdit/QTextEdithold an explicitQPalettewith no automatic dark-mode following, unlike GTK4/macOS/Win32.apply_theme_ui_()re-appliesset_text_color(palette.text_primary)to every native field on the Qt6 shell (room search, quick switcher, message search, forward picker, find-in-room, topic/room-settings/image-pack fields, encryption/QR-grant fields) on every theme change, not just construction. - Accent-color themes —
tk::AccentTheme(Blue/Forest/Sunset/Violet/System), independent ofThemePreference, picked via a combo in Settings → Appearance (persisted asSettings::ThemeAccent, owntheme_accentkey inapp_settings.json, defaulting toSystem).Theme::variant(mode, accent)generates each non-Blue, non-System accent from a single seed hue, overriding only the ~11 accent-dependentPalettefields (accent/hover/pressed, chip "me" colors, unread badge, avatar-initials disc, selection) — every neutral/surface field is the existing hand-tuned Light/Dark palette, shared across accents. Blue itself is never regenerated (kept as the original literal palette), so the default theme has zero risk from this feature. A newtk::color_contrastmodule (relative_luminance/contrast_ratio/meets_wcag_aa) gates every accent/mode combination against WCAG AA intest_tk_theme_contrast.cpp; per-hue lightness constants are individually calibrated (not a single shared formula) since HSL isn't perceptually uniform across hues.Systemresolves to the same unmodified palette as Blue at this shared layer;MainWindow::apply_theme_ui_(Win32) is the one platform that overlays a real OS accent color on top when it seesAccentTheme::System— a pre-existing "match the Windows accent color" convention now gated on that explicit choice instead of always applying regardless of the picked accent (the bug that made every non-Blue accent look unchanged on Windows).
- All four platforms — system-tray icon with Show App / Quit popup menu. Closing the main window hides it (the SDK keeps running, sync stays warm); Quit on the tray menu does the real exit.
- Cross-platform
tesseract::ITrayIconabstraction; per-platform impls created after login (mirrorsINotifier). - Qt6 —
QSystemTrayIcon;is_available()fromQSystemTrayIcon::isSystemTrayAvailable. Falls back to plain quit when no system tray is present. - GTK4 — pure
org.kde.StatusNotifierItem+com.canonical.dbusmenuimplementation over GDBus (GtkSniTrayIcon; icon rendered with gdk-pixbuf + cairo). Replaces the formerlibayatana-appindicator3tray, which pulled libgtk-3 into the GTK4 process and abortedgtk_init()with "GTK 2/3 symbols detected" — there is no longer any appindicator (GTK3) dependency. - Win32 —
Shell_NotifyIconagainst a hidden helper HWND;TrackPopupMenuExfor the right-click menu;WM_CLOSEintercepted inMainWindow's wnd_proc. - Click behavior — a tray-icon click shows a hidden window, raises a visible-but-inactive one, and hides the active one (not a naive visibility toggle), on all four shells. On Wayland, raising an inactive window needs the compositor's consent: Plasma's tray host issues a granted xdg-activation token and delivers it via the
ProvideXdgActivationTokenD-Bus call beforeActivate. Qt6 consumes the token Qt's SNI adaptor places inXDG_ACTIVATION_TOKEN;GtkSniTrayIconhandlesProvideXdgActivationTokenitself and feeds the token togtk_window_set_startup_id()before presenting. Without it (X11, non-KDE hosts, or a self-issued token with no fresh input serial) the compositor only flags the window as demanding attention. - macOS —
NSStatusItemwith a template menu-bar icon;windowShouldClose:hides the window; Quit calls[NSApp terminate:nil]. - Unread overlay — when any signed-in account has rooms with notifications, the tray icon gets a small coloured dot in the bottom-right (accent blue for unread, destructive red for highlights / mentions). Aggregation lives in
ShellBase::compute_tray_unreadoverper_account_rooms_; theITrayIcon::set_unreadhook is implemented per shell (QPainter overlay on Qt6, pre-rendered Cairo PNGs swapped viaapp_indicator_set_icon_fullon GTK4, GDI+ ARGB compositing intoCreateIconIndirecton Win32,NSImage lockFocus+NSBezierPathon macOS).
- Launch at login — Settings → General toggle, default off, backed by a new cross-platform
tesseract::IAutostartabstraction (mirrorsINotifier/IScreenLock): registryRunkey on Windows,SMAppServiceon macOS 13+, XDG autostart.desktopfiles on Linux.is_enabled()always queries the OS directly so the checkbox self-heals if registration is removed outside the app. - Autostart launch behavior — a launch via the OS autostart mechanism starts hidden to the tray only when a saved session restores silently; otherwise the window force-shows so the user can log in. macOS detects a login-item launch via a best-effort
kAEOpenApplication/keyAEPropDataApple Event check. - Shared
parse_launch_args()(client/src/launch_args.cpp, unit-tested) — replaces each shell's previous ad hoc single-argumentargvscanning, so--autostartand amatrix:URI can coexist on the command line.
- Cross-platform
tesseract::INotifier/Notificationabstraction; per-platform impls created after login. - Push-rule evaluation via
evaluate_push_rulesinsdk/src/client.rs; fires onVectorDiff::PushBack(live events only);is_mentionfromAction::is_highlight(). - Win32 — WinRT
Windows.UI.Notifications.ToastNotificationManager;ToastGenericXML with sender, optional room name (omitted for DMs), 120-char body preview;WM_TESSERACT_NOTIFY_CLICKnavigates to the room. AUMID registered inHKCU\Software\Classes\AppUserModelId\at startup (required for non-packaged apps);notify()wrapped intry/catch(winrt::hresult_error)for robustness. - Qt6 —
QDBusInterfaceagainstorg.freedesktop.Notifications(legacy D-Bus, used everywhere except Flatpak); always a fresh popup (replaces_id=0, never updates a prior one in place); click navigates + raises window. - GTK4 —
GDBusConnection(session bus); same legacy-D-Bus/Flatpak-portal split and always-fresh-popup behavior as Qt6, via a sharedtesseract::linux_notify::NotificationCorrelationhelper (ui/shared/linux_notification_reply.h) for the id↔room/event bookkeeping both notifiers need. - macOS —
UNUserNotificationCenter;UNUserNotificationCenterDelegateonMainWindowController; in-foreground suppression when the source room is active; click navigates to the room. - Image & sticker previews —
m.image/m.stickernotifications embed the message picture (SDK fetch, 2 MiB cap, E2EE-transparent; a dedicatedm.stickerpush handler — stickers are a distinct event type). Win32 large inline<image>+ circular avatarappLogoOverride; macOSUNNotificationAttachment; Linux single image slot. Gated by thenotification_image_previewssetting. - Lock-screen privacy gate — cross-platform
tesseract::IScreenLock(Win32 WTS, macOScom.apple.screenIsLocked, Linux logindLockedHint);ShellBase::notification_image_allowed_()strips the picture whenever the screen is locked (avatars are not gated). - Wayland foreground activation — Qt6 and GTK4 notifiers use
org.freedesktop.portal.Notificationonly inside Flatpak (the sandbox's D-Bus proxy blocks the legacy interface there). Everywhere else, including plain Wayland, they use the legacyorg.freedesktop.Notificationsinterface and rely on its KDE/GNOME de-factoActivationToken(uint id, string token)signal (mirroring KDE's ownknotifications) for thexdg_activation_v1token, passed to the compositor beforeactivateWindow()/gtk_window_present(). This requires sending a"desktop-entry"hint onNotify()(QGuiApplication::setDesktopFileName("tesseract-matrix")in Qt6'smain.cpp; a literal"tesseract-matrix-gtk"in GTK4, matching each shell's own installed.desktopbasename) so the daemon knows which app to mint the token for. Switched away from routing Wayland through the portal because Plasma's Notification portal backend (implemented inplasma-workspace, notxdg-desktop-portal-kde) only reached interface v1 as of this writing — no inline-reply support at all on that path, and Wayland+KDE users got no working notifications through it either. - Per-room notification settings — a Notifications section in
RoomInfoPanelwith a four-option dropdown (Default / All messages / Mentions / Off) mapped to Matrix per-room push rules (RuleKind::Override+EventMatchfor "off",RuleKind::Roomfor "all"/"mentions", no rule for "default"); backed by a new sharedtk::ComboBoxwidget and wired through both the main window and pop-out room windows; Rustclient.rsreads/writesm.push_rules. - All platforms suppress the notification when the window is focused and the target room is already open.
- Quick-reply — Windows toast
<input>/<action>XML with foreground activation (unpackaged apps can't get true background activation); macOSUNNotificationCategory+UNTextInputNotificationActionwithout the.foregroundoption so replying doesn't raise the app; Linux implements both the KDE-only legacy D-Bus "inline-reply" extension (now reachable on Wayland too, since the legacy interface is used everywhere except Flatpak — see Wayland foreground activation above) and the portal's standardized "im.reply-with-text" button purpose (interface v2+, gracefully inert until Plasma implements it; only exercised via Flatpak now).event_idis threaded through the notification pipeline so a reply sends as a proper threaded reply (m.in_reply_to);ShellBase::send_notification_reply_dispatches the send and reports a failure via a follow-up notification.
- Corrosion fetched at configure time (no global Rust toolchain install requirement beyond
rustup). WHOLE_ARCHIVElink for the 3-way circular dependency betweentesseract_sdk_bridge_cxx,tesseract_client, andtesseract_sdk_ffi-static.- Cross-platform CMake presets —
windows-debug,windows-release,linux-debug,linux-release(builds GTK4 + Qt6),macos-appkit-{arm64,x86_64}-{debug,release}. - CPack installer packaging — NSIS and MSIX (Store + direct sideload) on Windows, DMG on macOS, DEB/RPM/AppImage plus a Flatpak/Flathub manifest and AUR
PKGBUILDs on Linux (see PACKAGING.md). Windows also has a winget manifest (Tesseract.Matrix, points at the NSIS installer); first upstream submission towinget-pkgsis still pending. - Bundled SQLite via matrix-sdk's
bundled-sqlitefeature; no system OpenSSL dep (TLS uses rustls).
Update this file after every major feature lands — append a new bullet (or extend an existing one) in the right category, refresh the test counts in the table at the top, and bump the "Last updated" date.