Files
Kecalek/specs/agent-f-chat-screen.md
filip fe861cfafa 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>
2026-03-11 01:19:17 +01:00

8.2 KiB
Raw Blame History

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

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)