Исправлены множественная пересылка и поведение media picker в gesture-навигации
This commit is contained in:
@@ -2135,6 +2135,47 @@ fun ChatDetailScreen(
|
||||
viewModel
|
||||
.clearReplyMessages()
|
||||
},
|
||||
onShowForwardOptions = { panelMessages ->
|
||||
if (panelMessages.isEmpty()) {
|
||||
return@MessageInputBar
|
||||
}
|
||||
val forwardMessages =
|
||||
panelMessages.map { msg ->
|
||||
ForwardManager.ForwardMessage(
|
||||
messageId =
|
||||
msg.messageId,
|
||||
text = msg.text,
|
||||
timestamp =
|
||||
msg.timestamp,
|
||||
isOutgoing =
|
||||
msg.isOutgoing,
|
||||
senderPublicKey =
|
||||
msg.publicKey.ifEmpty {
|
||||
if (msg.isOutgoing) currentUserPublicKey
|
||||
else user.publicKey
|
||||
},
|
||||
originalChatPublicKey =
|
||||
user.publicKey,
|
||||
senderName =
|
||||
msg.senderName.ifEmpty {
|
||||
if (msg.isOutgoing) currentUserName.ifEmpty { "You" }
|
||||
else user.title.ifEmpty { user.username.ifEmpty { "User" } }
|
||||
},
|
||||
attachments =
|
||||
msg.attachments
|
||||
.filter {
|
||||
it.type != AttachmentType.MESSAGES
|
||||
}
|
||||
.map { it.copy(localUri = "") }
|
||||
)
|
||||
}
|
||||
|
||||
ForwardManager.setForwardMessages(
|
||||
forwardMessages,
|
||||
showPicker = false
|
||||
)
|
||||
showForwardPicker = true
|
||||
},
|
||||
chatTitle = chatTitle,
|
||||
isBlocked = isBlocked,
|
||||
showEmojiPicker =
|
||||
@@ -3313,12 +3354,40 @@ fun ChatDetailScreen(
|
||||
}
|
||||
|
||||
val forwardMessages = ForwardManager.consumeForwardMessages()
|
||||
ForwardManager.clear()
|
||||
if (forwardMessages.isEmpty()) {
|
||||
ForwardManager.clear()
|
||||
return@ForwardChatPickerBottomSheet
|
||||
}
|
||||
|
||||
// Реальная отправка forward во все выбранные чаты.
|
||||
// Desktop parity: если выбран один чат, не отправляем сразу.
|
||||
// Открываем чат с forward-панелью, чтобы пользователь мог
|
||||
// добавить подпись и отправить вручную.
|
||||
if (selectedDialogs.size == 1) {
|
||||
val targetDialog = selectedDialogs.first()
|
||||
ForwardManager.setForwardMessages(
|
||||
forwardMessages,
|
||||
showPicker = false
|
||||
)
|
||||
ForwardManager.selectChat(targetDialog.opponentKey)
|
||||
|
||||
if (targetDialog.opponentKey != user.publicKey) {
|
||||
val searchUser =
|
||||
SearchUser(
|
||||
title = targetDialog.opponentTitle,
|
||||
username =
|
||||
targetDialog.opponentUsername,
|
||||
publicKey = targetDialog.opponentKey,
|
||||
verified = targetDialog.verified,
|
||||
online = targetDialog.isOnline
|
||||
)
|
||||
onNavigateToChat(searchUser)
|
||||
}
|
||||
return@ForwardChatPickerBottomSheet
|
||||
}
|
||||
|
||||
ForwardManager.clear()
|
||||
|
||||
// Мультивыбор оставляем прямой отправкой как раньше.
|
||||
selectedDialogs.forEach { dialog ->
|
||||
viewModel.sendForwardDirectly(
|
||||
dialog.opponentKey,
|
||||
|
||||
@@ -2243,6 +2243,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val sender = myPublicKey
|
||||
val privateKey = myPrivateKey
|
||||
val replyMsgs = _replyMessages.value
|
||||
val replyMsgsToSend = replyMsgs.toList()
|
||||
val isForward = _isForwardMode.value
|
||||
|
||||
// Разрешаем отправку пустого текста если есть reply/forward
|
||||
@@ -2271,10 +2272,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val timestamp = System.currentTimeMillis()
|
||||
|
||||
// 🔥 Формируем ReplyData для отображения в UI (только первое сообщение)
|
||||
// Работает и для reply, и для forward
|
||||
// Используется для обычного reply (не forward).
|
||||
val replyData: ReplyData? =
|
||||
if (replyMsgs.isNotEmpty()) {
|
||||
val firstReply = replyMsgs.first()
|
||||
if (replyMsgsToSend.isNotEmpty()) {
|
||||
val firstReply = replyMsgsToSend.first()
|
||||
// 🖼️ Получаем attachments из текущих сообщений для превью
|
||||
// Fallback на firstReply.attachments для forward из другого чата
|
||||
val replyAttachments =
|
||||
@@ -2302,8 +2303,38 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
)
|
||||
} else null
|
||||
|
||||
// Сохраняем reply для отправки ПЕРЕД очисткой
|
||||
val replyMsgsToSend = replyMsgs.toList()
|
||||
// 📨 В forward режиме показываем ВСЕ пересылаемые сообщения в optimistic bubble,
|
||||
// а не только первое. Иначе визуально выглядит как будто отправилось одно сообщение.
|
||||
val optimisticForwardedMessages: List<ReplyData> =
|
||||
if (isForward && replyMsgsToSend.isNotEmpty()) {
|
||||
replyMsgsToSend.map { msg ->
|
||||
val senderDisplayName =
|
||||
if (msg.isOutgoing) "You"
|
||||
else msg.senderName.ifEmpty {
|
||||
opponentTitle.ifEmpty { opponentUsername.ifEmpty { "User" } }
|
||||
}
|
||||
val resolvedAttachments =
|
||||
_messages.value.find { it.id == msg.messageId }?.attachments
|
||||
?: msg.attachments.filter { it.type != AttachmentType.MESSAGES }
|
||||
ReplyData(
|
||||
messageId = msg.messageId,
|
||||
senderName = senderDisplayName,
|
||||
text = msg.text,
|
||||
isFromMe = msg.isOutgoing,
|
||||
isForwarded = true,
|
||||
forwardedFromName = senderDisplayName,
|
||||
attachments = resolvedAttachments,
|
||||
senderPublicKey = msg.publicKey.ifEmpty {
|
||||
if (msg.isOutgoing) myPublicKey ?: "" else opponentKey ?: ""
|
||||
},
|
||||
recipientPrivateKey = myPrivateKey ?: ""
|
||||
)
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
// Сохраняем режим forward для отправки ПЕРЕД очисткой
|
||||
val isForwardToSend = isForward
|
||||
|
||||
// 1. 🚀 Optimistic UI - мгновенно показываем сообщение с reply bubble
|
||||
@@ -2314,7 +2345,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
isOutgoing = true,
|
||||
timestamp = Date(timestamp),
|
||||
status = MessageStatus.SENDING,
|
||||
replyData = replyData // Данные для reply bubble
|
||||
replyData = if (isForwardToSend) null else replyData,
|
||||
forwardedMessages = optimisticForwardedMessages
|
||||
)
|
||||
|
||||
// <20> Безопасное добавление с проверкой дубликатов
|
||||
|
||||
@@ -46,6 +46,7 @@ import app.rosette.android.ui.keyboard.KeyboardTransitionCoordinator
|
||||
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
|
||||
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import com.rosetta.messenger.ui.utils.NavigationModeUtils
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import android.net.Uri
|
||||
@@ -158,6 +159,9 @@ fun ChatAttachAlert(
|
||||
viewModel: AttachAlertViewModel = viewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val hasNativeNavigationBar = remember(context) {
|
||||
NavigationModeUtils.hasNativeNavigationBar(context)
|
||||
}
|
||||
val density = LocalDensity.current
|
||||
val imeInsets = WindowInsets.ime
|
||||
val focusManager = LocalFocusManager.current
|
||||
@@ -204,6 +208,23 @@ fun ChatAttachAlert(
|
||||
// Keyboard helpers
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
fun hideUnderlyingChatKeyboard() {
|
||||
focusManager.clearFocus(force = true)
|
||||
activity?.currentFocus?.clearFocus()
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
val servedToken =
|
||||
activity?.currentFocus?.windowToken
|
||||
?: activity?.window?.decorView?.findFocus()?.windowToken
|
||||
?: activity?.window?.decorView?.windowToken
|
||||
servedToken?.let { token ->
|
||||
imm.hideSoftInputFromWindow(token, 0)
|
||||
imm.hideSoftInputFromWindow(token, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
activity?.window?.insetsController?.hide(android.view.WindowInsets.Type.ime())
|
||||
}
|
||||
}
|
||||
|
||||
fun hideKeyboard() {
|
||||
AttachAlertDebugLog.log("AttachAlert", "hideKeyboard() called, captionInputActive=$captionInputActive, shouldShow=$shouldShow, isClosing=$isClosing")
|
||||
pendingCaptionFocus = false
|
||||
@@ -277,10 +298,14 @@ fun ChatAttachAlert(
|
||||
if (coordinator.emojiHeight == 0.dp) {
|
||||
// Use saved keyboard height minus nav bar (same as spacer)
|
||||
val savedPx = KeyboardHeightProvider.getSavedKeyboardHeight(context)
|
||||
val navBarPx = (context as? Activity)?.window?.decorView?.let { view ->
|
||||
androidx.core.view.ViewCompat.getRootWindowInsets(view)
|
||||
?.getInsets(androidx.core.view.WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
|
||||
} ?: 0
|
||||
val navBarPx = if (hasNativeNavigationBar) {
|
||||
(context as? Activity)?.window?.decorView?.let { view ->
|
||||
androidx.core.view.ViewCompat.getRootWindowInsets(view)
|
||||
?.getInsets(androidx.core.view.WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
|
||||
} ?: 0
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val effectivePx = if (savedPx > 0) (savedPx - navBarPx).coerceAtLeast(0) else 0
|
||||
coordinator.emojiHeight = if (effectivePx > 0) {
|
||||
with(density) { effectivePx.toDp() }
|
||||
@@ -302,7 +327,11 @@ fun ChatAttachAlert(
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenHeight = configuration.screenHeightDp.dp
|
||||
val screenHeightPx = with(density) { screenHeight.toPx() }
|
||||
val navigationBarInsetPx = WindowInsets.navigationBars.getBottom(density).toFloat()
|
||||
val navigationBarInsetPx = if (hasNativeNavigationBar) {
|
||||
WindowInsets.navigationBars.getBottom(density).toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
val statusBarInsetPx = WindowInsets.statusBars.getTop(density).toFloat()
|
||||
|
||||
val collapsedHeightPx = screenHeightPx * 0.72f
|
||||
@@ -465,6 +494,9 @@ fun ChatAttachAlert(
|
||||
|
||||
LaunchedEffect(showSheet) {
|
||||
if (showSheet && state.editingItem == null) {
|
||||
// Close chat keyboard before showing picker so no IME layer remains under it.
|
||||
hideUnderlyingChatKeyboard()
|
||||
kotlinx.coroutines.delay(16)
|
||||
// Telegram pattern: set ADJUST_NOTHING on Activity before showing popup
|
||||
// This prevents the system from resizing the layout when focus changes
|
||||
activity?.window?.let { win ->
|
||||
@@ -677,11 +709,20 @@ fun ChatAttachAlert(
|
||||
val origNavBarColor = window?.navigationBarColor ?: 0
|
||||
val origLightNav = insetsController?.isAppearanceLightNavigationBars ?: true
|
||||
val origLightStatus = insetsController?.isAppearanceLightStatusBars ?: false
|
||||
val origContrastEnforced =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window?.isNavigationBarContrastEnforced
|
||||
} else {
|
||||
null
|
||||
}
|
||||
onDispose {
|
||||
window?.statusBarColor = origStatusBarColor
|
||||
window?.navigationBarColor = origNavBarColor
|
||||
insetsController?.isAppearanceLightNavigationBars = origLightNav
|
||||
insetsController?.isAppearanceLightStatusBars = origLightStatus
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && origContrastEnforced != null) {
|
||||
window?.isNavigationBarContrastEnforced = origContrastEnforced
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onDispose { }
|
||||
@@ -711,16 +752,28 @@ fun ChatAttachAlert(
|
||||
window.statusBarColor = android.graphics.Color.argb(scrimInt, 0, 0, 0)
|
||||
insetsController?.isAppearanceLightStatusBars = false
|
||||
}
|
||||
// Telegram-like: nav bar follows picker surface, not black scrim.
|
||||
val navBaseColor = if (dark) 0xFF1C1C1E.toInt() else 0xFFFFFFFF.toInt()
|
||||
val navAlpha = (state.openProgress * 255f).toInt().coerceIn(0, 255)
|
||||
window.navigationBarColor = android.graphics.Color.argb(
|
||||
navAlpha,
|
||||
android.graphics.Color.red(navBaseColor),
|
||||
android.graphics.Color.green(navBaseColor),
|
||||
android.graphics.Color.blue(navBaseColor)
|
||||
)
|
||||
insetsController?.isAppearanceLightNavigationBars = !dark
|
||||
if (hasNativeNavigationBar) {
|
||||
// Telegram-like: for 2/3-button navigation keep picker-surface tint.
|
||||
val navBaseColor = if (dark) 0xFF1C1C1E.toInt() else 0xFFFFFFFF.toInt()
|
||||
val navAlpha = (state.openProgress * 255f).toInt().coerceIn(0, 255)
|
||||
window.navigationBarColor = android.graphics.Color.argb(
|
||||
navAlpha,
|
||||
android.graphics.Color.red(navBaseColor),
|
||||
android.graphics.Color.green(navBaseColor),
|
||||
android.graphics.Color.blue(navBaseColor)
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = true
|
||||
}
|
||||
insetsController?.isAppearanceLightNavigationBars = !dark
|
||||
} else {
|
||||
// Telegram-like on gesture navigation: transparent stable nav area.
|
||||
window.navigationBarColor = android.graphics.Color.TRANSPARENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
insetsController?.isAppearanceLightNavigationBars = !dark
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import com.rosetta.messenger.ui.components.AppleEmojiTextField
|
||||
import com.rosetta.messenger.ui.components.KeyboardHeightProvider
|
||||
import com.rosetta.messenger.ui.components.OptimizedEmojiPicker
|
||||
import com.rosetta.messenger.ui.utils.NavigationModeUtils
|
||||
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
|
||||
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -136,6 +137,9 @@ fun MediaPickerBottomSheet(
|
||||
onPhotoPreviewRequested: ((Uri, ThumbnailPosition?) -> Unit)? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val hasNativeNavigationBar = remember(context) {
|
||||
NavigationModeUtils.hasNativeNavigationBar(context)
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
val density = LocalDensity.current
|
||||
val imeInsets = WindowInsets.ime
|
||||
@@ -295,7 +299,11 @@ fun MediaPickerBottomSheet(
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenHeight = configuration.screenHeightDp.dp
|
||||
val screenHeightPx = with(density) { screenHeight.toPx() }
|
||||
val navigationBarInsetPx = WindowInsets.navigationBars.getBottom(density).toFloat()
|
||||
val navigationBarInsetPx = if (hasNativeNavigationBar) {
|
||||
WindowInsets.navigationBars.getBottom(density).toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
val statusBarInsetPx = WindowInsets.statusBars.getTop(density).toFloat()
|
||||
|
||||
// 🔄 Высоты в пикселях для точного контроля
|
||||
@@ -384,6 +392,9 @@ fun MediaPickerBottomSheet(
|
||||
// Запускаем анимацию когда showSheet меняется
|
||||
LaunchedEffect(showSheet) {
|
||||
if (showSheet && editingItem == null) {
|
||||
// Ensure IME from chat is closed before picker opens.
|
||||
hideKeyboard()
|
||||
delay(16)
|
||||
shouldShow = true
|
||||
isClosing = false
|
||||
showAlbumMenu = false
|
||||
@@ -562,12 +573,21 @@ fun MediaPickerBottomSheet(
|
||||
val origNavBarColor = window?.navigationBarColor ?: 0
|
||||
val origLightNav = insetsController?.isAppearanceLightNavigationBars ?: true
|
||||
val origLightStatus = insetsController?.isAppearanceLightStatusBars ?: false
|
||||
val origContrastEnforced =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window?.isNavigationBarContrastEnforced
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
onDispose {
|
||||
window?.statusBarColor = origStatusBarColor
|
||||
window?.navigationBarColor = origNavBarColor
|
||||
insetsController?.isAppearanceLightNavigationBars = origLightNav
|
||||
insetsController?.isAppearanceLightStatusBars = origLightStatus
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && origContrastEnforced != null) {
|
||||
window?.isNavigationBarContrastEnforced = origContrastEnforced
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onDispose { }
|
||||
@@ -601,16 +621,28 @@ fun MediaPickerBottomSheet(
|
||||
)
|
||||
insetsController?.isAppearanceLightStatusBars = false
|
||||
}
|
||||
// Telegram-like: nav bar follows picker surface, not scrim.
|
||||
val navBaseColor = if (dark) 0xFF1C1C1E.toInt() else 0xFFFFFFFF.toInt()
|
||||
val navAlpha = (state.openProgress * 255f).toInt().coerceIn(0, 255)
|
||||
window.navigationBarColor = android.graphics.Color.argb(
|
||||
navAlpha,
|
||||
android.graphics.Color.red(navBaseColor),
|
||||
android.graphics.Color.green(navBaseColor),
|
||||
android.graphics.Color.blue(navBaseColor)
|
||||
)
|
||||
insetsController?.isAppearanceLightNavigationBars = !dark
|
||||
if (hasNativeNavigationBar) {
|
||||
// Telegram-like: for 2/3-button navigation keep picker-surface tint.
|
||||
val navBaseColor = if (dark) 0xFF1C1C1E.toInt() else 0xFFFFFFFF.toInt()
|
||||
val navAlpha = (state.openProgress * 255f).toInt().coerceIn(0, 255)
|
||||
window.navigationBarColor = android.graphics.Color.argb(
|
||||
navAlpha,
|
||||
android.graphics.Color.red(navBaseColor),
|
||||
android.graphics.Color.green(navBaseColor),
|
||||
android.graphics.Color.blue(navBaseColor)
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = true
|
||||
}
|
||||
insetsController?.isAppearanceLightNavigationBars = !dark
|
||||
} else {
|
||||
// Telegram-like on gesture navigation: transparent stable nav area.
|
||||
window.navigationBarColor = android.graphics.Color.TRANSPARENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
insetsController?.isAppearanceLightNavigationBars = !dark
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2207,6 +2239,9 @@ fun PhotoPreviewWithCaptionScreen(
|
||||
val backgroundColor = if (isDarkTheme) Color.Black else Color.White
|
||||
|
||||
val context = LocalContext.current
|
||||
val hasNativeNavigationBar = remember(context) {
|
||||
NavigationModeUtils.hasNativeNavigationBar(context)
|
||||
}
|
||||
val view = LocalView.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val density = LocalDensity.current
|
||||
@@ -2291,7 +2326,8 @@ fun PhotoPreviewWithCaptionScreen(
|
||||
|
||||
// 🔥 КЛЮЧЕВОЕ: imePadding применяется ТОЛЬКО когда emoji box НЕ виден
|
||||
val shouldUseImePadding = !coordinator.isEmojiBoxVisible
|
||||
val shouldAddNavBarPadding = !isKeyboardVisible && !coordinator.isEmojiBoxVisible
|
||||
val shouldAddNavBarPadding =
|
||||
hasNativeNavigationBar && !isKeyboardVisible && !coordinator.isEmojiBoxVisible
|
||||
|
||||
// Логируем состояние при каждой рекомпозиции
|
||||
Surface(
|
||||
|
||||
@@ -36,6 +36,8 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
|
||||
import app.rosette.android.ui.keyboard.KeyboardTransitionCoordinator
|
||||
import coil.compose.AsyncImage
|
||||
@@ -88,6 +90,7 @@ fun MessageInputBar(
|
||||
replyMessages: List<ChatViewModel.ReplyMessage> = emptyList(),
|
||||
isForwardMode: Boolean = false,
|
||||
onCloseReply: () -> Unit = {},
|
||||
onShowForwardOptions: (List<ChatViewModel.ReplyMessage>) -> Unit = {},
|
||||
chatTitle: String = "",
|
||||
isBlocked: Boolean = false,
|
||||
showEmojiPicker: Boolean = false,
|
||||
@@ -218,6 +221,11 @@ fun MessageInputBar(
|
||||
|
||||
val canSend = remember(value, hasReply) { value.isNotBlank() || hasReply }
|
||||
var isSending by remember { mutableStateOf(false) }
|
||||
var showForwardCancelDialog by remember { mutableStateOf(false) }
|
||||
var forwardCancelDialogCount by remember { mutableIntStateOf(0) }
|
||||
var forwardCancelDialogMessages by remember {
|
||||
mutableStateOf<List<ChatViewModel.ReplyMessage>>(emptyList())
|
||||
}
|
||||
val mentionPattern = remember { Regex("@([\\w\\d_]*)$") }
|
||||
val mentionedPattern = remember { Regex("@([\\w\\d_]{1,})") }
|
||||
val mentionMatch = remember(value, isGroupChat) { if (isGroupChat) mentionPattern.find(value) else null }
|
||||
@@ -661,7 +669,18 @@ fun MessageInputBar(
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = onCloseReply
|
||||
onClick = {
|
||||
if (panelIsForwardMode && panelReplyMessages.isNotEmpty()) {
|
||||
val sourceMessages =
|
||||
if (replyMessages.isNotEmpty()) replyMessages
|
||||
else panelReplyMessages
|
||||
forwardCancelDialogCount = sourceMessages.size
|
||||
forwardCancelDialogMessages = sourceMessages
|
||||
showForwardCancelDialog = true
|
||||
} else {
|
||||
onCloseReply()
|
||||
}
|
||||
}
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@@ -765,6 +784,122 @@ fun MessageInputBar(
|
||||
}
|
||||
}
|
||||
|
||||
if (showForwardCancelDialog) {
|
||||
val titleText =
|
||||
if (forwardCancelDialogCount == 1) "1 message"
|
||||
else "$forwardCancelDialogCount messages"
|
||||
val bodyText =
|
||||
"What would you like to do with $titleText from your chat with ${chatTitle.ifBlank { "this user" }}?"
|
||||
val popupInteraction = remember { MutableInteractionSource() }
|
||||
val cardInteraction = remember { MutableInteractionSource() }
|
||||
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3C) else Color(0xFFE5E5EA)
|
||||
val cardColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White
|
||||
val bodyColor = if (isDarkTheme) Color(0xFFD1D1D6) else Color(0xFF3C3C43)
|
||||
|
||||
Popup(
|
||||
alignment = Alignment.Center,
|
||||
onDismissRequest = {
|
||||
showForwardCancelDialog = false
|
||||
forwardCancelDialogMessages = emptyList()
|
||||
},
|
||||
properties =
|
||||
PopupProperties(
|
||||
focusable = false,
|
||||
dismissOnBackPress = true,
|
||||
dismissOnClickOutside = true,
|
||||
clippingEnabled = false
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.36f))
|
||||
.clickable(
|
||||
interactionSource = popupInteraction,
|
||||
indication = null
|
||||
) { showForwardCancelDialog = false },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Surface(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(0.88f)
|
||||
.widthIn(max = 360.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable(
|
||||
interactionSource = cardInteraction,
|
||||
indication = null
|
||||
) {},
|
||||
color = cardColor,
|
||||
shadowElevation = 18.dp
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
Text(
|
||||
text = titleText,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (isDarkTheme) Color.White else Color.Black,
|
||||
modifier = Modifier.padding(horizontal = 20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Text(
|
||||
text = bodyText,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 21.sp,
|
||||
color = bodyColor,
|
||||
modifier = Modifier.padding(horizontal = 20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
Divider(thickness = 0.5.dp, color = dividerColor)
|
||||
TextButton(
|
||||
onClick = {
|
||||
showForwardCancelDialog = false
|
||||
val panelMessages =
|
||||
if (replyMessages.isNotEmpty()) {
|
||||
replyMessages
|
||||
} else if (forwardCancelDialogMessages.isNotEmpty()) {
|
||||
forwardCancelDialogMessages
|
||||
} else if (liveReplyMessages.isNotEmpty()) {
|
||||
liveReplyMessages
|
||||
} else {
|
||||
animatedReplyMessages
|
||||
}
|
||||
onShowForwardOptions(panelMessages)
|
||||
forwardCancelDialogMessages = emptyList()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().height(52.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Show forwarding options",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = PrimaryBlue
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
showForwardCancelDialog = false
|
||||
forwardCancelDialogMessages = emptyList()
|
||||
onCloseReply()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().height(52.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Cancel Forwarding",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFFFF5D73)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EMOJI PICKER
|
||||
if (!isBlocked) {
|
||||
AnimatedKeyboardTransition(
|
||||
|
||||
Reference in New Issue
Block a user