diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowRecipientSelectionFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowRecipientSelectionFragment.kt index 909d7271b5..e3a53742d2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowRecipientSelectionFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowRecipientSelectionFragment.kt @@ -4,7 +4,6 @@ import android.graphics.Color import android.os.Bundle import android.view.View import android.view.ViewGroup -import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.navigation.fragment.findNavController @@ -19,29 +18,25 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.SearchConfigur import org.thoughtcrime.securesms.database.RecipientTable import org.thoughtcrime.securesms.util.activityViewModel import org.thoughtcrime.securesms.util.navigation.safeNavigate -import org.thoughtcrime.securesms.util.viewModel -import kotlin.getValue /** * Allows the user to select a recipient to send a gift to. */ -class GiftFlowRecipientSelectionFragment : Fragment(R.layout.gift_flow_recipient_selection_fragment), MultiselectForwardFragment.Callback, SearchConfigurationProvider { +class GiftFlowRecipientSelectionFragment : Fragment(R.layout.multiselect_forward_activity), MultiselectForwardFragment.Callback, SearchConfigurationProvider { private val viewModel: GiftFlowViewModel by activityViewModel { GiftFlowViewModel() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val toolbar = view.findViewById(R.id.toolbar) - toolbar.setNavigationOnClickListener { requireActivity().onBackPressed() } - if (savedInstanceState == null) { childFragmentManager.beginTransaction() .replace( - R.id.multiselect_container, + R.id.fragment_container, MultiselectForwardFragment.create( MultiselectForwardFragmentArgs( multiShareArgs = emptyList(), + title = R.string.GiftFlowRecipientSelectionFragment__choose_recipient, forceDisableAddMessage = true, selectSingleRecipient = true ) @@ -79,6 +74,10 @@ class GiftFlowRecipientSelectionFragment : Fragment(R.layout.gift_flow_recipient override fun exitFlow() = Unit + override fun navigateUp() { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + override fun onSearchInputFocused() = Unit override fun setResult(bundle: Bundle) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt index 65867ab87e..1abb9a4e2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt @@ -94,6 +94,8 @@ class ContactSearchViewModel( val isDisplayingContextMenu: StateFlow = internalDisplayingContextMenu val scrollRequests: SharedFlow = internalScrollRequests + val query: StateFlow = rawQuery + init { viewModelScope.launch { rawQuery.drop(1).debounce(300).collect { query -> @@ -157,8 +159,6 @@ class ContactSearchViewModel( pagedData.value = PagedData.createForStateFlow(pagedDataSource, pagingConfig) } - fun getQuery(): String? = rawQuery.value - fun setQuery(query: String?) { rawQuery.value = query } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/RecipientSearchBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/RecipientSearchBar.kt index dcfe9eda2c..287a7eeacc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/RecipientSearchBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/RecipientSearchBar.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource @@ -50,7 +51,8 @@ fun RecipientSearchBar( onQueryChange: (String) -> Unit, onSearch: (String) -> Unit, modifier: Modifier = Modifier, - enabledKeyboardTypes: List = listOf(KeyboardType.Text, KeyboardType.Phone) + enabledKeyboardTypes: List = listOf(KeyboardType.Text, KeyboardType.Phone), + onFocusChanged: (Boolean) -> Unit = {} ) { val state = rememberSearchBarState() var keyboardType by remember(enabledKeyboardTypes) { mutableStateOf(enabledKeyboardTypes.first()) } @@ -67,6 +69,7 @@ fun RecipientSearchBar( TextField( value = query, onValueChange = onQueryChange, + modifier = Modifier.onFocusChanged { onFocusChanged(it.isFocused) }, placeholder = { Text(hint) }, singleLine = true, textStyle = TextStyle(textDirection = TextDirection.ContentOrLtr), diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt index d3b49512c7..1d2dcb1f4a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt @@ -5,7 +5,6 @@ import android.content.Intent import android.os.Bundle import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContract -import androidx.appcompat.widget.Toolbar import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment @@ -29,14 +28,6 @@ open class MultiselectForwardActivity : FragmentWrapperActivity(), MultiselectFo override val contentViewId: Int = R.layout.multiselect_forward_activity - override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { - super.onCreate(savedInstanceState, ready) - - val toolbar: Toolbar = findViewById(R.id.toolbar) - toolbar.setTitle(args.title) - toolbar.setNavigationOnClickListener { exitFlow() } - } - override fun getFragment(): Fragment { return MultiselectForwardFragment.create(args) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardBottomBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardBottomBar.kt new file mode 100644 index 0000000000..2a42ba1145 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardBottomBar.kt @@ -0,0 +1,303 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversation.mutiselect.forward + +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import org.signal.core.ui.compose.DayNightPreviews +import org.signal.core.ui.compose.IconButtons +import org.signal.core.ui.compose.Previews +import org.signal.core.ui.compose.TextFields +import org.signal.core.ui.compose.horizontalGutters +import org.signal.core.ui.isSplitPane +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.recipients.rememberRecipientField + +@Composable +fun MultiselectForwardBottomBar( + isSplitPane: Boolean, + state: MultiselectForwardBottomBarState, + events: (MultiselectForwardBottomBarEvent) -> Unit, + modifier: Modifier = Modifier +) { + val gutter = dimensionResource(org.signal.core.ui.R.dimen.gutter) + + Column( + modifier = modifier + ) { + HorizontalDivider( + modifier = Modifier.padding(bottom = 12.dp) + ) + + Row { + if (isSplitPane) { + Spacer(modifier = Modifier.weight(1f)) + } + + Column( + modifier = Modifier.weight(1f) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + val shouldDisplayAddMessageInThisRow = state.isSendButtonVisible && !state.isAddMessageVisible + Selection( + state, + modifier = Modifier + .weight(1f) + .heightIn(min = 44.dp) + .padding(end = if (shouldDisplayAddMessageInThisRow) 8.dp else gutter) + ) + + if (shouldDisplayAddMessageInThisRow) { + Send( + state = state, + events = events, + modifier = Modifier.padding(end = gutter) + ) + } + } + + if (state.isAddMessageVisible) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = spacedBy(8.dp), + modifier = Modifier + .heightIn(min = 44.dp) + .horizontalGutters() + ) { + AddMessage( + state = state, + events = events, + modifier = Modifier + .weight(1f) + .heightIn(min = 44.dp) + ) + + if (state.isSendButtonVisible) { + Send( + state = state, + events = events + ) + } + } + } + } + } + } +} + +@Composable +private fun Selection( + state: MultiselectForwardBottomBarState, + modifier: Modifier = Modifier +) { + LazyRow( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + itemsIndexed(items = state.selection, key = { _, contact -> contact.key }) { index, contact -> + val name by rememberDisplayName(contact) + + Text( + text = "$name${if (index != state.selection.lastIndex) ", " else " "}", + modifier = Modifier.padding(start = if (index == 0) dimensionResource(org.signal.core.ui.R.dimen.gutter) else 0.dp) + ) + } + } +} + +@Composable +private fun AddMessage( + state: MultiselectForwardBottomBarState, + events: (MultiselectForwardBottomBarEvent) -> Unit, + modifier: Modifier = Modifier +) { + TextFields.TextField( + value = state.message, + onValueChange = { events(MultiselectForwardBottomBarEvent.AddMessageUpdate(it)) }, + placeholder = { + Text(text = stringResource(R.string.MultiselectForwardFragment__add_a_message)) + }, + shape = RoundedCornerShape(50), + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent + ), + contentPadding = PaddingValues( + start = 16.dp, + end = 16.dp, + top = 10.dp, + bottom = 10.dp + ), + modifier = modifier + ) +} + +@Composable +private fun Send( + state: MultiselectForwardBottomBarState, + events: (MultiselectForwardBottomBarEvent) -> Unit, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + val background = remember(context) { state.sendButtonColors.background.resolve(context) } + val foreground = remember(context) { state.sendButtonColors.foreground.resolve(context) } + + IconButtons.IconButton( + enabled = state.isSendButtonEnabled, + onClick = { events(MultiselectForwardBottomBarEvent.SendClick) }, + modifier = modifier, + colors = IconButtons.iconButtonColors( + contentColor = Color(foreground), + containerColor = Color(background) + ) + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.symbol_send_fill_24), + contentDescription = stringResource(R.string.ShareActivity__send) + ) + } +} + +@Composable +private fun rememberDisplayName(contact: MultiselectForwardBottomBarState.SelectedContact): State = when (contact) { + is MultiselectForwardBottomBarState.SelectedContact.KnownRecipient -> { + val context = LocalContext.current + if (contact.recipient.isSelf) { + val noteToSelf = stringResource(R.string.note_to_self) + rememberUpdatedState(noteToSelf) + } else { + rememberRecipientField(contact.recipient) { getShortDisplayName(context) } + } + } + + is MultiselectForwardBottomBarState.SelectedContact.UnknownRecipient -> rememberUpdatedState(contact.e164) +} + +@DayNightPreviews +@Composable +private fun MultiselectForwardBottomBarPreview() { + Previews.Preview { + MultiselectForwardBottomBar( + isSplitPane = false, + state = rememberPreviewState(), + events = {}, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@DayNightPreviews +@Composable +private fun MultiselectForwardBottomBarPreviewWithSend() { + Previews.Preview { + MultiselectForwardBottomBar( + isSplitPane = false, + state = rememberPreviewState( + isSendButtonVisible = true + ), + events = {}, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@DayNightPreviews +@Composable +private fun MultiselectForwardBottomBarPreviewWithAddMessage() { + Previews.Preview { + MultiselectForwardBottomBar( + isSplitPane = false, + state = rememberPreviewState( + isAddMessageVisible = true + ), + events = {}, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@DayNightPreviews +@Composable +private fun MultiselectForwardBottomBarPreviewWithBoth() { + Previews.Preview { + MultiselectForwardBottomBar( + isSplitPane = false, + state = rememberPreviewState( + isSendButtonVisible = true, + isAddMessageVisible = true + ), + events = {}, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@DayNightPreviews +@Composable +private fun MultiselectForwardBottomBarPreviewWithSplit() { + Previews.Preview { + MultiselectForwardBottomBar( + isSplitPane = true, + state = rememberPreviewState( + isSendButtonVisible = true, + isAddMessageVisible = true + ), + events = {}, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@Composable +private fun rememberPreviewState( + isSendButtonVisible: Boolean = false, + isAddMessageVisible: Boolean = false +): MultiselectForwardBottomBarState { + return remember { + MultiselectForwardBottomBarState( + selection = listOf( + MultiselectForwardBottomBarState.SelectedContact.KnownRecipient(Recipient(id = RecipientId.from(1), isResolving = false, systemContactName = "Miles")), + MultiselectForwardBottomBarState.SelectedContact.KnownRecipient(Recipient(id = RecipientId.from(2), isResolving = false, systemContactName = "Peter")), + MultiselectForwardBottomBarState.SelectedContact.KnownRecipient(Recipient(id = RecipientId.from(3), isResolving = false, systemContactName = "May")) + ), + isSendButtonVisible = isSendButtonVisible, + isAddMessageVisible = isAddMessageVisible + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardBottomBarEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardBottomBarEvent.kt new file mode 100644 index 0000000000..fe15c8c90e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardBottomBarEvent.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversation.mutiselect.forward + +sealed interface MultiselectForwardBottomBarEvent { + data class AddMessageUpdate(val message: String) : MultiselectForwardBottomBarEvent + data object SendClick : MultiselectForwardBottomBarEvent +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardBottomBarState.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardBottomBarState.kt new file mode 100644 index 0000000000..395d12dfc8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardBottomBarState.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversation.mutiselect.forward + +import androidx.compose.runtime.annotation.RememberInComposition +import org.thoughtcrime.securesms.color.ViewColorSet +import org.thoughtcrime.securesms.recipients.Recipient + +data class MultiselectForwardBottomBarState @RememberInComposition constructor( + val selection: List = emptyList(), + val message: String = "", + val sendButtonColors: ViewColorSet = ViewColorSet.PRIMARY, + val isSendButtonEnabled: Boolean = true, + val isSendButtonVisible: Boolean = false, + val isAddMessageVisible: Boolean = false +) { + sealed interface SelectedContact { + + val key: String + + data class KnownRecipient(val recipient: Recipient) : SelectedContact { + override val key: String = recipient.id.toString() + } + + data class UnknownRecipient(val e164: String) : SelectedContact { + override val key: String = e164 + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt index a8331f8f30..ce8a84e6c4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt @@ -1,24 +1,38 @@ package org.thoughtcrime.securesms.conversation.mutiselect.forward -import android.content.res.ColorStateList -import android.graphics.Rect import android.os.Bundle import android.os.Handler import android.os.Looper +import android.view.Gravity import android.view.LayoutInflater -import android.view.TouchDelegate import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.animation.AnimationUtils -import android.widget.EditText +import android.widget.FrameLayout import android.widget.TextView import android.widget.Toast import androidx.annotation.PluralsRes import androidx.annotation.StringRes -import androidx.appcompat.widget.AppCompatImageView +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalResources +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.os.bundleOf -import androidx.core.view.ViewCompat -import androidx.core.view.doOnNextLayout import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment @@ -26,23 +40,25 @@ import androidx.fragment.app.FragmentManager import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import kotlinx.collections.immutable.persistentHashMapOf import kotlinx.coroutines.launch import org.signal.core.ui.BottomSheetUtil +import org.signal.core.ui.compose.ComposeFragment +import org.signal.core.ui.compose.LocalFragmentManager +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.core.ui.rememberIsSplitPane import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.getParcelableArrayListCompat import org.signal.core.util.getParcelableCompat import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.color.ViewColorSet -import org.thoughtcrime.securesms.components.ContactFilterView import org.thoughtcrime.securesms.components.TooltipPopup import org.thoughtcrime.securesms.components.WrapperDialogFragment -import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter import org.thoughtcrime.securesms.contacts.paged.ContactSearchCallbacks import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration import org.thoughtcrime.securesms.contacts.paged.ContactSearchError @@ -50,29 +66,27 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey import org.thoughtcrime.securesms.contacts.paged.ContactSearchPagedDataSourceRepository import org.thoughtcrime.securesms.contacts.paged.ContactSearchRepository import org.thoughtcrime.securesms.contacts.paged.ContactSearchState -import org.thoughtcrime.securesms.contacts.paged.ContactSearchView import org.thoughtcrime.securesms.contacts.paged.ContactSearchViewModel import org.thoughtcrime.securesms.database.RecipientTable import org.thoughtcrime.securesms.database.model.IdentityRecord import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseGroupStoryBottomSheet import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseStoryTypeBottomSheet +import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet import org.thoughtcrime.securesms.search.SearchRepository -import org.thoughtcrime.securesms.sharing.ShareSelectionAdapter -import org.thoughtcrime.securesms.sharing.ShareSelectionMappingModel import org.thoughtcrime.securesms.stories.GroupStoryEducationSheet import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.stories.Stories.getHeaderAction import org.thoughtcrime.securesms.stories.settings.create.CreateStoryFlowDialogFragment import org.thoughtcrime.securesms.stories.settings.create.CreateStoryWithViewersFragment import org.thoughtcrime.securesms.stories.settings.privacy.ChooseInitialMyStoryMembershipBottomSheetDialogFragment -import org.thoughtcrime.securesms.util.FullscreenHelper import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.fragments.findListener import org.thoughtcrime.securesms.util.fragments.requireListener +import org.thoughtcrime.securesms.util.viewModel import org.thoughtcrime.securesms.util.views.SimpleProgressDialog import org.thoughtcrime.securesms.util.visible @@ -91,38 +105,35 @@ import org.thoughtcrime.securesms.util.visible * It is up to the user of this fragment to handle the result accordingly utilizing a fragment result listener. */ class MultiselectForwardFragment : - Fragment(R.layout.multiselect_forward_fragment), + ComposeFragment(), SafetyNumberBottomSheet.Callbacks, ChooseStoryTypeBottomSheet.Callback, GroupStoryEducationSheet.Callback, WrapperDialogFragment.WrapperDialogFragmentCallback, ChooseInitialMyStoryMembershipBottomSheetDialogFragment.Callback { - private val viewModel: MultiselectForwardViewModel by viewModels(factoryProducer = this::createViewModelFactory) + private val viewModel: MultiselectForwardViewModel by viewModel { + MultiselectForwardViewModel(args) + } + private val contactSearchViewModel: ContactSearchViewModel by viewModels { ContactSearchViewModel.Factory( selectionLimits = RemoteConfig.shareSelectionLimit, isMultiSelect = !args.selectSingleRecipient, repository = ContactSearchRepository(), performSafetyNumberChecks = true, - arbitraryRepository = null, + arbitraryRepository = findListener()?.getArbitraryRepository(), searchRepository = SearchRepository(requireContext().getString(R.string.note_to_self)), contactSearchPagedDataSourceRepository = ContactSearchPagedDataSourceRepository(requireContext()) ) } private val disposables = LifecycleDisposable() - private lateinit var contactFilterView: ContactFilterView - private lateinit var addMessage: EditText - private lateinit var contactSearch: ContactSearchView - private lateinit var callback: Callback private var dismissibleDialog: SimpleProgressDialog.DismissibleDialog? = null private var handler: Handler? = null - private fun createViewModelFactory(): MultiselectForwardViewModel.Factory { - return MultiselectForwardViewModel.Factory(args.storySendRequirements, args.multiShareArgs, args.forceSelectionOnly, MultiselectForwardRepository()) - } + private var bottomBarHeightPx by mutableIntStateOf(0) private val args: MultiselectForwardFragmentArgs by lazy { requireArguments().getParcelableCompat(ARGS, MultiselectForwardFragmentArgs::class.java)!! @@ -136,105 +147,109 @@ class MultiselectForwardFragment : } } + @Composable + override fun FragmentContent() { + CompositionLocalProvider(LocalFragmentManager provides childFragmentManager) { + MultiselectForwardScreen( + isSplitPane = !args.isWrappedInBottomSheet && LocalResources.current.rememberIsSplitPane(), + args = args, + contactSearchViewModel = contactSearchViewModel, + callback = callback, + mapStateToConfiguration = this@MultiselectForwardFragment::getConfiguration, + contactSearchCallbacks = remember { SearchCallbacks() }, + additionalEntries = findListener()?.getAdditionalEntries() ?: persistentHashMapOf(), + bottomContentPadding = with(LocalDensity.current) { bottomBarHeightPx.toDp() } + ) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { Log.d(TAG, "onViewCreated()") view.minimumHeight = resources.displayMetrics.heightPixels - contactSearch = view.findViewById(R.id.contact_selection_list) - contactSearch.bind( - viewModel = contactSearchViewModel, - fragmentManager = childFragmentManager, - displayOptions = ContactSearchAdapter.DisplayOptions( - displayCheckBox = !args.selectSingleRecipient, - displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER, - displayStoryRing = true - ), - mapStateToConfiguration = this::getConfiguration, - callbacks = object : ContactSearchCallbacks.Simple() { - override fun onBeforeContactsSelected(view: View?, contactSearchKeys: Set): Set { - val filtered: Set = filterContacts(view, contactSearchKeys) - Log.d(TAG, "onBeforeContactsSelected() Attempting to select: ${contactSearchKeys.map { it.toString() }}, Filtered selection: ${filtered.map { it.toString() } }") - return filtered - } - } - ) - - callback = findListener()!! + callback = requireListener() disposables.bindTo(viewLifecycleOwner.lifecycle) - contactFilterView = view.findViewById(R.id.contact_filter_edit_text) - contactFilterView.visible = args.isSearchEnabled - - contactFilterView.setOnSearchInputFocusChangedListener { _, hasFocus -> - if (hasFocus) { - callback.onSearchInputFocused() - } - } - - contactFilterView.setOnFilterChangedListener { - contactSearchViewModel.setQuery(it) - } - val container = callback.getContainer() val title: TextView? = container.findViewById(R.id.title) - val bottomBarAndSpacer = LayoutInflater.from(requireContext()).inflate(R.layout.multiselect_forward_fragment_bottom_bar_and_spacer, container, false) - val bottomBar: ViewGroup = bottomBarAndSpacer.findViewById(R.id.bottom_bar) - val bottomBarSpacer: View = bottomBarAndSpacer.findViewById(R.id.bottom_bar_spacer) - val shareSelectionRecycler: RecyclerView = bottomBar.findViewById(R.id.selected_list) - val shareSelectionAdapter = ShareSelectionAdapter() - val sendButtonFrame: View = bottomBar.findViewById(R.id.share_confirm_frame) - val sendButton: AppCompatImageView = bottomBar.findViewById(R.id.share_confirm) - val backgroundHelper: View = bottomBar.findViewById(R.id.background_helper) + val bottomBar = ComposeView(requireContext()) + bottomBar.layoutParams = when (container) { + is CoordinatorLayout -> CoordinatorLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply { gravity = Gravity.BOTTOM } + is FrameLayout -> FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT, Gravity.BOTTOM) + else -> ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT) + } + bottomBar.setContent { + val state by viewModel.bottomBarState.collectAsStateWithLifecycle() + val imeController = LocalSoftwareKeyboardController.current + val isSplitPane = !args.isWrappedInBottomSheet && LocalResources.current.rememberIsSplitPane() - val sendButtonColors: ViewColorSet = args.sendButtonColors - sendButton.setColorFilter(sendButtonColors.foreground.resolve(requireContext())) - ViewCompat.setBackgroundTintList(sendButton, ColorStateList.valueOf(sendButtonColors.background.resolve(requireContext()))) - - FullscreenHelper.configureBottomBarLayout(requireActivity(), bottomBarSpacer, bottomBar) - bottomBar.setOnTouchListener { _, _ -> true } - - backgroundHelper.setBackgroundColor(callback.getDialogBackgroundColor()) - bottomBarSpacer.setBackgroundColor(callback.getDialogBackgroundColor()) - - title?.setText(args.title) - - addMessage = bottomBar.findViewById(R.id.add_message) - - sendButton.doOnNextLayout { - val rect = Rect() - sendButton.getHitRect(rect) - rect.top -= sendButtonFrame.paddingTop - rect.left -= sendButtonFrame.paddingStart - rect.right += sendButtonFrame.paddingEnd - rect.bottom += sendButtonFrame.paddingBottom - sendButtonFrame.touchDelegate = TouchDelegate(rect, sendButton) + SignalTheme { + Surface( + color = remember { Color(callback.getDialogBackgroundColor()) }, + // Swallow touches that miss the bar's children so they don't toggle a contact row behind it. + modifier = Modifier.pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + awaitPointerEvent().changes.forEach { it.consume() } + } + } + } + ) { + MultiselectForwardBottomBar( + state = state, + events = { + when (it) { + is MultiselectForwardBottomBarEvent.AddMessageUpdate -> { + viewModel.setMessage(it.message) + } + MultiselectForwardBottomBarEvent.SendClick -> { + imeController?.hide() + onSend() + } + } + }, + isSplitPane = isSplitPane, + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + ) + } + } } - sendButton.setOnClickListener { - ViewUtil.hideKeyboard(requireContext(), it) - onSend(it) + if (args.isWrappedInBottomSheet) { + title?.setText(args.title) } - sendButton.visible = !args.selectSingleRecipient - - shareSelectionRecycler.adapter = shareSelectionAdapter - bottomBar.visible = false - container.addView(bottomBarAndSpacer) + bottomBar.addOnLayoutChangeListener { _, _, top, _, bottom, _, _, _, _ -> + bottomBarHeightPx = if (bottomBar.isVisible) bottom - top else 0 + } + + container.addView(bottomBar) viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { contactSearchViewModel.selectionState.collect { contactSelection -> if (contactSelection.isNotEmpty() && args.selectSingleRecipient) { - onSend(sendButton) + onSend() return@collect } - shareSelectionAdapter.submitList(contactSelection.mapIndexed { index, key -> ShareSelectionMappingModel(key.requireShareContact(), index == 0) }) + viewModel.setShareSelection( + contactSelection.map { key -> + val contact = key.requireShareContact() + contact.recipientId + .map { MultiselectForwardBottomBarState.SelectedContact.KnownRecipient(Recipient.resolved(it)) } + .orElseGet { + MultiselectForwardBottomBarState.SelectedContact.UnknownRecipient(contact.number) + } + } + ) - addMessage.visible = !args.forceDisableAddMessage && contactSelection.any { key -> !key.requireRecipientSearchKey().isStory } && args.multiShareArgs.isNotEmpty() + viewModel.setAddMessageVisible(!args.forceDisableAddMessage && contactSelection.any { key -> !key.requireRecipientSearchKey().isStory } && args.multiShareArgs.isNotEmpty()) if (contactSelection.isNotEmpty() && !bottomBar.isVisible) { bottomBar.animation = AnimationUtils.loadAnimation(requireContext(), R.anim.slide_fade_from_bottom) @@ -245,6 +260,7 @@ class MultiselectForwardFragment : } else if (contactSelection.isEmpty() && bottomBar.isVisible) { bottomBar.animation = AnimationUtils.loadAnimation(requireContext(), R.anim.slide_fade_to_bottom) bottomBar.visible = false + bottomBarHeightPx = 0 } } } @@ -277,19 +293,20 @@ class MultiselectForwardFragment : dismissibleDialog?.dismiss() dismissibleDialog = SimpleProgressDialog.showDelayed(requireContext()) } + MultiselectForwardState.Stage.SomeFailed -> dismissWithSuccess(R.plurals.MultiselectForwardFragment_messages_sent) MultiselectForwardState.Stage.AllFailed -> dismissAndShowToast(R.plurals.MultiselectForwardFragment_messages_failed_to_send) MultiselectForwardState.Stage.Success -> dismissWithSuccess(R.plurals.MultiselectForwardFragment_messages_sent) is MultiselectForwardState.Stage.SelectionConfirmed -> dismissWithSelection(it.stage.selectedContacts) } - sendButton.isEnabled = it.stage == MultiselectForwardState.Stage.Selection + viewModel.setSendEnabled(it.stage == MultiselectForwardState.Stage.Selection) } setFragmentResultListener(CreateStoryWithViewersFragment.REQUEST_KEY) { _, bundle -> val recipientId: RecipientId = bundle.getParcelableCompat(CreateStoryWithViewersFragment.STORY_RECIPIENT, RecipientId::class.java)!! contactSearchViewModel.setKeysSelected(setOf(ContactSearchKey.RecipientSearchKey(recipientId, true))) - contactFilterView.clear() + contactSearchViewModel.setQuery(null) } setFragmentResultListener(ChooseGroupStoryBottomSheet.GROUP_STORY) { _, bundle -> @@ -297,7 +314,7 @@ class MultiselectForwardFragment : val keys: Set = groups.map { ContactSearchKey.RecipientSearchKey(it, true) }.toSet() contactSearchViewModel.addToVisibleGroupStories(keys) contactSearchViewModel.setKeysSelected(keys) - contactFilterView.clear() + contactSearchViewModel.setQuery(null) } } @@ -344,7 +361,7 @@ class MultiselectForwardFragment : .setMessage(R.string.MultiselectForwardFragment__forwarded_messages_are_now) .setPositiveButton(resources.getQuantityString(R.plurals.MultiselectForwardFragment_send_d_messages, messageCount, messageCount)) { d, _ -> d.dismiss() - viewModel.confirmFirstSend(addMessage.text.toString(), contactSearchViewModel.getSelectedContacts()) + viewModel.confirmFirstSend(contactSearchViewModel.getSelectedContacts()) } .setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() @@ -353,9 +370,9 @@ class MultiselectForwardFragment : .show() } - private fun onSend(sendButton: View) { - sendButton.isEnabled = false - viewModel.send(addMessage.text.toString(), contactSearchViewModel.getSelectedContacts()) + private fun onSend() { + viewModel.setSendEnabled(false) + viewModel.send(contactSearchViewModel.getSelectedContacts()) } private fun displaySafetyNumberConfirmation(identityRecords: List, selectedContacts: List) { @@ -387,7 +404,7 @@ class MultiselectForwardFragment : callback.exitFlow() } - private fun getMessageCount(): Int = args.multiShareArgs.size + if (addMessage.text.isNotEmpty()) 1 else 0 + private fun getMessageCount(): Int = args.multiShareArgs.size + if (viewModel.bottomBarState.value.message.isNotEmpty()) 1 else 0 private fun handleMessageExpired() { Log.d(TAG, "handleMessageExpired") @@ -413,7 +430,7 @@ class MultiselectForwardFragment : } override fun sendAnywayAfterSafetyNumberChangedInBottomSheet(destinations: List) { - viewModel.confirmSafetySend(addMessage.text.toString(), destinations.toSet()) + viewModel.confirmSafetySend(destinations.toSet()) } override fun onMessageResentAfterSafetyNumberChangeInBottomSheet() { @@ -439,9 +456,11 @@ class MultiselectForwardFragment : Stories.MediaTransform.SendRequirements.REQUIRES_CLIP -> { displayTooltip(view, R.string.MultiselectForwardFragment__videos_will_be_trimmed) } + Stories.MediaTransform.SendRequirements.CAN_NOT_SEND -> { displayTooltip(view, R.string.MultiselectForwardFragment__videos_sent_to_stories_cant) } + Stories.MediaTransform.SendRequirements.VALID_DURATION -> Unit } } @@ -551,6 +570,14 @@ class MultiselectForwardFragment : contactSearchViewModel.refresh() } + private inner class SearchCallbacks : ContactSearchCallbacks.Simple() { + override fun onBeforeContactsSelected(view: View?, contactSearchKeys: Set): Set { + val filtered: Set = filterContacts(view, contactSearchKeys) + Log.d(TAG, "onBeforeContactsSelected() Attempting to select: ${contactSearchKeys.map { it.toString() }}, Filtered selection: ${filtered.map { it.toString() }}") + return filtered + } + } + interface Callback { fun onFinishForwardAction() fun exitFlow() @@ -559,12 +586,16 @@ class MultiselectForwardFragment : fun getContainer(): ViewGroup fun getDialogBackgroundColor(): Int fun getStorySendRequirements(): Stories.MediaTransform.SendRequirements? = null + + /** + * Called when the user presses the navigation icon in the toolbar. Defaults to exitFlow(). + */ + fun navigateUp() = exitFlow() } companion object { private val TAG = Log.tag(MultiselectForwardActivity::class.java) - const val DIALOG_TITLE = "title" const val ARGS = "args" const val RESULT_KEY = "result_key" const val RESULT_SELECTION = "result_selection_recipients" @@ -592,7 +623,7 @@ class MultiselectForwardFragment : } private fun showDialogFragment(supportFragmentManager: FragmentManager, fragment: DialogFragment, multiselectForwardFragmentArgs: MultiselectForwardFragmentArgs) { - fragment.arguments = bundleOf(ARGS to multiselectForwardFragmentArgs, DIALOG_TITLE to multiselectForwardFragmentArgs.title) + fragment.arguments = bundleOf(ARGS to multiselectForwardFragmentArgs) fragment.show(supportFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt index e0ed84fb5b..fe2fd781f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt @@ -6,6 +6,7 @@ import android.os.Parcelable import androidx.annotation.ColorInt import androidx.annotation.StringRes import androidx.annotation.WorkerThread +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.signal.core.models.media.Media import org.signal.core.util.StreamUtil @@ -42,6 +43,7 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor( @StringRes val title: Int = R.string.MultiselectForwardFragment__forward_to, val forceDisableAddMessage: Boolean = false, val forceSelectionOnly: Boolean = false, + val forceHideToolbar: Boolean = false, val selectSingleRecipient: Boolean = false, val sendButtonColors: ViewColorSet = ViewColorSet.PRIMARY, val storySendRequirements: Stories.MediaTransform.SendRequirements = Stories.MediaTransform.SendRequirements.CAN_NOT_SEND, @@ -50,6 +52,9 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor( val isWrappedInBottomSheet: Boolean = false ) : Parcelable { + @IgnoredOnParcel + val isToolbarVisible: Boolean = !forceHideToolbar && !isWrappedInBottomSheet + fun withSendButtonTint(@ColorInt sendButtonTint: Int) = copy(sendButtonColors = ViewColorSet.forCustomColor(sendButtonTint)) companion object { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFullScreenDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFullScreenDialogFragment.kt index 54bbf4d76d..03c25acf2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFullScreenDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFullScreenDialogFragment.kt @@ -1,25 +1,32 @@ package org.thoughtcrime.securesms.conversation.mutiselect.forward import android.os.Bundle +import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat +import androidx.fragment.app.DialogFragment import androidx.fragment.app.setFragmentResult import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.FullScreenDialogFragment -import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment.Companion.DIALOG_TITLE import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.util.fragments.findListener -class MultiselectForwardFullScreenDialogFragment : FullScreenDialogFragment(), MultiselectForwardFragment.Callback { - override fun getTitle(): Int = requireArguments().getInt(DIALOG_TITLE) +class MultiselectForwardFullScreenDialogFragment : DialogFragment(), MultiselectForwardFragment.Callback { - override fun getDialogLayoutResource(): Int = R.layout.fragment_container + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen) + } override fun onFinishForwardAction() { findListener()?.onFinishForwardAction() } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.multiselect_forward_activity, container, false) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { if (savedInstanceState == null) { val fragment = MultiselectForwardFragment() @@ -40,7 +47,7 @@ class MultiselectForwardFullScreenDialogFragment : FullScreenDialogFragment(), M } override fun getContainer(): ViewGroup { - return requireView().findViewById(R.id.full_screen_dialog_content) as ViewGroup + return requireView().findViewById(R.id.fragment_container_wrapper)!! } override fun setResult(bundle: Bundle) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardRepository.kt index 8fed574719..fbd1a3b066 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardRepository.kt @@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.sharing.MultiShareSender import org.thoughtcrime.securesms.stories.Stories import java.util.Optional -class MultiselectForwardRepository { +object MultiselectForwardRepository { class MultiselectForwardResultHandlers( val onAllMessageSentSuccessfully: () -> Unit, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardScreen.kt new file mode 100644 index 0000000000..a8ba90b77d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardScreen.kt @@ -0,0 +1,178 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversation.mutiselect.forward + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.collections.immutable.persistentHashMapOf +import org.signal.core.ui.compose.SignalIcons +import org.signal.core.ui.compose.horizontalGutters +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.contacts.paged.ContactSearch +import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter +import org.thoughtcrime.securesms.contacts.paged.ContactSearchCallbacks +import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration +import org.thoughtcrime.securesms.contacts.paged.ContactSearchState +import org.thoughtcrime.securesms.contacts.paged.ContactSearchViewModel +import org.thoughtcrime.securesms.conversation.RecipientSearchBar +import org.thoughtcrime.securesms.util.adapter.mapping.compose.MappingEntryProvider + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MultiselectForwardScreen( + isSplitPane: Boolean, + args: MultiselectForwardFragmentArgs, + contactSearchViewModel: ContactSearchViewModel, + callback: MultiselectForwardFragment.Callback, + mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration, + additionalEntries: MappingEntryProvider = persistentHashMapOf(), + contactSearchCallbacks: ContactSearchCallbacks, + bottomContentPadding: Dp = 0.dp +) { + if (args.isWrappedInBottomSheet) { + MultiselectForwardContent( + isSplitPane = isSplitPane, + args = args, + contactSearchViewModel = contactSearchViewModel, + callback = callback, + mapStateToConfiguration = mapStateToConfiguration, + contactSearchCallbacks = contactSearchCallbacks, + additionalEntries = additionalEntries, + bottomContentPadding = bottomContentPadding + ) + } else { + Scaffold( + topBar = { + if (args.isToolbarVisible) { + TopAppBar( + title = { + if (!isSplitPane) { + Text(text = stringResource(args.title)) + } + }, + navigationIcon = { + IconButton( + onClick = { + callback.navigateUp() + } + ) { + Icon( + imageVector = SignalIcons.ArrowStart.imageVector, + contentDescription = stringResource(R.string.DSLSettingsToolbar__navigate_up) + ) + } + } + ) + } + } + ) { + MultiselectForwardContent( + isSplitPane = isSplitPane, + args = args, + contactSearchViewModel = contactSearchViewModel, + callback = callback, + mapStateToConfiguration = mapStateToConfiguration, + contactSearchCallbacks = contactSearchCallbacks, + modifier = Modifier.padding(it), + additionalEntries = additionalEntries, + bottomContentPadding = bottomContentPadding + ) + } + } +} + +@Composable +private fun MultiselectForwardContent( + isSplitPane: Boolean, + args: MultiselectForwardFragmentArgs, + contactSearchViewModel: ContactSearchViewModel, + callback: MultiselectForwardFragment.Callback, + mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration, + contactSearchCallbacks: ContactSearchCallbacks, + additionalEntries: MappingEntryProvider, + bottomContentPadding: Dp = 0.dp, + modifier: Modifier = Modifier +) { + Row(modifier = modifier) { + if (isSplitPane) { + Box( + modifier = Modifier + .weight(1f) + .horizontalGutters() + .padding(top = 4.dp) + ) { + Text( + text = stringResource(args.title), + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground + ) + } + } + + Column( + modifier = Modifier + .weight(1f) + .padding(bottom = bottomContentPadding) + ) { + if (args.isSearchEnabled) { + val query by contactSearchViewModel.query.collectAsStateWithLifecycle() + RecipientSearchBar( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + .horizontalGutters(), + query = query ?: "", + onQueryChange = { + contactSearchViewModel.setQuery(it) + }, + onSearch = { + contactSearchViewModel.setQuery(it) + }, + onFocusChanged = { + if (it) { + callback.onSearchInputFocused() + } + } + ) + } + + ContactSearch( + viewModel = contactSearchViewModel, + mapStateToConfiguration = mapStateToConfiguration, + modifier = Modifier + .fillMaxWidth() + .weight(1f), + additionalEntries = additionalEntries, + displayOptions = remember { + ContactSearchAdapter.DisplayOptions( + displayCheckBox = !args.selectSingleRecipient, + displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER, + displayStoryRing = true + ) + }, + callbacks = contactSearchCallbacks + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardViewModel.kt index a6dcbfe70c..652643d650 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardViewModel.kt @@ -2,38 +2,44 @@ package org.thoughtcrime.securesms.conversation.mutiselect.forward import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.mediasend.v2.UntrustedRecords -import org.thoughtcrime.securesms.sharing.MultiShareArgs -import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.util.livedata.Store class MultiselectForwardViewModel( - private val storySendRequirements: Stories.MediaTransform.SendRequirements, - private val records: List, - private val isSelectionOnly: Boolean, - private val repository: MultiselectForwardRepository, + private val args: MultiselectForwardFragmentArgs, private val identityChangesSince: Long = System.currentTimeMillis() ) : ViewModel() { private val store = Store( MultiselectForwardState( - storySendRequirements = storySendRequirements + storySendRequirements = args.storySendRequirements ) ) val state: LiveData = store.stateLiveData val snapshot: MultiselectForwardState get() = store.state + private val internalBottomBarState = MutableStateFlow( + MultiselectForwardBottomBarState( + sendButtonColors = args.sendButtonColors, + isSendButtonVisible = !args.selectSingleRecipient + ) + ) + + val bottomBarState: StateFlow = internalBottomBarState + private val disposables = CompositeDisposable() init { - if (records.isNotEmpty()) { - disposables += repository.checkAllSelectedMediaCanBeSentToStories(records).subscribe { sendRequirements -> + if (args.multiShareArgs.isNotEmpty()) { + disposables += MultiselectForwardRepository.checkAllSelectedMediaCanBeSentToStories(args.multiShareArgs).subscribe { sendRequirements -> store.update { it.copy(storySendRequirements = sendRequirements) } } } @@ -43,7 +49,23 @@ class MultiselectForwardViewModel( disposables.clear() } - fun send(additionalMessage: String, selectedContacts: Set) { + fun setSendEnabled(isSendEnabled: Boolean) { + internalBottomBarState.update { it.copy(isSendButtonEnabled = isSendEnabled) } + } + + fun setShareSelection(selection: List) { + internalBottomBarState.update { it.copy(selection = selection) } + } + + fun setAddMessageVisible(isAddMessageVisible: Boolean) { + internalBottomBarState.update { it.copy(isAddMessageVisible = isAddMessageVisible) } + } + + fun setMessage(message: String) { + internalBottomBarState.update { it.copy(message = message) } + } + + fun send(selectedContacts: Set) { if (SignalStore.tooltips.showMultiForwardDialog()) { SignalStore.tooltips.markMultiForwardDialogSeen() store.update { it.copy(stage = MultiselectForwardState.Stage.FirstConfirmation) } @@ -51,7 +73,7 @@ class MultiselectForwardViewModel( store.update { it.copy(stage = MultiselectForwardState.Stage.LoadingIdentities) } UntrustedRecords.checkForBadIdentityRecords(selectedContacts.filterIsInstance(ContactSearchKey.RecipientSearchKey::class.java).toSet(), identityChangesSince) { identityRecords -> if (identityRecords.isEmpty()) { - performSend(additionalMessage, selectedContacts) + performSend(selectedContacts) } else { store.update { state -> state.copy( @@ -66,26 +88,26 @@ class MultiselectForwardViewModel( } } - fun confirmFirstSend(additionalMessage: String, selectedContacts: Set) { - send(additionalMessage, selectedContacts) + fun confirmFirstSend(selectedContacts: Set) { + send(selectedContacts) } - fun confirmSafetySend(additionalMessage: String, selectedContacts: Set) { - send(additionalMessage, selectedContacts) + fun confirmSafetySend(selectedContacts: Set) { + send(selectedContacts) } fun cancelSend() { store.update { it.copy(stage = MultiselectForwardState.Stage.Selection) } } - private fun performSend(additionalMessage: String, selectedContacts: Set) { + private fun performSend(selectedContacts: Set) { store.update { it.copy(stage = MultiselectForwardState.Stage.SendPending) } - if (records.isEmpty() || isSelectionOnly) { + if (args.multiShareArgs.isEmpty() || args.forceSelectionOnly) { store.update { it.copy(stage = MultiselectForwardState.Stage.SelectionConfirmed(selectedContacts)) } } else { - repository.send( - additionalMessage = additionalMessage, - multiShareArgs = records, + MultiselectForwardRepository.send( + additionalMessage = bottomBarState.value.message, + multiShareArgs = args.multiShareArgs, shareContacts = selectedContacts, MultiselectForwardRepository.MultiselectForwardResultHandlers( onAllMessageSentSuccessfully = { store.update { it.copy(stage = MultiselectForwardState.Stage.Success) } }, @@ -95,15 +117,4 @@ class MultiselectForwardViewModel( ) } } - - class Factory( - private val storySendRequirements: Stories.MediaTransform.SendRequirements, - private val records: List, - private val isSelectionOnly: Boolean, - private val repository: MultiselectForwardRepository - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return requireNotNull(modelClass.cast(MultiselectForwardViewModel(storySendRequirements, records, isSelectionOnly, repository))) - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/SearchConfigurationProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/SearchConfigurationProvider.kt index a2a18830ac..74f1cfdb26 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/SearchConfigurationProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/SearchConfigurationProvider.kt @@ -1,9 +1,12 @@ package org.thoughtcrime.securesms.conversation.mutiselect.forward +import androidx.compose.runtime.Composable import androidx.fragment.app.FragmentManager +import kotlinx.collections.immutable.persistentHashMapOf import org.thoughtcrime.securesms.contacts.paged.ArbitraryRepository import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration import org.thoughtcrime.securesms.contacts.paged.ContactSearchState +import org.thoughtcrime.securesms.util.adapter.mapping.compose.MappingEntryProvider /** * Allows a parent of MultiselectForwardFragment to provide a custom search page configuration. @@ -21,4 +24,10 @@ interface SearchConfigurationProvider { * @return An ArbitraryRepository or null. Returning null will result in not being able to use the Arbitrary section, keys, or data. */ fun getArbitraryRepository(): ArbitraryRepository? = null + + /** + * @return a mapping of additional entries. Recommended to put your arbitrary stuff here. + */ + @Composable + fun getAdditionalEntries(): MappingEntryProvider = persistentHashMapOf() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index f36057b974..f112538975 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -431,7 +431,7 @@ public class ConversationListFragment extends MainFragment implements Conversati maybeScheduleRefreshProfileJob(); ConversationListFragmentExtensionsKt.listenToEventBusWhileResumed(this, mainNavigationViewModel.getDetailLocation()); - String query = contactSearchViewModel.getQuery(); + String query = contactSearchViewModel.getQuery().getValue(); if (query != null) { onSearchQueryUpdated(query); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/StoriesMultiselectForwardActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/StoriesMultiselectForwardActivity.kt index c90baa5fbd..127674ec41 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/StoriesMultiselectForwardActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/StoriesMultiselectForwardActivity.kt @@ -3,58 +3,39 @@ package org.thoughtcrime.securesms.mediasend.v2.stories import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Bundle import android.os.Parcelable -import android.view.ViewGroup -import android.widget.ImageView import androidx.activity.result.contract.ActivityResultContract +import androidx.compose.runtime.Composable import androidx.fragment.app.FragmentManager -import com.bumptech.glide.Glide import kotlinx.parcelize.Parcelize import org.signal.core.util.getParcelableArrayListExtraCompat -import org.signal.glide.decryptableuri.DecryptableUri -import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.contacts.paged.ArbitraryRepository import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration +import org.thoughtcrime.securesms.contacts.paged.ContactSearchData import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey import org.thoughtcrime.securesms.contacts.paged.ContactSearchState import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardActivity import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs import org.thoughtcrime.securesms.stories.Stories -import org.thoughtcrime.securesms.util.visible +import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel +import org.thoughtcrime.securesms.util.adapter.mapping.compose.MappingEntryProvider +import org.thoughtcrime.securesms.util.adapter.mapping.compose.rememberMappingEntryProvider class StoriesMultiselectForwardActivity : MultiselectForwardActivity() { companion object { private const val PREVIEW_MEDIA = "preview_media" - } - - override val contentViewId: Int = R.layout.stories_multiselect_forward_activity - - override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { - super.onCreate(savedInstanceState, ready) - - val preview1View: ImageView = findViewById(R.id.preview_media_1) - val preview2View: ImageView = findViewById(R.id.preview_media_2) - val previewMedia: List = intent.getParcelableArrayListExtraCompat(PREVIEW_MEDIA, Uri::class.java)!! - - preview1View.visible = previewMedia.isNotEmpty() - preview2View.visible = previewMedia.size > 1 - - if (previewMedia.isNotEmpty()) { - Glide.with(this) - .load(DecryptableUri(previewMedia.first())) - .into(preview1View) - } - - if (previewMedia.size > 1) { - Glide.with(this).load(DecryptableUri(previewMedia[1])).into(preview2View) - } + private const val PREVIEW_ITEM = "preview_item" } override fun getSearchConfiguration(fragmentManager: FragmentManager, contactSearchState: ContactSearchState): ContactSearchConfiguration? { return ContactSearchConfiguration.build { query = contactSearchState.query + addSection( + ContactSearchConfiguration.Section.Arbitrary(setOf(PREVIEW_ITEM)) + ) + addSection( ContactSearchConfiguration.Section.Stories( groupStories = contactSearchState.groupStories, @@ -65,9 +46,40 @@ class StoriesMultiselectForwardActivity : MultiselectForwardActivity() { } } - @Suppress("WrongViewCast") - override fun getContainer(): ViewGroup { - return findViewById(R.id.content) + private val previewMedia: List by lazy { intent.getParcelableArrayListExtraCompat(PREVIEW_MEDIA, Uri::class.java).orEmpty() } + + override fun getArbitraryRepository(): ArbitraryRepository { + return object : ArbitraryRepository { + override fun getSize(section: ContactSearchConfiguration.Section.Arbitrary, query: String?): Int = if (previewMedia.isEmpty()) 0 else 1 + + override fun getData( + section: ContactSearchConfiguration.Section.Arbitrary, + query: String?, + startIndex: Int, + endIndex: Int, + totalSearchSize: Int + ): List { + return if (previewMedia.isEmpty()) emptyList() else listOf(ContactSearchData.Arbitrary(PREVIEW_ITEM)) + } + + override fun getMappingModel(arbitrary: ContactSearchData.Arbitrary): MappingModel<*> { + return PreviewEntryMappingModel() + } + } + } + + @Composable + override fun getAdditionalEntries(): MappingEntryProvider { + return rememberMappingEntryProvider { + entry { + StoryMediaPreviews(previews = previewMedia) + } + } + } + + private class PreviewEntryMappingModel : MappingModel { + override fun areItemsTheSame(newItem: PreviewEntryMappingModel): Boolean = true + override fun areContentsTheSame(newItem: PreviewEntryMappingModel): Boolean = true } class SelectionContract : ActivityResultContract>() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/StoryMediaPreviews.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/StoryMediaPreviews.kt new file mode 100644 index 0000000000..4a7127c585 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/StoryMediaPreviews.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.mediasend.v2.stories + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.unit.dp +import org.signal.core.ui.compose.DayNightPreviews +import org.signal.core.ui.compose.Previews +import org.signal.glide.compose.GlideImage +import org.signal.glide.compose.GlideImageScaleType +import org.signal.glide.decryptableuri.DecryptableUri + +/** + * The stacked, overlapping story media thumbnails shown as the first item in the contact list in [StoriesMultiselectForwardActivity]. + * + * The first preview sits in front with a background-colored stroke; a second, if present, is tucked behind it, + * rotated and offset to the side. + */ +@Composable +fun StoryMediaPreviews( + previews: List, + modifier: Modifier = Modifier +) { + if (previews.isEmpty()) { + return + } + + Box( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 22.5.dp), + contentAlignment = Alignment.Center + ) { + if (previews.size > 1) { + Box( + modifier = Modifier + .size(width = 110.dp, height = 177.dp) + .offset(x = (-28).dp) + .rotate(-15f) + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant) + ) { + GlideImage( + model = remember { DecryptableUri(previews[1]) }, + scaleType = GlideImageScaleType.CENTER_CROP, + modifier = Modifier.fillMaxSize() + ) + } + } + + Box( + modifier = Modifier + .size(width = 120.dp, height = 215.dp) + .border(width = 3.dp, color = MaterialTheme.colorScheme.background, shape = RoundedCornerShape(12.dp)) + .padding(1.5.dp) + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant) + ) { + GlideImage( + model = remember { DecryptableUri(previews[0]) }, + scaleType = GlideImageScaleType.CENTER_CROP, + modifier = Modifier.fillMaxSize() + ) + } + } +} + +@DayNightPreviews +@Composable +private fun StoryMediaPreviewsPreview() { + Previews.Preview { + StoryMediaPreviews( + previews = listOf(Uri.EMPTY, Uri.EMPTY) + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareActivity.kt index 9580f5f440..c19b7a5f5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareActivity.kt @@ -15,7 +15,6 @@ import androidx.activity.viewModels import androidx.core.content.ContextCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.text.buildSpannedString -import com.google.android.material.appbar.MaterialToolbar import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.kotlin.subscribeBy import org.signal.core.models.media.Media @@ -35,7 +34,6 @@ import org.thoughtcrime.securesms.conversation.ConversationIntents import org.thoughtcrime.securesms.conversation.MessageSendType import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs -import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFullScreenDialogFragment import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity.Companion.share import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.sharing.MultiShareDialogs @@ -44,9 +42,7 @@ import org.thoughtcrime.securesms.sharing.MultiShareSender.MultiShareSendResultC import org.thoughtcrime.securesms.sharing.interstitial.ShareInterstitialActivity import org.thoughtcrime.securesms.util.ConversationUtil import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme -import org.thoughtcrime.securesms.util.visible import java.util.concurrent.TimeUnit -import org.signal.core.ui.R as CoreUiR class ShareActivity : PassphraseRequiredActivity(), MultiselectForwardFragment.Callback { @@ -86,7 +82,7 @@ class ShareActivity : PassphraseRequiredActivity(), MultiselectForwardFragment.C } override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { - setContentView(R.layout.share_activity_v2) + setContentView(R.layout.multiselect_forward_activity) val isIntentValid = getUnresolvedShareData().either( onSuccess = { @@ -110,16 +106,6 @@ class ShareActivity : PassphraseRequiredActivity(), MultiselectForwardFragment.C } } - val toolbar = findViewById(R.id.toolbar) - - if (intent?.getBooleanExtra(EXTRA_NAVIGATION, false) == true) { - toolbar.setTitle(getTitleFromExtras()) - toolbar.setNavigationIcon(CoreUiR.drawable.symbol_arrow_start_24) - toolbar.setNavigationOnClickListener { finish() } - } else { - toolbar.visible = false - } - lifecycleDisposable.bindTo(this) lifecycleDisposable += viewModel.events.subscribe { shareEvent -> when (shareEvent) { @@ -193,7 +179,7 @@ class ShareActivity : PassphraseRequiredActivity(), MultiselectForwardFragment.C viewModel.onContactSelectionConfirmed(contactSearchKeys) } - override fun getContainer(): ViewGroup = findViewById(R.id.container) + override fun getContainer(): ViewGroup = findViewById(R.id.fragment_container_wrapper) override fun getDialogBackgroundColor(): Int = ContextCompat.getColor(this, R.color.signal_background_primary) @@ -255,7 +241,9 @@ class ShareActivity : PassphraseRequiredActivity(), MultiselectForwardFragment.C } private fun ensureFragment(resolvedShareData: ResolvedShareData) { - if (!supportFragmentManager.isStateSaved && supportFragmentManager.fragments.none { it is MultiselectForwardFullScreenDialogFragment }) { + val hasToolbar = intent?.getBooleanExtra(EXTRA_NAVIGATION, false) == true + + if (!supportFragmentManager.isStateSaved && supportFragmentManager.fragments.none { it is MultiselectForwardFragment }) { supportFragmentManager.beginTransaction() .replace( R.id.fragment_container, @@ -264,7 +252,8 @@ class ShareActivity : PassphraseRequiredActivity(), MultiselectForwardFragment.C multiShareArgs = listOf(resolvedShareData.toMultiShareArgs()), title = getTitleFromExtras(), forceDisableAddMessage = true, - forceSelectionOnly = true + forceSelectionOnly = true, + forceHideToolbar = !hasToolbar ) ) ).commitNow() diff --git a/app/src/main/res/layout/gift_flow_recipient_selection_fragment.xml b/app/src/main/res/layout/gift_flow_recipient_selection_fragment.xml deleted file mode 100644 index 4c012078fe..0000000000 --- a/app/src/main/res/layout/gift_flow_recipient_selection_fragment.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/multiselect_forward_activity.xml b/app/src/main/res/layout/multiselect_forward_activity.xml index 6f721e136b..ffe0461452 100644 --- a/app/src/main/res/layout/multiselect_forward_activity.xml +++ b/app/src/main/res/layout/multiselect_forward_activity.xml @@ -1,6 +1,5 @@ - - - - \ No newline at end of file + android:layout_height="match_parent" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/multiselect_forward_fragment.xml b/app/src/main/res/layout/multiselect_forward_fragment.xml deleted file mode 100644 index ea7f312ed7..0000000000 --- a/app/src/main/res/layout/multiselect_forward_fragment.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/multiselect_forward_fragment_bottom_bar.xml b/app/src/main/res/layout/multiselect_forward_fragment_bottom_bar.xml deleted file mode 100644 index fbbcd9c370..0000000000 --- a/app/src/main/res/layout/multiselect_forward_fragment_bottom_bar.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/multiselect_forward_fragment_bottom_bar_and_spacer.xml b/app/src/main/res/layout/multiselect_forward_fragment_bottom_bar_and_spacer.xml deleted file mode 100644 index 2026955038..0000000000 --- a/app/src/main/res/layout/multiselect_forward_fragment_bottom_bar_and_spacer.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/share_activity_v2.xml b/app/src/main/res/layout/share_activity_v2.xml deleted file mode 100644 index 7441e7a1f4..0000000000 --- a/app/src/main/res/layout/share_activity_v2.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/stories_multiselect_forward_activity.xml b/app/src/main/res/layout/stories_multiselect_forward_activity.xml deleted file mode 100644 index ff2634db63..0000000000 --- a/app/src/main/res/layout/stories_multiselect_forward_activity.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - -