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>
This commit is contained in:
132
specs/agent-j-file-sharing-ui.md
Normal file
132
specs/agent-j-file-sharing-ui.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user