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>
231 lines
8.2 KiB
Markdown
231 lines
8.2 KiB
Markdown
# 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)
|