# UX implementation plan — 2026-10-07 Implementation status, 2026-10-08: packages 0–8 implemented and deployed after operator approval. See [0–3 results](UX-IMPLEMENTATION-RESULTS.md), [4–6/reconnect results](UX-STATE-RESULTS.md), [phone results](UX-PHONE-RESULTS.md) and [reply results](UX-REPLY-RESULTS.md) for checks and remaining native validation. The operator confirmed notifications/reply, corrected reconnect, the continuous iPhone camera/status-area background and readable encrypted quote/answer display on Monal Stable 6.4.21. Package 9 remains planned. The original proposal and acceptance criteria below are retained. Original proposal, based on completed browser exploration in [UX-AUDIT-REPORT.md](UX-AUDIT-REPORT.md). The package descriptions below retain the original requirements; current status is recorded above. Target: chat.0ff.space, especially the installed PWA on the operator's iPhone 17 Pro / iOS 26.6. Preserve the current Svelte/IndexedDB/XMPP architecture and legacy OMEMO compatibility documented in [PLAN.md](PLAN.md) and [OMEMO-PLAN.md](OMEMO-PLAN.md). ## Recommended delivery order | Order | Package | Result for the user | Size / risk | Dependency | | --- | --- | --- | --- | --- | | 0 | Candidate test setup | Checks run against the changed client and expose regressions | Small / low | None | | 1 | Current OMEMO device lists | Newly added devices receive readable new messages | Medium / high | 0 | | 2 | Accurate displayed status | Decryption failures do not imply successful reading | Medium / medium | 0; validate with 1 | | 3 | Media capability before upload | Unsupported encrypted media is explained before selecting/recording/uploading | Small / medium | 0; current-device policy from 1 | | 4 | Drafts and reading position | Switching views preserves unfinished text and the visible message | Medium / medium | 0 | | 5 | Offline startup and retry | Saved chats remain available offline and reconnect without reloading | Medium / medium | 4 for draft acceptance | | 6 | Group information and people | Tapping name/count opens a list with useful person actions | Medium–large / medium | 4; membership permissions below | | 7 | Phone controls and dialog focus | Frequent actions are easier to tap; menus/dialogs manage focus | Small–medium / low | Apply as controls from 6 are added | | 8 | Quoted replies | Replies identify their original message and support navigation back | Medium–large / high | 1, 4, 7; protocol decision | | 9 | Encrypted attachments | Direct image/file/voice sending works across supported clients | Large / high | 1, 3; attachment interoperability decision | Size is relative scope, not a time estimate. Each package gets a separate reviewable commit, and a package may need multiple commits. Suggested releases: A = 1–3, B = 4–6, C = 7–8. Package 9 is a separate feature release once its compatibility design is verified. Do not hold group information or drafts until full encrypted-media support is ready. ## 0. Turn the exploratory checks into candidate regressions Add a configurable base URL and explicit expected results to the relevant audit runners. Keep the original deployed findings as historical evidence. Use `npm run build`, then `npm run preview -- --host 127.0.0.1` for the production-built candidate, with the existing preview WebSocket proxy and dedicated A/B accounts. The preview configuration already provides that proxy; the production build registers its service worker. Loopback testing does not require a new public hostname or server configuration. Use reusable private browser profiles so ordinary retests do not create unnecessary OMEMO devices. Create a fresh B device deliberately only for the device-addition regression. After package 1, that regression must pass without deleting a cache entry; the prior diagnostic cache intervention must no longer be part of the successful path. Acceptance: record candidate Git revision/build identity, origin, engine, viewport and test setup. Run Chromium and WebKit sequentially with one worker on blunix; avoid simultaneous builds. Keep credentials, profiles, keys and captures outside Git. Local preview proves candidate behavior; deployed HTTPS/CSP/worker/push integration is checked separately after a release. Fix the inconclusive WebKit scroll setup before using it as an acceptance assertion. Its offline internal error remains a test limitation. ## 1. Refresh device lists without changing the encryption protocol Affected: `src/crypto/omemo.ts`, `src/crypto/omemo-db.ts`, `src/xmpp/session.svelte.ts`, and existing OMEMO tests. The current caps already announce device-list notifications, but the PEP message handler processes only avatar metadata. Handle valid legacy OMEMO device-list events, update the correct account/peer cache and notify the fingerprint/capability UI. Validate the sender and payload; never take another peer's list from unrelated stanza metadata. An explicit empty published list differs from an unavailable or malformed response. Use a bounded freshness rule as a fallback for missed PEP events, including after reconnect and when starting to send again. Suggested initial maximum age: five minutes; verify this with request-count tests. Coalesce concurrent refreshes. Stale or uncertain capability must not silently select plaintext for a contact already known to support OMEMO. Keep existing identity and ratchet state; changing the target list does not require regenerating keys. For peers without a reliable PEP subscription, refresh before the first send of a conversation/send burst; do not assume notifications will arrive or rely solely on the age bound. Include non-roster direct peers in tests. Acceptance: A opens B's chat, B adds a browser device, then A sends a new message that B reads without reload/storage deletion. Exercise removal, reconnect, an own additional device, a missed event, empty list, fetch failure and concurrent sends. Removed devices stop receiving newly wrapped keys after refresh. Old pre-device backlog remains an explicit expected limitation. Preserve dedup-before-decrypt and verify current Monal interop for this package rather than changing namespaces or cryptographic formats. ## 2. Separate local attention state from remote displayed markers Affected: `src/xmpp/session.svelte.ts`, `src/sync/sync.ts`, `src/store/db.ts`, `src/ui/Chat.svelte` and status rendering. Do not send a displayed marker merely because an undecryptable placeholder is visible. Keep local badge handling separate so visiting an error does not trap the user with an uncleared badge. Make the error actionable without claiming that old messages can necessarily be recovered or decrypted. Choose marker selection with cumulative semantics in mind: a later marker can imply that earlier messages were displayed too. Proposed first policy: do not mark a newest undecryptable item as displayed; resume advancement when a later readable message is actually displayed. Keep the earlier error visible. Do not block all later markers forever on an unreadable gap or on expected pre-device backlog. Record this policy before coding and exercise the mixed-message case, not just a single error followed by no messages. Display external markers as remote reported display state, not proof of human reading or successful decryption. Use the status label "Displayed" rather than promising "Read" and add accessible status text to ticks. [XEP-0333](https://xmpp.org/extensions/xep-0333.html) defines cumulative displayed markers, not a cryptographic confirmation that content was read; the policy above is a product proposal. Acceptance: readable delivery transitions remain correct; a new failed message does not trigger a marker for itself; later readable messages, backlog, own-device synchronization and out-of-order markers follow the recorded policy. Later cumulative markers do not remove a stored error or claim successful decryption of earlier messages. Do not downgrade an already received monotonic remote marker to fabricate a different state. ## 3. Explain unsupported encrypted media before handling files Affected: `src/xmpp/session.svelte.ts`, `src/ui/Chat.svelte`, upload tests. Centralize conversation media capability in Session. For unsupported encrypted direct media, provide a visible short explanation beside the composer/attachment entry, and make recording unavailable with the same reason. Recheck capability before selecting/recording where possible and always before upload. Cover paste, drag/drop and programmatic send paths; the UI alone is insufficient. Unknown/offline capability is not permission to upload plaintext. Keep supported group uploads working. Acceptance: choosing or dropping unsupported media makes zero upload slot requests and zero upload PUTs; microphone permission is not requested for a known unsupported voice-send path. Error feedback is accessible. Group image/file upload and cancellation still work. This package communicates the existing limitation; package 9 provides actual encrypted attachment support. Never bypass the current encryption rejection to make sending work. ## 4. Preserve per-conversation drafts and reading anchors Affected: `src/store/db.ts`, `src/ui/Chat.svelte`, `src/ui/ChatList.svelte`, `src/ui/Main.svelte` and a small state helper if needed. Store drafts per account and conversation in the existing local database; use its meta store unless an actual index/schema need justifies a migration. Preserve multiline text through list/settings/chat switches, browser Back, reload and saved-session restart. Show a draft preview in the chat list. Logout removes drafts with the account's local chat data. Keep this local; cross-device draft synchronization is outside this package. Capture the exact draft being sent. Clear only that successfully sent draft; text entered while sending must survive. Failed sends retain the draft. Persist pending text before navigation and guard asynchronous load/save races between peers, accounts and tabs. Save a message key and offset for the first visible message, including whether the reader was at the bottom. Restore the anchor after loading the required local page; handle deleted/missing anchors gracefully. While reading older messages, incoming content and image sizing should preserve position. Offer a jump-to-latest control with a useful accessible label. Acceptance: independent drafts in A/B conversations and accounts; failed send and typing during send; rapid view switches; restart; logout; image load, incoming message and older-history pagination. Return to the same visible message rather than merely the same scrollTop. First opening uses the existing latest-message policy until unread positioning is separately designed. Tests check visible content, not implementation-specific fields. ## 5. Load saved history before network authentication completes Affected: `src/App.svelte`, `src/xmpp/session.svelte.ts`, `src/ui/Main.svelte`, list/chat connection feedback; `public/sw.js` only if an actual worker defect is established. Separate the locally selected remembered account from network status. With an opted-in saved session, open its local database and render history while offline. `last-jid` alone must not expose another account's cached messages automatically. No remembered session means the login form. Show clear offline/reconnecting state, disable Send and server actions, and retry on restored connectivity, app activation and an explicit Retry control. Coalesce attempts, retain backoff, and treat network failures separately from rejected/expired credentials. Authentication rejection returns to login; logout clears local state under the existing policy. Acceptance: fully closed browser/profile restart offline shows existing messages and draft; recovery succeeds without reload/password for a valid saved session; repeated connectivity changes produce no duplicate sends or parallel sessions. Test no token, invalid token, offline empty history and logout. Keep shell/update/cache regression checks; WebKit's Linux cache failure does not by itself justify changing the production worker. ## 6. Open group info from the name/count and select people Affected: `src/sync/muc.ts`, `src/xmpp/session.svelte.ts`, `src/ui/Chat.svelte`, `src/ui/Main.svelte`, new group-info UI and MUC tests. Make the group name/avatar/count an accessible entry to a dedicated group info view. Include name, subject, available people/roles and existing invite/leave actions. Back restores the draft and reading anchor. Make message nicknames open the same person's actions. Start a room-private conversation for an occupant; offer real contact actions only if the room discloses a usable account identity and that action is permitted. Replace the internal nickname set with participant records that retain available role/affiliation/identity, handle nickname changes and reconnect, and keep the existing count derived from those records. Label this as occupants/present participants; presence and hibernated sessions are not proof that someone is actively using the app. Where permitted, request members, admins and owners for the complete affiliation view, deduplicate by valid identity and label absent members. Keep occupant identity distinct from membership identity. Show forbidden, loading, error and empty states honestly. Ordinary member B was forbidden from querying all three affiliation lists in the audited room. Product decision before changing room/server policy: should every ordinary member be able to see absent members and their identities? The first UI package uses the existing permissions. A complete ordinary-member list requires an explicit room-privacy decision and an additional fixture test; do not silently relax anonymity/list permissions or call the partial list complete. [XEP-0045 member lists](https://xmpp.org/extensions/xep-0045.html#modifymember). Acceptance: one tap opens info; owner/admin/ordinary-member, an absent member, join/leave, nickname change and permission denial; person selection opens the correct action/view. List navigation, invite and leave preserve the intended account/room context. A forbidden request never renders as an empty group, and a member-only query never omits owners/admins silently. ## 7. Improve touch controls, menus and modal focus Affected: Chat, Fingerprint, group-info UI and shared styles as necessary. Aim for at least 44 × 44 CSS-pixel hit areas on frequent phone controls while keeping icons compact. Add useful accessible names, visible focus and correct expanded state. Dialogs take focus, contain it while modal, close with Escape and return focus to their trigger. Menus support keyboard navigation and dismissal; new group/person/reply controls follow this too. Acceptance: 320/402 px portrait, compact landscape, desktop and large text; controls remain reachable without horizontal overflow. Verify focus entry/containment/return and Escape with the browser. Validate touch comfort, VoiceOver and actual iOS safe areas on the phone. Existing control sizes were not a proven WCAG AA target-size violation; do not claim otherwise. ## 8. Add quoted replies after deciding compatible representation Affected: message parser/store/send path, Chat composer/bubbles and draft state. Plan structured reply metadata and stable IDs using [XEP-0461](https://xmpp.org/extensions/xep-0461.html), with a fallback for clients or histories that lack the referenced message. Check the actual Monal version; a protocol document does not prove client interoperability. Provide an explicit Reply action, quoted composer preview with Cancel, and tap-to-original navigation. Swipe-to-reply is an enhancement after the explicit action works. Use room-issued stable IDs where appropriate, keep references within the correct conversation and handle edits/missing history. For direct encryption, the quote text must remain inside the encrypted payload. Establish how reply references/fallback integrate with the deployed legacy OMEMO representation before emitting new XML. Do not copy sensitive quote text into plaintext metadata or infer that modern OMEMO examples fit this legacy namespace. If compatibility remains unresolved, ship the other packages and retain this package as a defined feature gate. Acceptance: supported clients receive and render the quote correctly; unsupported clients get useful text without duplicate quotes; known IDs navigate correctly; missing originals show a clear fallback; drafts preserve and cancel the quote; encryption/carbons/archive copies remain coherent. ## 9. Design and implement encrypted image/file/voice sharing This is a new interoperability feature, not a removal of the rejection. Before implementation, inspect the actual Monal encrypted-upload format and compare available legacy-compatible representations. Modern [XEP-0447](https://xmpp.org/extensions/xep-0447.html) is a research input, not an assumed compatibility contract. Record the selected format, metadata protection, size limits, decryption/playback and failure/cancellation rules. **Done, 2026-10-08: see `OMEMO-PLAN.md`, "Encrypted attachments: format decided".** The format is XEP-0454 `aesgcm://`; XEP-0447 is rejected because Monal implements no stateless file sharing. That section also records the metadata, size, host, failure and test rules this paragraph asked for, and verifies that blunix needs no CORS or CSP change. **Implemented and released, 2026-10-08.** See `OMEMO-PLAN.md`, "Encrypted attachments implemented" and "accepted live", and `UX-MEDIA-RESULTS.md`. Live as generation 557; the browser half of the acceptance below passes in full. What is left is device-bound: real Monal in both directions and the iPhone recording/playback half. Encrypt the blob before HTTP upload; distribute the necessary descriptor only through the supported encrypted message path. Verify that a known OMEMO peer never receives an unencrypted attachment or descriptor fallback. Make download/decrypt/render work with the trusted-media/CSP rules, release temporary object URLs and handle corrupted/truncated files explicitly. Acceptance: image, generic file and voice message round trips between two PWA profiles and the actual Monal client, including another own device; no plaintext file upload; cancellation, failed fetch and integrity failure; real iPhone recording/permissions/playback. This acceptance cannot be fully replaced by a synthetic microphone or two copies of this web client. ## Backlog after the confirmed corrections Contact picker and guided Join/Create distinction, local message search and per-chat mute are source/UI inventory findings, not fully exercised task failures. Give them their own short designs after the main packages; do not add group administration or a server-wide notification policy as an incidental part of group info. A mute design must distinguish in-app notifications from relay-delivered closed-PWA pushes. Search must stay account-scoped and explain local-history coverage. ## Verification and rollout For application changes, run the repository's `npm run check`, `npm test` and `npm run build`, plus focused candidate browser checks for that package. Add meaningful tests for device events/freshness, status selection, pre-upload capability and persisted state. Do not add tests that merely mirror cosmetic markup. Re-run broader checks only after new changes or failures justify them. Review diffs independently of passing test output. After implementation is authorized, prepare a release with the existing workflow: client commit/push, separate `nix flake update chat-client` commit in blunix, NixOS switch, verify the deployed asset/version and relevant live flows. Keep the previous generation as rollback. This plan request itself does not execute that rollout. Do not alter LLDAP, room privacy, public routes or existing personal accounts as a side effect of frontend work. On the iPhone, test only the remaining native integration for the changed package: keyboard/safe areas, lock/background recovery, trusted push-open, large text/VoiceOver and actual media hardware where applicable. Record those checks separately from Linux simulation. Do not require this device pass to begin implementing already confirmed application bugs. ## Model recommendation Keep Sol High for planning and the ordinary implementation/test cycle. This is a task-specific recommendation: the audit provides concrete evidence, bounded work packages and acceptance criteria. OpenAI describes GPT-6.1 Sol as suitable for complex technical projects where time/cost also matter; the exact High choice here is our recommendation, not a benchmark claim. [Official model-selection guidance](https://developers.openai.com/api/docs/guides/model-selection). Use a stronger reasoning setting or a separate review only if a package reveals unresolved protocol/crypto questions, conflicting evidence or repeated failures. Tests and real client interoperability remain the acceptance evidence regardless of model. No model switch or agent delegation was performed to write this plan.