Files
Kecalek/specs/agent-j-file-sharing-ui.md
filip fe861cfafa Initial commit: Kecalek Android client
Complete Android client for encrypted chat platform.
78+ Kotlin files: crypto (X3DH, Double Ratchet, AES-GCM, Ed25519, X25519,
RSA-PSS), network (TCP/TLS, 50 endpoints), Hilt DI, Room+SQLCipher DB,
Jetpack Compose UI with Catppuccin Mocha theme.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 01:19:17 +01:00

4.8 KiB

Agent J: File Sharing UI

Phase: 4 (Feature Completion)

Depends on: Agent F (Chat Screen), Agent C (models with FileInfo/ImageInfo)

Context

File sharing uses chunked encrypted upload/download (AES-256-GCM). The UI handles image/file picking, thumbnail display, download progress, and file type icons. Encryption logic is handled by ChatClient (Claude Code) — this agent only handles UI.

Task

Create file/image picker integration, download UI, file type icons, and thumbnail display.

Files to Create

1. ui/chat/AttachmentSheet.kt

Bottom sheet shown when attachment button is tapped:

  • "Image" option with gallery icon
    • Opens system image picker (ActivityResultContracts.PickVisualMedia)
    • Shows selected image preview before sending
  • "File" option with document icon
    • Opens system file picker (ActivityResultContracts.OpenDocument)
    • Shows selected file name + size before sending
  • "Camera" option with camera icon
    • Opens camera to take photo (ActivityResultContracts.TakePicture)

2. ui/chat/ImageThumbnail.kt

Composable for image message display in chat:

  • Shows base64 JPEG thumbnail from ImageInfo.thumbnail
  • Max width 200dp, maintain aspect ratio
  • Rounded corners (8dp)
  • Loading shimmer while full image loads
  • On tap: navigate to ImageViewer with full image URL
  • Download progress overlay (if downloading full resolution)

3. ui/chat/FileCard.kt

Composable for file attachment display in chat:

  • Horizontal layout:
    • File type icon (40dp, left):
      • PDF: Red document icon
      • Image: Blue image icon
      • Video: Purple play icon
      • Audio: Green music icon
      • Archive: Yellow archive icon
      • Default: Gray document icon
    • Center (weight 1f):
      • Filename (bold, 1 line, ellipsize end)
      • File size (formatted: KB, MB) + mime type
    • Download button/progress (right):
      • Download icon (if not downloaded)
      • CircularProgressIndicator (if downloading)
      • Checkmark (if downloaded)
  • Background: Surface1 with 1dp Surface2 border, rounded 8dp
  • On tap: download if not downloaded, open if downloaded

4. ui/chat/DownloadProgress.kt

Reusable download progress composable:

  • Circular progress indicator with percentage text
  • Cancel button
  • File size downloaded / total

5. util/FileUtils.kt

package com.kecalek.chat.util

import android.content.Context
import android.net.Uri
import android.webkit.MimeTypeMap

object FileUtils {
    fun getFileName(context: Context, uri: Uri): String {
        // Query ContentResolver for display name
        val cursor = context.contentResolver.query(uri, null, null, null, null)
        cursor?.use {
            if (it.moveToFirst()) {
                val nameIndex = it.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
                if (nameIndex >= 0) return it.getString(nameIndex)
            }
        }
        return uri.lastPathSegment ?: "unknown"
    }

    fun getFileSize(context: Context, uri: Uri): Long {
        val cursor = context.contentResolver.query(uri, null, null, null, null)
        cursor?.use {
            if (it.moveToFirst()) {
                val sizeIndex = it.getColumnIndex(android.provider.OpenableColumns.SIZE)
                if (sizeIndex >= 0) return it.getLong(sizeIndex)
            }
        }
        return 0
    }

    fun getMimeType(context: Context, uri: Uri): String {
        return context.contentResolver.getType(uri)
            ?: MimeTypeMap.getSingleton()
                .getMimeTypeFromExtension(uri.toString().substringAfterLast('.'))
            ?: "application/octet-stream"
    }

    fun formatFileSize(bytes: Long): String = when {
        bytes < 1024 -> "$bytes B"
        bytes < 1024 * 1024 -> "${bytes / 1024} KB"
        bytes < 1024 * 1024 * 1024 -> "${"%.1f".format(bytes / (1024.0 * 1024.0))} MB"
        else -> "${"%.1f".format(bytes / (1024.0 * 1024.0 * 1024.0))} GB"
    }

    fun getFileTypeIcon(mimeType: String): FileTypeIcon = when {
        mimeType.startsWith("image/") -> FileTypeIcon.IMAGE
        mimeType.startsWith("video/") -> FileTypeIcon.VIDEO
        mimeType.startsWith("audio/") -> FileTypeIcon.AUDIO
        mimeType == "application/pdf" -> FileTypeIcon.PDF
        mimeType.contains("zip") || mimeType.contains("tar") || mimeType.contains("rar") -> FileTypeIcon.ARCHIVE
        else -> FileTypeIcon.DOCUMENT
    }

    enum class FileTypeIcon { PDF, IMAGE, VIDEO, AUDIO, ARCHIVE, DOCUMENT }
}

Constraints

  • Use ActivityResultContracts for file/image picking (no deprecated startActivityForResult)
  • Thumbnail display from base64 data (not URL loading)
  • Max image display width: 200dp in chat
  • File cards: consistent height, ellipsize long filenames
  • Download progress: 0-100% with cancel option

DO NOT

  • Implement actual file encryption/decryption
  • Implement actual chunked upload/download
  • Handle server communication
  • Store files on disk (that's ChatClient's job)