diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt index 0df53ef53c..8aba01322b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt @@ -16,6 +16,7 @@ import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import android.widget.Toast +import androidx.activity.OnBackPressedCallback import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -250,6 +251,22 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner } } + val callback = object : OnBackPressedCallback(toolbarViewModel.state.value.mode == MainToolbarMode.ACTION_MODE) { + override fun handleOnBackPressed() { + toolbarCallback.onCloseActionModeClick() + } + } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + toolbarViewModel.state.collect { state -> + callback.isEnabled = state.mode == MainToolbarMode.ACTION_MODE + } + } + } + + onBackPressedDispatcher.addCallback(this, callback) + shareDataTimestampViewModel.setTimestampFromActivityCreation(savedInstanceState, intent) setContent { @@ -810,6 +827,15 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner toolbarViewModel.emitEvent(MainToolbarViewModel.Event.Chats.CloseArchive) } + override fun onCloseActionModeClick() { + supportFragmentManager.fragments.forEach { fragment -> + when (fragment) { + is ConversationListFragment -> fragment.endActionModeIfActive() + is CallLogFragment -> fragment.CallLogActionModeCallback().onActionModeWillEnd() + } + } + } + override fun onSearchQueryUpdated(query: String) { toolbarViewModel.setSearchQuery(query) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogActionMode.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogActionMode.kt index 8dc9eb1e89..4e5dc99a3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogActionMode.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogActionMode.kt @@ -1,72 +1,37 @@ package org.thoughtcrime.securesms.calls.log import android.content.res.Resources -import android.view.Menu -import android.view.MenuItem -import androidx.appcompat.view.ActionMode -import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.main.MainToolbarViewModel class CallLogActionMode( - private val callback: Callback -) : ActionMode.Callback { + private val callback: Callback, + private val mainToolbarViewModel: MainToolbarViewModel +) { - private var actionMode: ActionMode? = null private var count: Int = 0 - override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { - mode?.title = getTitle(1) - return true - } - - override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { - return false - } - - override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { - return true - } - - override fun onDestroyActionMode(mode: ActionMode?) { - callback.onResetSelectionState() - endIfActive() - } - - fun isInActionMode(): Boolean { - return actionMode != null - } - fun getCount(): Int { - return if (actionMode != null) count else 0 + return if (mainToolbarViewModel.isInActionMode()) count else 0 } fun setCount(count: Int) { this.count = count - actionMode?.title = getTitle(count) + mainToolbarViewModel.setActionModeCount(count) } fun start() { - actionMode = callback.startActionMode(this) + callback.startActionMode() } fun end() { - callback.onActionModeWillEnd() - actionMode?.finish() - count = 0 - actionMode = null - } - - private fun getTitle(callLogsSelected: Int): String { - return callback.getResources().getQuantityString(R.plurals.ConversationListFragment_s_selected, callLogsSelected, callLogsSelected) - } - - private fun endIfActive() { - if (actionMode != null) { - end() + if (mainToolbarViewModel.isInActionMode()) { + callback.onActionModeWillEnd() + count = 0 } } interface Callback { - fun startActionMode(callback: ActionMode.Callback): ActionMode? + fun startActionMode() fun onActionModeWillEnd() fun getResources(): Resources fun onResetSelectionState() diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt index d709793bef..9f5b8b0767 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt @@ -7,8 +7,6 @@ import android.view.View import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ActionMode import androidx.compose.material3.SnackbarDuration import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.fragment.app.Fragment @@ -77,7 +75,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal private val disposables = LifecycleDisposable() private val callLogContextMenu = CallLogContextMenu(this, this) - private val callLogActionMode = CallLogActionMode(CallLogActionModeCallback()) + private lateinit var callLogActionMode: CallLogActionMode private val conversationUpdateTick: ConversationUpdateTick = ConversationUpdateTick(this::onTimestampTick) private var callLogAdapter: CallLogAdapter? = null @@ -91,6 +89,8 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal viewLifecycleOwner.lifecycle.addObserver(conversationUpdateTick) viewLifecycleOwner.lifecycle.addObserver(viewModel.callLogPeekHelper) + callLogActionMode = CallLogActionMode(CallLogActionModeCallback(), mainToolbarViewModel) + val callLogAdapter = CallLogAdapter(this) disposables.bindTo(viewLifecycleOwner) @@ -134,7 +134,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal .subscribe { (selected, totalCount) -> if (selected.isNotEmpty(totalCount)) { callLogActionMode.setCount(selected.count(totalCount)) - } else if (callLogActionMode.isInActionMode()) { + } else if (mainToolbarViewModel.isInActionMode()) { callLogActionMode.end() } } @@ -275,7 +275,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal } override fun canStartNestedScroll(): Boolean { - return !callLogActionMode.isInActionMode() && !isSearchOpen() + return !mainToolbarViewModel.isInActionMode() && !isSearchOpen() } } @@ -464,17 +464,16 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal override fun onBottomActionBarVisibilityChanged(visibility: Int) = Unit } - private inner class CallLogActionModeCallback : CallLogActionMode.Callback { - override fun startActionMode(callback: ActionMode.Callback): ActionMode? { - val actionMode = (requireActivity() as AppCompatActivity).startSupportActionMode(callback) + inner class CallLogActionModeCallback : CallLogActionMode.Callback { + override fun startActionMode() { requireListener().onMultiSelectStarted() signalBottomActionBarController.setVisibility(true) - return actionMode } override fun onActionModeWillEnd() { requireListener().onMultiSelectFinished() signalBottomActionBarController.setVisibility(false) + viewModel.clearSelected() } override fun getResources(): Resources = resources diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java index 38c3d82051..b7d4ca32a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java @@ -26,9 +26,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.WorkerThread; -import androidx.appcompat.view.ActionMode; import androidx.compose.material3.SnackbarDuration; -import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.RecyclerView; import org.signal.core.util.concurrent.LifecycleDisposable; @@ -48,7 +46,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers; import kotlin.Unit; -public class ConversationListArchiveFragment extends ConversationListFragment implements ActionMode.Callback +public class ConversationListArchiveFragment extends ConversationListFragment { private View coordinator; private RecyclerView list; 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 90f9a86b01..9abc185b9d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -30,8 +30,6 @@ import android.os.AsyncTask; import android.os.Bundle; import android.text.TextUtils; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; @@ -44,9 +42,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.PluralsRes; import androidx.annotation.WorkerThread; -import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; -import androidx.appcompat.view.ActionMode; import androidx.compose.material3.SnackbarDuration; import androidx.compose.ui.platform.ComposeView; import androidx.coordinatorlayout.widget.CoordinatorLayout; @@ -184,8 +180,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers; import kotlin.Unit; -public class ConversationListFragment extends MainFragment implements ActionMode.Callback, - ConversationListAdapter.OnConversationClickListener, +public class ConversationListFragment extends MainFragment implements ConversationListAdapter.OnConversationClickListener, ClearFilterViewHolder.OnClearFilterClickListener, ChatFolderAdapter.Callbacks, ConversationListAdapter.EmptyFolderViewHolder.OnFolderSettingsClickListener @@ -201,8 +196,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode private static final int MAX_CHATS_ABOVE_FOLD = 7; private static final int MAX_CONTACTS_ABOVE_FOLD = 5; private static final int MAX_GROUP_MEMBERSHIPS_ABOVE_FOLD = 5; - - private ActionMode actionMode; private View coordinator; private RecyclerView chatFolderList; private RecyclerView list; @@ -1150,22 +1143,20 @@ public class ConversationListFragment extends MainFragment implements ActionMode } private void startActionMode() { - actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(ConversationListFragment.this); ViewUtil.animateIn(bottomActionBar, bottomActionBar.getEnterAnimation()); requireCallback().onMultiSelectStarted(); } - private void endActionModeIfActive() { - if (actionMode != null) { + public void endActionModeIfActive() { + if (mainToolbarViewModel.isInActionMode()) { endActionMode(); } } private void endActionMode() { - actionMode.finish(); - actionMode = null; ViewUtil.animateOut(bottomActionBar, bottomActionBar.getExitAnimation()); requireCallback().onMultiSelectFinished(); + viewModel.endSelection(); } void updateEmptyState(boolean isConversationEmpty) { @@ -1186,7 +1177,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode @Override public void onConversationClick(@NonNull Conversation conversation) { - if (actionMode == null) { + if (!mainToolbarViewModel.isInActionMode()) { handleCreateConversation(conversation.getThreadRecord().getThreadId(), conversation.getThreadRecord().getRecipient(), conversation.getThreadRecord().getDistributionType()); } else { viewModel.toggleConversationSelected(conversation); @@ -1195,7 +1186,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode @Override public boolean onConversationLongClick(@NonNull Conversation conversation, @NonNull View view) { - if (actionMode != null) { + if (mainToolbarViewModel.isInActionMode()) { onConversationClick(conversation); return true; } @@ -1269,29 +1260,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode return true; } - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - mode.setTitle(requireContext().getResources().getQuantityString(R.plurals.ConversationListFragment_s_selected, 1, 1)); - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - updateMultiSelectState(); - return false; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return true; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - viewModel.endSelection(); - endActionModeIfActive(); - } - @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) public void onEvent(MessageSender.MessageSentEvent event) { EventBus.getDefault().removeStickyEvent(event); @@ -1351,8 +1319,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode boolean hasUnmuted = Stream.of(viewModel.currentSelectedConversations()).anyMatch(conversation -> !conversation.getThreadRecord().getRecipient().live().get().isMuted()); boolean canPin = viewModel.getPinnedCount() < MAXIMUM_PINNED_CONVERSATIONS; - if (actionMode != null) { - actionMode.setTitle(requireContext().getResources().getQuantityString(R.plurals.ConversationListFragment_s_selected, count, count)); + if (mainToolbarViewModel.isInActionMode()) { + mainToolbarViewModel.setActionModeCount(count); } List items = new ArrayList<>(); @@ -1622,7 +1590,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode viewHolder instanceof ConversationListAdapter.HeaderViewHolder || viewHolder instanceof ClearFilterViewHolder || viewHolder instanceof ConversationListAdapter.EmptyFolderViewHolder || - actionMode != null || + mainToolbarViewModel.isInActionMode() || viewHolder.itemView.isSelected() || activeAdapter == searchAdapter) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainToolbar.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainToolbar.kt index da4cb439ce..fdbc934a4f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/MainToolbar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainToolbar.kt @@ -78,6 +78,7 @@ import org.thoughtcrime.securesms.avatar.AvatarImage import org.thoughtcrime.securesms.calls.log.CallLogFilter import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageSmall import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter +import org.thoughtcrime.securesms.dependencies.GooglePlayBillingDependencies.context import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.rememberRecipientField @@ -98,6 +99,7 @@ interface MainToolbarCallback { fun onStoryPrivacyClick() fun onCloseSearchClick() fun onCloseArchiveClick() + fun onCloseActionModeClick() fun onSearchQueryUpdated(query: String) fun onNotificationProfileTooltipDismissed() @@ -118,6 +120,7 @@ interface MainToolbarCallback { override fun onStoryPrivacyClick() = Unit override fun onCloseSearchClick() = Unit override fun onCloseArchiveClick() = Unit + override fun onCloseActionModeClick() = Unit override fun onSearchQueryUpdated(query: String) = Unit override fun onNotificationProfileTooltipDismissed() = Unit } @@ -144,7 +147,8 @@ data class MainToolbarState( val hasPassphrase: Boolean = false, val proxyState: ProxyState = ProxyState.NONE, @StringRes val searchHint: Int = R.string.SearchToolbar_search, - val searchQuery: String = "" + val searchQuery: String = "", + val actionModeCount: Int = 0 ) { enum class ProxyState(@DrawableRes val icon: Int) { NONE(-1), @@ -161,7 +165,10 @@ fun MainToolbar( callback: MainToolbarCallback ) { if (state.mode == MainToolbarMode.ACTION_MODE) { - TopAppBar(title = {}) + ActionModeToolbar( + state = state, + callback = callback + ) return } @@ -209,6 +216,32 @@ fun MainToolbar( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ActionModeToolbar( + state: MainToolbarState, + callback: MainToolbarCallback +) { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = state.toolbarColor ?: MaterialTheme.colorScheme.surface + ), + navigationIcon = { + IconButtons.IconButton(onClick = { + callback.onCloseActionModeClick() + }) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.symbol_x_24), + contentDescription = stringResource(R.string.CallScreenTopBar__go_back) + ) + } + }, + title = { + Text(text = context.resources.getQuantityString(R.plurals.ConversationListFragment_s_selected, state.actionModeCount, state.actionModeCount)) + } + ) +} + @Composable private fun SearchToolbar( state: MainToolbarState, diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainToolbarViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainToolbarViewModel.kt index 202ec7f50f..83d9282393 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/MainToolbarViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainToolbarViewModel.kt @@ -73,6 +73,14 @@ class MainToolbarViewModel : ViewModel() { } } + fun setActionModeCount(count: Int) { + internalStateFlow.update { + it.copy(actionModeCount = count) + } + } + + fun isInActionMode(): Boolean = state.value.mode == MainToolbarMode.ACTION_MODE + fun presentToolbarForConversationListFragment() { setToolbarMode(MainToolbarMode.FULL, destination = MainNavigationListLocation.CHATS, overwriteSearchMode = false) }