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,230 @@
# Agent F: Chat Screen
## Phase: 2 (UI Shells)
## Depends on: Agent A, Agent B (theme + navigation), Agent C (models)
## Context
The chat screen is the core messaging interface. It shows encrypted messages in bubbles,
supports replies, file attachments, reactions, pins, and real-time updates.
## Task
Create ChatScreen, MessageBubble, MessageInput, ImageViewer, and ChatViewModel skeleton.
## Files to Create
### 1. ui/chat/ChatScreen.kt
Jetpack Compose screen with:
- **Top bar (custom)**:
- Back arrow (left)
- Circular avatar (32dp)
- Conversation name + "Encrypted" / "Verified" label below (small, Green or Overlay1)
- Action buttons (right): Search icon, Info/Group icon
- **Message list** (LazyColumn, reverseLayout = true):
- Messages grouped by date (date separator headers)
- Each message is a MessageBubble
- Scroll to bottom FAB (shown when not at bottom)
- **Reply preview bar** (shown when replying to a message):
- Vertical Lavender bar + replied message text preview + close button
- **Search overlay** (shown when search active):
- Search text field with prev/next buttons + match count
- Highlighted matches in messages
- **Message input** at bottom (MessageInput composable)
### 2. ui/chat/MessageBubble.kt
Composable for a single message:
- **Own messages**: Right-aligned, Lavender background (alpha 0.15)
- **Other's messages**: Left-aligned, Surface0 background
- **Content layout** (vertical):
- Sender name (only in groups, for other's messages, bold, Lavender color)
- Forwarded header (if forwarded): "Forwarded from Username" with blue left border
- Reply reference (if reply): small gray box with replied message text (1 line)
- Text content with:
- Link detection (Blue, underlined)
- @mention highlighting (Blue, bold)
- Image thumbnail (if image): clickable, shows full-size on tap
- File card (if file): icon + filename + size, clickable to download
- **Bottom row** (horizontal):
- Timestamp (Overlay1, small)
- Pin icon (if pinned)
- Read receipt indicators:
- 1 checkmark = sent
- 2 checkmarks = delivered
- 2 blue checkmarks = read
- Reaction badges (if reactions): row of emoji+count chips below message
- **Deleted message**: "This message was deleted" in italic, Overlay1 color
- **Long-press context menu**:
- Reply
- React (submenu with 6 emoji)
- Copy text
- Forward
- Pin / Unpin
- Delete (own messages only, Red color)
- **Message shape**: Rounded rectangle (12dp), with tail on sender side
### 3. ui/chat/MessageInput.kt
Composable for message input area:
- **Layout** (horizontal):
- Attachment button (left, paperclip icon)
- On click: shows bottom sheet with "Image" and "File" options
- Text field (weight 1f, pill-shaped, Surface1 background)
- Placeholder: "Message..."
- Auto-grow up to 4 lines
- @mention detection triggers autocomplete popup
- Send button (right, Lavender, circular, arrow icon)
- Shown only when text is non-empty or attachment selected
- **@mention autocomplete**: Popup above input showing matching member names
- **Attachment preview**: Shows selected image/file name before sending
### 4. ui/chat/ImageViewer.kt
Full-screen image viewer:
- **Black background** with translucent system bars
- **Zoomable image** (pinch-to-zoom, double-tap zoom, pan)
- **Top bar**: Back button + filename (transparent background)
- **Bottom bar**: Share button + Save button
- **Gesture**: Swipe down to dismiss
### 5. ui/chat/ChatViewModel.kt
```kotlin
package com.kecalek.chat.ui.chat
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import com.kecalek.chat.data.model.Conversation
import com.kecalek.chat.data.model.ConversationMember
import com.kecalek.chat.data.model.Message
import javax.inject.Inject
data class ChatUiState(
val messages: List<Message> = emptyList(),
val conversation: Conversation? = null,
val members: List<ConversationMember> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null,
val replyingTo: Message? = null,
val isSearchActive: Boolean = false,
val searchQuery: String = "",
val searchResults: List<Int> = emptyList(), // indices into messages
val currentSearchIndex: Int = -1,
val currentUserId: String = "",
val verificationStatus: String = "encrypted", // "encrypted" or "verified"
)
@HiltViewModel
class ChatViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
// TODO: Inject repositories
) : ViewModel() {
val conversationId: String = savedStateHandle["conversationId"] ?: ""
private val _uiState = MutableStateFlow(ChatUiState())
val uiState: StateFlow<ChatUiState> = _uiState.asStateFlow()
fun loadMessages() {
// TODO: Load from cache + incremental sync from server
}
fun sendMessage(text: String) {
// TODO: Encrypt and send message
}
fun sendImage(uri: String) {
// TODO: Encrypt and upload image
}
fun sendFile(uri: String) {
// TODO: Encrypt and upload file
}
fun deleteMessage(messageId: String) {
// TODO: Soft-delete message
}
fun reactToMessage(messageId: String, reaction: String) {
// TODO: Add/remove reaction
}
fun pinMessage(messageId: String) {
// TODO: Pin/unpin message
}
fun forwardMessage(messageId: String, targetConversationId: String) {
// TODO: Forward message to another conversation
}
fun setReplyTo(message: Message?) {
_uiState.value = _uiState.value.copy(replyingTo = message)
}
fun toggleSearch() {
val current = _uiState.value
_uiState.value = current.copy(
isSearchActive = !current.isSearchActive,
searchQuery = "",
searchResults = emptyList(),
currentSearchIndex = -1,
)
}
fun search(query: String) {
// TODO: Search through local message cache
}
fun nextSearchResult() {
// TODO: Navigate to next search result
}
fun prevSearchResult() {
// TODO: Navigate to previous search result
}
fun markAsRead() {
// TODO: Mark visible messages as read
}
fun downloadFile(fileId: String) {
// TODO: Download and decrypt file
}
}
```
## Message Bubble Visual Spec
```
┌──────────────────────────────────────────┐
│ Forwarded from Alice (fwd) │
│ ┌────────────────────────────────────┐ │
│ │ Replying to: original message... │ │
│ └────────────────────────────────────┘ │
│ Bob (sender name, groups only) │
│ │
│ Message text content here with │
│ @mentions highlighted in blue │
│ │
│ ┌────────────────────────────────────┐ │
│ │ [Image Thumbnail] │ │
│ └────────────────────────────────────┘ │
│ │
│ 12:34 📌 ✓✓ │
│ 👍2 ❤1 │
└──────────────────────────────────────────┘
```
## Constraints
- Use Material 3 components
- LazyColumn with reverseLayout for chat (newest at bottom)
- Message bubbles must be efficient for long conversations
- Use `remember` for expensive computations in bubbles
- Maximum bubble width: 80% of screen width
- Image thumbnails: max 200dp width, maintain aspect ratio
- Context menu via `DropdownMenu` on long-press
## DO NOT
- Implement actual encryption/decryption
- Implement actual file upload/download
- Handle server communication
- Implement QR code scanning (that's Agent G)