android: redesign chats search as fullscreen telegram-like flow
This commit is contained in:
@@ -567,3 +567,13 @@
|
|||||||
- create group/channel -> open management panel,
|
- create group/channel -> open management panel,
|
||||||
- saved -> open saved chat if present,
|
- saved -> open saved chat if present,
|
||||||
- unsupported items show clear feedback instead of silent no-op.
|
- unsupported items show clear feedback instead of silent no-op.
|
||||||
|
|
||||||
|
### Step 90 - Fullscreen chats search redesign (Telegram-like)
|
||||||
|
- Reworked chats search mode into a fullscreen flow:
|
||||||
|
- top rounded search field with inline clear button,
|
||||||
|
- horizontal category chips (`Chats`, `Channels`, `Apps`, `Posts`),
|
||||||
|
- dedicated recent avatars row for the active category.
|
||||||
|
- Added search-mode content states:
|
||||||
|
- empty query -> `Recent` list block (history-style chat rows),
|
||||||
|
- non-empty query -> local matches + `Global search` and `Messages` sections.
|
||||||
|
- Kept search action in chats top bar; while search mode is active, app bar switches to back-navigation + empty title (content drives the page).
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
@@ -34,6 +35,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
@@ -70,6 +72,7 @@ import androidx.compose.material.icons.filled.NotificationsOff
|
|||||||
import androidx.compose.material.icons.filled.PushPin
|
import androidx.compose.material.icons.filled.PushPin
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.DragHandle
|
import androidx.compose.material.icons.filled.DragHandle
|
||||||
|
import androidx.compose.material.icons.filled.Clear
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import ru.daemonlord.messenger.domain.chat.model.ChatItem
|
import ru.daemonlord.messenger.domain.chat.model.ChatItem
|
||||||
@@ -231,7 +234,7 @@ fun ChatListScreen(
|
|||||||
Text(
|
Text(
|
||||||
when {
|
when {
|
||||||
selectedChatIds.isNotEmpty() -> selectedChatIds.size.toString()
|
selectedChatIds.isNotEmpty() -> selectedChatIds.size.toString()
|
||||||
isSearchMode -> "Search"
|
isSearchMode -> ""
|
||||||
state.isConnecting -> "Connecting..."
|
state.isConnecting -> "Connecting..."
|
||||||
state.selectedTab == ChatTab.ARCHIVED -> "Archived"
|
state.selectedTab == ChatTab.ARCHIVED -> "Archived"
|
||||||
else -> "Chats"
|
else -> "Chats"
|
||||||
@@ -311,14 +314,7 @@ fun ChatListScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (isSearchMode) {
|
} else if (!isSearchMode) {
|
||||||
IconButton(onClick = { localSearchQuery = "" }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Close,
|
|
||||||
contentDescription = "Clear search",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
isSearchMode = true
|
isSearchMode = true
|
||||||
@@ -394,119 +390,6 @@ fun ChatListScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (isSearchMode) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = localSearchQuery,
|
|
||||||
onValueChange = {
|
|
||||||
localSearchQuery = it
|
|
||||||
onSearchChanged(it)
|
|
||||||
onGlobalSearchChanged(it)
|
|
||||||
},
|
|
||||||
label = { Text(text = "Search chats") },
|
|
||||||
singleLine = true,
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Search,
|
|
||||||
contentDescription = "Search",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.horizontalScroll(rememberScrollState())
|
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
SearchSection.entries.forEach { section ->
|
|
||||||
FilterChip(
|
|
||||||
label = section.label,
|
|
||||||
selected = searchSection == section,
|
|
||||||
onClick = { searchSection = section },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (searchSection == SearchSection.Chats) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.horizontalScroll(rememberScrollState())
|
|
||||||
.padding(horizontal = 16.dp, vertical = 6.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
|
||||||
) {
|
|
||||||
state.chats.take(8).forEach { chat ->
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier.widthIn(max = 72.dp),
|
|
||||||
) {
|
|
||||||
if (!chat.avatarUrl.isNullOrBlank()) {
|
|
||||||
AsyncImage(
|
|
||||||
model = chat.avatarUrl,
|
|
||||||
contentDescription = "Recent ${chat.displayTitle}",
|
|
||||||
modifier = Modifier
|
|
||||||
.size(56.dp)
|
|
||||||
.clip(CircleShape),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(56.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Text(chat.displayTitle.firstOrNull()?.uppercase() ?: "?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = chat.displayTitle,
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state.globalSearchQuery.trim().length >= 2) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.75f),
|
|
||||||
shape = RoundedCornerShape(10.dp),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(10.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
) {
|
|
||||||
if (state.globalUsers.isNotEmpty()) {
|
|
||||||
Text("Users", fontWeight = FontWeight.SemiBold)
|
|
||||||
state.globalUsers.take(5).forEach { user ->
|
|
||||||
Text("• ${user.name}${user.username?.let { " (@$it)" } ?: ""}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state.globalMessages.isNotEmpty()) {
|
|
||||||
Text("Messages", fontWeight = FontWeight.SemiBold)
|
|
||||||
state.globalMessages.take(5).forEach { message ->
|
|
||||||
Text(
|
|
||||||
text = "• [chat ${message.chatId}] ${(message.text ?: "[${message.type}]").take(60)}",
|
|
||||||
modifier = Modifier.clickable { onOpenChat(message.chatId) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state.globalUsers.isEmpty() && state.globalMessages.isEmpty()) {
|
|
||||||
Text("No global results")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isSearchMode) {
|
if (!isSearchMode) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -536,73 +419,85 @@ fun ChatListScreen(
|
|||||||
onClick = { onFilterSelected(ChatListFilter.CHANNELS) },
|
onClick = { onFilterSelected(ChatListFilter.CHANNELS) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
PullToRefreshBox(
|
||||||
|
isRefreshing = state.isRefreshing,
|
||||||
|
onRefresh = onRefresh,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
state.isLoading -> {
|
||||||
|
CenterState(text = "Loading chats...", loading = true)
|
||||||
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
!state.errorMessage.isNullOrBlank() -> {
|
||||||
PullToRefreshBox(
|
CenterState(text = state.errorMessage, loading = false)
|
||||||
isRefreshing = state.isRefreshing,
|
}
|
||||||
onRefresh = onRefresh,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
) {
|
|
||||||
when {
|
|
||||||
state.isLoading -> {
|
|
||||||
CenterState(text = "Loading chats...", loading = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
!state.errorMessage.isNullOrBlank() -> {
|
state.chats.isEmpty() -> {
|
||||||
CenterState(text = state.errorMessage, loading = false)
|
CenterState(text = "No chats found", loading = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.chats.isEmpty() -> {
|
else -> {
|
||||||
CenterState(text = "No chats found", loading = false)
|
LazyColumn(
|
||||||
}
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = listState,
|
||||||
else -> {
|
) {
|
||||||
LazyColumn(
|
if (state.selectedTab == ChatTab.ALL && state.archivedChatsCount > 0) {
|
||||||
modifier = Modifier.fillMaxSize(),
|
item(key = "archive_row") {
|
||||||
state = listState,
|
ArchiveRow(
|
||||||
) {
|
count = state.archivedChatsCount,
|
||||||
if (state.selectedTab == ChatTab.ALL && state.archivedChatsCount > 0) {
|
unreadCount = state.archivedUnreadCount,
|
||||||
item(key = "archive_row") {
|
onClick = { onTabSelected(ChatTab.ARCHIVED) },
|
||||||
ArchiveRow(
|
)
|
||||||
count = state.archivedChatsCount,
|
}
|
||||||
unreadCount = state.archivedUnreadCount,
|
|
||||||
onClick = { onTabSelected(ChatTab.ARCHIVED) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
items(
|
||||||
items(
|
items = state.chats,
|
||||||
items = state.chats,
|
key = { it.id },
|
||||||
key = { it.id },
|
) { chat ->
|
||||||
) { chat ->
|
ChatRow(
|
||||||
ChatRow(
|
chat = chat,
|
||||||
chat = chat,
|
isSelecting = selectedChatIds.isNotEmpty(),
|
||||||
isSelecting = selectedChatIds.isNotEmpty(),
|
isSelected = selectedChatIds.contains(chat.id),
|
||||||
isSelected = selectedChatIds.contains(chat.id),
|
onClick = {
|
||||||
onClick = {
|
if (selectedChatIds.isNotEmpty()) {
|
||||||
if (selectedChatIds.isNotEmpty()) {
|
selectedChatIds = if (selectedChatIds.contains(chat.id)) {
|
||||||
|
selectedChatIds - chat.id
|
||||||
|
} else {
|
||||||
|
selectedChatIds + chat.id
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onOpenChat(chat.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
selectedChatIds = if (selectedChatIds.contains(chat.id)) {
|
selectedChatIds = if (selectedChatIds.contains(chat.id)) {
|
||||||
selectedChatIds - chat.id
|
selectedChatIds
|
||||||
} else {
|
} else {
|
||||||
selectedChatIds + chat.id
|
selectedChatIds + chat.id
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
onOpenChat(chat.id)
|
)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
selectedChatIds = if (selectedChatIds.contains(chat.id)) {
|
|
||||||
selectedChatIds
|
|
||||||
} else {
|
|
||||||
selectedChatIds + chat.id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ChatSearchFullscreen(
|
||||||
|
state = state,
|
||||||
|
searchSection = searchSection,
|
||||||
|
searchQuery = localSearchQuery,
|
||||||
|
onSearchChanged = {
|
||||||
|
localSearchQuery = it
|
||||||
|
onSearchChanged(it)
|
||||||
|
onGlobalSearchChanged(it)
|
||||||
|
},
|
||||||
|
onSectionChanged = { searchSection = it },
|
||||||
|
onOpenChat = onOpenChat,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (managementExpanded) {
|
if (managementExpanded) {
|
||||||
Surface(
|
Surface(
|
||||||
@@ -790,6 +685,378 @@ fun ChatListScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ChatSearchFullscreen(
|
||||||
|
state: ChatListUiState,
|
||||||
|
searchSection: SearchSection,
|
||||||
|
searchQuery: String,
|
||||||
|
onSearchChanged: (String) -> Unit,
|
||||||
|
onSectionChanged: (SearchSection) -> Unit,
|
||||||
|
onOpenChat: (Long) -> Unit,
|
||||||
|
) {
|
||||||
|
val trimmedQuery = searchQuery.trim()
|
||||||
|
val sectionChats = remember(state.chats, searchSection) {
|
||||||
|
when (searchSection) {
|
||||||
|
SearchSection.Chats -> state.chats
|
||||||
|
SearchSection.Channels -> state.chats.filter { it.type.equals("channel", ignoreCase = true) }
|
||||||
|
SearchSection.Apps -> emptyList()
|
||||||
|
SearchSection.Posts -> emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val localQueryResults = remember(sectionChats, trimmedQuery) {
|
||||||
|
if (trimmedQuery.isBlank()) {
|
||||||
|
emptyList()
|
||||||
|
} else {
|
||||||
|
sectionChats.filter {
|
||||||
|
it.displayTitle.contains(trimmedQuery, ignoreCase = true) ||
|
||||||
|
(it.counterpartUsername?.contains(trimmedQuery, ignoreCase = true) == true) ||
|
||||||
|
(it.handle?.contains(trimmedQuery, ignoreCase = true) == true) ||
|
||||||
|
(it.lastMessageText?.contains(trimmedQuery, ignoreCase = true) == true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val recentCircleChats = remember(sectionChats) { sectionChats.take(8) }
|
||||||
|
val recentHistoryChats = remember(sectionChats) { sectionChats.take(14) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = searchQuery,
|
||||||
|
onValueChange = onSearchChanged,
|
||||||
|
placeholder = { Text("Поиск чатов") },
|
||||||
|
singleLine = true,
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Search,
|
||||||
|
contentDescription = "Search",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
if (searchQuery.isNotBlank()) {
|
||||||
|
IconButton(onClick = { onSearchChanged("") }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Clear,
|
||||||
|
contentDescription = "Clear",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(),
|
||||||
|
shape = RoundedCornerShape(22.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(rememberScrollState())
|
||||||
|
.padding(top = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
SearchSection.entries.forEach { section ->
|
||||||
|
FilterChip(
|
||||||
|
label = section.label,
|
||||||
|
selected = searchSection == section,
|
||||||
|
onClick = { onSectionChanged(section) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
if (trimmedQuery.isBlank()) {
|
||||||
|
if (recentCircleChats.isNotEmpty()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(rememberScrollState())
|
||||||
|
.padding(bottom = 10.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
recentCircleChats.forEach { chat ->
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 74.dp)
|
||||||
|
.clickable { onOpenChat(chat.id) },
|
||||||
|
) {
|
||||||
|
ChatAvatar(chat = chat, size = 56.dp)
|
||||||
|
Text(
|
||||||
|
text = chat.displayTitle,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 6.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Недавние",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Очистить",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
items(recentHistoryChats, key = { "recent_${it.id}" }) { chat ->
|
||||||
|
SearchChatRow(chat = chat, onClick = { onOpenChat(chat.id) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
if (localQueryResults.isNotEmpty()) {
|
||||||
|
items(localQueryResults.take(6), key = { "local_${it.id}" }) { chat ->
|
||||||
|
SearchChatRow(chat = chat, onClick = { onOpenChat(chat.id) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item(key = "global_header") {
|
||||||
|
SectionHeader(
|
||||||
|
title = "Глобальный поиск",
|
||||||
|
action = "Показать больше",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (state.globalUsers.isNotEmpty()) {
|
||||||
|
items(state.globalUsers.take(8), key = { "user_${it.id}" }) { user ->
|
||||||
|
SearchUserRow(
|
||||||
|
title = user.name,
|
||||||
|
subtitle = buildString {
|
||||||
|
append("@")
|
||||||
|
append(user.username ?: user.id.toString())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item(key = "global_empty") {
|
||||||
|
Text(
|
||||||
|
text = "Нет результатов",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item(key = "messages_header") {
|
||||||
|
SectionHeader(
|
||||||
|
title = "Сообщения",
|
||||||
|
action = "Из всех чатов",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(state.globalMessages.take(12), key = { "msg_${it.id}" }) { message ->
|
||||||
|
SearchMessageRow(
|
||||||
|
state = state,
|
||||||
|
messageText = message.text?.take(70).orEmpty().ifBlank { "[${message.type}]" },
|
||||||
|
time = formatChatTime(message.createdAt),
|
||||||
|
onClick = { onOpenChat(message.chatId) },
|
||||||
|
chatId = message.chatId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SectionHeader(
|
||||||
|
title: String,
|
||||||
|
action: String,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp, bottom = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = action,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SearchChatRow(
|
||||||
|
chat: ChatItem,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
ChatAvatar(chat = chat, size = 48.dp)
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = chat.displayTitle,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
val subtitle = when {
|
||||||
|
chat.type.equals("private", ignoreCase = true) -> chat.counterpartUsername?.let { "@$it" } ?: "private"
|
||||||
|
chat.type.equals("channel", ignoreCase = true) -> "канал"
|
||||||
|
else -> "группа"
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (chat.unreadCount > 0) {
|
||||||
|
BadgeChip(label = chat.unreadCount.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SearchUserRow(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title.firstOrNull()?.uppercase() ?: "?",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SearchMessageRow(
|
||||||
|
state: ChatListUiState,
|
||||||
|
messageText: String,
|
||||||
|
time: String,
|
||||||
|
chatId: Long,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val chat = state.chats.firstOrNull { it.id == chatId }
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
if (chat != null) {
|
||||||
|
ChatAvatar(chat = chat, size = 48.dp)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text("M")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = chat?.displayTitle ?: "Chat $chatId",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = messageText,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = time,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ChatAvatar(
|
||||||
|
chat: ChatItem,
|
||||||
|
size: androidx.compose.ui.unit.Dp,
|
||||||
|
) {
|
||||||
|
if (!chat.avatarUrl.isNullOrBlank()) {
|
||||||
|
AsyncImage(
|
||||||
|
model = chat.avatarUrl,
|
||||||
|
contentDescription = "Avatar for ${chat.displayTitle}",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(size)
|
||||||
|
.clip(CircleShape),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(size)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = chat.displayTitle.firstOrNull()?.uppercase() ?: "?",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
private fun ChatRow(
|
private fun ChatRow(
|
||||||
|
|||||||
Reference in New Issue
Block a user