package com.craigvg.lichun_android.ui.screens.messaging import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.compose.ui.res.stringResource import com.craigvg.lichun_android.R import com.craigvg.lichun_android.utils.HapticFeedback import com.craigvg.lichun_android.viewmodel.GameStateViewModel import com.craigvg.lichun_android.domain.models.ConversationMessage import com.craigvg.lichun_android.ui.screens.messaging.components.ChatHeaderCard import com.craigvg.lichun_android.ui.screens.messaging.components.ExpandableMessageInput import com.craigvg.lichun_android.ui.theme.AppColors import com.craigvg.lichun_android.ui.theme.AppSpacing import com.craigvg.lichun_android.ui.theme.AppTypography import com.craigvg.lichun_android.viewmodel.PlayerViewModel import kotlinx.coroutines.launch import androidx.lifecycle.compose.collectAsStateWithLifecycle /** * Individual chat interface with message history and expandable input * Ported from iOS ChatView.swift */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun ChatScreen( characterId: String, playerViewModel: PlayerViewModel = hiltViewModel(), gameStateViewModel: GameStateViewModel = hiltViewModel(), onBack: () -> Unit = {} ) { val person by playerViewModel.person.collectAsStateWithLifecycle() val conversations by playerViewModel.activeConversations.collectAsStateWithLifecycle() val view = LocalView.current var messageInput by remember { mutableStateOf("") } var sendError by remember { mutableStateOf(null) } val listState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() // Find the conversation for this character val conversation = conversations.find { it.character == characterId } val messages = conversation?.conversation ?: emptyList() // Find the character from relationships val character = playerViewModel.findPersonById(characterId) val canSendMessage = person.calcEnergy >= 10 Scaffold( topBar = { ChatHeaderCard( characterName = character?.fullName ?: "Unknown", characterImageUrl = character?.image ?: "", onBack = onBack ) }, bottomBar = { ExpandableMessageInput( messageInput = messageInput, onMessageChange = { messageInput = it }, onSend = { if (messageInput.isNotBlank() && canSendMessage) { try { playerViewModel.sendChatMessage(characterId, messageInput) HapticFeedback.light(view) gameStateViewModel.onMessageSent(characterId) messageInput = "" sendError = null // Scroll to bottom coroutineScope.launch { if (messages.isNotEmpty()) { listState.animateScrollToItem(messages.size - 1) } } } catch (e: Exception) { sendError = "Failed to send message. Please try again." } } }, canSend = canSendMessage, currentEnergy = person.calcEnergy ) }, containerColor = AppColors.background ) { paddingValues -> Column( modifier = Modifier .fillMaxSize() .padding(paddingValues) .background( Brush.verticalGradient( colors = listOf( AppColors.surfaceSubtle, AppColors.background ) ) ) ) { // Error banner if (sendError != null) { Card( modifier = Modifier .fillMaxWidth() .padding(horizontal = AppSpacing.md, vertical = AppSpacing.xs), shape = RoundedCornerShape(AppSpacing.cornerRadius), colors = CardDefaults.cardColors(containerColor = AppColors.error.copy(alpha = 0.15f)) ) { Row( modifier = Modifier.padding(AppSpacing.sm), verticalAlignment = Alignment.CenterVertically ) { Icon( imageVector = Icons.Default.ErrorOutline, contentDescription = null, tint = AppColors.error, modifier = Modifier.size(18.dp) ) Spacer(modifier = Modifier.width(AppSpacing.xs)) Text( text = sendError ?: "", style = AppTypography.caption, color = AppColors.error, modifier = Modifier.weight(1f) ) IconButton( onClick = { sendError = null }, modifier = Modifier.size(24.dp) ) { Icon( imageVector = Icons.Default.Close, contentDescription = "Dismiss", tint = AppColors.error, modifier = Modifier.size(16.dp) ) } } } } if (messages.isEmpty()) { Box(modifier = Modifier.weight(1f)) { EmptyChatState(characterName = character?.firstname ?: "them") } } else { LazyColumn( state = listState, modifier = Modifier .weight(1f) .fillMaxWidth() .padding(horizontal = AppSpacing.md), verticalArrangement = Arrangement.spacedBy(AppSpacing.sm), contentPadding = PaddingValues(vertical = AppSpacing.md) ) { items(messages) { message -> MessageBubble( message = message, isFromPlayer = message.sender == "player" ) } } } } } // Auto-scroll to bottom when messages change LaunchedEffect(messages.size) { if (messages.isNotEmpty()) { listState.animateScrollToItem(messages.size - 1) } } } @Composable private fun MessageBubble( message: ConversationMessage, isFromPlayer: Boolean ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = if (isFromPlayer) Arrangement.End else Arrangement.Start ) { Card( modifier = Modifier.widthIn(max = 280.dp), shape = RoundedCornerShape( topStart = 16.dp, topEnd = 16.dp, bottomStart = if (isFromPlayer) 16.dp else 4.dp, bottomEnd = if (isFromPlayer) 4.dp else 16.dp ), colors = CardDefaults.cardColors( containerColor = if (isFromPlayer) { AppColors.primary } else { AppColors.surfaceElevated } ), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Column( modifier = Modifier.padding(AppSpacing.sm) ) { Text( text = message.message, style = AppTypography.body, color = if (isFromPlayer) Color.White else AppColors.primaryText ) Spacer(modifier = Modifier.height(4.dp)) Text( text = message.time, style = AppTypography.micro, color = if (isFromPlayer) Color.White.copy(alpha = 0.7f) else AppColors.secondaryText, modifier = Modifier.align(Alignment.End) ) } } } } @Composable private fun EmptyChatState(characterName: String) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(AppSpacing.xl) ) { Icon( imageVector = Icons.Default.ChatBubbleOutline, contentDescription = null, tint = AppColors.disabledText, modifier = Modifier.size(64.dp) ) Spacer(modifier = Modifier.height(AppSpacing.md)) Text( text = stringResource(R.string.start_a_conversation), style = AppTypography.headline, color = AppColors.secondaryText, textAlign = TextAlign.Center ) Spacer(modifier = Modifier.height(AppSpacing.xs)) Text( text = stringResource(R.string.say_hello_format, characterName), style = AppTypography.body, color = AppColors.disabledText, textAlign = TextAlign.Center ) } } }