Исправлены множественная пересылка и поведение media picker в gesture-навигации
This commit is contained in:
@@ -2135,6 +2135,47 @@ fun ChatDetailScreen(
|
|||||||
viewModel
|
viewModel
|
||||||
.clearReplyMessages()
|
.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,
|
chatTitle = chatTitle,
|
||||||
isBlocked = isBlocked,
|
isBlocked = isBlocked,
|
||||||
showEmojiPicker =
|
showEmojiPicker =
|
||||||
@@ -3313,12 +3354,40 @@ fun ChatDetailScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val forwardMessages = ForwardManager.consumeForwardMessages()
|
val forwardMessages = ForwardManager.consumeForwardMessages()
|
||||||
ForwardManager.clear()
|
|
||||||
if (forwardMessages.isEmpty()) {
|
if (forwardMessages.isEmpty()) {
|
||||||
|
ForwardManager.clear()
|
||||||
return@ForwardChatPickerBottomSheet
|
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 ->
|
selectedDialogs.forEach { dialog ->
|
||||||
viewModel.sendForwardDirectly(
|
viewModel.sendForwardDirectly(
|
||||||
dialog.opponentKey,
|
dialog.opponentKey,
|
||||||
|
|||||||
@@ -2243,6 +2243,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val sender = myPublicKey
|
val sender = myPublicKey
|
||||||
val privateKey = myPrivateKey
|
val privateKey = myPrivateKey
|
||||||
val replyMsgs = _replyMessages.value
|
val replyMsgs = _replyMessages.value
|
||||||
|
val replyMsgsToSend = replyMsgs.toList()
|
||||||
val isForward = _isForwardMode.value
|
val isForward = _isForwardMode.value
|
||||||
|
|
||||||
// Разрешаем отправку пустого текста если есть reply/forward
|
// Разрешаем отправку пустого текста если есть reply/forward
|
||||||
@@ -2271,10 +2272,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val timestamp = System.currentTimeMillis()
|
val timestamp = System.currentTimeMillis()
|
||||||
|
|
||||||
// 🔥 Формируем ReplyData для отображения в UI (только первое сообщение)
|
// 🔥 Формируем ReplyData для отображения в UI (только первое сообщение)
|
||||||
// Работает и для reply, и для forward
|
// Используется для обычного reply (не forward).
|
||||||
val replyData: ReplyData? =
|
val replyData: ReplyData? =
|
||||||
if (replyMsgs.isNotEmpty()) {
|
if (replyMsgsToSend.isNotEmpty()) {
|
||||||
val firstReply = replyMsgs.first()
|
val firstReply = replyMsgsToSend.first()
|
||||||
// 🖼️ Получаем attachments из текущих сообщений для превью
|
// 🖼️ Получаем attachments из текущих сообщений для превью
|
||||||
// Fallback на firstReply.attachments для forward из другого чата
|
// Fallback на firstReply.attachments для forward из другого чата
|
||||||
val replyAttachments =
|
val replyAttachments =
|
||||||
@@ -2302,8 +2303,38 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
)
|
)
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
// Сохраняем reply для отправки ПЕРЕД очисткой
|
// 📨 В forward режиме показываем ВСЕ пересылаемые сообщения в optimistic bubble,
|
||||||
val replyMsgsToSend = replyMsgs.toList()
|
// а не только первое. Иначе визуально выглядит как будто отправилось одно сообщение.
|
||||||
|
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
|
val isForwardToSend = isForward
|
||||||
|
|
||||||
// 1. 🚀 Optimistic UI - мгновенно показываем сообщение с reply bubble
|
// 1. 🚀 Optimistic UI - мгновенно показываем сообщение с reply bubble
|
||||||
@@ -2314,7 +2345,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
isOutgoing = true,
|
isOutgoing = true,
|
||||||
timestamp = Date(timestamp),
|
timestamp = Date(timestamp),
|
||||||
status = MessageStatus.SENDING,
|
status = MessageStatus.SENDING,
|
||||||
replyData = replyData // Данные для reply bubble
|
replyData = if (isForwardToSend) null else replyData,
|
||||||
|
forwardedMessages = optimisticForwardedMessages
|
||||||
)
|
)
|
||||||
|
|
||||||
// <20> Безопасное добавление с проверкой дубликатов
|
// <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.AnimatedKeyboardTransition
|
||||||
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
|
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
|
import com.rosetta.messenger.ui.utils.NavigationModeUtils
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -158,6 +159,9 @@ fun ChatAttachAlert(
|
|||||||
viewModel: AttachAlertViewModel = viewModel()
|
viewModel: AttachAlertViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val hasNativeNavigationBar = remember(context) {
|
||||||
|
NavigationModeUtils.hasNativeNavigationBar(context)
|
||||||
|
}
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val imeInsets = WindowInsets.ime
|
val imeInsets = WindowInsets.ime
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
@@ -204,6 +208,23 @@ fun ChatAttachAlert(
|
|||||||
// Keyboard helpers
|
// 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() {
|
fun hideKeyboard() {
|
||||||
AttachAlertDebugLog.log("AttachAlert", "hideKeyboard() called, captionInputActive=$captionInputActive, shouldShow=$shouldShow, isClosing=$isClosing")
|
AttachAlertDebugLog.log("AttachAlert", "hideKeyboard() called, captionInputActive=$captionInputActive, shouldShow=$shouldShow, isClosing=$isClosing")
|
||||||
pendingCaptionFocus = false
|
pendingCaptionFocus = false
|
||||||
@@ -277,10 +298,14 @@ fun ChatAttachAlert(
|
|||||||
if (coordinator.emojiHeight == 0.dp) {
|
if (coordinator.emojiHeight == 0.dp) {
|
||||||
// Use saved keyboard height minus nav bar (same as spacer)
|
// Use saved keyboard height minus nav bar (same as spacer)
|
||||||
val savedPx = KeyboardHeightProvider.getSavedKeyboardHeight(context)
|
val savedPx = KeyboardHeightProvider.getSavedKeyboardHeight(context)
|
||||||
val navBarPx = (context as? Activity)?.window?.decorView?.let { view ->
|
val navBarPx = if (hasNativeNavigationBar) {
|
||||||
|
(context as? Activity)?.window?.decorView?.let { view ->
|
||||||
androidx.core.view.ViewCompat.getRootWindowInsets(view)
|
androidx.core.view.ViewCompat.getRootWindowInsets(view)
|
||||||
?.getInsets(androidx.core.view.WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
|
?.getInsets(androidx.core.view.WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
|
||||||
} ?: 0
|
} ?: 0
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
val effectivePx = if (savedPx > 0) (savedPx - navBarPx).coerceAtLeast(0) else 0
|
val effectivePx = if (savedPx > 0) (savedPx - navBarPx).coerceAtLeast(0) else 0
|
||||||
coordinator.emojiHeight = if (effectivePx > 0) {
|
coordinator.emojiHeight = if (effectivePx > 0) {
|
||||||
with(density) { effectivePx.toDp() }
|
with(density) { effectivePx.toDp() }
|
||||||
@@ -302,7 +327,11 @@ fun ChatAttachAlert(
|
|||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
val screenHeight = configuration.screenHeightDp.dp
|
val screenHeight = configuration.screenHeightDp.dp
|
||||||
val screenHeightPx = with(density) { screenHeight.toPx() }
|
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 statusBarInsetPx = WindowInsets.statusBars.getTop(density).toFloat()
|
||||||
|
|
||||||
val collapsedHeightPx = screenHeightPx * 0.72f
|
val collapsedHeightPx = screenHeightPx * 0.72f
|
||||||
@@ -465,6 +494,9 @@ fun ChatAttachAlert(
|
|||||||
|
|
||||||
LaunchedEffect(showSheet) {
|
LaunchedEffect(showSheet) {
|
||||||
if (showSheet && state.editingItem == null) {
|
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
|
// Telegram pattern: set ADJUST_NOTHING on Activity before showing popup
|
||||||
// This prevents the system from resizing the layout when focus changes
|
// This prevents the system from resizing the layout when focus changes
|
||||||
activity?.window?.let { win ->
|
activity?.window?.let { win ->
|
||||||
@@ -677,11 +709,20 @@ fun ChatAttachAlert(
|
|||||||
val origNavBarColor = window?.navigationBarColor ?: 0
|
val origNavBarColor = window?.navigationBarColor ?: 0
|
||||||
val origLightNav = insetsController?.isAppearanceLightNavigationBars ?: true
|
val origLightNav = insetsController?.isAppearanceLightNavigationBars ?: true
|
||||||
val origLightStatus = insetsController?.isAppearanceLightStatusBars ?: false
|
val origLightStatus = insetsController?.isAppearanceLightStatusBars ?: false
|
||||||
|
val origContrastEnforced =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
window?.isNavigationBarContrastEnforced
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
onDispose {
|
onDispose {
|
||||||
window?.statusBarColor = origStatusBarColor
|
window?.statusBarColor = origStatusBarColor
|
||||||
window?.navigationBarColor = origNavBarColor
|
window?.navigationBarColor = origNavBarColor
|
||||||
insetsController?.isAppearanceLightNavigationBars = origLightNav
|
insetsController?.isAppearanceLightNavigationBars = origLightNav
|
||||||
insetsController?.isAppearanceLightStatusBars = origLightStatus
|
insetsController?.isAppearanceLightStatusBars = origLightStatus
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && origContrastEnforced != null) {
|
||||||
|
window?.isNavigationBarContrastEnforced = origContrastEnforced
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onDispose { }
|
onDispose { }
|
||||||
@@ -711,7 +752,8 @@ fun ChatAttachAlert(
|
|||||||
window.statusBarColor = android.graphics.Color.argb(scrimInt, 0, 0, 0)
|
window.statusBarColor = android.graphics.Color.argb(scrimInt, 0, 0, 0)
|
||||||
insetsController?.isAppearanceLightStatusBars = false
|
insetsController?.isAppearanceLightStatusBars = false
|
||||||
}
|
}
|
||||||
// Telegram-like: nav bar follows picker surface, not black scrim.
|
if (hasNativeNavigationBar) {
|
||||||
|
// Telegram-like: for 2/3-button navigation keep picker-surface tint.
|
||||||
val navBaseColor = if (dark) 0xFF1C1C1E.toInt() else 0xFFFFFFFF.toInt()
|
val navBaseColor = if (dark) 0xFF1C1C1E.toInt() else 0xFFFFFFFF.toInt()
|
||||||
val navAlpha = (state.openProgress * 255f).toInt().coerceIn(0, 255)
|
val navAlpha = (state.openProgress * 255f).toInt().coerceIn(0, 255)
|
||||||
window.navigationBarColor = android.graphics.Color.argb(
|
window.navigationBarColor = android.graphics.Color.argb(
|
||||||
@@ -720,7 +762,18 @@ fun ChatAttachAlert(
|
|||||||
android.graphics.Color.green(navBaseColor),
|
android.graphics.Color.green(navBaseColor),
|
||||||
android.graphics.Color.blue(navBaseColor)
|
android.graphics.Color.blue(navBaseColor)
|
||||||
)
|
)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
window.isNavigationBarContrastEnforced = true
|
||||||
|
}
|
||||||
insetsController?.isAppearanceLightNavigationBars = !dark
|
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.AppleEmojiTextField
|
||||||
import com.rosetta.messenger.ui.components.KeyboardHeightProvider
|
import com.rosetta.messenger.ui.components.KeyboardHeightProvider
|
||||||
import com.rosetta.messenger.ui.components.OptimizedEmojiPicker
|
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.rememberKeyboardTransitionCoordinator
|
||||||
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
|
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -136,6 +137,9 @@ fun MediaPickerBottomSheet(
|
|||||||
onPhotoPreviewRequested: ((Uri, ThumbnailPosition?) -> Unit)? = null
|
onPhotoPreviewRequested: ((Uri, ThumbnailPosition?) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val hasNativeNavigationBar = remember(context) {
|
||||||
|
NavigationModeUtils.hasNativeNavigationBar(context)
|
||||||
|
}
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val imeInsets = WindowInsets.ime
|
val imeInsets = WindowInsets.ime
|
||||||
@@ -295,7 +299,11 @@ fun MediaPickerBottomSheet(
|
|||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
val screenHeight = configuration.screenHeightDp.dp
|
val screenHeight = configuration.screenHeightDp.dp
|
||||||
val screenHeightPx = with(density) { screenHeight.toPx() }
|
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 statusBarInsetPx = WindowInsets.statusBars.getTop(density).toFloat()
|
||||||
|
|
||||||
// 🔄 Высоты в пикселях для точного контроля
|
// 🔄 Высоты в пикселях для точного контроля
|
||||||
@@ -384,6 +392,9 @@ fun MediaPickerBottomSheet(
|
|||||||
// Запускаем анимацию когда showSheet меняется
|
// Запускаем анимацию когда showSheet меняется
|
||||||
LaunchedEffect(showSheet) {
|
LaunchedEffect(showSheet) {
|
||||||
if (showSheet && editingItem == null) {
|
if (showSheet && editingItem == null) {
|
||||||
|
// Ensure IME from chat is closed before picker opens.
|
||||||
|
hideKeyboard()
|
||||||
|
delay(16)
|
||||||
shouldShow = true
|
shouldShow = true
|
||||||
isClosing = false
|
isClosing = false
|
||||||
showAlbumMenu = false
|
showAlbumMenu = false
|
||||||
@@ -562,12 +573,21 @@ fun MediaPickerBottomSheet(
|
|||||||
val origNavBarColor = window?.navigationBarColor ?: 0
|
val origNavBarColor = window?.navigationBarColor ?: 0
|
||||||
val origLightNav = insetsController?.isAppearanceLightNavigationBars ?: true
|
val origLightNav = insetsController?.isAppearanceLightNavigationBars ?: true
|
||||||
val origLightStatus = insetsController?.isAppearanceLightStatusBars ?: false
|
val origLightStatus = insetsController?.isAppearanceLightStatusBars ?: false
|
||||||
|
val origContrastEnforced =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
window?.isNavigationBarContrastEnforced
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
onDispose {
|
onDispose {
|
||||||
window?.statusBarColor = origStatusBarColor
|
window?.statusBarColor = origStatusBarColor
|
||||||
window?.navigationBarColor = origNavBarColor
|
window?.navigationBarColor = origNavBarColor
|
||||||
insetsController?.isAppearanceLightNavigationBars = origLightNav
|
insetsController?.isAppearanceLightNavigationBars = origLightNav
|
||||||
insetsController?.isAppearanceLightStatusBars = origLightStatus
|
insetsController?.isAppearanceLightStatusBars = origLightStatus
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && origContrastEnforced != null) {
|
||||||
|
window?.isNavigationBarContrastEnforced = origContrastEnforced
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onDispose { }
|
onDispose { }
|
||||||
@@ -601,7 +621,8 @@ fun MediaPickerBottomSheet(
|
|||||||
)
|
)
|
||||||
insetsController?.isAppearanceLightStatusBars = false
|
insetsController?.isAppearanceLightStatusBars = false
|
||||||
}
|
}
|
||||||
// Telegram-like: nav bar follows picker surface, not scrim.
|
if (hasNativeNavigationBar) {
|
||||||
|
// Telegram-like: for 2/3-button navigation keep picker-surface tint.
|
||||||
val navBaseColor = if (dark) 0xFF1C1C1E.toInt() else 0xFFFFFFFF.toInt()
|
val navBaseColor = if (dark) 0xFF1C1C1E.toInt() else 0xFFFFFFFF.toInt()
|
||||||
val navAlpha = (state.openProgress * 255f).toInt().coerceIn(0, 255)
|
val navAlpha = (state.openProgress * 255f).toInt().coerceIn(0, 255)
|
||||||
window.navigationBarColor = android.graphics.Color.argb(
|
window.navigationBarColor = android.graphics.Color.argb(
|
||||||
@@ -610,7 +631,18 @@ fun MediaPickerBottomSheet(
|
|||||||
android.graphics.Color.green(navBaseColor),
|
android.graphics.Color.green(navBaseColor),
|
||||||
android.graphics.Color.blue(navBaseColor)
|
android.graphics.Color.blue(navBaseColor)
|
||||||
)
|
)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
window.isNavigationBarContrastEnforced = true
|
||||||
|
}
|
||||||
insetsController?.isAppearanceLightNavigationBars = !dark
|
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 backgroundColor = if (isDarkTheme) Color.Black else Color.White
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val hasNativeNavigationBar = remember(context) {
|
||||||
|
NavigationModeUtils.hasNativeNavigationBar(context)
|
||||||
|
}
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
@@ -2291,7 +2326,8 @@ fun PhotoPreviewWithCaptionScreen(
|
|||||||
|
|
||||||
// 🔥 КЛЮЧЕВОЕ: imePadding применяется ТОЛЬКО когда emoji box НЕ виден
|
// 🔥 КЛЮЧЕВОЕ: imePadding применяется ТОЛЬКО когда emoji box НЕ виден
|
||||||
val shouldUseImePadding = !coordinator.isEmojiBoxVisible
|
val shouldUseImePadding = !coordinator.isEmojiBoxVisible
|
||||||
val shouldAddNavBarPadding = !isKeyboardVisible && !coordinator.isEmojiBoxVisible
|
val shouldAddNavBarPadding =
|
||||||
|
hasNativeNavigationBar && !isKeyboardVisible && !coordinator.isEmojiBoxVisible
|
||||||
|
|
||||||
// Логируем состояние при каждой рекомпозиции
|
// Логируем состояние при каждой рекомпозиции
|
||||||
Surface(
|
Surface(
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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.AnimatedKeyboardTransition
|
||||||
import app.rosette.android.ui.keyboard.KeyboardTransitionCoordinator
|
import app.rosette.android.ui.keyboard.KeyboardTransitionCoordinator
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
@@ -88,6 +90,7 @@ fun MessageInputBar(
|
|||||||
replyMessages: List<ChatViewModel.ReplyMessage> = emptyList(),
|
replyMessages: List<ChatViewModel.ReplyMessage> = emptyList(),
|
||||||
isForwardMode: Boolean = false,
|
isForwardMode: Boolean = false,
|
||||||
onCloseReply: () -> Unit = {},
|
onCloseReply: () -> Unit = {},
|
||||||
|
onShowForwardOptions: (List<ChatViewModel.ReplyMessage>) -> Unit = {},
|
||||||
chatTitle: String = "",
|
chatTitle: String = "",
|
||||||
isBlocked: Boolean = false,
|
isBlocked: Boolean = false,
|
||||||
showEmojiPicker: Boolean = false,
|
showEmojiPicker: Boolean = false,
|
||||||
@@ -218,6 +221,11 @@ fun MessageInputBar(
|
|||||||
|
|
||||||
val canSend = remember(value, hasReply) { value.isNotBlank() || hasReply }
|
val canSend = remember(value, hasReply) { value.isNotBlank() || hasReply }
|
||||||
var isSending by remember { mutableStateOf(false) }
|
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 mentionPattern = remember { Regex("@([\\w\\d_]*)$") }
|
||||||
val mentionedPattern = remember { Regex("@([\\w\\d_]{1,})") }
|
val mentionedPattern = remember { Regex("@([\\w\\d_]{1,})") }
|
||||||
val mentionMatch = remember(value, isGroupChat) { if (isGroupChat) mentionPattern.find(value) else null }
|
val mentionMatch = remember(value, isGroupChat) { if (isGroupChat) mentionPattern.find(value) else null }
|
||||||
@@ -661,7 +669,18 @@ fun MessageInputBar(
|
|||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
indication = null,
|
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
|
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
|
// EMOJI PICKER
|
||||||
if (!isBlocked) {
|
if (!isBlocked) {
|
||||||
AnimatedKeyboardTransition(
|
AnimatedKeyboardTransition(
|
||||||
|
|||||||
Reference in New Issue
Block a user