monal-rebuild/ux-implementation-plan.md
pijukka 1802100aa0 test(chat): accept encrypted attachments live, and record the release
New ux-media-runner.cjs exercises package 9 against the deployed bundle
with two Linux browser profiles and the dedicated audit accounts. All ten
checks pass with zero page errors: every outgoing attachment stanza is
OMEMO-encrypted with no jabber❌oob child and no cleartext aesgcm string,
the second profile decrypts image, file and voice independently (the 1x1
PNG decodes 1x1, the generic file downloads as the exact plaintext bytes),
upload sizes are plaintext + 16 to the byte, the only plaintext PUT in the
run is the group upload that is plaintext by design, cancellation sends
nothing, a 404 and an integrity failure read differently with a working
retry, a foreign host is explained without a request, and the group path
keeps its oob, name and media type.

Three checks first failed on the runner's own assumptions rather than the
app's behaviour, recorded in UX-MEDIA-RESULTS.md so the next run is not
misled: Prosody serves uploads immutable so a re-read never reaches the
network, persistent profiles make a bare waitFor match an older
attachment, and connect-src has no blob: so an in-page fetch of our own
object URL is CSP-blocked — the app only assigns it to src/href.

Release: client 031766c pinned by blunix d23cb82, live as generation 557,
Nix build and HTTPS-served bundle byte-identical, sw.js and CSP unchanged,
ten services active, zero failed units, rollback 556. The group fixture was
destroyed and the seeded row removed. Still device-bound: the real Monal
round trip and iPhone recording/playback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-08 16:53:28 +02:00

350 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.