mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-19 08:09:12 +01:00
Prepare conversation fragment navigation for two-pane conversation settings.
This commit is contained in:
committed by
Cody Henthorne
parent
78d3db319c
commit
72cbe61f6c
@@ -118,6 +118,7 @@ import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.conversation.NewConversationActivity
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationFragment
|
||||
import org.thoughtcrime.securesms.conversation.v2.MotionEventRelay
|
||||
import org.thoughtcrime.securesms.conversation.v2.ShareDataTimestampViewModel
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment
|
||||
@@ -192,7 +193,15 @@ import org.thoughtcrime.securesms.window.rememberThreePaneScaffoldNavigatorDeleg
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider, Material3OnScrollHelperBinder, ConversationListFragment.Callback, CallLogFragment.Callback, GooglePayComponent {
|
||||
class MainActivity :
|
||||
PassphraseRequiredActivity(),
|
||||
VoiceNoteMediaControllerOwner,
|
||||
MainNavigator.NavigatorProvider,
|
||||
Material3OnScrollHelperBinder,
|
||||
ConversationListFragment.Callback,
|
||||
ConversationFragment.NavigationHost,
|
||||
CallLogFragment.Callback,
|
||||
GooglePayComponent {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(MainActivity::class)
|
||||
@@ -496,6 +505,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
}
|
||||
|
||||
is MainNavigationDetailLocation.Calls -> callsNavHostController.navigateToDetailLocation(location)
|
||||
|
||||
is MainNavigationDetailLocation.Stories -> storiesNavHostController.navigateToDetailLocation(location)
|
||||
}
|
||||
}
|
||||
@@ -1276,4 +1286,8 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun navigateTo(location: MainNavigationDetailLocation) {
|
||||
mainNavigationViewModel.goTo(location)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.components.settings.conversation
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
|
||||
class CallInfoActivity : ConversationSettingsActivity(), ConversationSettingsFragment.Callback {
|
||||
class CallInfoActivity : ConversationSettingsActivity(), ConversationSettingsFragment.TransitionCallback {
|
||||
|
||||
override val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme()
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.util.Pair
|
||||
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
|
||||
@@ -17,7 +16,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.DynamicConversationSettingsTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
|
||||
open class ConversationSettingsActivity : DSLSettingsActivity(), ConversationSettingsFragment.Callback {
|
||||
open class ConversationSettingsActivity : DSLSettingsActivity(), ConversationSettingsFragment.TransitionCallback {
|
||||
|
||||
override val dynamicTheme: DynamicTheme = DynamicConversationSettingsTheme()
|
||||
|
||||
@@ -27,7 +26,7 @@ open class ConversationSettingsActivity : DSLSettingsActivity(), ConversationSet
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
}
|
||||
|
||||
override fun onContentWillRender() {
|
||||
override fun onReadyForEnterTransition() {
|
||||
ActivityCompat.startPostponedEnterTransition(this)
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,11 @@ private const val REQUEST_CODE_ADD_CONTACT = 2
|
||||
private const val REQUEST_CODE_ADD_MEMBERS_TO_GROUP = 3
|
||||
private const val REQUEST_CODE_RETURN_FROM_MEDIA = 4
|
||||
|
||||
/**
|
||||
* Settings screen for a conversation.
|
||||
*
|
||||
* Hosts that want shared element enter transitions should implement [TransitionCallback].
|
||||
*/
|
||||
class ConversationSettingsFragment :
|
||||
DSLSettingsFragment(
|
||||
layoutId = R.layout.conversation_settings_fragment,
|
||||
@@ -156,7 +161,7 @@ class ConversationSettingsFragment :
|
||||
}
|
||||
)
|
||||
|
||||
private lateinit var callback: Callback
|
||||
private var transitionCallback: TransitionCallback? = null
|
||||
|
||||
private lateinit var toolbar: Toolbar
|
||||
private lateinit var toolbarAvatarContainer: FrameLayout
|
||||
@@ -172,8 +177,7 @@ class ConversationSettingsFragment :
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
callback = context as Callback
|
||||
transitionCallback = context as? TransitionCallback
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -300,7 +304,7 @@ class ConversationSettingsFragment :
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList()) {
|
||||
if (state.isLoaded) {
|
||||
(view?.parent as? ViewGroup)?.doOnPreDraw {
|
||||
callback.onContentWillRender()
|
||||
transitionCallback?.onReadyForEnterTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1136,7 +1140,13 @@ class ConversationSettingsFragment :
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onContentWillRender()
|
||||
/**
|
||||
* Implemented by hosts that postpone enter transitions (for example, shared element flows).
|
||||
*
|
||||
* Called when this fragment has loaded enough UI state to safely run the postponed enter
|
||||
* transition.
|
||||
*/
|
||||
interface TransitionCallback {
|
||||
fun onReadyForEnterTransition()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,6 +564,8 @@ class ConversationFragment :
|
||||
private lateinit var conversationItemDecorations: ConversationItemDecorations
|
||||
private lateinit var optionsMenuCallback: ConversationOptionsMenuCallback
|
||||
|
||||
private var navigationHost: NavigationHost? = null
|
||||
|
||||
private var animationsAllowed = false
|
||||
private var pinnedShortcutReceiver: BroadcastReceiver? = null
|
||||
private var searchMenuItem: MenuItem? = null
|
||||
@@ -636,6 +638,11 @@ class ConversationFragment :
|
||||
|
||||
//region Android Lifecycle
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
navigationHost = context as? NavigationHost
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
SignalLocalMetrics.ConversationOpen.start()
|
||||
@@ -2904,11 +2911,7 @@ class ConversationFragment :
|
||||
|
||||
private fun handleDisplayDetails(conversationMessage: ConversationMessage) {
|
||||
val recipientSnapshot = viewModel.recipientSnapshot ?: return
|
||||
if (requireActivity() is MainActivity) {
|
||||
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Chats.MessageDetails(recipientSnapshot.id, conversationMessage.messageRecord.id))
|
||||
} else {
|
||||
MessageDetailsFragment.create(conversationMessage.messageRecord, recipientSnapshot.id).show(requireActivity().supportFragmentManager, MESSAGE_DETAILS_TAG)
|
||||
}
|
||||
navigateTo(MainNavigationDetailLocation.Chats.MessageDetails(recipientSnapshot.id, MessageId(conversationMessage.messageRecord.id)))
|
||||
}
|
||||
|
||||
private fun handleDeleteMessages(messageParts: Set<MultiselectPart>) {
|
||||
@@ -3436,10 +3439,8 @@ class ConversationFragment :
|
||||
.show(childFragmentManager)
|
||||
} else if (messageRecord.hasFailedWithNetworkFailures()) {
|
||||
ConversationDialogs.displayMessageCouldNotBeSentDialog(requireContext(), messageRecord)
|
||||
} else if (requireActivity() is MainActivity) {
|
||||
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Chats.MessageDetails(recipientId, messageRecord.id))
|
||||
} else {
|
||||
MessageDetailsFragment.create(messageRecord, recipientId).show(requireActivity().supportFragmentManager, MESSAGE_DETAILS_TAG)
|
||||
navigateTo(MainNavigationDetailLocation.Chats.MessageDetails(recipientId, MessageId(messageRecord.id)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4059,15 +4060,9 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
override fun handleManageGroup() {
|
||||
val recipient = viewModel.recipientSnapshot ?: return
|
||||
val intent = ConversationSettingsActivity.forGroup(requireContext(), recipient.requireGroupId())
|
||||
val bundle = ConversationSettingsActivity.createTransitionBundle(
|
||||
requireContext(),
|
||||
binding.conversationTitleView.root.findViewById(R.id.contact_photo_image),
|
||||
binding.toolbar
|
||||
)
|
||||
|
||||
requireActivity().startActivity(intent, bundle)
|
||||
viewModel.recipientSnapshot?.let { recipient ->
|
||||
navigateToConversationSettingsStandalone(recipient)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleLeavePushGroup() {
|
||||
@@ -4101,24 +4096,11 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
override fun handleConversationSettings() {
|
||||
val recipient = viewModel.recipientSnapshot ?: return
|
||||
if (recipient.isGroup) {
|
||||
handleManageGroup()
|
||||
return
|
||||
viewModel.recipientSnapshot?.let { recipient ->
|
||||
if (!viewModel.hasMessageRequestState || recipient.isBlocked) {
|
||||
navigateToConversationSettingsStandalone(recipient)
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel.hasMessageRequestState && !recipient.isBlocked) {
|
||||
return
|
||||
}
|
||||
|
||||
val intent = ConversationSettingsActivity.forRecipient(requireContext(), recipient.id)
|
||||
val bundle = ConversationSettingsActivity.createTransitionBundle(
|
||||
requireActivity(),
|
||||
binding.conversationTitleView.root.findViewById(R.id.contact_photo_image),
|
||||
binding.toolbar
|
||||
)
|
||||
|
||||
requireActivity().startActivity(intent, bundle)
|
||||
}
|
||||
|
||||
override fun handleSelectMessageExpiration() {
|
||||
@@ -4173,6 +4155,49 @@ class ConversationFragment :
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes to the appropriate destination based on the current window configuration.
|
||||
*
|
||||
* In split-pane mode, delegates to the [NavigationHost] to display content in the detail pane. Otherwise, opens the destination as a standalone screen.
|
||||
*/
|
||||
private fun navigateTo(location: MainNavigationDetailLocation.Chats) {
|
||||
val host = navigationHost
|
||||
if (host != null && resources.getWindowSizeClass().isSplitPane()) {
|
||||
host.navigateTo(location)
|
||||
} else {
|
||||
when (location) {
|
||||
is MainNavigationDetailLocation.Chats.MessageDetails -> navigateToMessageDetailsStandalone(location)
|
||||
is MainNavigationDetailLocation.Chats.Conversation -> error("ConversationFragment shouldn't navigate to another conversation - use the main navigation infrastructure instead.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens message details as a standalone (single-pane) screen. Use [navigateTo] as the entry point.
|
||||
*/
|
||||
private fun navigateToMessageDetailsStandalone(location: MainNavigationDetailLocation.Chats.MessageDetails) {
|
||||
MessageDetailsFragment.create(location.messageId, location.recipientId)
|
||||
.show(requireActivity().supportFragmentManager, MESSAGE_DETAILS_TAG)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens conversation settings as a standalone (single-pane) screen.
|
||||
*/
|
||||
private fun navigateToConversationSettingsStandalone(recipient: Recipient) {
|
||||
val intent = if (recipient.isPushGroup) {
|
||||
ConversationSettingsActivity.forGroup(requireContext(), recipient.requireGroupId())
|
||||
} else {
|
||||
ConversationSettingsActivity.forRecipient(requireContext(), recipient.id)
|
||||
}
|
||||
|
||||
val bundle = ConversationSettingsActivity.createTransitionBundle(
|
||||
requireActivity(),
|
||||
binding.conversationTitleView.root.findViewById(R.id.contact_photo_image),
|
||||
binding.toolbar
|
||||
)
|
||||
requireActivity().startActivity(intent, bundle)
|
||||
}
|
||||
|
||||
private inner class OnReactionsSelectedListener : ConversationReactionOverlay.OnReactionSelectedListener {
|
||||
override fun onReactionSelected(messageRecord: MessageRecord, emoji: String?) {
|
||||
reactionDelegate.hide()
|
||||
@@ -5121,4 +5146,11 @@ class ConversationFragment :
|
||||
override fun onDoubleTapEditEducationSheetNext(conversationMessage: ConversationMessage) {
|
||||
handleEditMessage(conversationMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional hook for host activity to intercept and handle detail navigation, used by split-pane layouts.
|
||||
*/
|
||||
interface NavigationHost {
|
||||
fun navigateTo(location: MainNavigationDetailLocation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the primary key in a [MessageId].
|
||||
*/
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class MessageId(
|
||||
val id: Long
|
||||
@@ -14,6 +17,13 @@ data class MessageId(
|
||||
return "$id|true"
|
||||
}
|
||||
|
||||
class NavType : androidx.navigation.NavType<MessageId>(false) {
|
||||
override fun get(bundle: Bundle, key: String): MessageId? = bundle.getLong(key, -1).takeIf { it >= 0 }?.let { MessageId(it) }
|
||||
override fun parseValue(value: String): MessageId = MessageId(value.toLong())
|
||||
override fun put(bundle: Bundle, key: String, value: MessageId) = bundle.putLong(key, value.id)
|
||||
override fun serializeAsValue(value: MessageId): String = value.id.toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Returns null for invalid IDs. Useful when pulling a possibly-unset ID from a database, or something like that.
|
||||
|
||||
@@ -52,6 +52,7 @@ import org.thoughtcrime.securesms.compose.FragmentBackPressedState
|
||||
import org.thoughtcrime.securesms.conversation.ConversationArgs
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationFragment
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.serialization.JsonSerializableNavType
|
||||
@@ -145,7 +146,8 @@ fun NavGraphBuilder.chatNavGraphBuilder(
|
||||
|
||||
composable<MainNavigationDetailLocation.Chats.MessageDetails>(
|
||||
typeMap = mapOf(
|
||||
typeOf<RecipientId>() to JsonSerializableNavType(RecipientId.serializer())
|
||||
typeOf<RecipientId>() to JsonSerializableNavType(RecipientId.serializer()),
|
||||
typeOf<MessageId>() to MessageId.NavType()
|
||||
)
|
||||
) { navBackStackEntry ->
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -14,6 +14,7 @@ import kotlinx.serialization.Transient
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogRow
|
||||
import org.thoughtcrime.securesms.conversation.ConversationArgs
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
|
||||
@@ -67,7 +68,7 @@ sealed class MainNavigationDetailLocation : Parcelable {
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MessageDetails(val recipientId: RecipientId, val messageId: Long) : Chats() {
|
||||
data class MessageDetails(val recipientId: RecipientId, val messageId: MessageId) : Chats() {
|
||||
@Transient
|
||||
@IgnoredOnParcel
|
||||
override val controllerKey: RecipientId = recipientId
|
||||
|
||||
@@ -15,7 +15,9 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestManager
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.requireParcelableCompat
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.WrapperDialogFragment
|
||||
@@ -30,6 +32,7 @@ import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
import org.thoughtcrime.securesms.conversation.ui.edit.EditMessageHistoryDialog.Companion.show
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackController
|
||||
@@ -92,9 +95,9 @@ class MessageDetailsFragment : Fragment(), MessageDetailsAdapter.Callbacks {
|
||||
}
|
||||
|
||||
private fun initializeViewModel() {
|
||||
val recipientId = requireArguments().getParcelable<RecipientId>(RECIPIENT_EXTRA)
|
||||
val messageId = requireArguments().getLong(MESSAGE_ID_EXTRA, -1)
|
||||
val factory = MessageDetailsViewModel.Factory(recipientId, messageId)
|
||||
val recipientId = requireArguments().getParcelableCompat(RECIPIENT_EXTRA, RecipientId::class.java)
|
||||
val messageId = requireArguments().requireParcelableCompat(MESSAGE_ID_EXTRA, MessageId::class.java)
|
||||
val factory = MessageDetailsViewModel.Factory(recipientId, messageId.id)
|
||||
|
||||
viewModel = ViewModelProvider(this, factory)[MessageDetailsViewModel::class.java]
|
||||
viewModel.messageDetails.observe(viewLifecycleOwner) { details: MessageDetails? ->
|
||||
@@ -427,16 +430,16 @@ class MessageDetailsFragment : Fragment(), MessageDetailsAdapter.Callbacks {
|
||||
private const val MESSAGE_ID_EXTRA = "message_id"
|
||||
private const val RECIPIENT_EXTRA = "recipient_id"
|
||||
|
||||
fun args(recipientId: RecipientId, messageId: Long): Bundle {
|
||||
fun args(recipientId: RecipientId, messageId: MessageId): Bundle {
|
||||
return bundleOf(
|
||||
MESSAGE_ID_EXTRA to messageId,
|
||||
RECIPIENT_EXTRA to recipientId
|
||||
)
|
||||
}
|
||||
|
||||
fun create(message: MessageRecord, recipientId: RecipientId): Dialog {
|
||||
fun create(messageId: MessageId, recipientId: RecipientId): Dialog {
|
||||
return Dialog().apply {
|
||||
arguments = args(recipientId, message.id)
|
||||
arguments = args(recipientId, messageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user