diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/message/mapper/MessageMappers.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/message/mapper/MessageMappers.kt index 1c4b2ee..82130e0 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/data/message/mapper/MessageMappers.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/message/mapper/MessageMappers.kt @@ -48,6 +48,7 @@ fun MessageLocalModel.toDomain(currentUserId: Long?): MessageItem { chatId = message.chatId, senderId = message.senderId, senderDisplayName = message.senderDisplayName, + senderUsername = message.senderUsername, type = message.type, text = message.text, createdAt = message.createdAt, diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepository.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepository.kt index 12e2f25..fc15b6a 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepository.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepository.kt @@ -686,6 +686,7 @@ class NetworkMessageRepository @Inject constructor( chatId = chatId, senderId = senderId, senderDisplayName = senderDisplayName, + senderUsername = senderUsername, type = type, text = text, createdAt = createdAt, diff --git a/android/app/src/main/java/ru/daemonlord/messenger/domain/message/model/MessageItem.kt b/android/app/src/main/java/ru/daemonlord/messenger/domain/message/model/MessageItem.kt index b5dae5f..9980658 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/domain/message/model/MessageItem.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/domain/message/model/MessageItem.kt @@ -5,6 +5,7 @@ data class MessageItem( val chatId: Long, val senderId: Long, val senderDisplayName: String?, + val senderUsername: String? = null, val type: String, val text: String?, val createdAt: String, diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt index 049733c..7a82d54 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt @@ -48,6 +48,7 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.Button +import androidx.compose.material3.AssistChip import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.AlertDialog @@ -161,6 +162,8 @@ import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import ru.daemonlord.messenger.BuildConfig import ru.daemonlord.messenger.core.audio.AppAudioFocusCoordinator +import ru.daemonlord.messenger.domain.chat.model.ChatBanItem +import ru.daemonlord.messenger.domain.chat.model.ChatMemberItem import ru.daemonlord.messenger.domain.message.model.MessageItem import ru.daemonlord.messenger.ui.chat.voice.VoiceRecorder import java.time.Instant @@ -290,6 +293,12 @@ fun ChatRoute( onToggleChatNotifications = viewModel::onToggleChatNotifications, onClearHistory = viewModel::onClearHistory, onDeleteChat = viewModel::onDeleteOrLeaveChat, + onPromoteMember = viewModel::promoteMember, + onDemoteMember = viewModel::demoteMember, + onBanMember = viewModel::banMember, + onKickMember = viewModel::kickMember, + onTransferOwnership = viewModel::transferOwnership, + onUnbanMember = viewModel::unbanMember, ) } @@ -327,6 +336,12 @@ fun ChatScreen( onToggleChatNotifications: () -> Unit, onClearHistory: () -> Unit, onDeleteChat: () -> Unit, + onPromoteMember: (Long) -> Unit, + onDemoteMember: (Long) -> Unit, + onBanMember: (Long) -> Unit, + onKickMember: (Long) -> Unit, + onTransferOwnership: (Long) -> Unit, + onUnbanMember: (Long) -> Unit, ) { val context = LocalContext.current val view = LocalView.current @@ -361,14 +376,28 @@ fun ChatScreen( } val isChannelChat = state.chatType.equals("channel", ignoreCase = true) val isPrivateChat = state.chatType.equals("private", ignoreCase = true) + val canShowMembersTab = state.chatType.equals("group", ignoreCase = true) || + (state.chatType.equals("channel", ignoreCase = true) && state.canManageMembers) val timelineItems = remember(state.messages) { buildChatTimelineItems(state.messages) } - val senderNameByUserId = remember(state.messages) { - state.messages + val senderNameByUserId = remember(state.messages, state.chatMembers) { + val fromMembers = state.chatMembers.associate { member -> + val resolved = member.name.ifBlank { + member.username?.takeIf { it.isNotBlank() }?.let { "@$it" } ?: "User #${member.userId}" + } + member.userId to resolved + } + val fromMessages = state.messages .mapNotNull { message -> val name = message.senderDisplayName?.trim().orEmpty() - if (name.isBlank()) null else message.senderId to name + val resolved = when { + name.isNotBlank() -> name + !message.senderUsername.isNullOrBlank() -> "@${message.senderUsername}" + else -> null + } + if (resolved.isNullOrBlank()) null else message.senderId to resolved } .toMap() + fromMembers + fromMessages } val replyAuthorByMessageId = remember(state.messages, senderNameByUserId) { state.messages.associate { message -> @@ -429,6 +458,20 @@ fun ChatScreen( lastVisibleIndex < lastIndex - 1 } } + LaunchedEffect(canShowMembersTab) { + if (!canShowMembersTab && chatInfoTab == ChatInfoTab.Members) { + chatInfoTab = ChatInfoTab.Media + } + } + val availableInfoTabs = remember(canShowMembersTab) { + buildList { + add(ChatInfoTab.Media) + add(ChatInfoTab.Files) + add(ChatInfoTab.Links) + add(ChatInfoTab.Voice) + if (canShowMembersTab) add(ChatInfoTab.Members) + } + } val isLightTheme = MaterialTheme.colorScheme.background.luminance() > 0.5f val chatTopBarColor = if (isLightTheme) { MaterialTheme.colorScheme.surface.copy(alpha = 0.98f) @@ -1237,7 +1280,7 @@ fun ChatScreen( .horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - ChatInfoTab.entries.forEach { tab -> + availableInfoTabs.forEach { tab -> val selected = chatInfoTab == tab Surface( shape = RoundedCornerShape(16.dp), @@ -1305,6 +1348,16 @@ fun ChatScreen( forceCycleSpeedAudioSourceId = null } }, + members = state.chatMembers, + bans = state.chatBans, + canManageMembers = state.canManageMembers, + canTransferOwnership = state.chatRole.equals("owner", ignoreCase = true), + onPromoteMember = onPromoteMember, + onDemoteMember = onDemoteMember, + onBanMember = onBanMember, + onKickMember = onKickMember, + onTransferOwnership = onTransferOwnership, + onUnbanMember = onUnbanMember, onEntryClick = { entry -> when (entry.type) { ChatInfoEntryType.Media -> { @@ -1343,6 +1396,7 @@ fun ChatScreen( "${stringResource(id = R.string.chat_reply_to)} ${ state.replyToMessage?.senderDisplayName ?.takeIf { it.isNotBlank() } + ?: state.replyToMessage?.senderUsername?.takeIf { it.isNotBlank() }?.let { "@$it" } ?: state.replyToMessage?.senderId?.let { senderNameByUserId[it] } ?: stringResource(id = R.string.common_unknown_user) }" @@ -2112,6 +2166,7 @@ private fun MessageBubble( } val senderName = message.senderDisplayName ?.takeIf { it.isNotBlank() } + ?: message.senderUsername?.takeIf { it.isNotBlank() }?.let { "@$it" } ?: senderNameByUserId[message.senderId] val legacyTextUrl = message.text?.trim()?.takeIf { it.startsWith("http", ignoreCase = true) } val hasLegacyStickerImage = message.attachments.isEmpty() && @@ -2191,8 +2246,21 @@ private fun MessageBubble( } if (message.replyToMessageId != null) { - val replyAuthor = message.replyPreviewSenderName + val replyAuthorFromPreview = message.replyPreviewSenderName ?.takeIf { it.isNotBlank() } + ?.let { preview -> + val legacyUserId = Regex("""User\s*#(\d+)""", RegexOption.IGNORE_CASE) + .find(preview) + ?.groupValues + ?.getOrNull(1) + ?.toLongOrNull() + if (legacyUserId != null) { + senderNameByUserId[legacyUserId] ?: preview + } else { + preview + } + } + val replyAuthor = replyAuthorFromPreview ?: replyAuthorByMessageId[message.replyToMessageId] ?: "Unknown user" val replySnippet = message.replyPreviewText?.takeIf { it.isNotBlank() } ?: "[media]" @@ -3364,6 +3432,7 @@ private enum class ChatInfoTab(val title: String) { Files("Files"), Links("Links"), Voice("Voice"), + Members("Members"), } private enum class ChatInfoEntryType { @@ -3396,8 +3465,34 @@ private fun ChatInfoTabContent( onForceStopAudioSourceHandled: (String) -> Unit, onForceToggleAudioSourceHandled: (String) -> Unit, onForceCycleSpeedAudioSourceHandled: (String) -> Unit, + members: List, + bans: List, + canManageMembers: Boolean, + canTransferOwnership: Boolean, + onPromoteMember: (Long) -> Unit, + onDemoteMember: (Long) -> Unit, + onBanMember: (Long) -> Unit, + onKickMember: (Long) -> Unit, + onTransferOwnership: (Long) -> Unit, + onUnbanMember: (Long) -> Unit, onEntryClick: (ChatInfoEntry) -> Unit, ) { + if (tab == ChatInfoTab.Members) { + ChatMembersTabContent( + members = members, + bans = bans, + canManageMembers = canManageMembers, + canTransferOwnership = canTransferOwnership, + onPromoteMember = onPromoteMember, + onDemoteMember = onDemoteMember, + onBanMember = onBanMember, + onKickMember = onKickMember, + onTransferOwnership = onTransferOwnership, + onUnbanMember = onUnbanMember, + ) + return + } + val filtered = remember(tab, entries) { entries.filter { when (tab) { @@ -3405,6 +3500,7 @@ private fun ChatInfoTabContent( ChatInfoTab.Files -> it.type == ChatInfoEntryType.File ChatInfoTab.Links -> it.type == ChatInfoEntryType.Link ChatInfoTab.Voice -> it.type == ChatInfoEntryType.Voice + ChatInfoTab.Members -> false } } } @@ -3625,6 +3721,158 @@ private fun ChatInfoTabContent( private val urlRegex = Regex("""https?://[^\s]+""") +@Composable +private fun ChatMembersTabContent( + members: List, + bans: List, + canManageMembers: Boolean, + canTransferOwnership: Boolean, + onPromoteMember: (Long) -> Unit, + onDemoteMember: (Long) -> Unit, + onBanMember: (Long) -> Unit, + onKickMember: (Long) -> Unit, + onTransferOwnership: (Long) -> Unit, + onUnbanMember: (Long) -> Unit, +) { + if (members.isEmpty() && bans.isEmpty()) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 20.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = "No members data", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + return + } + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .height(320.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (members.isNotEmpty()) { + item { + Text( + text = "Members (${members.size})", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + items(members, key = { it.userId }) { member -> + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.55f)) + .padding(horizontal = 10.dp, vertical = 9.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = member.name.ifBlank { member.username?.let { "@$it" } ?: "User #${member.userId}" }, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = "${member.role} • id ${member.userId}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + if (canManageMembers && !member.role.equals("owner", ignoreCase = true)) { + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (member.role.equals("member", ignoreCase = true)) { + AssistChip( + onClick = { onPromoteMember(member.userId) }, + label = { Text("Promote") }, + ) + } + if (member.role.equals("admin", ignoreCase = true)) { + AssistChip( + onClick = { onDemoteMember(member.userId) }, + label = { Text("Demote") }, + ) + } + if (canTransferOwnership) { + AssistChip( + onClick = { onTransferOwnership(member.userId) }, + label = { Text("Transfer owner") }, + ) + } + AssistChip( + onClick = { onBanMember(member.userId) }, + label = { Text("Ban") }, + ) + AssistChip( + onClick = { onKickMember(member.userId) }, + label = { Text("Kick") }, + ) + } + } + } + } + } + + if (bans.isNotEmpty()) { + item { + Text( + text = "Banned (${bans.size})", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + items(bans, key = { it.userId }) { ban -> + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.55f)) + .padding(horizontal = 10.dp, vertical = 9.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = ban.name.ifBlank { ban.username?.let { "@$it" } ?: "User #${ban.userId}" }, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = "id ${ban.userId}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + if (canManageMembers) { + AssistChip( + onClick = { onUnbanMember(ban.userId) }, + label = { Text("Unban") }, + ) + } + } + } + } + } +} + private fun buildChatInfoEntries(messages: List): List { val entries = mutableListOf() messages @@ -3674,7 +3922,7 @@ private fun buildChatInfoEntries(messages: List): List() + private var membersLoadKey: String? = null init { activeChatTracker.setActiveChat(chatId) @@ -504,6 +505,45 @@ class ChatViewModel @Inject constructor( } } + fun promoteMember(userId: Long) { + updateMemberRole(userId = userId, role = "admin") + } + + fun demoteMember(userId: Long) { + updateMemberRole(userId = userId, role = "member") + } + + fun transferOwnership(userId: Long) { + updateMemberRole(userId = userId, role = "owner") + } + + fun kickMember(userId: Long) { + viewModelScope.launch { + when (val result = chatRepository.removeMember(chatId = chatId, userId = userId)) { + is AppResult.Success -> refreshMembersAndBans() + is AppResult.Error -> _uiState.update { it.copy(errorMessage = result.reason.toUiMessage()) } + } + } + } + + fun banMember(userId: Long) { + viewModelScope.launch { + when (val result = chatRepository.banMember(chatId = chatId, userId = userId)) { + is AppResult.Success -> refreshMembersAndBans() + is AppResult.Error -> _uiState.update { it.copy(errorMessage = result.reason.toUiMessage()) } + } + } + } + + fun unbanMember(userId: Long) { + viewModelScope.launch { + when (val result = chatRepository.unbanMember(chatId = chatId, userId = userId)) { + is AppResult.Success -> refreshMembersAndBans() + is AppResult.Error -> _uiState.update { it.copy(errorMessage = result.reason.toUiMessage()) } + } + } + } + fun onSendClick() { val text = uiState.value.inputText.trim() if (text.isBlank()) return @@ -712,16 +752,54 @@ class ChatViewModel @Inject constructor( } it.copy( chatType = chat.type, + chatRole = role, chatTitle = chatTitle, chatSubtitle = chatSubtitle, chatAvatarUrl = chat.avatarUrl ?: chat.counterpartAvatarUrl, chatUnreadCount = chat.unreadCount.coerceAtLeast(0), + canManageMembers = role == "owner" || role == "admin", canSendMessages = canSend, sendRestrictionText = restriction, pinnedMessageId = chat.pinnedMessageId, pinnedMessage = pinnedMessage, ) } + + val shouldLoadMembers = chat.type.equals("group", ignoreCase = true) || + (chat.type.equals("channel", ignoreCase = true) && (role == "owner" || role == "admin")) + val nextLoadKey = "${chat.id}:${chat.type.lowercase()}:${role ?: "none"}" + if (shouldLoadMembers && membersLoadKey != nextLoadKey) { + membersLoadKey = nextLoadKey + refreshMembersAndBans() + } + } + } + } + + private fun updateMemberRole(userId: Long, role: String) { + viewModelScope.launch { + when (val result = chatRepository.updateMemberRole(chatId = chatId, userId = userId, role = role)) { + is AppResult.Success -> refreshMembersAndBans() + is AppResult.Error -> _uiState.update { it.copy(errorMessage = result.reason.toUiMessage()) } + } + } + } + + private fun refreshMembersAndBans() { + viewModelScope.launch { + val membersResult = chatRepository.listMembers(chatId = chatId) + val bansResult = chatRepository.listBans(chatId = chatId) + _uiState.update { + it.copy( + chatMembers = (membersResult as? AppResult.Success)?.data ?: it.chatMembers, + chatBans = (bansResult as? AppResult.Success)?.data ?: it.chatBans, + errorMessage = listOf(membersResult, bansResult) + .filterIsInstance() + .firstOrNull() + ?.reason + ?.toUiMessage() + ?: it.errorMessage, + ) } } } diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/MessageUiState.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/MessageUiState.kt index 74987da..24c8ea9 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/MessageUiState.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/MessageUiState.kt @@ -2,6 +2,8 @@ package ru.daemonlord.messenger.ui.chat import ru.daemonlord.messenger.domain.message.model.MessageItem import ru.daemonlord.messenger.domain.message.model.MessageReaction +import ru.daemonlord.messenger.domain.chat.model.ChatMemberItem +import ru.daemonlord.messenger.domain.chat.model.ChatBanItem data class MessageUiState( val chatId: Long = 0L, @@ -14,7 +16,11 @@ data class MessageUiState( val chatSubtitle: String = "", val chatAvatarUrl: String? = null, val chatType: String = "", + val chatRole: String? = null, val chatUnreadCount: Int = 0, + val chatMembers: List = emptyList(), + val chatBans: List = emptyList(), + val canManageMembers: Boolean = false, val messages: List = emptyList(), val pinnedMessageId: Long? = null, val pinnedMessage: MessageItem? = null, diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt index 23049dd..846a164 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt @@ -61,6 +61,7 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -83,10 +84,10 @@ import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material.icons.filled.Clear -import androidx.appcompat.app.AppCompatDelegate import kotlinx.coroutines.flow.collectLatest import coil.compose.AsyncImage import ru.daemonlord.messenger.R +import ru.daemonlord.messenger.domain.settings.model.AppThemeMode import ru.daemonlord.messenger.domain.chat.model.ChatItem import java.time.Instant import java.time.LocalDate @@ -151,6 +152,7 @@ fun ChatListRoute( onRemoveMember = viewModel::removeMember, onBanMember = viewModel::banMember, onUnbanMember = viewModel::unbanMember, + onToggleDayNightMode = viewModel::toggleDayNightMode, ) } @@ -191,6 +193,7 @@ fun ChatListScreen( onRemoveMember: (Long, Long) -> Unit, onBanMember: (Long, Long) -> Unit, onUnbanMember: (Long, Long) -> Unit, + onToggleDayNightMode: ((AppThemeMode) -> Unit) -> Unit, ) { val context = LocalContext.current var managementExpanded by remember { mutableStateOf(false) } @@ -411,9 +414,8 @@ fun ChatListScreen( ) { DropdownMenuItem( text = { - val isNight = AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES Text( - if (isNight) { + if (MaterialTheme.colorScheme.background.luminance() < 0.5f) { stringResource(id = R.string.menu_day_mode) } else { stringResource(id = R.string.menu_night_mode) @@ -423,13 +425,13 @@ fun ChatListScreen( leadingIcon = { Icon(Icons.Filled.LightMode, contentDescription = null) }, onClick = { showDefaultMenu = false - val isNight = AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES - if (isNight) { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - Toast.makeText(context, context.getString(R.string.toast_day_mode_enabled), Toast.LENGTH_SHORT).show() - } else { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - Toast.makeText(context, context.getString(R.string.toast_night_mode_enabled), Toast.LENGTH_SHORT).show() + onToggleDayNightMode { nextMode -> + val toastRes = when (nextMode) { + AppThemeMode.LIGHT -> R.string.toast_day_mode_enabled + AppThemeMode.DARK -> R.string.toast_night_mode_enabled + AppThemeMode.SYSTEM -> R.string.toast_day_mode_enabled + } + Toast.makeText(context, context.getString(toastRes), Toast.LENGTH_SHORT).show() } }, ) diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListViewModel.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListViewModel.kt index a8e83eb..3e0b526 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListViewModel.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListViewModel.kt @@ -1,5 +1,6 @@ package ru.daemonlord.messenger.ui.chats +import androidx.appcompat.app.AppCompatDelegate import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -24,6 +25,8 @@ import ru.daemonlord.messenger.domain.common.AppResult import ru.daemonlord.messenger.domain.realtime.RealtimeManager import ru.daemonlord.messenger.domain.realtime.model.RealtimeConnectionState import ru.daemonlord.messenger.domain.realtime.usecase.HandleRealtimeEventsUseCase +import ru.daemonlord.messenger.domain.settings.model.AppThemeMode +import ru.daemonlord.messenger.domain.settings.repository.ThemeRepository import ru.daemonlord.messenger.domain.search.repository.SearchRepository import javax.inject.Inject @@ -38,6 +41,7 @@ class ChatListViewModel @Inject constructor( private val chatRepository: ChatRepository, private val chatSearchRepository: ChatSearchRepository, private val searchRepository: SearchRepository, + private val themeRepository: ThemeRepository, ) : ViewModel() { private val selectedTab = MutableStateFlow(ChatTab.ALL) @@ -170,6 +174,29 @@ class ChatListViewModel @Inject constructor( } } + fun toggleDayNightMode(onResult: (AppThemeMode) -> Unit = {}) { + viewModelScope.launch { + val current = themeRepository.getThemeMode() + val next = when (current) { + AppThemeMode.DARK -> AppThemeMode.LIGHT + AppThemeMode.LIGHT -> AppThemeMode.DARK + AppThemeMode.SYSTEM -> { + val isNight = AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES + if (isNight) AppThemeMode.LIGHT else AppThemeMode.DARK + } + } + themeRepository.setThemeMode(next) + AppCompatDelegate.setDefaultNightMode( + when (next) { + AppThemeMode.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO + AppThemeMode.DARK -> AppCompatDelegate.MODE_NIGHT_YES + AppThemeMode.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } + ) + onResult(next) + } + } + fun onManagementChatSelected(chatId: Long?) { _uiState.update { it.copy(selectedManageChatId = chatId) } if (chatId != null) {