tasks: add TASK-16 culver messenger+contacts; re-point TASK-01 to culver-contacts store
This commit is contained in:
parent
8578e654d2
commit
2d95f3743a
2 changed files with 153 additions and 9 deletions
|
|
@ -1,9 +1,12 @@
|
|||
# TASK 01 — Favorites: wife's name on her number
|
||||
|
||||
**Status:** blocked on a contacts-store decision. qtpim retired
|
||||
2026-07-20 (see archive/06) — the store needs a non-Qt-PIM design
|
||||
(small owned store the Person layer reads directly) before this lands.
|
||||
**Size:** small, once the contacts store exists.
|
||||
**Status:** blocked on TASK-16. The contacts-store decision resolved
|
||||
2026-07-21: the "small owned store the Person layer reads directly" is
|
||||
**culver-contacts** (`~/Projects/culver`, `crates/contacts`) — qtpim
|
||||
retired 2026-07-20 (archive/06). This lands once TASK-16.B (store keyed
|
||||
to the machine identity) and 16.C (contact card UI) are done; the insert
|
||||
below then writes through the gated store instead of a qtpim engine.
|
||||
**Size:** small, once TASK-16.B/C exist.
|
||||
|
||||
## Goal
|
||||
|
||||
|
|
@ -13,11 +16,11 @@ motivator for the whole contacts/Person layer.
|
|||
|
||||
## Prerequisites
|
||||
|
||||
- TASK-06 done: `qtpim` aarch64 built and installed on the phone, the
|
||||
sqlite contacts engine live (`~/Projects/qtcontacts-souveraine`, path 3
|
||||
per `~/Projects/SouveraineOS/docs/contacts-design.md`).
|
||||
- Chatty identity-layer steps 1–3 already exist (`~/Projects/Chatty`
|
||||
branch `identity-layer`); the Person layer reads the contacts store.
|
||||
- TASK-16.B done: culver-contacts store keyed to the machine identity
|
||||
(Personal class, `docs/STORAGE-ENCRYPTION.md`) — the write below goes
|
||||
through the gated store.
|
||||
- TASK-16.C done: the contact card / `ContactModel` bridge, so the added
|
||||
name is viewable and the resolver renders it in the inbox.
|
||||
|
||||
## Steps
|
||||
|
||||
|
|
|
|||
141
docs/tasks/16-culver-messenger-contacts.md
Normal file
141
docs/tasks/16-culver-messenger-contacts.md
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# TASK 16 — culver: usable messenger + contacts as the Personal-class store
|
||||
|
||||
**Status:** open. Rooms sync today; the app is not yet usable and the
|
||||
contacts store does not yet meet the security bar. Unblocks TASK-01.
|
||||
**Size:** not single-session — four distinct work units (A–D), each
|
||||
session-sized. Do them in order; A is the smallest and most load-bearing.
|
||||
**Repo:** `~/Projects/culver` (Gitea `Fimeg/culver`, CI on `primary`).
|
||||
|
||||
## Where culver is (verified 2026-07-21)
|
||||
|
||||
- Matrix sync is real and further along than any reference bridge:
|
||||
`SyncService` sliding sync, per-room `Timeline`, E2EE crypto store,
|
||||
reactions/threads/edits/redaction. `crates/matrix` (1510 LOC) is not
|
||||
the weak link.
|
||||
- Runtime architecture is clean: QML → invokable → `AppCommand` (mpsc) →
|
||||
`runtime.rs` → `Protocol`/`Store` → `CoreEvent` → `ModelUpdate` → GUI
|
||||
thread. `crates/app/src/runtime.rs`.
|
||||
- `crates/contacts` (1374 LOC) is real and is **the qtpim replacement**
|
||||
(qtpim retired — archive/06): owned SQLite store, vCard 4.0 import/
|
||||
export, number/MXID/email → identity resolver. The runtime already
|
||||
uses it for name resolution via the `resolve_display` hook.
|
||||
|
||||
## The four problems (observed on device + read in tree)
|
||||
|
||||
### A. Tap a room → no messages render *(highest leverage)*
|
||||
|
||||
The QML and the `ChatModel` bridge were written to **two different
|
||||
contracts** and never reconciled. Same failure *class* as the
|
||||
2026-07-20 "matrix login never fires" bug (`#[auto_cxx_name]`), one
|
||||
level up: here the invokable **names themselves** don't exist, so QML
|
||||
silently no-ops instead of erroring.
|
||||
|
||||
| QML calls / binds | Real bridge (`crates/app/src/bridge/chat.rs`) | Effect |
|
||||
|---|---|---|
|
||||
| `chatModel.open(cid)` (Main.qml:58) | `load_conversation` → `loadConversation` | **`LoadTimeline` never dispatched → timeline stays empty** |
|
||||
| `chat.loadOlder()` (ChatView.qml:112) | `paginate_back` → `paginateBack` | back-pagination dead |
|
||||
| delegate roles `messageId, bodyText, bodyKind, senderDisplay, threadReplyCount, inReplyToId` | actual roles `id, body, html, sender, replyCount, inReplyTo` | **every row binding undefined → blank rows even once data arrives** |
|
||||
| `chat.avatar / protocol / subtitle / typing` | not qproperties | header half-empty |
|
||||
| `chat.threadModel` (ThreadView.qml) | doesn't exist — thread arrives via `thread_ready` signal + `thread_rows` | thread pane can't bind |
|
||||
|
||||
Two layers, both broken: fix `open`→`loadConversation` and
|
||||
`loadOlder`→`paginateBack`, **and** align the ~14 role names, **and**
|
||||
add the missing `subtitle`/`avatar`/`protocol` qproperties (or drop the
|
||||
QML that references them). Decide the ThreadView contract
|
||||
(signal-driven vs. a real `threadModel`) and make one side match.
|
||||
|
||||
`LoadTimeline` in `runtime.rs` reads **only from the store**
|
||||
(`store.messages`) — correct, because the matrix backend streams
|
||||
`MessageReceived` into the store on sync. So once the names line up,
|
||||
tapping a room shows content with no backend change.
|
||||
|
||||
**Acceptance:** tap any synced room → its messages render; scroll up
|
||||
paginates history; header shows title + protocol glyph.
|
||||
|
||||
### B. Contacts as the Personal-class store *(the security bar)*
|
||||
|
||||
The store works but is **plaintext today** — `Connection::open(path)`,
|
||||
`PRAGMA journal_mode = WAL`, names/numbers/MXIDs/emails/inline photo
|
||||
BLOBs all in the clear in the shared DB file (`crates/contacts/src/
|
||||
store.rs`). And `export_vcard` / `import_vcards` are unguarded free
|
||||
functions — a plaintext off-system egress path. So "won't open without
|
||||
the system key" and "cannot be shared off system" are the **design
|
||||
intent, not the current behaviour.**
|
||||
|
||||
Bring it to the bar, per `docs/STORAGE-ENCRYPTION.md`:
|
||||
|
||||
- **Contacts = Personal class.** Key evicted on lock; rooted in the
|
||||
machine seed via the Argon2id-wrapped Personal class key. A locked/
|
||||
stolen device yields ciphertext. Use the primitive TASK-11 already
|
||||
shipped: souveraine-secrets seals AES-256-GCM under a machined-
|
||||
signature-wrapped store key (`source=Machined`, reboot-verified). The
|
||||
contact DB key comes from that rail, not a file on disk. Store won't
|
||||
open when the rail is unavailable — that is the point.
|
||||
- **"Share a contact" is a Personal→egress operation, not a function
|
||||
call.** `export_vcard` stops being freely callable and becomes an
|
||||
authorized op gated by **stepUp + agent interaction** — the
|
||||
`org.souveraine.Secrets.Manage` stepUp dialog (TASK-11 "ingress
|
||||
pending") is the seam. Import (ingress) stays ungated; egress is the
|
||||
boundary.
|
||||
|
||||
**Acceptance:** contact DB is ciphertext at rest and does not open
|
||||
without the machine identity (verify: move the file to a bare box → no
|
||||
plaintext). A vCard export attempt raises stepUp and does nothing
|
||||
without it. Round-trip still green for a legitimately-authorized share.
|
||||
|
||||
### C. Contact card UI *(built ON TOP of B, not before)*
|
||||
|
||||
The cards at the top of the inbox can't open because there is **no
|
||||
contact-card bridge or view** — `crates/contacts` has no UI surface
|
||||
(only the `resolve_display` name hook is wired). Add a `ContactModel`
|
||||
bridge + card view. Build it against the **gated** store from B, so
|
||||
opening a card is a keyed read and "share" from the card triggers the
|
||||
stepUp. Building the UI first would bake the plaintext/free-share model
|
||||
we are moving away from.
|
||||
|
||||
**Acceptance:** tapping a contact chip/avatar opens a card (name,
|
||||
numbers, MXIDs, photo); "share" on the card triggers stepUp.
|
||||
|
||||
### D. Notifications: culver → NotifyEvents *(small)*
|
||||
|
||||
Notifications on the phone already work — the **shell owns the surface**
|
||||
(`org.freedesktop.Notifications` is quickshell's; TASK archive/04 built
|
||||
the `NotifyEvents` fan-out seam + lock card). culver just isn't feeding
|
||||
it: nothing in the tree emits. Chatty's `im.received` uses the
|
||||
`notify-send`/libnotify pipe that lands in banner + lock card.
|
||||
|
||||
Emit on inbound `MessageReceived` for conversations that are **not the
|
||||
open one and not muted** (mute state already exists — `Conversation::
|
||||
muted`, Matrix `RoomNotificationMode::Mute`). One emit point in
|
||||
`runtime.rs`'s event fold.
|
||||
|
||||
**Acceptance:** an inbound message to a non-open, non-muted conversation
|
||||
raises a banner (and the lock card while locked) via the same pipe
|
||||
Chatty uses.
|
||||
|
||||
## Also queued (not blocking A–D)
|
||||
|
||||
Inbox favorites redesign: replace the pinned `ListView` section
|
||||
(`Inbox.qml`) with a **horizontal circles rail** along the top, ordered
|
||||
by last-contacted, later honouring a pinned state. New component, not a
|
||||
fix — do it after A lands so there is content to open.
|
||||
|
||||
## Build / deploy
|
||||
|
||||
Per tree conventions: build on archdev, never the laptop. culver builds
|
||||
in archdev `~/culver`; CI runs on push to `primary`. cxx-qt 0.9 keeps
|
||||
snake_case Qt-facing names — every `extern "RustQt"` block needs
|
||||
`#[auto_cxx_name]` (root cause of the class of bug in A). Deploy to the
|
||||
Pixel 3 from the laptop over USB.
|
||||
|
||||
## Connects to
|
||||
|
||||
- **TASK-01** (favorites — wife's name on her number): *this is its
|
||||
unblock.* culver-contacts is the "small owned store the Person layer
|
||||
reads directly" TASK-01 has been blocked on since qtpim retired
|
||||
(archive/06). Re-point TASK-01 here.
|
||||
- **TASK-11** (souveraine-secrets): supplies the at-rest key + stepUp
|
||||
for B.
|
||||
- **archive/04** (notification server): supplies the `NotifyEvents`
|
||||
seam for D.
|
||||
- `docs/STORAGE-ENCRYPTION.md`: Personal-class placement for B.
|
||||
Loading…
Reference in a new issue