New-message composer: overlay, shared editor, mentions and queue #192

Merged
robbertbos merged 1 commit from fase-5-composer into main 2026-08-05 13:37:11 +00:00
Owner

Compose a new Mattermost message to a channel or person, independent of a saved
card, and send it now (with a pre-send undo window) or schedule it - consuming
the existing outgoing-message backend.

Composer

  • ComposeOverlay: a centered Modal (shared components/Modal.vue: focus-trap
    • scroll-lock + Escape + backdrop) with the full editor stack, TargetPicker and
      a send-now / schedule split-button. Content and target autosave to a concept; you can work on several concepts
      at once. During the undo window the pending send locks its concept (hidden
      from Concepten, not reopenable); the concept is deleted only after Mattermost
      accepted the message, so a failed send or a crash never loses composed text
      or attachments.
  • TargetPicker: a WCAG combobox over the account's channels/conversations plus
    a debounced people search to start a DM. The search matches every typed token
    against the channel name and its team name ("off topic groep x").
  • Entry point: a "Nieuw bericht" top-nav item + a c shortcut, behind a
    device-local single-key-shortcuts preference (WCAG 2.1.4).

Shared editor + mentions

  • MessageEditor bundles the MarkdownEditor + formatting toolbar + link bar +
    emoji picker + optional @-mention autocomplete, used by both the reply
    composer and the new-message composer so they cannot drift.
  • Mentions are scoped by context: a card's channel for a reply, the chosen
    target channel for a new message (new GET /api/mm/mention-candidates, sharing
    the reply endpoint's member/autocomplete logic).

Backend

  • Card-less write endpoints (POST /api/messages, /api/scheduled-posts) and per-user
    new-message drafts (multiple concepts per user). The composer-facing API is renamed from "cardless"
    to "new message"; the data-layer term (a row with card_id IS NULL)
    deliberately stays "card-less".
  • A single MAX_FILE_REFS for the attachment cap. A dev Mattermost mock behind a
    dedicated dev_mm_mock flag makes the composer testable locally without a real
    server, committed with full coverage.

Housekeeping

  • Shared AttachmentChipList (image thumbnails everywhere) and a
    useMessageComposer attachment core remove duplication between the two
    composers.
  • Rebased onto main and migrated to NLDD's primitive tokens.

Undo is a pre-send delay toast (lib/delayedSend), not a post-send delete.

Review round

A deep multi-agent review of everything since v2026.7.24 (with adversarial
verification per finding) confirmed 28 issues; all are fixed in the six
commits from c512a6a to 77a8839. Highlights:

  • Critical: sending a new message with attachments always failed and
    destroyed the concept (draft delete reaped the parked files before the
    delayed send fired). The concept now lives until the send succeeded.
  • Critical: merge-importing a backup duplicated a still-pending scheduled
    post (dedupe keyed on an idempotency key that exports never carry) - the
    message fired twice. Dedupe now uses content-shaped keys.
  • Duplicate-send windows closed: ambiguous transport errors fail closed
    instead of retrying outside Mattermost's 30s dedupe cache, the runner claim
    is an atomic state-guarded UPDATE, replies carry a pending_post_id, and the
    composers reuse one dedupe key across retries.
  • Export/import round-trips DM and target-less concepts and multiple drafts.
  • Recovery path: a failed/cancelled card-less scheduled message can be
    restored as a concept, including server, target and attachments.
  • Plus: UTC normalization for scheduled_at, dev_mm_mock production veto,
    deterministic migration downgrade, silent-autosave feedback, a11y fixes
    (Server select name, error-text contrast) and authorization tests for
    mention-candidates.
Compose a new Mattermost message to a channel or person, independent of a saved card, and send it now (with a pre-send undo window) or schedule it - consuming the existing outgoing-message backend. ## Composer - **ComposeOverlay**: a centered Modal (shared `components/Modal.vue`: focus-trap + scroll-lock + Escape + backdrop) with the full editor stack, TargetPicker and a send-now / schedule split-button. Content and target autosave to a concept; you can work on several concepts at once. During the undo window the pending send locks its concept (hidden from Concepten, not reopenable); the concept is deleted only after Mattermost accepted the message, so a failed send or a crash never loses composed text or attachments. - **TargetPicker**: a WCAG combobox over the account's channels/conversations plus a debounced people search to start a DM. The search matches every typed token against the channel name *and* its team name ("off topic groep x"). - **Entry point**: a "Nieuw bericht" top-nav item + a `c` shortcut, behind a device-local single-key-shortcuts preference (WCAG 2.1.4). ## Shared editor + mentions - **MessageEditor** bundles the MarkdownEditor + formatting toolbar + link bar + emoji picker + optional @-mention autocomplete, used by *both* the reply composer and the new-message composer so they cannot drift. - **Mentions** are scoped by context: a card's channel for a reply, the chosen target channel for a new message (new `GET /api/mm/mention-candidates`, sharing the reply endpoint's member/autocomplete logic). ## Backend - Card-less write endpoints (`POST /api/messages`, `/api/scheduled-posts`) and per-user new-message drafts (multiple concepts per user). The composer-facing API is renamed from "cardless" to "new message"; the data-layer term (a row with `card_id IS NULL`) deliberately stays "card-less". - A single `MAX_FILE_REFS` for the attachment cap. A dev Mattermost mock behind a dedicated `dev_mm_mock` flag makes the composer testable locally without a real server, committed with full coverage. ## Housekeeping - Shared `AttachmentChipList` (image thumbnails everywhere) and a `useMessageComposer` attachment core remove duplication between the two composers. - Rebased onto main and migrated to NLDD's primitive tokens. Undo is a pre-send delay toast (`lib/delayedSend`), not a post-send delete. ## Review round A deep multi-agent review of everything since v2026.7.24 (with adversarial verification per finding) confirmed 28 issues; all are fixed in the six commits from `c512a6a` to `77a8839`. Highlights: - **Critical:** sending a new message with attachments always failed and destroyed the concept (draft delete reaped the parked files before the delayed send fired). The concept now lives until the send succeeded. - **Critical:** merge-importing a backup duplicated a still-pending scheduled post (dedupe keyed on an idempotency key that exports never carry) - the message fired twice. Dedupe now uses content-shaped keys. - Duplicate-send windows closed: ambiguous transport errors fail closed instead of retrying outside Mattermost's 30s dedupe cache, the runner claim is an atomic state-guarded UPDATE, replies carry a pending_post_id, and the composers reuse one dedupe key across retries. - Export/import round-trips DM and target-less concepts and multiple drafts. - Recovery path: a failed/cancelled card-less scheduled message can be restored as a concept, including server, target and attachments. - Plus: UTC normalization for scheduled_at, dev_mm_mock production veto, deterministic migration downgrade, silent-autosave feedback, a11y fixes (Server select name, error-text contrast) and authorization tests for mention-candidates.
robbertbos force-pushed fase-5-composer from a4db9cb918
All checks were successful
CI / pre-commit (pull_request) Successful in 1m23s
CI / frontend-test (pull_request) Successful in 5m19s
CI / release-scripts (pull_request) Successful in 10s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 1m1s
security-scan / Python SAST (bandit) (pull_request) Successful in 38s
security-scan / JS SCA (npm audit) (pull_request) Successful in 41s
CI / backend-test (pull_request) Successful in 8m12s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 23s
security-scan / SBOM (trivy) (pull_request) Successful in 17s
CI / backend-test-postgres (pull_request) Successful in 10m10s
test-build / build (backend) (pull_request) Successful in 1m55s
test-build / build (frontend) (pull_request) Successful in 2m14s
CI / e2e (pull_request) Successful in 9m21s
test-build / build (pull_request) Successful in 0s
to 1a0e760f87
All checks were successful
CI / release-scripts (pull_request) Successful in 25s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 32s
security-scan / SBOM (trivy) (pull_request) Successful in 32s
security-scan / JS SCA (npm audit) (pull_request) Successful in 37s
security-scan / Python SAST (bandit) (pull_request) Successful in 38s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 1m0s
test-build / build (backend) (pull_request) Successful in 1m18s
test-build / build (frontend) (pull_request) Successful in 1m22s
CI / backend-test (pull_request) Successful in 1m23s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m33s
CI / pre-commit (pull_request) Successful in 1m38s
CI / e2e (pull_request) Successful in 4m22s
CI / backend-test-postgres (pull_request) Successful in 4m53s
2026-07-26 06:22:30 +00:00
Compare
robbertbos changed title from Card-less composer: new-message overlay + queue integration to New-message composer: overlay, shared editor, mentions and queue 2026-07-26 10:11:02 +00:00
robbertbos force-pushed fase-5-composer from 814b79ce9c
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 13s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 21s
security-scan / JS SCA (npm audit) (pull_request) Successful in 22s
CI / pre-commit (pull_request) Successful in 30s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 42s
CI / backend-test (pull_request) Successful in 57s
test-build / build (backend) (pull_request) Successful in 59s
test-build / build (frontend) (pull_request) Successful in 1m7s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m14s
CI / e2e (pull_request) Successful in 3m21s
CI / backend-test-postgres (pull_request) Successful in 4m37s
to 0fd75a9814
All checks were successful
CI / release-scripts (pull_request) Successful in 10s
security-scan / JS SCA (npm audit) (pull_request) Successful in 31s
security-scan / Python SAST (bandit) (pull_request) Successful in 32s
security-scan / SBOM (trivy) (pull_request) Successful in 12s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
CI / pre-commit (pull_request) Successful in 41s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 51s
CI / backend-test (pull_request) Successful in 1m10s
CI / frontend-test (pull_request) Successful in 1m24s
test-build / build (backend) (pull_request) Successful in 58s
test-build / build (frontend) (pull_request) Successful in 1m2s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 3m26s
CI / backend-test-postgres (pull_request) Successful in 4m38s
2026-08-05 13:32:25 +00:00
Compare
robbertbos deleted branch fase-5-composer 2026-08-05 13:37:11 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
robbertbos/waggle!192
No description provided.