Skip to content

feat(mobile): attachments and saved views — web/mobile parity complete - #144

Merged
acamarata merged 3 commits into
mainfrom
feat/mobile-attachments
Aug 22, 2026
Merged

feat(mobile): attachments and saved views — web/mobile parity complete#144
acamarata merged 3 commits into
mainfrom
feat/mobile-attachments

Conversation

@acamarata

Copy link
Copy Markdown
Collaborator

Closes the two remaining web/mobile parity gaps in P5 T-07. Attachments and saved views were the only features present on web and missing on mobile.

Attachments

expo-document-picker (~13.1.6, the SDK 53 pin from expo/bundledNativeModules.json), a useAttachments hook holding the upload contract, and an AttachmentList mounted in TaskDetailScreen.

Upload is three steps and all three are required: presign → PUT the bytes → record the row with the storage path the server chose. Skipping the third leaves an object nothing references, which presents to the user as a successful upload with no attachment — so each step reports a distinct error, including record-failed for exactly that orphan case.

Details that are easy to get wrong, and are commented in place:

  • The PUT carries no Authorization header. The signature in the query string is the authorisation; a header invalidates it.
  • bucket and uploader_id are never sent. Hasura presets the uploader, and bucket is not client-insertable because getDownloadUrl honours it and signs with the storage root credentials.
  • Size is checked before the presign, not after the bytes are sent — on cellular that is a real cost.
  • Uploads are disabled offline rather than queued, unlike comments: a presigned URL expires in 15 minutes and the picker's cache URI is not guaranteed to outlive that.

Saved views

Mobile ListScreen has no filter UI at all, so there was nowhere to "apply" a view the way web does. Rather than redesign ListScreen, this mirrors the existing SmartViewScreen pattern: pick a view, see the tasks it matches, tap through to detail. Creating and editing views stays on web, where the filter UI that produces them lives.

Filtering is client-side over tasks useSmartViews already loads, not a GraphQL where-clause. np_saved_views.filters is a jsonb blob written by another client, so its shape is not guaranteed — building a where-clause from it risks a query that errors outright, whereas an unknown value here simply matches nothing. A view naming a deleted priority shows an empty result rather than silently falling back to everything. Undated tasks sort last in both directions, because an undated task is unscheduled, not "earliest".

A test mock that was hiding a real failure mode

TaskDetailScreen.test.tsx stubbed only useQuery. Mounting a component that mutates broke all three of its tests with useMutation is not a function. Extended rather than worked around.

Verification

before after
tests 374 394
useAttachments coverage 91%
overall coverage 74.5% 77.5% (gate: 60%)

Typecheck clean; lint 0 errors (43 pre-existing warnings).

The upload logic is covered directly, including every failure branch, because a silent failure at any step is indistinguishable from success to the user.

Depends on

nself-org/packages#11 — the shared CREATE_ATTACHMENT must stop sending bucket (the hardened Hasura permission rejects it), and GET_UPLOAD_URL/GET_DOWNLOAD_URL are added there rather than re-hand-written here.

Attachments were the only feature present on web but missing on mobile.
Adds expo-document-picker (~13.1.6, the SDK 53 pin), a useAttachments hook
holding the upload contract, and an AttachmentList mounted in
TaskDetailScreen.

Upload is three steps and all three are required: presign, PUT the bytes
to the returned URL, then record the row with the storage path the SERVER
chose. Skipping the third leaves an object nothing references, which
presents to the user as a successful upload with no attachment — so each
step reports a distinct error, including record-failed for exactly that
orphan case.

The PUT deliberately carries no Authorization header. The signature in the
query string is the authorisation and a header invalidates it.

`bucket` and `uploader_id` are never sent: Hasura presets the uploader, and
bucket is not client-insertable because getDownloadUrl honours it and signs
with the storage root credentials.

Size is checked before the presign rather than after the bytes are sent,
which on cellular is a real cost.

Uploads are disabled offline rather than queued, unlike comments: a
presigned URL expires in 15 minutes and the picker's cache URI is not
guaranteed to outlive that.

TaskDetailScreen's urql mock only stubbed useQuery, so mounting a component
that mutates broke all three of its tests with "useMutation is not a
function". Extended rather than worked around.

383 tests pass (was 374). Coverage of the upload logic went from 21% to
91%; overall 74.5% -> 77.7%, above the 60% gate. Typecheck clean.
…l copy

graphql-attachments.ts was the only file in web/src/lib carrying its own
hand-written GraphQL strings. graphql-saved-views, graphql-account and
graphql-ws-client all import theirs from @nself/ntask-core.

That local copy is why the field names drifted: it asked for filename,
size_bytes and user_id while the schema has file_name, file_size_bytes and
uploader_id, so every attachment query failed validation. Fixing the names
in place left the duplication that caused it, and a second copy of the same
strings would drift again.

Now a thin wrapper over the shared operations, matching the other three
files. The presign actions come from the package too, so web and mobile
issue byte-identical operations.

461 web tests pass; typecheck and build clean.
Saved views were the second and last feature present on web but missing on
mobile. Views created on web are now usable on a phone.

Scope note: mobile ListScreen has no filter UI at all, so there was nowhere
to "apply" a view the way web does. Rather than redesign ListScreen, this
mirrors the existing SmartViewScreen pattern — pick a view, see the tasks it
matches, tap through to detail. Creating and editing views stays on web,
where the filter UI that produces them lives.

Filtering happens client-side over the tasks useSmartViews already loads,
not as a GraphQL where-clause. np_saved_views.filters is a jsonb blob
written by another client, so its shape is not guaranteed; building a
where-clause from it risks a query that errors outright, whereas an unknown
value here simply matches nothing. A view naming a deleted priority shows an
empty result rather than silently falling back to everything.

Undated tasks sort last in both directions — an undated task is
unscheduled, not "earliest".

394 tests pass (was 383). applyFilters is covered directly, including the
malformed-input cases, since it is the part that receives data it did not
produce.
@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ntask-web Ignored Ignored Preview Aug 22, 2026 11:23pm

Request Review

@acamarata
acamarata merged commit 69a6b37 into main Aug 22, 2026
18 of 21 checks passed
@acamarata
acamarata deleted the feat/mobile-attachments branch August 22, 2026 23:46
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