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:
filip
2026-03-11 01:19:17 +01:00
commit fe861cfafa
134 changed files with 19078 additions and 0 deletions

View 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)