feat: add unknown private chat actions banner
feat: show add-contact and block actions for unknown users in private chats refactor: expose counterpart user ids through chat domain models
This commit is contained in:
@@ -28,6 +28,7 @@ interface ChatDao {
|
||||
c.muted,
|
||||
c.unread_count,
|
||||
c.unread_mentions_count,
|
||||
c.counterpart_user_id,
|
||||
COALESCE(c.counterpart_name, u.display_name) AS counterpart_name,
|
||||
COALESCE(c.counterpart_username, u.username) AS counterpart_username,
|
||||
COALESCE(c.counterpart_avatar_url, u.avatar_url) AS counterpart_avatar_url,
|
||||
@@ -62,6 +63,7 @@ interface ChatDao {
|
||||
c.muted,
|
||||
c.unread_count,
|
||||
c.unread_mentions_count,
|
||||
c.counterpart_user_id,
|
||||
COALESCE(c.counterpart_name, u.display_name) AS counterpart_name,
|
||||
COALESCE(c.counterpart_username, u.username) AS counterpart_username,
|
||||
COALESCE(c.counterpart_avatar_url, u.avatar_url) AS counterpart_avatar_url,
|
||||
|
||||
@@ -27,6 +27,8 @@ data class ChatListLocalModel(
|
||||
val unreadCount: Int,
|
||||
@ColumnInfo(name = "unread_mentions_count")
|
||||
val unreadMentionsCount: Int,
|
||||
@ColumnInfo(name = "counterpart_user_id")
|
||||
val counterpartUserId: Long?,
|
||||
@ColumnInfo(name = "counterpart_name")
|
||||
val counterpartName: String?,
|
||||
@ColumnInfo(name = "counterpart_username")
|
||||
|
||||
@@ -18,6 +18,7 @@ fun ChatListLocalModel.toDomain(): ChatItem {
|
||||
muted = muted,
|
||||
unreadCount = unreadCount,
|
||||
unreadMentionsCount = unreadMentionsCount,
|
||||
counterpartUserId = counterpartUserId,
|
||||
counterpartUsername = counterpartUsername,
|
||||
counterpartName = counterpartName,
|
||||
counterpartAvatarUrl = counterpartAvatarUrl,
|
||||
@@ -46,6 +47,7 @@ fun ChatEntity.toDomain(): ChatItem {
|
||||
muted = muted,
|
||||
unreadCount = unreadCount,
|
||||
unreadMentionsCount = unreadMentionsCount,
|
||||
counterpartUserId = counterpartUserId,
|
||||
counterpartUsername = counterpartUsername,
|
||||
counterpartName = counterpartName,
|
||||
counterpartAvatarUrl = counterpartAvatarUrl,
|
||||
|
||||
@@ -13,6 +13,7 @@ data class ChatItem(
|
||||
val muted: Boolean,
|
||||
val unreadCount: Int,
|
||||
val unreadMentionsCount: Int,
|
||||
val counterpartUserId: Long?,
|
||||
val counterpartUsername: String?,
|
||||
val counterpartName: String?,
|
||||
val counterpartAvatarUrl: String?,
|
||||
|
||||
@@ -57,8 +57,10 @@ import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
@@ -438,6 +440,8 @@ fun ChatRoute(
|
||||
onToggleChatNotifications = viewModel::onToggleChatNotifications,
|
||||
onClearHistory = viewModel::onClearHistory,
|
||||
onDeleteChat = viewModel::onDeleteOrLeaveChat,
|
||||
onAddCounterpartToContacts = viewModel::onAddCounterpartToContacts,
|
||||
onBlockCounterpart = viewModel::onBlockCounterpart,
|
||||
onPromoteMember = viewModel::promoteMember,
|
||||
onDemoteMember = viewModel::demoteMember,
|
||||
onBanMember = viewModel::banMember,
|
||||
@@ -526,6 +530,8 @@ private data class ChatScreenActions(
|
||||
val onToggleChatNotifications: () -> Unit,
|
||||
val onClearHistory: () -> Unit,
|
||||
val onDeleteChat: () -> Unit,
|
||||
val onAddCounterpartToContacts: () -> Unit,
|
||||
val onBlockCounterpart: () -> Unit,
|
||||
val onPromoteMember: (Long) -> Unit,
|
||||
val onDemoteMember: (Long) -> Unit,
|
||||
val onBanMember: (Long) -> Unit,
|
||||
@@ -578,6 +584,8 @@ private fun ChatScreen(
|
||||
val onToggleChatNotifications = actions.onToggleChatNotifications
|
||||
val onClearHistory = actions.onClearHistory
|
||||
val onDeleteChat = actions.onDeleteChat
|
||||
val onAddCounterpartToContacts = actions.onAddCounterpartToContacts
|
||||
val onBlockCounterpart = actions.onBlockCounterpart
|
||||
val onPromoteMember = actions.onPromoteMember
|
||||
val onDemoteMember = actions.onDemoteMember
|
||||
val onBanMember = actions.onBanMember
|
||||
@@ -591,6 +599,10 @@ private fun ChatScreen(
|
||||
val allViewerMediaItems = remember(state.messages) { buildChatViewerMediaItems(state.messages) }
|
||||
val isChannelChat = state.chatType.equals("channel", ignoreCase = true)
|
||||
val isPrivateChat = state.chatType.equals("private", ignoreCase = true)
|
||||
val showUnknownPrivateChatBanner = isPrivateChat &&
|
||||
state.counterpartUserId != null &&
|
||||
!state.isCounterpartContact &&
|
||||
!state.isCounterpartBlocked
|
||||
val canShowMembersTab = state.chatType.equals("group", ignoreCase = true) ||
|
||||
(state.chatType.equals("channel", ignoreCase = true) && state.canManageMembers)
|
||||
val timelineItems = remember(state.messages, context) { buildChatTimelineItems(state.messages, context) }
|
||||
@@ -1087,6 +1099,52 @@ private fun ChatScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = showUnknownPrivateChatBanner,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.72f),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.chat_unknown_user_banner_title),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.chat_unknown_user_banner_subtitle),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
FilledTonalButton(
|
||||
onClick = onAddCounterpartToContacts,
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(stringResource(id = R.string.chat_unknown_user_add_contact))
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onBlockCounterpart,
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(stringResource(id = R.string.chat_unknown_user_block))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val strip = topAudioStrip
|
||||
if (strip != null) {
|
||||
Row(
|
||||
|
||||
@@ -21,6 +21,7 @@ import ru.daemonlord.messenger.core.notifications.ActiveChatTracker
|
||||
import ru.daemonlord.messenger.core.notifications.NotificationDispatcher
|
||||
import ru.daemonlord.messenger.core.token.TokenRepository
|
||||
import ru.daemonlord.messenger.R
|
||||
import ru.daemonlord.messenger.domain.account.repository.AccountRepository
|
||||
import ru.daemonlord.messenger.domain.chat.repository.ChatRepository
|
||||
import ru.daemonlord.messenger.domain.chat.usecase.ObserveChatUseCase
|
||||
import ru.daemonlord.messenger.domain.chat.usecase.ObserveChatsUseCase
|
||||
@@ -68,6 +69,7 @@ class ChatViewModel @Inject constructor(
|
||||
private val listMessageReactionsUseCase: ListMessageReactionsUseCase,
|
||||
private val toggleMessageReactionUseCase: ToggleMessageReactionUseCase,
|
||||
private val chatRepository: ChatRepository,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val observeChatUseCase: ObserveChatUseCase,
|
||||
private val observeChatsUseCase: ObserveChatsUseCase,
|
||||
private val handleRealtimeEventsUseCase: HandleRealtimeEventsUseCase,
|
||||
@@ -86,6 +88,7 @@ class ChatViewModel @Inject constructor(
|
||||
private var lastReadMessageId: Long? = null
|
||||
private val reactionsRequestedMessageIds = mutableSetOf<Long>()
|
||||
private var membersLoadKey: String? = null
|
||||
private var privateChatRelationKey: Long? = null
|
||||
private var typingResetJob: Job? = null
|
||||
|
||||
init {
|
||||
@@ -774,6 +777,7 @@ class ChatViewModel @Inject constructor(
|
||||
}
|
||||
it.copy(
|
||||
chatType = chat.type,
|
||||
counterpartUserId = chat.counterpartUserId,
|
||||
chatRole = role,
|
||||
chatMuted = chat.muted,
|
||||
chatTitle = chatTitle,
|
||||
@@ -800,6 +804,56 @@ class ChatViewModel @Inject constructor(
|
||||
membersLoadKey = nextLoadKey
|
||||
refreshMembersAndBans()
|
||||
}
|
||||
refreshPrivateChatRelationship(
|
||||
chatType = chat.type,
|
||||
counterpartUserId = chat.counterpartUserId,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onAddCounterpartToContacts() {
|
||||
val userId = uiState.value.counterpartUserId ?: return
|
||||
viewModelScope.launch {
|
||||
when (val result = accountRepository.addContact(userId = userId)) {
|
||||
is AppResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isCounterpartContact = true,
|
||||
isCounterpartBlocked = false,
|
||||
errorMessage = null,
|
||||
)
|
||||
}
|
||||
refreshPrivateChatRelationship(
|
||||
chatType = uiState.value.chatType,
|
||||
counterpartUserId = userId,
|
||||
force = true,
|
||||
)
|
||||
}
|
||||
is AppResult.Error -> _uiState.update { it.copy(errorMessage = result.reason.toUiMessage()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onBlockCounterpart() {
|
||||
val userId = uiState.value.counterpartUserId ?: return
|
||||
viewModelScope.launch {
|
||||
when (val result = accountRepository.blockUser(userId = userId)) {
|
||||
is AppResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isCounterpartContact = false,
|
||||
isCounterpartBlocked = true,
|
||||
errorMessage = null,
|
||||
)
|
||||
}
|
||||
refreshPrivateChatRelationship(
|
||||
chatType = uiState.value.chatType,
|
||||
counterpartUserId = userId,
|
||||
force = true,
|
||||
)
|
||||
}
|
||||
is AppResult.Error -> _uiState.update { it.copy(errorMessage = result.reason.toUiMessage()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -901,6 +955,37 @@ class ChatViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshPrivateChatRelationship(
|
||||
chatType: String,
|
||||
counterpartUserId: Long?,
|
||||
force: Boolean = false,
|
||||
) {
|
||||
if (!chatType.equals("private", ignoreCase = true) || counterpartUserId == null) {
|
||||
privateChatRelationKey = null
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isCounterpartContact = false,
|
||||
isCounterpartBlocked = false,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!force && privateChatRelationKey == counterpartUserId) return
|
||||
privateChatRelationKey = counterpartUserId
|
||||
viewModelScope.launch {
|
||||
val contactsResult = accountRepository.listContacts()
|
||||
val blockedResult = accountRepository.listBlockedUsers()
|
||||
val isContact = (contactsResult as? AppResult.Success)?.data?.any { it.id == counterpartUserId } == true
|
||||
val isBlocked = (blockedResult as? AppResult.Success)?.data?.any { it.id == counterpartUserId } == true
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isCounterpartContact = isContact,
|
||||
isCounterpartBlocked = isBlocked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMemberRole(userId: Long, role: String) {
|
||||
viewModelScope.launch {
|
||||
when (val result = chatRepository.updateMemberRole(chatId = chatId, userId = userId, role = role)) {
|
||||
|
||||
@@ -18,9 +18,12 @@ data class MessageUiState(
|
||||
val chatSubtitle: String = "",
|
||||
val chatAvatarUrl: String? = null,
|
||||
val chatType: String = "",
|
||||
val counterpartUserId: Long? = null,
|
||||
val chatRole: String? = null,
|
||||
val chatMuted: Boolean = false,
|
||||
val chatUnreadCount: Int = 0,
|
||||
val isCounterpartContact: Boolean = false,
|
||||
val isCounterpartBlocked: Boolean = false,
|
||||
val chatMembers: List<ChatMemberItem> = emptyList(),
|
||||
val chatBans: List<ChatBanItem> = emptyList(),
|
||||
val canManageMembers: Boolean = false,
|
||||
|
||||
@@ -136,6 +136,10 @@
|
||||
<string name="chat_status_online">в сети</string>
|
||||
<string name="chat_status_last_seen_recently">был(а) недавно</string>
|
||||
<string name="chat_status_typing">печатает…</string>
|
||||
<string name="chat_unknown_user_banner_title">Незнакомый пользователь</string>
|
||||
<string name="chat_unknown_user_banner_subtitle">Можно добавить этого человека в контакты или заблокировать прямо из чата.</string>
|
||||
<string name="chat_unknown_user_add_contact">Добавить контакт</string>
|
||||
<string name="chat_unknown_user_block">Заблокировать</string>
|
||||
<string name="chat_type_group">группа</string>
|
||||
<string name="chat_type_channel">канал</string>
|
||||
<string name="chat_info_tab_media">Медиа</string>
|
||||
|
||||
@@ -136,6 +136,10 @@
|
||||
<string name="chat_status_online">online</string>
|
||||
<string name="chat_status_last_seen_recently">last seen recently</string>
|
||||
<string name="chat_status_typing">typing…</string>
|
||||
<string name="chat_unknown_user_banner_title">Unknown user</string>
|
||||
<string name="chat_unknown_user_banner_subtitle">You can add this person to contacts or block them right from the chat.</string>
|
||||
<string name="chat_unknown_user_add_contact">Add contact</string>
|
||||
<string name="chat_unknown_user_block">Block user</string>
|
||||
<string name="chat_type_group">group</string>
|
||||
<string name="chat_type_channel">channel</string>
|
||||
<string name="chat_info_tab_media">Media</string>
|
||||
|
||||
Reference in New Issue
Block a user