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