14 changed files with 923 additions and 137 deletions
@ -0,0 +1,94 @@ |
|||||||
|
package com.utun.chat.ui.components |
||||||
|
|
||||||
|
import androidx.compose.foundation.background |
||||||
|
import androidx.compose.foundation.clickable |
||||||
|
import androidx.compose.foundation.layout.* |
||||||
|
import androidx.compose.foundation.shape.CircleShape |
||||||
|
import androidx.compose.material.icons.Icons |
||||||
|
import androidx.compose.material.icons.filled.CallEnd |
||||||
|
import androidx.compose.material.icons.filled.PhoneInTalk |
||||||
|
import androidx.compose.material3.Icon |
||||||
|
import androidx.compose.material3.IconButton |
||||||
|
import androidx.compose.material3.Text |
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.runtime.collectAsState |
||||||
|
import androidx.compose.runtime.getValue |
||||||
|
import androidx.compose.ui.Alignment |
||||||
|
import androidx.compose.ui.Modifier |
||||||
|
import androidx.compose.ui.graphics.Color |
||||||
|
import androidx.compose.ui.text.font.FontWeight |
||||||
|
import androidx.compose.ui.text.style.TextOverflow |
||||||
|
import androidx.compose.ui.unit.dp |
||||||
|
import androidx.compose.ui.unit.sp |
||||||
|
import com.utun.chat.viewmodel.ChatViewModel |
||||||
|
import com.utun.chat.viewmodel.ChatViewModel.CallPhase |
||||||
|
|
||||||
|
private fun fmtDuration(sec: Int): String { |
||||||
|
val m = sec / 60 |
||||||
|
val s = sec % 60 |
||||||
|
return "%02d:%02d".format(m, s) |
||||||
|
} |
||||||
|
|
||||||
|
/* Постоянная полоса активного звонка: показывается поверх интерфейса, когда |
||||||
|
* звонок свёрнут. Клик по полосе открывает CallScreen, красная кнопка — завершить. */ |
||||||
|
@Composable |
||||||
|
fun ActiveCallBanner( |
||||||
|
viewModel: ChatViewModel, |
||||||
|
onOpen: () -> Unit, |
||||||
|
onHangup: () -> Unit |
||||||
|
) { |
||||||
|
val st = viewModel.callState.collectAsState().value ?: return |
||||||
|
val phase = st.phase |
||||||
|
if (phase != CallPhase.OUTGOING && phase != CallPhase.RINGING && phase != CallPhase.ACTIVE) return |
||||||
|
|
||||||
|
val name = st.peerName.ifEmpty { "0x%016x".format(st.peerNodeId) } |
||||||
|
val status = when (phase) { |
||||||
|
CallPhase.ACTIVE -> "Разговор " + fmtDuration(st.durationMs) |
||||||
|
CallPhase.RINGING -> "Гудки…" |
||||||
|
else -> "Звоним…" |
||||||
|
} |
||||||
|
|
||||||
|
Row( |
||||||
|
modifier = Modifier |
||||||
|
.fillMaxWidth() |
||||||
|
.statusBarsPadding() |
||||||
|
.background(Color(0xFF1F6FEB)) |
||||||
|
.clickable(onClick = onOpen) |
||||||
|
.padding(start = 12.dp, end = 4.dp, top = 6.dp, bottom = 6.dp), |
||||||
|
verticalAlignment = Alignment.CenterVertically |
||||||
|
) { |
||||||
|
Icon( |
||||||
|
Icons.Filled.PhoneInTalk, |
||||||
|
contentDescription = "Звонок", |
||||||
|
tint = Color.White, |
||||||
|
modifier = Modifier.size(22.dp) |
||||||
|
) |
||||||
|
Spacer(Modifier.width(12.dp)) |
||||||
|
Column(modifier = Modifier.weight(1f)) { |
||||||
|
Text( |
||||||
|
name, |
||||||
|
color = Color.White, |
||||||
|
fontSize = 14.sp, |
||||||
|
fontWeight = FontWeight.Medium, |
||||||
|
maxLines = 1, |
||||||
|
overflow = TextOverflow.Ellipsis |
||||||
|
) |
||||||
|
Text( |
||||||
|
status, |
||||||
|
color = Color(0xFFD5E4FF), |
||||||
|
fontSize = 12.sp, |
||||||
|
maxLines = 1, |
||||||
|
overflow = TextOverflow.Ellipsis |
||||||
|
) |
||||||
|
} |
||||||
|
Spacer(Modifier.width(8.dp)) |
||||||
|
Box( |
||||||
|
modifier = Modifier.size(40.dp).background(Color(0xFFE53935), CircleShape), |
||||||
|
contentAlignment = Alignment.Center |
||||||
|
) { |
||||||
|
IconButton(onClick = onHangup) { |
||||||
|
Icon(Icons.Filled.CallEnd, "Завершить", tint = Color.White, modifier = Modifier.size(22.dp)) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,97 @@ |
|||||||
|
package com.utun.chat.ui.screens |
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column |
||||||
|
import androidx.compose.foundation.layout.Row |
||||||
|
import androidx.compose.foundation.layout.Spacer |
||||||
|
import androidx.compose.foundation.layout.fillMaxSize |
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth |
||||||
|
import androidx.compose.foundation.layout.height |
||||||
|
import androidx.compose.foundation.layout.imePadding |
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding |
||||||
|
import androidx.compose.foundation.layout.padding |
||||||
|
import androidx.compose.foundation.layout.width |
||||||
|
import androidx.compose.foundation.lazy.LazyColumn |
||||||
|
import androidx.compose.foundation.lazy.items |
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState |
||||||
|
import androidx.compose.material.icons.Icons |
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack |
||||||
|
import androidx.compose.material.icons.automirrored.filled.Send |
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api |
||||||
|
import androidx.compose.material3.Icon |
||||||
|
import androidx.compose.material3.IconButton |
||||||
|
import androidx.compose.material3.MaterialTheme |
||||||
|
import androidx.compose.material3.OutlinedTextField |
||||||
|
import androidx.compose.material3.Text |
||||||
|
import androidx.compose.material3.TopAppBar |
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.runtime.LaunchedEffect |
||||||
|
import androidx.compose.runtime.getValue |
||||||
|
import androidx.compose.runtime.mutableStateOf |
||||||
|
import androidx.compose.runtime.remember |
||||||
|
import androidx.compose.runtime.setValue |
||||||
|
import androidx.compose.ui.Modifier |
||||||
|
import androidx.compose.ui.unit.dp |
||||||
|
import com.utun.chat.data.DmConversation |
||||||
|
import com.utun.chat.data.Message |
||||||
|
import com.utun.chat.ui.components.MessageBubble |
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class) |
||||||
|
@Composable |
||||||
|
fun DmChatScreen( |
||||||
|
conv: DmConversation, |
||||||
|
messages: List<Message>, |
||||||
|
onSendMessage: (String) -> Unit, |
||||||
|
onBack: () -> Unit |
||||||
|
) { |
||||||
|
var text by remember { mutableStateOf("") } |
||||||
|
val listState = rememberLazyListState() |
||||||
|
|
||||||
|
LaunchedEffect(messages.size) { |
||||||
|
if (messages.lastOrNull()?.isOutgoing == true) { |
||||||
|
listState.scrollToItem(0) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxSize().imePadding()) { |
||||||
|
TopAppBar( |
||||||
|
title = { Text(conv.displayName) }, |
||||||
|
navigationIcon = { |
||||||
|
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") } |
||||||
|
} |
||||||
|
) |
||||||
|
LazyColumn( |
||||||
|
modifier = Modifier.weight(1f).fillMaxWidth(), |
||||||
|
state = listState, |
||||||
|
reverseLayout = true |
||||||
|
) { |
||||||
|
item { Spacer(Modifier.height(8.dp)) } |
||||||
|
items(messages.reversed()) { msg -> |
||||||
|
MessageBubble(msg) |
||||||
|
} |
||||||
|
} |
||||||
|
Row( |
||||||
|
modifier = Modifier |
||||||
|
.fillMaxWidth() |
||||||
|
.padding(8.dp) |
||||||
|
.navigationBarsPadding(), |
||||||
|
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically |
||||||
|
) { |
||||||
|
OutlinedTextField( |
||||||
|
value = text, |
||||||
|
onValueChange = { text = it }, |
||||||
|
modifier = Modifier.weight(1f), |
||||||
|
placeholder = { Text("Сообщение") }, |
||||||
|
maxLines = 4 |
||||||
|
) |
||||||
|
Spacer(Modifier.width(8.dp)) |
||||||
|
IconButton(onClick = { |
||||||
|
if (text.isNotBlank()) { |
||||||
|
onSendMessage(text) |
||||||
|
text = "" |
||||||
|
} |
||||||
|
}) { |
||||||
|
Icon(Icons.AutoMirrored.Filled.Send, "Send", tint = MaterialTheme.colorScheme.primary) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue