refactor(uploads): drop legacy multipart helpers; route Android UI through presigned flow
Android UI Tests / ui-tests (pull_request) Has been cancelled

The KMP shared layer's task-completion-with-images path now exclusively
uses the presigned-URL flow: each image is compressed, uploaded directly
to B2 via APILayer.uploadImage, and the resulting upload_ids are passed
to /api/task-completions/ as JSON. Bytes never traverse our API server.

Changes:
  - TaskCompletionViewModel.createTaskCompletionWithImages now does the
    presign→POST→collect-ids dance internally. The signature stays the
    same so the three Android UI call sites (TasksScreen, AllTasksScreen,
    ResidenceDetailScreen, CompleteTaskDialog, CompleteTaskScreen) need
    no changes.
  - APILayer.createTaskCompletionWithImages removed (dead).
  - TaskCompletionApi.createCompletionWithImages removed (the multipart
    HTTP helper that posted to the legacy POST /api/task-completions/
    multipart endpoint).
  - TaskCompletionCreateRequest.imageUrls field removed.
  - Three Swift call sites (CompleteTaskView, WidgetActionProcessor,
    PushNotificationManager) updated to drop the imageUrls argument.
  - Two Kotlin call sites (CompleteTaskDialog, CompleteTaskScreen) updated.

Image uploads now match WhatsApp/Slack-class architecture: client-side
compression + direct-to-storage upload + lightweight JSON entity create.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-05-01 15:19:46 -07:00
parent fa0ce30257
commit b2d03ef8b2
11 changed files with 47 additions and 96 deletions
@@ -25,38 +25,61 @@ class TaskCompletionViewModel : ViewModel() {
}
/**
* Create task completion with images.
* Create task completion with images, using the presigned-URL upload flow.
*
* For each image: compress, presign + POST direct to B2, collect the
* upload_id. Once all uploads succeed, create the completion with the
* collected upload_ids in a single JSON request. Bytes never traverse
* our API server.
*
* If any individual upload fails, the whole batch fails — partial
* pending_uploads rows are reaped server-side by the hourly cleanup
* cron, so there's nothing to clean up client-side.
*
* @param request The completion request data
* @param images List of ImageData (from platform-specific image pickers)
*/
fun createTaskCompletionWithImages(
request: TaskCompletionCreateRequest,
images: List<com.tt.honeyDue.platform.ImageData> = emptyList()
images: List<com.tt.honeyDue.platform.ImageData> = emptyList(),
) {
viewModelScope.launch {
_createCompletionState.value = ApiResult.Loading
// Compress images and prepare for upload
val compressedImages = images.map { ImageCompressor.compressImage(it) }
val imageFileNames = images.mapIndexed { index, image ->
// Always use .jpg extension since we compress to JPEG
val baseName = image.fileName.ifBlank { "completion_$index" }
if (baseName.endsWith(".jpg", ignoreCase = true) ||
baseName.endsWith(".jpeg", ignoreCase = true)) {
baseName
} else {
// Remove any existing extension and add .jpg
baseName.substringBeforeLast('.', baseName) + ".jpg"
val uploadIds = mutableListOf<Int>()
for ((index, image) in images.withIndex()) {
val compressed = ImageCompressor.compressImage(image)
val fileName = run {
val base = image.fileName.ifBlank { "completion_$index" }
if (base.endsWith(".jpg", ignoreCase = true) ||
base.endsWith(".jpeg", ignoreCase = true)
) base else base.substringBeforeLast('.', base) + ".jpg"
}
val uploadResult = APILayer.uploadImage(
category = "completion",
contentType = "image/jpeg",
bytes = compressed,
fileName = fileName,
)
when (uploadResult) {
is ApiResult.Success -> uploadIds += uploadResult.data
is ApiResult.Error -> {
_createCompletionState.value = ApiResult.Error(uploadResult.message, uploadResult.code)
return@launch
}
else -> {
_createCompletionState.value = ApiResult.Error("Upload failed in unexpected state")
return@launch
}
}
}
// Use APILayer which handles DataManager updates and summary refresh
_createCompletionState.value = APILayer.createTaskCompletionWithImages(
request = request,
images = compressedImages,
imageFileNames = imageFileNames
)
val withUploads = if (uploadIds.isNotEmpty()) {
request.copy(uploadIds = uploadIds.toList())
} else {
request
}
_createCompletionState.value = APILayer.createTaskCompletion(withUploads)
}
}