diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java index ed2eb35746..5bccb201e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java @@ -10,7 +10,6 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModelProvider; import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController; @@ -19,7 +18,6 @@ import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferLock import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.stories.Stories; import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository; -import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsState; import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel; import org.thoughtcrime.securesms.util.AppStartup; import org.thoughtcrime.securesms.util.CachedInflater; @@ -60,8 +58,6 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot ConversationListTabRepository repository = new ConversationListTabRepository(); ConversationListTabsViewModel.Factory factory = new ConversationListTabsViewModel.Factory(repository); - navigator.onCreate(savedInstanceState); - handleGroupLinkInIntent(getIntent()); handleProxyInIntent(getIntent()); handleSignalMeIntent(getIntent()); @@ -69,18 +65,6 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot CachedInflater.from(this).clear(); conversationListTabsViewModel = new ViewModelProvider(this, factory).get(ConversationListTabsViewModel.class); - Transformations.distinctUntilChanged(Transformations.map(conversationListTabsViewModel.getState(), ConversationListTabsState::getTab)) - .observe(this, tab -> { - switch (tab) { - case CHATS: - getSupportFragmentManager().popBackStack(); - break; - case STORIES: - navigator.goToStories(); - break; - } - }); - updateTabVisibility(); } @@ -138,7 +122,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot } else { findViewById(R.id.conversation_list_tabs).setVisibility(View.GONE); WindowUtil.setNavigationBarColor(getWindow(), ContextCompat.getColor(this, R.color.signal_background_primary)); - navigator.goToChats(); + conversationListTabsViewModel.onChatsSelected(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainNavigator.java b/app/src/main/java/org/thoughtcrime/securesms/MainNavigator.java index a6082a23d2..9feb2c7f3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainNavigator.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MainNavigator.java @@ -2,26 +2,19 @@ package org.thoughtcrime.securesms; import android.app.Activity; import android.content.Intent; -import android.os.Bundle; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity; import org.thoughtcrime.securesms.conversation.ConversationIntents; -import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment; -import org.thoughtcrime.securesms.conversationlist.ConversationListFragment; import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity; import org.thoughtcrime.securesms.insights.InsightsLauncher; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.stories.landing.StoriesLandingFragment; public class MainNavigator { - public static final String STORIES_TAG = "STORIES"; - public static final int REQUEST_CONFIG_CHANGES = 901; private final MainActivity activity; @@ -38,16 +31,6 @@ public class MainNavigator { return ((MainActivity) activity).getNavigator(); } - public void onCreate(@Nullable Bundle savedInstanceState) { - if (savedInstanceState != null) { - return; - } - - getFragmentManager().beginTransaction() - .add(R.id.fragment_container, ConversationListFragment.newInstance()) - .commit(); - } - /** * @return True if the back pressed was handled in our own custom way, false if it should be given * to the system to do the default behavior. @@ -76,29 +59,6 @@ public class MainNavigator { activity.startActivityForResult(AppSettingsActivity.home(activity), REQUEST_CONFIG_CHANGES); } - public void goToArchiveList() { - getFragmentManager().beginTransaction() - .setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end) - .replace(R.id.fragment_container, ConversationListArchiveFragment.newInstance()) - .addToBackStack(null) - .commit(); - } - - public void goToStories() { - if (getFragmentManager().findFragmentByTag(STORIES_TAG) == null) { - getFragmentManager().beginTransaction() - .replace(R.id.fragment_container, new StoriesLandingFragment(), STORIES_TAG) - .addToBackStack(null) - .commit(); - } - } - - public void goToChats() { - if (getFragmentManager().findFragmentByTag(STORIES_TAG) != null) { - getFragmentManager().popBackStack(); - } - } - public void goToGroupCreation() { activity.startActivity(CreateGroupActivity.newIntent(activity)); } 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 4a193f9069..ce97352923 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java @@ -29,6 +29,9 @@ import androidx.annotation.WorkerThread; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.view.ActionMode; import androidx.appcompat.widget.Toolbar; +import androidx.navigation.NavController; +import androidx.navigation.NavHostController; +import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.snackbar.Snackbar; @@ -62,7 +65,7 @@ public class ConversationListArchiveFragment extends ConversationListFragment im @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - toolbar = new Stub<>(view.findViewById(R.id.toolbar_basic)); + toolbar = requireCallback().getBasicToolbar(); super.onViewCreated(view, savedInstanceState); @@ -71,8 +74,7 @@ public class ConversationListArchiveFragment extends ConversationListFragment im cameraFab = view.findViewById(R.id.camera_fab); emptyState = new Stub<>(view.findViewById(R.id.empty_state)); - ((AppCompatActivity) requireActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true); - toolbar.get().setNavigationOnClickListener(v -> requireActivity().onBackPressed()); + toolbar.get().setNavigationOnClickListener(v -> NavHostFragment.findNavController(this).popBackStack()); toolbar.get().setTitle(R.string.AndroidManifest_archived_conversations); fab.hide(); 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 0c8e6d75ee..403de838d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -44,6 +44,7 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.IdRes; @@ -63,6 +64,8 @@ import androidx.core.view.ViewCompat; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavHostController; +import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -187,7 +190,6 @@ import static org.thoughtcrime.securesms.components.TooltipPopup.POSITION_BELOW; public class ConversationListFragment extends MainFragment implements ActionMode.Callback, ConversationListAdapter.OnConversationClickListener, ConversationListSearchAdapter.EventListener, - MainNavigator.BackHandler, MegaphoneActionController { public static final short MESSAGE_REQUESTS_REQUEST_CODE_CREATE_NAME = 32562; @@ -206,12 +208,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode private TextView searchEmptyState; private PulsingFloatingActionButton fab; private PulsingFloatingActionButton cameraFab; - private Stub searchToolbar; - private ImageView notificationProfileStatus; - private ImageView proxyStatus; - private ImageView searchAction; private View toolbarShadow; - private View unreadPaymentsDot; private ConversationListViewModel viewModel; private RecyclerView.Adapter activeAdapter; private ConversationListAdapter defaultAdapter; @@ -225,7 +222,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode private Stub voiceNotePlayerViewStub; private VoiceNotePlayerView voiceNotePlayerView; private SignalBottomActionBar bottomActionBar; - private TopToastPopup previousTopToastPopup; protected ConversationListArchiveItemDecoration archiveDecoration; protected ConversationListItemAnimator itemAnimator; @@ -265,25 +261,16 @@ public class ConversationListFragment extends MainFragment implements ActionMode fab = view.findViewById(R.id.fab); cameraFab = view.findViewById(R.id.camera_fab); searchEmptyState = view.findViewById(R.id.search_no_results); - searchAction = view.findViewById(R.id.search_action); toolbarShadow = view.findViewById(R.id.conversation_list_toolbar_shadow); - notificationProfileStatus = view.findViewById(R.id.conversation_list_notification_profile_status); - proxyStatus = view.findViewById(R.id.conversation_list_proxy_status); - unreadPaymentsDot = view.findViewById(R.id.unread_payments_indicator); bottomActionBar = view.findViewById(R.id.conversation_list_bottom_action_bar); reminderView = new Stub<>(view.findViewById(R.id.reminder)); emptyState = new Stub<>(view.findViewById(R.id.empty_state)); - searchToolbar = new Stub<>(view.findViewById(R.id.search_toolbar)); megaphoneContainer = new Stub<>(view.findViewById(R.id.megaphone_container)); paymentNotificationView = new Stub<>(view.findViewById(R.id.payments_notification)); voiceNotePlayerViewStub = new Stub<>(view.findViewById(R.id.voice_note_player)); Toolbar toolbar = getToolbar(view); toolbar.setVisibility(View.VISIBLE); - ((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar); - - notificationProfileStatus.setOnClickListener(v -> handleNotificationProfile()); - proxyStatus.setOnClickListener(v -> onProxyStatusClicked()); fab.show(); cameraFab.show(); @@ -321,13 +308,18 @@ public class ConversationListFragment extends MainFragment implements ActionMode RatingManager.showRatingDialogIfNecessary(requireContext()); - TooltipCompat.setTooltipText(searchAction, getText(R.string.SearchToolbar_search_for_conversations_contacts_and_messages)); - } + TooltipCompat.setTooltipText(requireCallback().getSearchAction(), getText(R.string.SearchToolbar_search_for_conversations_contacts_and_messages)); - @Override - public void onDestroyView() { - previousTopToastPopup = null; - super.onDestroyView(); + requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (!closeSearchIfOpen()) { + if (!NavHostFragment.findNavController(ConversationListFragment.this).popBackStack()) { + requireActivity().finish(); + } + } + } + }); } @Override @@ -342,11 +334,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode InsightsLauncher.showInsightsModal(requireContext(), requireFragmentManager()); } - SimpleTask.run(getViewLifecycleOwner().getLifecycle(), Recipient::self, this::initializeProfileIcon); - - initializeSettingsTouchTarget(); - - if ((!searchToolbar.resolved() || !searchToolbar.get().isVisible()) && list.getAdapter() != defaultAdapter) { + if ((!requireCallback().getSearchToolbar().resolved() || !requireCallback().getSearchToolbar().get().isVisible()) && list.getAdapter() != defaultAdapter) { list.removeItemDecoration(searchAdapterDecoration); setAdapter(defaultAdapter); } @@ -431,16 +419,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode return false; } - @Override - public boolean onBackPressed() { - return closeSearchIfOpen(); - } - private boolean closeSearchIfOpen() { - if ((searchToolbar.resolved() && searchToolbar.get().isVisible()) || activeAdapter == searchAdapter) { + if ((requireCallback().getSearchToolbar().resolved() && requireCallback().getSearchToolbar().get().isVisible()) || activeAdapter == searchAdapter) { list.removeItemDecoration(searchAdapterDecoration); setAdapter(defaultAdapter); - searchToolbar.get().collapse(); + requireCallback().getSearchToolbar().get().collapse(); + requireCallback().onSearchClosed(); return true; } @@ -475,7 +459,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode @Override public void onShowArchiveClick() { - getNavigator().goToArchiveList(); + NavHostFragment.findNavController(this) + .navigate(ConversationListFragmentDirections.actionConversationListFragmentToConversationListArchiveFragment()); } @Override @@ -558,26 +543,13 @@ public class ConversationListFragment extends MainFragment implements ActionMode imm.hideSoftInputFromWindow(requireView().getWindowToken(), 0); } - private void initializeProfileIcon(@NonNull Recipient recipient) { - ImageView icon = requireView().findViewById(R.id.toolbar_icon); - - BadgeImageView imageView = requireView().findViewById(R.id.toolbar_badge); - imageView.setBadgeFromRecipient(recipient); - - AvatarUtil.loadIconIntoImageView(recipient, icon, getResources().getDimensionPixelSize(R.dimen.toolbar_avatar_size)); - } - - private void initializeSettingsTouchTarget() { - View touchArea = requireView().findViewById(R.id.toolbar_settings_touch_area); - touchArea.setOnClickListener(v -> getNavigator().goToAppSettings()); - } - private void initializeSearchListener() { - searchAction.setOnClickListener(v -> { - searchToolbar.get().display(searchAction.getX() + (searchAction.getWidth() / 2.0f), - searchAction.getY() + (searchAction.getHeight() / 2.0f)); + requireCallback().getSearchAction().setOnClickListener(v -> { + requireCallback().onSearchOpened(); + requireCallback().getSearchToolbar().get().display(requireCallback().getSearchAction().getX() + (requireCallback().getSearchAction().getWidth() / 2.0f), + requireCallback().getSearchAction().getY() + (requireCallback().getSearchAction().getHeight() / 2.0f)); - searchToolbar.get().setListener(new SearchToolbar.SearchListener() { + requireCallback().getSearchToolbar().get().setListener(new SearchToolbar.SearchListener() { @Override public void onSearchTextChange(String text) { String trimmed = text.trim(); @@ -602,6 +574,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode public void onSearchClosed() { list.removeItemDecoration(searchAdapterDecoration); setAdapter(defaultAdapter); + requireCallback().onSearchClosed(); } }); }); @@ -697,8 +670,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode viewModel.getMegaphone().observe(getViewLifecycleOwner(), this::onMegaphoneChanged); viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onConversationListChanged); viewModel.hasNoConversations().observe(getViewLifecycleOwner(), this::updateEmptyState); - viewModel.getNotificationProfiles().observe(getViewLifecycleOwner(), this::updateNotificationProfileStatus); - viewModel.getPipeState().observe(getViewLifecycleOwner(), this::updateProxyStatus); + viewModel.getNotificationProfiles().observe(getViewLifecycleOwner(), profiles -> requireCallback().updateNotificationProfileStatus(profiles)); + viewModel.getPipeState().observe(getViewLifecycleOwner(), pipeState -> requireCallback().updateProxyStatus(pipeState)); appForegroundObserver = new AppForegroundObserver.Listener() { @Override @@ -742,7 +715,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode private void animatePaymentUnreadStatusIn() { paymentNotificationView.get().setVisibility(View.VISIBLE); - unreadPaymentsDot.animate().alpha(1); + requireCallback().getUnreadPaymentsDot().animate().alpha(1); } private void animatePaymentUnreadStatusOut() { @@ -750,7 +723,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode paymentNotificationView.get().setVisibility(View.GONE); } - unreadPaymentsDot.animate().alpha(0); + requireCallback().getUnreadPaymentsDot().animate().alpha(0); } private void onSearchResultChanged(@Nullable SearchResult result) { @@ -1094,90 +1067,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode } } - private void updateNotificationProfileStatus(@NonNull List notificationProfiles) { - NotificationProfile activeProfile = NotificationProfiles.getActiveProfile(notificationProfiles); - - if (activeProfile != null) { - if (activeProfile.getId() != SignalStore.notificationProfileValues().getLastProfilePopup()) { - requireView().postDelayed(() -> { - SignalStore.notificationProfileValues().setLastProfilePopup(activeProfile.getId()); - SignalStore.notificationProfileValues().setLastProfilePopupTime(System.currentTimeMillis()); - - if (previousTopToastPopup != null && previousTopToastPopup.isShowing()) { - previousTopToastPopup.dismiss(); - } - - ViewGroup view = ((ViewGroup) requireView()); - Fragment fragment = getParentFragmentManager().findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); - if (fragment != null && fragment.isAdded() && fragment.getView() != null) { - view = ((ViewGroup) fragment.requireView()); - } - - try { - previousTopToastPopup = TopToastPopup.show(view, R.drawable.ic_moon_16, getString(R.string.ConversationListFragment__s_on, activeProfile.getName())); - } catch (Exception e) { - Log.w(TAG, "Unable to show toast popup", e); - } - }, 500L); - } - - notificationProfileStatus.setVisibility(View.VISIBLE); - } else { - notificationProfileStatus.setVisibility(View.GONE); - } - - if (!SignalStore.notificationProfileValues().getHasSeenTooltip() && Util.hasItems(notificationProfiles)) { - View target = findOverflowMenuButton(getToolbar(requireView())); - if (target != null) { - TooltipPopup.forTarget(target) - .setText(R.string.ConversationListFragment__turn_your_notification_profile_on_or_off_here) - .setBackgroundTint(ContextCompat.getColor(requireContext(), R.color.signal_button_primary)) - .setTextColor(ContextCompat.getColor(requireContext(), R.color.signal_button_primary_text)) - .setOnDismissListener(() -> SignalStore.notificationProfileValues().setHasSeenTooltip(true)) - .show(POSITION_BELOW); - } else { - Log.w(TAG, "Unable to find overflow menu to show Notification Profile tooltip"); - } - } - } - - private @Nullable View findOverflowMenuButton(@NonNull Toolbar viewGroup) { - for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) { - View v = viewGroup.getChildAt(i); - if (v instanceof ActionMenuView) { - return v; - } - } - return null; - } - - private void updateProxyStatus(@NonNull WebSocketConnectionState state) { - if (SignalStore.proxy().isProxyEnabled()) { - proxyStatus.setVisibility(View.VISIBLE); - - switch (state) { - case CONNECTING: - case DISCONNECTING: - case DISCONNECTED: - proxyStatus.setImageResource(R.drawable.ic_proxy_connecting_24); - break; - case CONNECTED: - proxyStatus.setImageResource(R.drawable.ic_proxy_connected_24); - break; - case AUTHENTICATION_FAILED: - case FAILED: - proxyStatus.setImageResource(R.drawable.ic_proxy_failed_24); - break; - } - } else { - proxyStatus.setVisibility(View.GONE); - } - } - - private void onProxyStatusClicked() { - startActivity(AppSettingsActivity.proxy(requireContext())); - } - protected void onPostSubmitList(int conversationCount) { if (conversationCount >= 6 && (SignalStore.onboarding().shouldShowInviteFriends() || SignalStore.onboarding().shouldShowNewGroup())) { SignalStore.onboarding().clearAll(); @@ -1361,8 +1250,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode bottomActionBar.setItems(items); } + protected Callback requireCallback() { + return ((Callback) getParentFragment().getParentFragment()); + } + protected Toolbar getToolbar(@NonNull View rootView) { - return rootView.findViewById(R.id.toolbar); + return requireCallback().getToolbar(); } protected @PluralsRes int getArchivedSnackbarTitleRes() { @@ -1668,6 +1561,19 @@ public class ConversationListFragment extends MainFragment implements ActionMode MainNavigator.get(requireActivity()).goToConversation(threadRecipientId, threadId, ThreadDatabase.DistributionTypes.DEFAULT, (int) messagePositionInThread); } } + + public interface Callback { + @NonNull Toolbar getToolbar(); + @NonNull ImageView getSearchAction(); + @NonNull Stub getSearchToolbar(); + @NonNull View getUnreadPaymentsDot(); + @NonNull Stub getBasicToolbar(); + + void updateNotificationProfileStatus(@NonNull List notificationProfiles); + void updateProxyStatus(@NonNull WebSocketConnectionState state); + void onSearchOpened(); + void onSearchClosed(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainActivityListHostFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainActivityListHostFragment.kt new file mode 100644 index 0000000000..779e9e0e0e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainActivityListHostFragment.kt @@ -0,0 +1,280 @@ +package org.thoughtcrime.securesms.main + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.ActionMenuView +import androidx.appcompat.widget.Toolbar +import androidx.core.content.ContextCompat +import androidx.core.view.children +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.NavController +import androidx.navigation.NavDestination +import androidx.navigation.findNavController +import androidx.navigation.fragment.findNavController +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.badges.BadgeImageView +import org.thoughtcrime.securesms.components.SearchToolbar +import org.thoughtcrime.securesms.components.TooltipPopup +import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity +import org.thoughtcrime.securesms.components.settings.app.notifications.manual.NotificationProfileSelectionFragment +import org.thoughtcrime.securesms.conversationlist.ConversationListFragment +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile +import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.stories.tabs.ConversationListTab +import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsState +import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel +import org.thoughtcrime.securesms.util.AvatarUtil +import org.thoughtcrime.securesms.util.BottomSheetUtil +import org.thoughtcrime.securesms.util.TopToastPopup +import org.thoughtcrime.securesms.util.TopToastPopup.Companion.show +import org.thoughtcrime.securesms.util.Util +import org.thoughtcrime.securesms.util.concurrent.SimpleTask +import org.thoughtcrime.securesms.util.views.Stub +import org.thoughtcrime.securesms.util.visible +import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState + +class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_fragment), ConversationListFragment.Callback { + + companion object { + private val TAG = Log.tag(MainActivityListHostFragment::class.java) + } + + private val conversationListTabsViewModel: ConversationListTabsViewModel by viewModels(ownerProducer = { requireActivity() }) + + private lateinit var _toolbar: Toolbar + private lateinit var _basicToolbar: Stub + private lateinit var notificationProfileStatus: ImageView + private lateinit var proxyStatus: ImageView + private lateinit var _searchToolbar: Stub + private lateinit var _searchAction: ImageView + private lateinit var _unreadPaymentsDot: View + + private var previousTopToastPopup: TopToastPopup? = null + + private val destinationChangedListener = DestinationChangedListener() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + _toolbar = view.findViewById(R.id.toolbar) + _basicToolbar = Stub(view.findViewById(R.id.toolbar_basic)) + notificationProfileStatus = view.findViewById(R.id.conversation_list_notification_profile_status) + proxyStatus = view.findViewById(R.id.conversation_list_proxy_status) + _searchAction = view.findViewById(R.id.search_action) + _searchToolbar = Stub(view.findViewById(R.id.search_toolbar)) + _unreadPaymentsDot = view.findViewById(R.id.unread_payments_indicator) + + notificationProfileStatus.setOnClickListener { handleNotificationProfile() } + proxyStatus.setOnClickListener { onProxyStatusClicked() } + + initializeSettingsTouchTarget() + + (requireActivity() as AppCompatActivity).setSupportActionBar(_toolbar) + + conversationListTabsViewModel.state.observe(viewLifecycleOwner) { state -> + val controller: NavController = requireView().findViewById(R.id.fragment_container).findNavController() + when (controller.currentDestination?.id) { + R.id.conversationListFragment -> goToStateFromConversationList(state, controller) + R.id.conversationListArchiveFragment -> goToStateFromConversationArchiveList(state, controller) + R.id.storiesLandingFragment -> goToStateFromStories(state, controller) + } + } + } + + private fun goToStateFromConversationArchiveList(state: ConversationListTabsState, navController: NavController) { + if (state.tab == ConversationListTab.CHATS) { + return + } else { + navController.navigate(R.id.action_conversationListArchiveFragment_to_storiesLandingFragment) + } + } + + private fun goToStateFromConversationList(state: ConversationListTabsState, navController: NavController) { + if (state.tab == ConversationListTab.CHATS) { + return + } else { + navController.navigate(R.id.action_conversationListFragment_to_storiesLandingFragment) + } + } + + private fun goToStateFromStories(state: ConversationListTabsState, navController: NavController) { + if (state.tab == ConversationListTab.STORIES) { + return + } else { + navController.popBackStack() + } + } + + override fun onResume() { + super.onResume() + SimpleTask.run(viewLifecycleOwner.lifecycle, { Recipient.self() }, ::initializeProfileIcon) + + requireView() + .findViewById(R.id.fragment_container) + .findNavController() + .addOnDestinationChangedListener(destinationChangedListener) + } + + override fun onPause() { + super.onPause() + requireView() + .findViewById(R.id.fragment_container) + .findNavController() + .removeOnDestinationChangedListener(destinationChangedListener) + } + + private fun presentToolbarForConversationListFragment() { + _toolbar.visible = true + _searchAction.visible = true + if (_basicToolbar.resolved()) { + _basicToolbar.get().visible = false + } + } + + private fun presentToolbarForConversationListArchiveFragment() { + _toolbar.visible = false + _basicToolbar.get().visible = true + } + + private fun presentToolbarForStoriesLandingFragment() { + _toolbar.visible = true + _searchAction.visible = false + if (_basicToolbar.resolved()) { + _basicToolbar.get().visible = false + } + } + + override fun onDestroyView() { + previousTopToastPopup = null + super.onDestroyView() + } + + override fun getToolbar(): Toolbar { + return _toolbar + } + + override fun getSearchAction(): ImageView { + return _searchAction + } + + override fun getSearchToolbar(): Stub { + return _searchToolbar + } + + override fun getUnreadPaymentsDot(): View { + return _unreadPaymentsDot + } + + override fun getBasicToolbar(): Stub { + return _basicToolbar + } + + override fun onSearchOpened() { + conversationListTabsViewModel.onSearchOpened() + } + + override fun onSearchClosed() { + conversationListTabsViewModel.onSearchClosed() + } + + private fun initializeProfileIcon(recipient: Recipient) { + Log.d(TAG, "Initializing profile icon") + val icon = requireView().findViewById(R.id.toolbar_icon) + val imageView: BadgeImageView = requireView().findViewById(R.id.toolbar_badge) + imageView.setBadgeFromRecipient(recipient) + AvatarUtil.loadIconIntoImageView(recipient, icon, resources.getDimensionPixelSize(R.dimen.toolbar_avatar_size)) + } + + private fun initializeSettingsTouchTarget() { + val touchArea = requireView().findViewById(R.id.toolbar_settings_touch_area) + touchArea.setOnClickListener { startActivity(AppSettingsActivity.home(requireContext())) } + } + + private fun handleNotificationProfile() { + NotificationProfileSelectionFragment.show(parentFragmentManager) + } + + private fun onProxyStatusClicked() { + startActivity(AppSettingsActivity.proxy(requireContext())) + } + + override fun updateProxyStatus(state: WebSocketConnectionState) { + if (SignalStore.proxy().isProxyEnabled) { + proxyStatus.visibility = View.VISIBLE + when (state) { + WebSocketConnectionState.CONNECTING, WebSocketConnectionState.DISCONNECTING, WebSocketConnectionState.DISCONNECTED -> proxyStatus.setImageResource(R.drawable.ic_proxy_connecting_24) + WebSocketConnectionState.CONNECTED -> proxyStatus.setImageResource(R.drawable.ic_proxy_connected_24) + WebSocketConnectionState.AUTHENTICATION_FAILED, WebSocketConnectionState.FAILED -> proxyStatus.setImageResource(R.drawable.ic_proxy_failed_24) + else -> proxyStatus.visibility = View.GONE + } + } else { + proxyStatus.visibility = View.GONE + } + } + + override fun updateNotificationProfileStatus(notificationProfiles: List) { + val activeProfile = NotificationProfiles.getActiveProfile(notificationProfiles) + if (activeProfile != null) { + if (activeProfile.id != SignalStore.notificationProfileValues().lastProfilePopup) { + requireView().postDelayed({ + SignalStore.notificationProfileValues().lastProfilePopup = activeProfile.id + SignalStore.notificationProfileValues().lastProfilePopupTime = System.currentTimeMillis() + if (previousTopToastPopup?.isShowing == true) { + previousTopToastPopup?.dismiss() + } + var view = requireView() as ViewGroup + val fragment = parentFragmentManager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) + if (fragment != null && fragment.isAdded && fragment.view != null) { + view = fragment.requireView() as ViewGroup + } + try { + previousTopToastPopup = show(view, R.drawable.ic_moon_16, getString(R.string.ConversationListFragment__s_on, activeProfile.name)) + } catch (e: Exception) { + Log.w(TAG, "Unable to show toast popup", e) + } + }, 500L) + } + notificationProfileStatus.visibility = View.VISIBLE + } else { + notificationProfileStatus.visibility = View.GONE + } + if (!SignalStore.notificationProfileValues().hasSeenTooltip && Util.hasItems(notificationProfiles)) { + val target: View? = findOverflowMenuButton(_toolbar) + if (target != null) { + TooltipPopup.forTarget(target) + .setText(R.string.ConversationListFragment__turn_your_notification_profile_on_or_off_here) + .setBackgroundTint(ContextCompat.getColor(requireContext(), R.color.signal_button_primary)) + .setTextColor(ContextCompat.getColor(requireContext(), R.color.signal_button_primary_text)) + .setOnDismissListener { SignalStore.notificationProfileValues().hasSeenTooltip = true } + .show(TooltipPopup.POSITION_BELOW) + } else { + Log.w(TAG, "Unable to find overflow menu to show Notification Profile tooltip") + } + } + } + + private fun findOverflowMenuButton(viewGroup: Toolbar): View? { + return viewGroup.children.find { it is ActionMenuView } + } + + private inner class DestinationChangedListener : NavController.OnDestinationChangedListener { + override fun onDestinationChanged(controller: NavController, destination: NavDestination, arguments: Bundle?) { + when (destination.id) { + R.id.conversationListFragment -> { + presentToolbarForConversationListFragment() + } + R.id.conversationListArchiveFragment -> { + presentToolbarForConversationListArchiveFragment() + } + R.id.storiesLandingFragment -> { + presentToolbarForStoriesLandingFragment() + } + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt index f17a87ebcd..b5398f03e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt @@ -3,14 +3,17 @@ package org.thoughtcrime.securesms.stories.landing import android.Manifest import android.content.Intent import android.graphics.Color +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.widget.Toast +import androidx.activity.OnBackPressedCallback import androidx.fragment.app.viewModels import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar -import org.thoughtcrime.securesms.MainNavigator import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.DSLConfiguration import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter @@ -34,13 +37,7 @@ import org.thoughtcrime.securesms.util.visible /** * The "landing page" for Stories. */ -class StoriesLandingFragment : - DSLSettingsFragment( - layoutId = R.layout.stories_landing_fragment, - menuId = R.menu.story_landing_menu, - titleId = R.string.ConversationListTabs__stories - ), - MainNavigator.BackHandler { +class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_landing_fragment) { private lateinit var emptyNotice: View private lateinit var cameraFab: View @@ -55,6 +52,16 @@ class StoriesLandingFragment : private val tabsViewModel: ConversationListTabsViewModel by viewModels(ownerProducer = { requireActivity() }) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + menu.clear() + inflater.inflate(R.menu.story_landing_menu, menu) + } + override fun bindAdapter(adapter: DSLSettingsAdapter) { StoriesLandingItem.register(adapter) MyStoriesItem.register(adapter) @@ -79,6 +86,15 @@ class StoriesLandingFragment : adapter.submitList(getConfiguration(it).toMappingModelList()) emptyNotice.visible = it.hasNoStories } + + requireActivity().onBackPressedDispatcher.addCallback( + viewLifecycleOwner, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + tabsViewModel.onChatsSelected() + } + } + ) } private fun getConfiguration(state: StoriesLandingState): DSLConfiguration { @@ -182,11 +198,6 @@ class StoriesLandingFragment : .show() } - override fun onBackPressed(): Boolean { - tabsViewModel.onChatsSelected() - return true - } - override fun onOptionsItemSelected(item: MenuItem): Boolean { return if (item.itemId == R.id.action_settings) { startActivity(StorySettingsActivity.getIntent(requireContext())) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsFragment.kt index 9816bd30f6..96a1efabec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsFragment.kt @@ -47,6 +47,8 @@ class ConversationListTabsFragment : Fragment(R.layout.conversation_list_tabs) { storiesUnreadIndicator.visible = state.unreadStoriesCount > 0 storiesUnreadIndicator.text = formatCount(state.unreadStoriesCount) + + requireView().visible = !state.isSearchOpen } private fun formatCount(count: Long): String { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsState.kt index 2ebf3c0dfd..7382ce37ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsState.kt @@ -3,5 +3,6 @@ package org.thoughtcrime.securesms.stories.tabs data class ConversationListTabsState( val tab: ConversationListTab = ConversationListTab.CHATS, val unreadChatsCount: Long = 0L, - val unreadStoriesCount: Long = 0L + val unreadStoriesCount: Long = 0L, + val isSearchOpen: Boolean = false ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsViewModel.kt index 550ca868bb..a26fd26cbe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabsViewModel.kt @@ -35,6 +35,14 @@ class ConversationListTabsViewModel(repository: ConversationListTabRepository) : store.update { it.copy(tab = ConversationListTab.STORIES) } } + fun onSearchOpened() { + store.update { it.copy(isSearchOpen = true) } + } + + fun onSearchClosed() { + store.update { it.copy(isSearchOpen = false) } + } + class Factory(private val repository: ConversationListTabRepository) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return modelClass.cast(ConversationListTabsViewModel(repository)) as T diff --git a/app/src/main/res/layout/conversation_list_archive_toolbar.xml b/app/src/main/res/layout/conversation_list_archive_toolbar.xml index aaa6290389..08cd99ed43 100644 --- a/app/src/main/res/layout/conversation_list_archive_toolbar.xml +++ b/app/src/main/res/layout/conversation_list_archive_toolbar.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/layout/conversation_list_fragment.xml b/app/src/main/res/layout/conversation_list_fragment.xml index 3f9f8b6d5c..1da77a31ac 100644 --- a/app/src/main/res/layout/conversation_list_fragment.xml +++ b/app/src/main/res/layout/conversation_list_fragment.xml @@ -14,160 +14,18 @@ android:layout_height="wrap_content" android:inflatedId="@+id/voice_note_player" android:layout="@layout/voice_note_player_stub" - app:layout_constraintTop_toBottomOf="@id/toolbar_barrier" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/stories_landing_fragment.xml b/app/src/main/res/layout/stories_landing_fragment.xml index c3d746a213..95d5e11bb4 100644 --- a/app/src/main/res/layout/stories_landing_fragment.xml +++ b/app/src/main/res/layout/stories_landing_fragment.xml @@ -4,16 +4,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + \ No newline at end of file