mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-20 00:35:47 +01:00
Fix regression in long press menu heihgt.
This commit is contained in:
+22
@@ -11,6 +11,8 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
/**
|
||||
* Asks a [MediaKeyboardScaffold] for a keyboard.
|
||||
@@ -32,6 +34,14 @@ class MediaKeyboardController(initialKeyboardHeightPx: Int = 0) {
|
||||
var isSystemKeyboardVisible: Boolean by mutableStateOf(false)
|
||||
internal set
|
||||
|
||||
/**
|
||||
* True while the system keyboard is on its way in or out. [isSystemKeyboardVisible] reads the
|
||||
* target of that animation, so it goes false the moment a hide is asked for, well before the
|
||||
* space is handed back.
|
||||
*/
|
||||
var isSystemKeyboardAnimating: Boolean by mutableStateOf(false)
|
||||
internal set
|
||||
|
||||
/**
|
||||
* True while the system keyboard has been asked for in place of one of ours but has yet to settle.
|
||||
* Holds the space across that gap, which the IME service round trip would otherwise leave empty.
|
||||
@@ -45,6 +55,18 @@ class MediaKeyboardController(initialKeyboardHeightPx: Int = 0) {
|
||||
|
||||
val isShowing: Boolean get() = current != null
|
||||
|
||||
/** True when no keyboard is up, on its way out, or being held space for. */
|
||||
val isSettled: Boolean get() = current == null && !isSystemKeyboardVisible && !isSystemKeyboardAnimating && !awaitingSystemKeyboard
|
||||
|
||||
/**
|
||||
* Suspends until [isSettled], so a caller can act on a content area that has been handed all of
|
||||
* its space back. Reads the same snapshot state the scaffold writes, so there is no settle to miss
|
||||
* for a keyboard that goes away without animating.
|
||||
*/
|
||||
suspend fun awaitSettled() {
|
||||
snapshotFlow { isSettled }.first { it }
|
||||
}
|
||||
|
||||
fun show(key: MediaKeyboardKey) {
|
||||
current = key
|
||||
awaitingSystemKeyboard = false
|
||||
|
||||
+10
-8
@@ -36,6 +36,7 @@ import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -122,10 +123,17 @@ fun MediaKeyboardScaffold(
|
||||
|
||||
// The target state, so it does not read as hidden for the whole closing animation.
|
||||
val systemKeyboardVisible = WindowInsets.isImeVisible
|
||||
val systemKeyboardAnimating = imeAnimationSource.getBottom(density) != imeAnimationTarget.getBottom(density)
|
||||
|
||||
// Written together, so nothing waiting on the controller can see a keyboard that is neither
|
||||
// visible nor still animating out.
|
||||
SideEffect {
|
||||
controller.isSystemKeyboardVisible = systemKeyboardVisible
|
||||
controller.isSystemKeyboardAnimating = systemKeyboardAnimating
|
||||
}
|
||||
|
||||
var hasReportedKeyboardVisibility by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(systemKeyboardVisible) {
|
||||
controller.isSystemKeyboardVisible = systemKeyboardVisible
|
||||
if (hasReportedKeyboardVisibility) {
|
||||
onEvent(MediaKeyboardEvents.SystemKeyboardVisibilityChanged(systemKeyboardVisible))
|
||||
}
|
||||
@@ -224,13 +232,7 @@ fun MediaKeyboardScaffold(
|
||||
}
|
||||
|
||||
val systemKeyboardTakingOverSpace = activeKey == null &&
|
||||
(
|
||||
controller.awaitingSystemKeyboard ||
|
||||
(
|
||||
imeAnimationTarget.getBottom(density) > 0 &&
|
||||
imeAnimationSource.getBottom(density) != imeAnimationTarget.getBottom(density)
|
||||
)
|
||||
)
|
||||
(controller.awaitingSystemKeyboard || (imeAnimationTarget.getBottom(density) > 0 && systemKeyboardAnimating))
|
||||
|
||||
val claimedBottomPx = {
|
||||
if (activeKey != null) {
|
||||
|
||||
@@ -123,6 +123,20 @@ class ChatInputController(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like [runAfterAllHidden], but suspends until the keyboards have finished animating out rather
|
||||
* than returning as soon as the hide has been asked for. For callers that measure themselves
|
||||
* against the content area, which stays shrunk for the length of that animation.
|
||||
*/
|
||||
suspend fun hideAllAndAwaitSettled(imeTarget: EditText) {
|
||||
if (controller.isSettled) {
|
||||
return
|
||||
}
|
||||
|
||||
hideAll(imeTarget)
|
||||
controller.awaitSettled()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key The keyboard to bring up, or take away if already showing.
|
||||
* @param imeTarget The field the system keyboard belongs to.
|
||||
|
||||
+127
-99
@@ -4115,11 +4115,6 @@ class ConversationFragment :
|
||||
(!recipient.isGroup || recipient.isActiveGroup) &&
|
||||
adapter.selectedItems.isEmpty()
|
||||
) {
|
||||
multiselectItemDecoration.setFocusedItem(MultiselectPart.Message(item.conversationMessage))
|
||||
binding.conversationItemRecycler.invalidateItemDecorations()
|
||||
binding.reactionsShade.visibility = View.VISIBLE
|
||||
binding.conversationItemRecycler.suppressLayout(true)
|
||||
|
||||
val target: InteractiveConversationElement? = if (itemView is InteractiveConversationElement) {
|
||||
itemView
|
||||
} else {
|
||||
@@ -4132,103 +4127,15 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
if (target != null) {
|
||||
val audioUri = messageRecord.getAudioUriForLongClick()
|
||||
if (audioUri != null) {
|
||||
getVoiceNoteMediaController().pausePlayback(audioUri)
|
||||
}
|
||||
|
||||
val childAdapterPosition = target.getAdapterPosition(binding.conversationItemRecycler)
|
||||
var mp4Holder: GiphyMp4ProjectionPlayerHolder? = null
|
||||
var videoBitmap: Bitmap? = null
|
||||
if (childAdapterPosition != RecyclerView.NO_POSITION) {
|
||||
mp4Holder = giphyMp4ProjectionRecycler.getCurrentHolder(childAdapterPosition)
|
||||
if (mp4Holder?.isVisible == true) {
|
||||
mp4Holder.pause()
|
||||
videoBitmap = mp4Holder.bitmap
|
||||
mp4Holder.hide()
|
||||
}
|
||||
}
|
||||
|
||||
val snapshot = ConversationItemSelection.snapshotView(target, binding.conversationItemRecycler, messageRecord, videoBitmap)
|
||||
|
||||
// Read before anything is asked to hide, or the keyboard cannot be brought back on dismiss.
|
||||
val focusedView = if (container.isInputShowing || !container.isKeyboardShowing) null else itemView.rootView.findFocus()
|
||||
val bodyBubble = target.bubbleView
|
||||
val selectedConversationModel = SelectedConversationModel(
|
||||
bitmap = snapshot,
|
||||
itemX = itemView.x,
|
||||
itemY = itemView.y + binding.conversationItemRecycler.translationY,
|
||||
bubbleY = bodyBubble.y,
|
||||
bubbleWidth = bodyBubble.width,
|
||||
audioUri = audioUri,
|
||||
isOutgoing = messageRecord.isOutgoing,
|
||||
focusedView = focusedView,
|
||||
snapshotMetrics = target.getSnapshotStrategy()?.snapshotMetrics ?: InteractiveConversationElement.SnapshotMetrics(
|
||||
snapshotOffset = bodyBubble.x,
|
||||
contextMenuPadding = bodyBubble.x
|
||||
)
|
||||
)
|
||||
|
||||
bodyBubble.visibility = View.INVISIBLE
|
||||
target.reactionsView.visibility = View.INVISIBLE
|
||||
|
||||
val quotedIndicatorVisible = target.quotedIndicatorView?.visibility == View.VISIBLE
|
||||
if (quotedIndicatorVisible) {
|
||||
ViewUtil.fadeOut(target.quotedIndicatorView!!, 150, View.INVISIBLE)
|
||||
// The overlay sizes itself to the content area, so every keyboard has to be all the way
|
||||
// out before it measures. Mid-animation it has half a screen to fit the menu into.
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
container.hideAllAndAwaitSettled(composeText)
|
||||
showReactionOverlay(itemView, item, target, focusedView)
|
||||
}
|
||||
|
||||
container.hideKeyboard(composeText)
|
||||
|
||||
viewModel.setHideScrollButtonsForReactionOverlay(true)
|
||||
|
||||
val targetViews: InteractiveConversationElement = target
|
||||
handleReaction(
|
||||
item.conversationMessage,
|
||||
ReactionsToolbarListener(item.conversationMessage),
|
||||
selectedConversationModel,
|
||||
object : OnHideListener {
|
||||
override fun startHide(focusedView: View?) {
|
||||
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) || activity == null || activity?.isFinishing == true) {
|
||||
return
|
||||
}
|
||||
|
||||
multiselectItemDecoration.hideShade(binding.conversationItemRecycler)
|
||||
ViewUtil.fadeOut(binding.reactionsShade, resources.getInteger(R.integer.reaction_scrubber_hide_duration), View.GONE)
|
||||
|
||||
if (focusedView == composeText || searchMenuItem?.isActionViewExpanded == true) {
|
||||
container.showSoftkey(composeText)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onHide() {
|
||||
viewModel.setIsReactionDelegateShowing(false)
|
||||
|
||||
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) || activity == null || activity?.isFinishing == true) {
|
||||
return
|
||||
}
|
||||
|
||||
binding.conversationItemRecycler.suppressLayout(false)
|
||||
if (selectedConversationModel.audioUri != null) {
|
||||
getVoiceNoteMediaController().resumePlayback(selectedConversationModel.audioUri, messageRecord.id)
|
||||
}
|
||||
|
||||
clearFocusedItem()
|
||||
|
||||
if (mp4Holder != null) {
|
||||
mp4Holder.show()
|
||||
mp4Holder.resume()
|
||||
}
|
||||
|
||||
bodyBubble.visibility = View.VISIBLE
|
||||
targetViews.reactionsView.visibility = View.VISIBLE
|
||||
|
||||
if (quotedIndicatorVisible && targetViews.quotedIndicatorView != null) {
|
||||
ViewUtil.fadeIn(targetViews.quotedIndicatorView!!, 150)
|
||||
}
|
||||
|
||||
viewModel.setHideScrollButtonsForReactionOverlay(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else if (item.conversationMessage.isActiveCollapsedHead) {
|
||||
viewModel.onExpandEvents(item.conversationMessage.messageRecord.id)
|
||||
@@ -4239,6 +4146,127 @@ class ConversationFragment :
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshots [target] and hands it to the reaction overlay. Split out of [onItemLongClick] because
|
||||
* it runs once the keyboards are out of the way, which is not until a few frames later.
|
||||
*/
|
||||
private fun showReactionOverlay(
|
||||
itemView: View,
|
||||
item: MultiselectPart,
|
||||
target: InteractiveConversationElement,
|
||||
focusedView: View?
|
||||
) {
|
||||
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) || activity == null || activity?.isFinishing == true) {
|
||||
return
|
||||
}
|
||||
|
||||
val messageRecord = item.getMessageRecord()
|
||||
|
||||
// The wait gave the list room to move on, so the row may be gone or bound to another message.
|
||||
if (isActionModeStarted() || adapter.selectedItems.isNotEmpty() || target.conversationMessage.messageRecord.id != messageRecord.id) {
|
||||
return
|
||||
}
|
||||
|
||||
multiselectItemDecoration.setFocusedItem(MultiselectPart.Message(item.conversationMessage))
|
||||
binding.conversationItemRecycler.invalidateItemDecorations()
|
||||
binding.reactionsShade.visibility = View.VISIBLE
|
||||
binding.conversationItemRecycler.suppressLayout(true)
|
||||
|
||||
val audioUri = messageRecord.getAudioUriForLongClick()
|
||||
if (audioUri != null) {
|
||||
getVoiceNoteMediaController().pausePlayback(audioUri)
|
||||
}
|
||||
|
||||
val childAdapterPosition = target.getAdapterPosition(binding.conversationItemRecycler)
|
||||
var mp4Holder: GiphyMp4ProjectionPlayerHolder? = null
|
||||
var videoBitmap: Bitmap? = null
|
||||
if (childAdapterPosition != RecyclerView.NO_POSITION) {
|
||||
mp4Holder = giphyMp4ProjectionRecycler.getCurrentHolder(childAdapterPosition)
|
||||
if (mp4Holder?.isVisible == true) {
|
||||
mp4Holder.pause()
|
||||
videoBitmap = mp4Holder.bitmap
|
||||
mp4Holder.hide()
|
||||
}
|
||||
}
|
||||
|
||||
val snapshot = ConversationItemSelection.snapshotView(target, binding.conversationItemRecycler, messageRecord, videoBitmap)
|
||||
|
||||
val bodyBubble = target.bubbleView
|
||||
val selectedConversationModel = SelectedConversationModel(
|
||||
bitmap = snapshot,
|
||||
itemX = itemView.x,
|
||||
itemY = itemView.y + binding.conversationItemRecycler.translationY,
|
||||
bubbleY = bodyBubble.y,
|
||||
bubbleWidth = bodyBubble.width,
|
||||
audioUri = audioUri,
|
||||
isOutgoing = messageRecord.isOutgoing,
|
||||
focusedView = focusedView,
|
||||
snapshotMetrics = target.getSnapshotStrategy()?.snapshotMetrics ?: InteractiveConversationElement.SnapshotMetrics(
|
||||
snapshotOffset = bodyBubble.x,
|
||||
contextMenuPadding = bodyBubble.x
|
||||
)
|
||||
)
|
||||
|
||||
bodyBubble.visibility = View.INVISIBLE
|
||||
target.reactionsView.visibility = View.INVISIBLE
|
||||
|
||||
val quotedIndicatorVisible = target.quotedIndicatorView?.visibility == View.VISIBLE
|
||||
if (quotedIndicatorVisible) {
|
||||
ViewUtil.fadeOut(target.quotedIndicatorView!!, 150, View.INVISIBLE)
|
||||
}
|
||||
|
||||
viewModel.setHideScrollButtonsForReactionOverlay(true)
|
||||
|
||||
handleReaction(
|
||||
item.conversationMessage,
|
||||
ReactionsToolbarListener(item.conversationMessage),
|
||||
selectedConversationModel,
|
||||
object : OnHideListener {
|
||||
override fun startHide(focusedView: View?) {
|
||||
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) || activity == null || activity?.isFinishing == true) {
|
||||
return
|
||||
}
|
||||
|
||||
multiselectItemDecoration.hideShade(binding.conversationItemRecycler)
|
||||
ViewUtil.fadeOut(binding.reactionsShade, resources.getInteger(R.integer.reaction_scrubber_hide_duration), View.GONE)
|
||||
|
||||
if (focusedView == composeText || searchMenuItem?.isActionViewExpanded == true) {
|
||||
container.showSoftkey(composeText)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onHide() {
|
||||
viewModel.setIsReactionDelegateShowing(false)
|
||||
|
||||
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) || activity == null || activity?.isFinishing == true) {
|
||||
return
|
||||
}
|
||||
|
||||
binding.conversationItemRecycler.suppressLayout(false)
|
||||
if (selectedConversationModel.audioUri != null) {
|
||||
getVoiceNoteMediaController().resumePlayback(selectedConversationModel.audioUri, messageRecord.id)
|
||||
}
|
||||
|
||||
clearFocusedItem()
|
||||
|
||||
if (mp4Holder != null) {
|
||||
mp4Holder.show()
|
||||
mp4Holder.resume()
|
||||
}
|
||||
|
||||
bodyBubble.visibility = View.VISIBLE
|
||||
target.reactionsView.visibility = View.VISIBLE
|
||||
|
||||
if (quotedIndicatorVisible && target.quotedIndicatorView != null) {
|
||||
ViewUtil.fadeIn(target.quotedIndicatorView!!, 150)
|
||||
}
|
||||
|
||||
viewModel.setHideScrollButtonsForReactionOverlay(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) {
|
||||
GroupDescriptionDialog.show(childFragmentManager, groupName, description, shouldLinkifyWebLinks)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user