feat(talk): меню сообщения — Ответить лично; рефактор оверлеев (ChatContent проще) [P1]
This commit is contained in:
@@ -799,8 +799,10 @@ fun TalkUnreadMarker(modifier: Modifier = Modifier) {
|
||||
fun TalkMessageActionSheet(
|
||||
message: TalkMessage?,
|
||||
canModify: Boolean,
|
||||
canReplyPrivately: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onReply: (TalkMessage) -> Unit,
|
||||
onReplyPrivately: (TalkMessage) -> Unit,
|
||||
onEdit: (TalkMessage) -> Unit,
|
||||
onDelete: (TalkMessage) -> Unit,
|
||||
onReaction: (TalkMessage, String) -> Unit,
|
||||
@@ -830,6 +832,9 @@ fun TalkMessageActionSheet(
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
)
|
||||
F7TextButton(text = "Ответить", onClick = { onReply(message) })
|
||||
if (canReplyPrivately) {
|
||||
F7TextButton(text = "Ответить лично", onClick = { onReplyPrivately(message) })
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
listOf("👍", "❤️", "😂").forEach { emoji ->
|
||||
F7TextButton(text = emoji, onClick = { onReaction(message, emoji) })
|
||||
|
||||
@@ -292,6 +292,7 @@ fun TalkScreen(
|
||||
onScheduleMeeting = onOpenCalendar,
|
||||
onToggleSystemMessages = vm::toggleSystemMessagesCollapse,
|
||||
onReply = { vm.setReplyTo(it) },
|
||||
onReplyPrivately = { vm.replyPrivately(session, it) },
|
||||
onReaction = { msg, emoji -> vm.addReaction(session, msg.id, emoji) },
|
||||
onDeleteMessage = { vm.deleteMessage(session, it.id) },
|
||||
onEditMessage = { msg, text -> vm.editMessage(session, msg.id, text) },
|
||||
@@ -416,128 +417,76 @@ private fun RoomListContent(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatContent(
|
||||
private fun MessageActionOverlays(
|
||||
actionMessage: TalkMessage?,
|
||||
session: AuthSession,
|
||||
state: TalkUiState,
|
||||
draft: String,
|
||||
onDraftChange: (String) -> Unit,
|
||||
onSend: () -> Unit,
|
||||
onAttach: () -> Unit,
|
||||
onVoiceToggle: () -> Unit,
|
||||
voiceRecording: Boolean,
|
||||
onRetry: () -> Unit,
|
||||
onStartCall: () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
onOpenRoomInfo: () -> Unit,
|
||||
onScheduleMeeting: () -> Unit,
|
||||
onToggleSystemMessages: () -> Unit,
|
||||
roomToken: String?,
|
||||
onClearAction: () -> Unit,
|
||||
onReply: (TalkMessage) -> Unit,
|
||||
onReplyPrivately: (TalkMessage) -> Unit,
|
||||
onReaction: (TalkMessage, String) -> Unit,
|
||||
onDeleteMessage: (TalkMessage) -> Unit,
|
||||
onEditMessage: (TalkMessage, String) -> Unit,
|
||||
onForwardNoteToSelf: (TalkMessage) -> Unit,
|
||||
onSetReminder: (TalkMessage, ReminderPreset) -> Unit,
|
||||
onMentionQuery: (String?) -> Unit,
|
||||
onMentionSelected: (TalkMention) -> Unit,
|
||||
onClearReply: () -> Unit,
|
||||
onClearHighlight: () -> Unit,
|
||||
onEditMessage: (TalkMessage, String) -> Unit,
|
||||
onDeleteMessage: (TalkMessage) -> Unit,
|
||||
) {
|
||||
var actionMessage by remember { mutableStateOf<TalkMessage?>(null) }
|
||||
var pendingDelete by remember { mutableStateOf<TalkMessage?>(null) }
|
||||
var editingMessage by remember { mutableStateOf<TalkMessage?>(null) }
|
||||
var editDraft by remember { mutableStateOf("") }
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val chatItems = remember(state.messages, state.openRoomLastReadMessageId, state.collapseSystemMessages) {
|
||||
buildChatItems(
|
||||
messages = state.messages,
|
||||
lastReadMessageId = state.openRoomLastReadMessageId,
|
||||
selfActorId = session.username,
|
||||
collapseSystemMessages = state.collapseSystemMessages,
|
||||
)
|
||||
}
|
||||
val systemMessageCount = remember(state.messages) {
|
||||
state.messages.count { it.isSystemMessage }
|
||||
}
|
||||
val showScrollDown by remember {
|
||||
derivedStateOf {
|
||||
if (chatItems.isEmpty()) return@derivedStateOf false
|
||||
val info = listState.layoutInfo
|
||||
val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: 0
|
||||
lastVisible < info.totalItemsCount - 1
|
||||
}
|
||||
}
|
||||
var stickToBottom by remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(showScrollDown) {
|
||||
stickToBottom = !showScrollDown
|
||||
}
|
||||
|
||||
LaunchedEffect(chatItems.size, state.highlightMessageId) {
|
||||
val targetId = state.highlightMessageId ?: return@LaunchedEffect
|
||||
val idx = chatItems.indexOfFirst { item ->
|
||||
item is TalkChatItem.MessageGroup && item.messages.any { it.id == targetId }
|
||||
}
|
||||
if (idx >= 0) {
|
||||
listState.animateScrollToItem(idx)
|
||||
kotlinx.coroutines.delay(2500)
|
||||
onClearHighlight()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(chatItems.size) {
|
||||
if (chatItems.isNotEmpty() && stickToBottom) {
|
||||
listState.animateScrollToItem(chatItems.lastIndex)
|
||||
}
|
||||
}
|
||||
var editingMessage by remember { mutableStateOf<TalkMessage?>(null) }
|
||||
var editDraft by remember { mutableStateOf("") }
|
||||
var pendingDelete by remember { mutableStateOf<TalkMessage?>(null) }
|
||||
|
||||
TalkMessageActionSheet(
|
||||
message = actionMessage,
|
||||
canModify = actionMessage?.let { msg ->
|
||||
msg.actorId.isNotBlank() && msg.actorId == session.username && !msg.isSystemMessage
|
||||
} == true,
|
||||
onDismiss = { actionMessage = null },
|
||||
canReplyPrivately = actionMessage?.let { msg ->
|
||||
msg.actorId.isNotBlank() && msg.actorId != session.username && !msg.isSystemMessage
|
||||
} == true,
|
||||
onDismiss = onClearAction,
|
||||
onReply = {
|
||||
onReply(it)
|
||||
actionMessage = null
|
||||
onClearAction()
|
||||
},
|
||||
onReplyPrivately = {
|
||||
onReplyPrivately(it)
|
||||
onClearAction()
|
||||
},
|
||||
onEdit = {
|
||||
editingMessage = it
|
||||
editDraft = it.text
|
||||
actionMessage = null
|
||||
onClearAction()
|
||||
},
|
||||
onDelete = {
|
||||
pendingDelete = it
|
||||
actionMessage = null
|
||||
onClearAction()
|
||||
},
|
||||
onReaction = { msg, emoji ->
|
||||
onReaction(msg, emoji)
|
||||
actionMessage = null
|
||||
onClearAction()
|
||||
},
|
||||
onCopy = { msg ->
|
||||
clipboard.setText(AnnotatedString(msg.displayText))
|
||||
Toast.makeText(context, "Скопировано", Toast.LENGTH_SHORT).show()
|
||||
actionMessage = null
|
||||
onClearAction()
|
||||
},
|
||||
onCopyLink = { msg ->
|
||||
val token = state.selectedRoomToken
|
||||
if (token != null) {
|
||||
clipboard.setText(AnnotatedString(TalkUrls.messageLink(session, token, msg.id)))
|
||||
if (roomToken != null) {
|
||||
clipboard.setText(AnnotatedString(TalkUrls.messageLink(session, roomToken, msg.id)))
|
||||
Toast.makeText(context, "Ссылка скопирована", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
actionMessage = null
|
||||
onClearAction()
|
||||
},
|
||||
onForwardNoteToSelf = { msg ->
|
||||
onForwardNoteToSelf(msg)
|
||||
Toast.makeText(context, "Переслано в «Личные заметки»", Toast.LENGTH_SHORT).show()
|
||||
actionMessage = null
|
||||
onClearAction()
|
||||
},
|
||||
onSetReminder = { msg, preset ->
|
||||
onSetReminder(msg, preset)
|
||||
Toast.makeText(context, "Напоминание установлено", Toast.LENGTH_SHORT).show()
|
||||
actionMessage = null
|
||||
onClearAction()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -589,6 +538,95 @@ private fun ChatContent(
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatContent(
|
||||
session: AuthSession,
|
||||
state: TalkUiState,
|
||||
draft: String,
|
||||
onDraftChange: (String) -> Unit,
|
||||
onSend: () -> Unit,
|
||||
onAttach: () -> Unit,
|
||||
onVoiceToggle: () -> Unit,
|
||||
voiceRecording: Boolean,
|
||||
onRetry: () -> Unit,
|
||||
onStartCall: () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
onOpenRoomInfo: () -> Unit,
|
||||
onScheduleMeeting: () -> Unit,
|
||||
onToggleSystemMessages: () -> Unit,
|
||||
onReply: (TalkMessage) -> Unit,
|
||||
onReplyPrivately: (TalkMessage) -> Unit,
|
||||
onReaction: (TalkMessage, String) -> Unit,
|
||||
onDeleteMessage: (TalkMessage) -> Unit,
|
||||
onEditMessage: (TalkMessage, String) -> Unit,
|
||||
onForwardNoteToSelf: (TalkMessage) -> Unit,
|
||||
onSetReminder: (TalkMessage, ReminderPreset) -> Unit,
|
||||
onMentionQuery: (String?) -> Unit,
|
||||
onMentionSelected: (TalkMention) -> Unit,
|
||||
onClearReply: () -> Unit,
|
||||
onClearHighlight: () -> Unit,
|
||||
) {
|
||||
var actionMessage by remember { mutableStateOf<TalkMessage?>(null) }
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val chatItems = remember(state.messages, state.openRoomLastReadMessageId, state.collapseSystemMessages) {
|
||||
buildChatItems(
|
||||
messages = state.messages,
|
||||
lastReadMessageId = state.openRoomLastReadMessageId,
|
||||
selfActorId = session.username,
|
||||
collapseSystemMessages = state.collapseSystemMessages,
|
||||
)
|
||||
}
|
||||
val systemMessageCount = remember(state.messages) {
|
||||
state.messages.count { it.isSystemMessage }
|
||||
}
|
||||
val showScrollDown by remember {
|
||||
derivedStateOf {
|
||||
if (chatItems.isEmpty()) return@derivedStateOf false
|
||||
val info = listState.layoutInfo
|
||||
val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: 0
|
||||
lastVisible < info.totalItemsCount - 1
|
||||
}
|
||||
}
|
||||
var stickToBottom by remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(showScrollDown) {
|
||||
stickToBottom = !showScrollDown
|
||||
}
|
||||
|
||||
LaunchedEffect(chatItems.size, state.highlightMessageId) {
|
||||
val targetId = state.highlightMessageId ?: return@LaunchedEffect
|
||||
val idx = chatItems.indexOfFirst { item ->
|
||||
item is TalkChatItem.MessageGroup && item.messages.any { it.id == targetId }
|
||||
}
|
||||
if (idx >= 0) {
|
||||
listState.animateScrollToItem(idx)
|
||||
kotlinx.coroutines.delay(2500)
|
||||
onClearHighlight()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(chatItems.size) {
|
||||
if (chatItems.isNotEmpty() && stickToBottom) {
|
||||
listState.animateScrollToItem(chatItems.lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
MessageActionOverlays(
|
||||
actionMessage = actionMessage,
|
||||
session = session,
|
||||
roomToken = state.selectedRoomToken,
|
||||
onClearAction = { actionMessage = null },
|
||||
onReply = onReply,
|
||||
onReplyPrivately = onReplyPrivately,
|
||||
onReaction = onReaction,
|
||||
onForwardNoteToSelf = onForwardNoteToSelf,
|
||||
onSetReminder = onSetReminder,
|
||||
onEditMessage = onEditMessage,
|
||||
onDeleteMessage = onDeleteMessage,
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
TalkChatBackground(
|
||||
|
||||
@@ -625,6 +625,22 @@ class TalkViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/** Ответить лично: открыть (или создать) 1:1 с автором сообщения. */
|
||||
fun replyPrivately(session: AuthSession, message: TalkMessage) {
|
||||
val userId = message.actorId
|
||||
if (userId.isBlank() || userId == session.username) return
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
runCatching { repository.createOneToOneRoom(session, userId) }
|
||||
.onSuccess { room -> openRoom(session, room.token) }
|
||||
.onFailure { t ->
|
||||
_state.value = _state.value.copy(
|
||||
error = t.message ?: "Не удалось открыть личный чат",
|
||||
unauthorized = t is UnauthorizedException,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Напоминание на сообщение по пресету (сегодня/завтра/выходные/неделя). */
|
||||
fun setMessageReminder(session: AuthSession, messageId: Long, preset: ReminderPreset) {
|
||||
val token = _state.value.selectedRoomToken ?: return
|
||||
|
||||
Reference in New Issue
Block a user