Dentolize · Upload Progress Walkthrough
On this pageBusiness viewTechnical view

Knowing how far along an upload is

Business view

Every upload in the app now reports two things while it's running: what percentage is done, and a line naming the total size and, once there's been time to measure it, how fast it's going ("12.4 MB · Speed: 1.8 MB/s"). If a phone stalls or drops signal mid-upload, the speed line will stop climbing — a clear signal to staff that something is wrong, rather than a spinner that looks identical whether the upload is progressing or stuck.

A recording (a clock-in photo taken live, or a dictated voice note) doesn't have a known size the moment it starts — the app doesn't know how big the final file will be until the recording finishes. The progress display handles that gracefully: it just shows nothing until the upload itself starts reporting real numbers, then fills in.

When a screen fires off more than one upload at once, the progress shown is for the whole batch, not just one file — the percentage reflects total bytes sent across every upload still running.

Technical view

The logic lives in one new hook: useUploadProgress (packages/clinic-mobile/src/hooks/useUploadProgress.js). It exposes four functions and two derived values:

MemberPurpose
start(uploadId, size?)Registers an upload. If nothing else is in flight, resets the batch's clock and byte counters first. size is optional — pass it when the caller already knows the final byte count (e.g. a photo's size), omit it for a recording whose size isn't known yet.
report(uploadId, { sent, total })Called from the upload's progress callback as bytes move. Starts the batch clock on the first call. Keeps whatever total was last known for that upload if the new report doesn't include one (useUploadProgress.js:40-43).
complete(uploadId, size?)Forces that upload's sent to equal its total, in case the platform never fires a final 100% progress event for a short upload.
end()Decrements the in-flight counter. Does not reset state by itself — the next start() call resets when the counter is back at zero (useUploadProgress.js:48-59).
percentundefined until any upload in the batch has a known total; otherwise the batch's sentBytes / totalBytes, rounded and capped at 100 (useUploadProgress.js:85).
detailEmpty string until totalBytes > 0; otherwise "<size> · Speed: <rate>/s", omitting the speed clause until 0.3s have passed and at least one byte has been sent (useUploadProgress.js:78, 86-88).

State is keyed by an arbitrary uploadId string chosen by the caller (typically a generated file name, or ` ${name}-${uri} when a name alone might collide — NewFileScreen.js:110). Each upload's { sent, total } is stored separately in a progress state object, and percent/detail are derived by summing across all entries (useUploadProgress.js:74-76`) — this is what makes multi-file batches read as one combined progress bar rather than one bar per file.

formatBytes (useUploadProgress.js:5-19) renders a byte count as B/KB/MB/GB with a suffix, showing no decimal for values ≥ 10 in a unit or for raw bytes, and one decimal otherwise (e.g. 5.3 KB but 12 MB).

AppSpinner

AppSpinner (packages/clinic-mobile/src/common/AppSpinner.js) gained a detail prop (AppSpinner.js:5) rendered as a second, smaller line of hint-colored text below the existing progress percentage (AppSpinner.js:14-18). Both lines are conditionally rendered — a caller that doesn't pass progress/detail gets the original plain spinner unchanged, which is why screens that don't upload files (or the small inline spinner in the system chat button) are unaffected by this change.