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>
8.2 KiB
8.2 KiB
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
- Attachment button (left, paperclip icon)
- @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
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
rememberfor expensive computations in bubbles - Maximum bubble width: 80% of screen width
- Image thumbnails: max 200dp width, maintain aspect ratio
- Context menu via
DropdownMenuon long-press
DO NOT
- Implement actual encryption/decryption
- Implement actual file upload/download
- Handle server communication
- Implement QR code scanning (that's Agent G)