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>
133 lines
4.8 KiB
Markdown
133 lines
4.8 KiB
Markdown
# 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
|
|
```kotlin
|
|
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)
|