Skip to content

Add the talk-note screen with pictures and file attachments - #702

Merged
JSdotNet merged 2 commits into
mainfrom
claude/mobile-inbox-talk-note-screen-c65e52
Sep 26, 2026
Merged

JSdotNet merged 2 commits into
mainfrom
claude/mobile-inbox-talk-note-screen-c65e52

Conversation

@JSdotNet

@JSdotNet JSdotNet commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Summary

This adds the phone's talk note: a note taken during a conference talk, with the slide photo and any handout, that lands in the desktop Inbox as one ordinary capture. It is plan item talk-note-screen of plan mobile-inbox-slice, after capture-outbox (#652) and attachment-store-service (#668). It builds the phone half of local ADR 0014.

It is called a talk note in the UI, the code and the devbook. It is never a "session note", because Sessions is a bounded context of its own.

Changes

  • /note screen (Backlog.Mobile.UI): title, a Markdown body that dictation appends to, the speaker as one @name, tags, three attach buttons (Take photo / Choose pictures / Choose files), the attachment strip, a "Send originals" switch, and a "Last note: waiting / uploading 2 of 3 / synced" line.
    • The draft lives in the scoped TalkNoteDraft, so it survives tab switches and backgrounding.
    • Picked files are staged on disk beside the database (talk-notes/drafts, then talk-notes/outbox).
  • IAttachmentPicker, mirroring ISpeechTranscriber:
    • AndroidAttachmentPicker uses MediaPicker.CapturePhotoAsync / PickPhotosAsync and FilePicker.PickMultipleAsync. The manifest gains CAMERA and the IMAGE_CAPTURE query.
    • WebAttachmentPicker uses hidden <InputFile multiple accept=…> elements plus attachments.js, for the harness.
  • Refusal on the tile: a file over 25 MB or off the allowlist shows the reason under its name. The note can't be sent until the file is removed.
    • The cap and the allowlist move from SyncAttachmentOptions (Api) to SyncAttachmentLimits in Sync.Abstractions. The options still default from them, so the service's behaviour is unchanged.
  • Downscaling: PictureDownscaler uses SkiaSharp 4.152.1 (new dependency, MIT). Pictures go out at a 1600 px longest edge as JPEG q85.
    • ExifOrientation writes a minimal APP1 segment that keeps only the orientation tag, so location, camera and time are dropped.
    • "Send originals" skips all of this. Files go as they are.
  • Outbox: a sent note is one entry of kind talk-note.
    • TalkNoteOutboxKind uploads each file (PUT /api/sync/attachments/{id} with X-Attachment-Sha256), then posts the capture naming them.
    • The new IStagedOutboxKind lets the kind checkpoint after each upload. An uploaded id is never re-sent, and each file gets the full attempt budget, because a checkpoint hands the attempts back.
    • DeviceOutbox now builds on the checkpointed entry after an attempt, and exposes Sending.
    • A 409/413/415 on a file refuses the note, naming the file. Files are released from the phone once the capture is delivered.
  • Inbox list: a talk note that hasn't gone yet shows marked waiting, like a pending capture.
  • AttachmentStrip is a new library component (Backlog.UI.Components/Inputs), with a storybook page (/attachment-strip), an index entry and a StorybookOrderTests folder route.
  • Devbook:
    • "Talk note" sub-feature under Mobile capture in .devbook/domain/capture/features.md.
    • "Talk Note Upload" sequence in .devbook/arc42/06-runtime-view.md, and the Capture Attachments and outbox-kind lines brought level with it.
    • SkiaSharp entry in .devbook/tech/mobile.md.
  • .gitattributes marks tests/**/Fixtures/** binary, so the fixture PDF and JPEG stay byte-exact.

Changed on the way

  • The button reads "Send to Inbox", not "Save". interaction-guidelines.md#auto-save-no-save-buttons forbids a Save button as the means of persistence, and the draft already keeps itself. It does exactly what the plan's "Save" describes.

Reviewer notes

  • These are not enforced on the phone yet. None blocks, and each could be a follow-up:
    • The service's tag bounds (20 tags, 100 characters each). A note over them would be refused by the service and parked.
    • A picture over 25 MB is refused on its tile even though downscaling would fit it.
    • Files staged on the strip are orphaned if the app is killed with a draft open.
  • The desktop thumbnail and file row are not here. They are desktop-attachment-intake, a separate plan item that has not landed.

Testing

  • Backlog.sln builds with 0 errors. The Android head (net10.0-android) builds clean.
  • All 23 unit-test projects are green (9,229 tests). New tests:
    • Backlog.Mobile.UI.UnitTests: generated title, draft surviving a route change, downscaler output for a fixture photo (1600×1200, EXIF reduced to orientation 6), a refused file naming its reason and never read, upload-before-capture order, per-attachment retry, a separate budget per upload, refusal by file name, "uploading 2 of 3", offline waiting and file release, originals vs downscaled, body/tags/speaker on the capture.
    • Backlog.UI.Components.UnitTests: AttachmentStrip.
  • Aspire QA at 375 px with Playwright, on this worktree's isolated AppHost. 9/9 scenarios pass:
    • paired the phone
    • draft survives a tab switch
    • fixture JPEG + PDF tiles
    • .exe refused on its tile, with Send disabled
    • sent to "synced"
    • traces show PUT /api/sync/attachments ×2 (201) before POST /api/sync/inbox (201)
    • the item appears in the desktop harness Inbox with body, tag and @ada
    • dismissed on the desktop, gone from the phone list
    • with sync stopped the note is "waiting", then flushes when sync is back
  • sync logged zero errors (the benign warnings were the ephemeral dev signing key and invalid_token after the deliberate restart). No browser console errors during the run.
  • node tools/devbook/check-metadata.mjs: 0 blocking.

Verification

devbook:verify-change and the plan item's criteria were checked against the change set; 114/114 mobile UI tests pass on a clean build.

Item Verdict
/note fields, scoped draft aligned
Picker (Android + harness), tiles, refusal reasons aligned
Downscale 1600 px JPEG, EXIF orientation only, send originals aligned
One outbox entry, uploads before capture, per-file retry, status, title fallback aligned ("Send to Inbox" label accepted)
Storybook page aligned
Named unit tests aligned
capture/features.md#talk-note aligned
06-runtime-view.md#talk-note-upload, #capture-attachments, #mobile-capture-and-sync aligned
tech/mobile.md#skiasharp aligned

The Android picker and a draft surviving real backgrounding can only be checked on a device. That is the plan's device-validation entry.

Related

Checklist

  • Scope is focused and excludes unrelated changes
  • Documentation or instruction files were updated when governance or behavior changed
  • Running Aspire instances for this checkout were stopped before PR creation (build\stop-aspire-before-pr.ps1)
  • Follow-up work is captured when the implementation is intentionally partial
  • Issues this PR closes are linked with a closing keyword, or there are none

🤖 Generated with Claude Code

JSdotNet and others added 2 commits September 27, 2026 00:32
A note taken during a conference talk (what was said, the slide photo, the
handout) now lands in the desktop Inbox as one ordinary capture. It is called a
talk note throughout, never a session note: Sessions is its own bounded context.

- /note in Backlog.Mobile.UI has a title, a Markdown body that dictation
  appends to, the speaker as one @name, tags, and an attachment strip. The
  draft lives in a scoped TalkNoteDraft, so it survives tab switches and
  backgrounding.
- IAttachmentPicker mirrors ISpeechTranscriber. Android uses MediaPicker and
  FilePicker (CAMERA and the IMAGE_CAPTURE query are in the manifest). The
  harness uses hidden <InputFile multiple> elements.
- A file over the 25 MB cap or off the ADR 0014 allowlist is refused on its
  tile with the reason, and the note is not sent until it is removed. The cap
  and the allowlist move to SyncAttachmentLimits in Sync.Abstractions, so the
  phone and the service share one list.
- Pictures are downscaled with SkiaSharp before they queue: 1600 px longest
  edge, JPEG, EXIF reduced to the orientation. A per-note "Send originals"
  switch keeps them as taken.
- A sent note is one outbox entry of kind talk-note. It uploads each file
  (PUT /api/sync/attachments/{id}) and checkpoints it through the new
  IStagedOutboxKind, so a retry never re-sends an uploaded id and each file
  gets its own attempt budget. It then posts the capture naming them.
  DeviceOutbox.Sending drives "waiting / uploading 2 of 3 / synced".
- Pending talk notes show in the phone's Inbox list, marked waiting.
- An AttachmentStrip library component with its storybook page.
- The button reads "Send to Inbox" rather than "Save".
  interaction-guidelines.md#auto-save-no-save-buttons forbids a Save button
  as the means of persistence.
- Devbook: a Talk note sub-feature in capture/features.md, a Talk Note Upload
  sequence in arc42/06, and a SkiaSharp entry in tech/mobile.md.

Plan item talk-note-screen of plan mobile-inbox-slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The QA run wrote two scratch files at the repository root (a path that lost
its separators), and they rode along in the previous commit. The fixture PDF
and JPEG are byte-exact inputs, so they are marked binary: a Windows
checkout's line-ending conversion would otherwise move the PDF's xref offsets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant