Приложение
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
package chats.presentation.chat_list
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.material3.*
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chats.presentation.components.ChatItem
|
||||
import stories.presentation.StoryViewModel
|
||||
import stories.presentation.components.StoryThumbnail
|
||||
import ru.knot.messager.R
|
||||
|
||||
@Composable
|
||||
fun HorizontalDividerComponent(
|
||||
modifier: Modifier = Modifier,
|
||||
thickness: androidx.compose.ui.unit.Dp = 1.dp,
|
||||
color: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.outlineVariant
|
||||
) {
|
||||
androidx.compose.material3.Divider(
|
||||
modifier = modifier,
|
||||
thickness = thickness,
|
||||
color = color
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChatListScreen(
|
||||
viewModel: ChatListViewModel,
|
||||
storyViewModel: StoryViewModel,
|
||||
onChatClick: (String) -> Unit,
|
||||
onStoryClick: (Int) -> Unit
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val storyState by storyViewModel.state.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.chats_title)) },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
if (state.isLoading && state.chats.isEmpty()) {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
} else {
|
||||
LazyColumn {
|
||||
// Ряд историй сверху списка чатов
|
||||
item {
|
||||
if (state.isStoriesEnabled && storyState.storyGroups.isNotEmpty()) {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
contentPadding = PaddingValues(horizontal = 8.dp)
|
||||
) {
|
||||
items(storyState.storyGroups.size) { index ->
|
||||
val group = storyState.storyGroups[index]
|
||||
StoryThumbnail(
|
||||
username = group.username,
|
||||
avatarUrl = group.avatar,
|
||||
hasUnseen = true, // В идеале проверяем по статусам
|
||||
onClick = { onStoryClick(index) }
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDividerComponent(
|
||||
thickness = 0.5.dp,
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.error != null && state.chats.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "${stringResource(R.string.error_occurred)}: ${state.error}",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
} else if (state.chats.isEmpty() && !state.isLoading) {
|
||||
item {
|
||||
Box(modifier = Modifier.fillParentMaxSize()) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_chats_found),
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items(state.chats) { chat ->
|
||||
ChatItem(chat = chat, onClick = onChatClick)
|
||||
HorizontalDividerComponent(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
thickness = 0.5.dp,
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package chats.presentation.chat_list
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import chats.domain.model.Chat
|
||||
import chats.domain.repository.ChatRepository
|
||||
import chats.data.remote.signalr.ChatHubClient
|
||||
import chats.data.remote.signalr.ChatEvent
|
||||
import core.network.ServerConfig
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import chats.data.repository.toDomain
|
||||
|
||||
data class ChatListState(
|
||||
val chats: List<Chat> = emptyList(),
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val isStoriesEnabled: Boolean = true
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class ChatListViewModel @Inject constructor(
|
||||
private val repository: ChatRepository,
|
||||
private val signalrClient: ChatHubClient,
|
||||
private val serverConfig: ServerConfig
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(ChatListState())
|
||||
val state: StateFlow<ChatListState> = _state.asStateFlow()
|
||||
|
||||
init {
|
||||
val isStoriesEnabled = try {
|
||||
serverConfig.getServerConfig().features.stories
|
||||
} catch (e: Exception) {
|
||||
true
|
||||
}
|
||||
_state.update { it.copy(isStoriesEnabled = isStoriesEnabled) }
|
||||
loadChats()
|
||||
observeSignalREvents()
|
||||
}
|
||||
|
||||
fun loadChats() {
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true) }
|
||||
try {
|
||||
val chats = repository.getChats()
|
||||
_state.update { it.copy(chats = chats, isLoading = false) }
|
||||
} catch (e: Exception) {
|
||||
_state.update { it.copy(isLoading = false, error = e.message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeSignalREvents() {
|
||||
signalrClient.events
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is ChatEvent.NewMessage -> {
|
||||
updateChatsWithNewMessage(event)
|
||||
}
|
||||
is ChatEvent.NewChat -> {
|
||||
_state.update { it.copy(chats = listOf(event.chat.toDomain()) + it.chats) }
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun updateChatsWithNewMessage(event: ChatEvent.NewMessage) {
|
||||
_state.update { currentState ->
|
||||
val updatedChats = currentState.chats.map { chat ->
|
||||
if (chat.id == event.message.chatId) {
|
||||
chat.copy(
|
||||
lastMessage = event.message.toDomain(),
|
||||
unreadCount = chat.unreadCount + 1
|
||||
)
|
||||
} else chat
|
||||
}.sortedByDescending { it.lastMessage?.createdAt }
|
||||
|
||||
currentState.copy(chats = updatedChats)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user