Remove ConversationTabs* and migrate to MainActivity.

This commit is contained in:
Alex Hart
2025-04-14 14:28:26 -03:00
committed by Cody Henthorne
parent 462fcdce16
commit 54191433e0
18 changed files with 276 additions and 458 deletions

View File

@@ -84,8 +84,10 @@ import org.thoughtcrime.securesms.main.MainBottomChrome
import org.thoughtcrime.securesms.main.MainBottomChromeCallback
import org.thoughtcrime.securesms.main.MainBottomChromeState
import org.thoughtcrime.securesms.main.MainMegaphoneState
import org.thoughtcrime.securesms.main.MainNavigationDestination
import org.thoughtcrime.securesms.main.MainNavigationBar
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
import org.thoughtcrime.securesms.main.MainNavigationListLocation
import org.thoughtcrime.securesms.main.MainNavigationRail
import org.thoughtcrime.securesms.main.MainNavigationViewModel
import org.thoughtcrime.securesms.main.MainToolbar
import org.thoughtcrime.securesms.main.MainToolbarCallback
@@ -104,9 +106,6 @@ import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment
import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.stories.settings.StorySettingsActivity
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsFragment
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel
import org.thoughtcrime.securesms.util.AppForegroundObserver
import org.thoughtcrime.securesms.util.AppStartup
import org.thoughtcrime.securesms.util.CachedInflater
@@ -131,23 +130,23 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
@JvmStatic
fun clearTopAndOpenTab(context: Context, startingTab: MainNavigationDestination): Intent {
fun clearTopAndOpenTab(context: Context, startingTab: MainNavigationListLocation): Intent {
return clearTop(context).putExtra(KEY_STARTING_TAB, startingTab)
}
}
private val dynamicTheme = DynamicNoActionBarTheme()
private val navigator = MainNavigator(this)
private val lifecycleDisposable = LifecycleDisposable()
private lateinit var mediaController: VoiceNoteMediaController
private lateinit var navigator: MainNavigator
override val voiceNoteMediaController: VoiceNoteMediaController
get() = mediaController
private val conversationListTabsViewModel: ConversationListTabsViewModel by viewModel {
val startingTab = intent.extras?.getSerializableCompat(KEY_STARTING_TAB, MainNavigationDestination::class.java)
ConversationListTabsViewModel(startingTab ?: MainNavigationDestination.CHATS, ConversationListTabRepository())
private val mainNavigationViewModel: MainNavigationViewModel by viewModel {
val startingTab = intent.extras?.getSerializableCompat(KEY_STARTING_TAB, MainNavigationListLocation::class.java)
MainNavigationViewModel(startingTab ?: MainNavigationListLocation.CHATS)
}
private val vitalsViewModel: VitalsViewModel by viewModel {
@@ -169,6 +168,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
private val mainBottomChromeCallback = BottomChromeCallback()
private val megaphoneActionController = MainMegaphoneActionController()
private val mainNavigationCallback = MainNavigationCallback()
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return motionEventRelay.offer(ev) || super.dispatchTouchEvent(ev)
@@ -186,20 +186,21 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
)
conversationListTabsViewModel
super.onCreate(savedInstanceState, ready)
navigator = MainNavigator(this, mainNavigationViewModel)
AppForegroundObserver.addListener(object : AppForegroundObserver.Listener {
override fun onForeground() {
navigator.viewModel.getNextMegaphone()
mainNavigationViewModel.getNextMegaphone()
}
})
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
navigator.viewModel.navigationEvents.collectLatest {
mainNavigationViewModel.navigationEvents.collectLatest {
when (it) {
MainNavigationViewModel.NavigationEvent.STORY_CAMERA_FIRST -> {
mainBottomChromeCallback.onCameraClick(MainNavigationDestination.STORIES)
mainBottomChromeCallback.onCameraClick(MainNavigationListLocation.STORIES)
}
}
}
@@ -207,12 +208,16 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
setContent {
val navState = rememberFragmentState()
val listHostState = rememberFragmentState()
val detailLocation by navigator.viewModel.detailLocation.collectAsStateWithLifecycle(MainNavigationDetailLocation.Empty)
val snackbar by navigator.viewModel.snackbar.collectAsStateWithLifecycle()
val detailLocation by mainNavigationViewModel.detailLocation.collectAsStateWithLifecycle(MainNavigationDetailLocation.Empty)
val snackbar by mainNavigationViewModel.snackbar.collectAsStateWithLifecycle()
val mainToolbarState by toolbarViewModel.state.collectAsStateWithLifecycle()
val megaphone by navigator.viewModel.megaphone.collectAsStateWithLifecycle()
val megaphone by mainNavigationViewModel.megaphone.collectAsStateWithLifecycle()
val mainNavigationState by mainNavigationViewModel.mainNavigationState.collectAsStateWithLifecycle()
val isNavigationVisible = remember(mainToolbarState.mode) {
mainToolbarState.mode == MainToolbarMode.FULL
}
val mainBottomChromeState = remember(mainToolbarState.destination, snackbar, mainToolbarState.mode, megaphone) {
MainBottomChromeState(
@@ -251,16 +256,20 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
AppScaffold(
navigator = scaffoldNavigator,
bottomNavContent = {
AndroidFragment(
clazz = ConversationListTabsFragment::class.java,
fragmentState = navState
)
if (isNavigationVisible) {
MainNavigationBar(
state = mainNavigationState,
onDestinationSelected = mainNavigationCallback
)
}
},
navRailContent = {
AndroidFragment(
clazz = ConversationListTabsFragment::class.java,
fragmentState = navState
)
if (isNavigationVisible) {
MainNavigationRail(
state = mainNavigationState,
onDestinationSelected = mainNavigationCallback
)
}
},
listContent = {
val listContainerColor = if (windowSizeClass.isMedium()) {
@@ -376,14 +385,14 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
handleDeepLinkIntent(intent)
val extras = intent.extras ?: return
val startingTab = extras.getSerializableCompat(KEY_STARTING_TAB, MainNavigationDestination::class.java)
val startingTab = extras.getSerializableCompat(KEY_STARTING_TAB, MainNavigationListLocation::class.java)
when (startingTab) {
MainNavigationDestination.CHATS -> conversationListTabsViewModel.onChatsSelected()
MainNavigationDestination.CALLS -> conversationListTabsViewModel.onCallsSelected()
MainNavigationDestination.STORIES -> {
MainNavigationListLocation.CHATS -> mainNavigationViewModel.onChatsSelected()
MainNavigationListLocation.CALLS -> mainNavigationViewModel.onCallsSelected()
MainNavigationListLocation.STORIES -> {
if (Stories.isFeatureEnabled()) {
conversationListTabsViewModel.onStoriesSelected()
mainNavigationViewModel.onStoriesSelected()
}
}
@@ -422,6 +431,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
vitalsViewModel.checkSlowNotificationHeuristics()
mainNavigationViewModel.refreshNavigationBarState()
}
override fun onStop() {
@@ -440,13 +450,13 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
if (resultCode == RESULT_OK && requestCode == CreateSvrPinActivity.REQUEST_NEW_PIN) {
getNavigator().getViewModel().setSnackbar(SnackbarState(message = getString(R.string.ConfirmKbsPinFragment__pin_created)))
getNavigator().getViewModel().onMegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL)
mainNavigationViewModel.setSnackbar(SnackbarState(message = getString(R.string.ConfirmKbsPinFragment__pin_created)))
mainNavigationViewModel.onMegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL)
}
if (resultCode == RESULT_OK && requestCode == UsernameEditFragment.REQUEST_CODE) {
val snackbarString = getString(R.string.ConversationListFragment_username_recovered_toast, SignalStore.account.username)
getNavigator().getViewModel().setSnackbar(
mainNavigationViewModel.setSnackbar(
SnackbarState(
message = snackbarString
)
@@ -605,12 +615,12 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
startActivity(NewCallActivity.createIntent(this@MainActivity))
}
override fun onCameraClick(destination: MainNavigationDestination) {
override fun onCameraClick(destination: MainNavigationListLocation) {
val onGranted = {
startActivity(
MediaSelectionActivity.camera(
context = this@MainActivity,
isStory = destination == MainNavigationDestination.STORIES
isStory = destination == MainNavigationListLocation.STORIES
)
)
}
@@ -636,11 +646,11 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
override fun onMegaphoneVisible(megaphone: Megaphone) {
navigator.viewModel.onMegaphoneVisible(megaphone)
mainNavigationViewModel.onMegaphoneVisible(megaphone)
}
override fun onSnackbarDismissed() {
navigator.viewModel.setSnackbar(null)
mainNavigationViewModel.setSnackbar(null)
}
}
@@ -654,7 +664,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
override fun onMegaphoneToastRequested(string: String) {
getNavigator().viewModel.setSnackbar(
mainNavigationViewModel.setSnackbar(
SnackbarState(
message = string
)
@@ -666,15 +676,25 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
override fun onMegaphoneSnooze(event: Megaphones.Event) {
getNavigator().viewModel.onMegaphoneSnoozed(event)
mainNavigationViewModel.onMegaphoneSnoozed(event)
}
override fun onMegaphoneCompleted(event: Megaphones.Event) {
getNavigator().viewModel.onMegaphoneCompleted(event)
mainNavigationViewModel.onMegaphoneCompleted(event)
}
override fun onMegaphoneDialogFragmentRequested(dialogFragment: DialogFragment) {
dialogFragment.show(supportFragmentManager, "megaphone_dialog")
}
}
private inner class MainNavigationCallback : (MainNavigationListLocation) -> Unit {
override fun invoke(location: MainNavigationListLocation) {
when (location) {
MainNavigationListLocation.CHATS -> mainNavigationViewModel.onChatsSelected()
MainNavigationListLocation.CALLS -> mainNavigationViewModel.onCallsSelected()
MainNavigationListLocation.STORIES -> mainNavigationViewModel.onStoriesSelected()
}
}
}
}

View File

@@ -3,11 +3,9 @@ package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.Intent;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
@@ -25,25 +23,16 @@ public class MainNavigator {
private final AppCompatActivity activity;
private final LifecycleDisposable lifecycleDisposable;
private final MainNavigationViewModel viewModel;
private MainNavigationViewModel viewModel;
public MainNavigator(@NonNull AppCompatActivity activity) {
public MainNavigator(@NonNull AppCompatActivity activity, @NonNull MainNavigationViewModel viewModel) {
this.activity = activity;
this.lifecycleDisposable = new LifecycleDisposable();
this.viewModel = viewModel;
lifecycleDisposable.bindTo(activity);
}
@MainThread
public @NonNull MainNavigationViewModel getViewModel() {
if (viewModel == null) {
viewModel = new ViewModelProvider(activity).get(MainNavigationViewModel.class);
}
return viewModel;
}
public static MainNavigator get(@NonNull Activity activity) {
if (!(activity instanceof MainActivity)) {
throw new IllegalArgumentException("Activity must be an instance of MainActivity!");

View File

@@ -13,7 +13,6 @@ import androidx.compose.material3.SnackbarDuration
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
@@ -44,14 +43,13 @@ import org.thoughtcrime.securesms.conversationlist.chatfilter.FilterLerp
import org.thoughtcrime.securesms.conversationlist.chatfilter.FilterPullState
import org.thoughtcrime.securesms.databinding.CallLogFragmentBinding
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.main.MainNavigationDestination
import org.thoughtcrime.securesms.main.MainNavigationListLocation
import org.thoughtcrime.securesms.main.MainNavigationViewModel
import org.thoughtcrime.securesms.main.MainToolbarMode
import org.thoughtcrime.securesms.main.MainToolbarViewModel
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
import org.thoughtcrime.securesms.main.SnackbarState
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.doAfterNextLayout
@@ -84,7 +82,6 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
private lateinit var signalBottomActionBarController: SignalBottomActionBarController
private val viewModel: CallLogViewModel by activityViewModels()
private val tabsViewModel: ConversationListTabsViewModel by viewModels(ownerProducer = { requireActivity() })
private val mainToolbarViewModel: MainToolbarViewModel by activityViewModels()
private val mainNavigationViewModel: MainNavigationViewModel by activityViewModels()
@@ -171,7 +168,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (!closeSearchIfOpen()) {
tabsViewModel.onChatsSelected()
mainNavigationViewModel.onChatsSelected()
}
}
}
@@ -196,8 +193,8 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
}
private fun initializeTapToScrollToTop(scrollToPositionDelegate: ScrollToPositionDelegate) {
disposables += tabsViewModel.tabClickEvents
.filter { it == MainNavigationDestination.CALLS }
disposables += mainNavigationViewModel.tabClickEvents
.filter { it == MainNavigationListLocation.CALLS }
.subscribeBy(onNext = {
scrollToPositionDelegate.resetScrollPosition()
})

View File

@@ -255,7 +255,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModelV2
import org.thoughtcrime.securesms.longmessage.LongMessageFragment
import org.thoughtcrime.securesms.main.MainNavigationDestination
import org.thoughtcrime.securesms.main.MainNavigationListLocation
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity
@@ -3055,7 +3055,7 @@ class ConversationFragment :
} else if ("username_edit" == action) {
startActivity(EditProfileActivity.getIntentForUsernameEdit(requireContext()))
} else if ("calls_tab" == action) {
startActivity(MainActivity.clearTopAndOpenTab(requireContext(), MainNavigationDestination.CALLS))
startActivity(MainActivity.clearTopAndOpenTab(requireContext(), MainNavigationListLocation.CALLS))
} else if ("chat_folder" == action) {
startActivity(AppSettingsActivity.chatFolders(requireContext()))
}

View File

@@ -137,7 +137,7 @@ public class ConversationListArchiveFragment extends ConversationListFragment im
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
getNavigator().getViewModel().setSnackbar(new SnackbarState(
mainNavigationViewModel.setSnackbar(new SnackbarState(
getResources().getQuantityString(R.plurals.ConversationListFragment_moved_conversations_to_inbox, 1, 1),
new SnackbarState.ActionState(
getString(R.string.ConversationListFragment_undo),

View File

@@ -132,7 +132,8 @@ import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
import org.thoughtcrime.securesms.keyvalue.AccountValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.main.MainNavigationDestination;
import org.thoughtcrime.securesms.main.MainNavigationListLocation;
import org.thoughtcrime.securesms.main.MainNavigationViewModel;
import org.thoughtcrime.securesms.main.MainToolbarMode;
import org.thoughtcrime.securesms.main.MainToolbarViewModel;
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder;
@@ -146,7 +147,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.search.MessageResult;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
@@ -225,13 +225,14 @@ public class ConversationListFragment extends MainFragment implements ActionMode
protected ConversationListArchiveItemDecoration archiveDecoration;
protected ConversationListItemAnimator itemAnimator;
private Stopwatch startupStopwatch;
private ConversationListTabsViewModel conversationListTabsViewModel;
private ContactSearchMediator contactSearchMediator;
private MainToolbarViewModel mainToolbarViewModel;
private ChatListBackHandler chatListBackHandler;
private BannerManager bannerManager;
protected MainNavigationViewModel mainNavigationViewModel;
public static ConversationListFragment newInstance() {
return new ConversationListFragment();
}
@@ -250,8 +251,9 @@ public class ConversationListFragment extends MainFragment implements ActionMode
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
startupStopwatch = new Stopwatch("startup");
mainToolbarViewModel = new ViewModelProvider(getActivity()).get(MainToolbarViewModel.class);
startupStopwatch = new Stopwatch("startup");
mainToolbarViewModel = new ViewModelProvider(requireActivity()).get(MainToolbarViewModel.class);
mainNavigationViewModel = new ViewModelProvider(requireActivity()).get(MainNavigationViewModel.class);
}
@Override
@@ -390,19 +392,17 @@ public class ConversationListFragment extends MainFragment implements ActionMode
chatListBackHandler = new ChatListBackHandler(false);
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), chatListBackHandler);
conversationListTabsViewModel = new ViewModelProvider(requireActivity()).get(ConversationListTabsViewModel.class);
lifecycleDisposable.bindTo(getViewLifecycleOwner());
lifecycleDisposable.add(conversationListTabsViewModel.getTabClickEvents().filter(tab -> tab == MainNavigationDestination.CHATS)
.subscribe(unused -> {
LinearLayoutManager layoutManager = (LinearLayoutManager) list.getLayoutManager();
int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();
if (firstVisibleItemPosition <= LIST_SMOOTH_SCROLL_TO_TOP_THRESHOLD) {
list.smoothScrollToPosition(0);
} else {
list.scrollToPosition(0);
}
}));
lifecycleDisposable.add(mainNavigationViewModel.getTabClickEvents().filter(tab -> tab == MainNavigationListLocation.CHATS)
.subscribe(unused -> {
LinearLayoutManager layoutManager = (LinearLayoutManager) list.getLayoutManager();
int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();
if (firstVisibleItemPosition <= LIST_SMOOTH_SCROLL_TO_TOP_THRESHOLD) {
list.smoothScrollToPosition(0);
} else {
list.scrollToPosition(0);
}
}));
requireCallback().bindScrollHelper(list, getViewLifecycleOwner(), chatFolderList, color -> {
for (int i = 0; i < chatFolderList.getChildCount(); i++) {
@@ -953,7 +953,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
.subscribe(() -> {
endActionModeIfActive();
getNavigator().getViewModel().setSnackbar(new SnackbarState(
mainNavigationViewModel.setSnackbar(new SnackbarState(
snackBarTitle,
new SnackbarState.ActionState(
getString(R.string.ConversationListFragment_undo),
@@ -1042,7 +1042,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
.toList());
if (toPin.size() + viewModel.getPinnedCount() > MAXIMUM_PINNED_CONVERSATIONS) {
getNavigator().getViewModel().setSnackbar(new SnackbarState(
mainNavigationViewModel.setSnackbar(new SnackbarState(
getString(R.string.conversation_list__you_can_only_pin_up_to_d_chats, MAXIMUM_PINNED_CONVERSATIONS),
null,
false,
@@ -1407,7 +1407,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(pinnedThreadIds -> {
getNavigator().getViewModel().setSnackbar(new SnackbarState(
mainNavigationViewModel.setSnackbar(new SnackbarState(
getResources().getQuantityString(R.plurals.ConversationListFragment_conversations_archived, 1, 1),
new SnackbarState.ActionState(
getString(R.string.ConversationListFragment_undo),

View File

@@ -5,13 +5,18 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.findNavController
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.rxjava3.kotlin.subscribeBy
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
@@ -21,8 +26,6 @@ import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsState
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.TopToastPopup
@@ -35,13 +38,13 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
private val TAG = Log.tag(MainActivityListHostFragment::class.java)
}
private val conversationListTabsViewModel: ConversationListTabsViewModel by viewModels(ownerProducer = { requireActivity() })
private val disposables: LifecycleDisposable = LifecycleDisposable()
private var previousTopToastPopup: TopToastPopup? = null
private val destinationChangedListener = DestinationChangedListener()
private val toolbarViewModel: MainToolbarViewModel by activityViewModels()
private val mainNavigationViewModel: MainNavigationViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
disposables.bindTo(viewLifecycleOwner)
@@ -50,18 +53,30 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
toolbarViewModel.setHasUnreadPayments(unread.isPresent)
}
disposables += conversationListTabsViewModel.state.subscribeBy { state ->
val controller: NavController = getChildNavController()
when (controller.currentDestination?.id) {
R.id.conversationListFragment -> goToStateFromConversationList(state, controller)
R.id.conversationListArchiveFragment -> Unit
R.id.storiesLandingFragment -> goToStateFromStories(state, controller)
R.id.callLogFragment -> goToStateFromCalling(state, controller)
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
launch {
mainNavigationViewModel.mainNavigationState.collectLatest { state ->
withContext(Dispatchers.Main) {
val controller: NavController = getChildNavController()
when (controller.currentDestination?.id) {
R.id.conversationListFragment -> goToStateFromConversationList(state, controller)
R.id.conversationListArchiveFragment -> Unit
R.id.storiesLandingFragment -> goToStateFromStories(state, controller)
R.id.callLogFragment -> goToStateFromCalling(state, controller)
}
}
}
}
disposables += conversationListTabsViewModel.getNotificationProfiles().subscribeBy { profiles ->
updateNotificationProfileStatus(profiles)
launch {
mainNavigationViewModel.getNotificationProfiles().collectLatest { profiles ->
withContext(Dispatchers.Main) {
updateNotificationProfileStatus(profiles)
}
}
}
}
}
}
@@ -69,11 +84,11 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
return requireView().findViewById<View>(R.id.fragment_container).findNavController()
}
private fun goToStateFromConversationList(state: ConversationListTabsState, navController: NavController) {
if (state.tab == MainNavigationDestination.CHATS) {
private fun goToStateFromConversationList(state: MainNavigationState, navController: NavController) {
if (state.selectedDestination == MainNavigationListLocation.CHATS) {
return
} else {
val destination = if (state.tab == MainNavigationDestination.STORIES) {
val destination = if (state.selectedDestination == MainNavigationListLocation.STORIES) {
R.id.action_conversationListFragment_to_storiesLandingFragment
} else {
R.id.action_conversationListFragment_to_callLogFragment
@@ -87,19 +102,19 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
}
}
private fun goToStateFromCalling(state: ConversationListTabsState, navController: NavController) {
when (state.tab) {
MainNavigationDestination.CALLS -> return
MainNavigationDestination.CHATS -> navController.popBackStack(R.id.conversationListFragment, false)
MainNavigationDestination.STORIES -> navController.navigate(R.id.action_callLogFragment_to_storiesLandingFragment)
private fun goToStateFromCalling(state: MainNavigationState, navController: NavController) {
when (state.selectedDestination) {
MainNavigationListLocation.CALLS -> return
MainNavigationListLocation.CHATS -> navController.popBackStack(R.id.conversationListFragment, false)
MainNavigationListLocation.STORIES -> navController.navigate(R.id.action_callLogFragment_to_storiesLandingFragment)
}
}
private fun goToStateFromStories(state: ConversationListTabsState, navController: NavController) {
when (state.tab) {
MainNavigationDestination.STORIES -> return
MainNavigationDestination.CHATS -> navController.popBackStack(R.id.conversationListFragment, false)
MainNavigationDestination.CALLS -> navController.navigate(R.id.action_storiesLandingFragment_to_callLogFragment)
private fun goToStateFromStories(state: MainNavigationState, navController: NavController) {
when (state.selectedDestination) {
MainNavigationListLocation.STORIES -> return
MainNavigationListLocation.CHATS -> navController.popBackStack(R.id.conversationListFragment, false)
MainNavigationListLocation.CALLS -> navController.navigate(R.id.action_storiesLandingFragment_to_callLogFragment)
}
}
@@ -112,7 +127,7 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
.findNavController()
.addOnDestinationChangedListener(destinationChangedListener)
if (conversationListTabsViewModel.isMultiSelectOpen()) {
if (toolbarViewModel.state.value.mode == MainToolbarMode.ACTION_MODE) {
presentToolbarForMultiselect()
}
}
@@ -126,19 +141,19 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
}
private fun presentToolbarForConversationListFragment() {
toolbarViewModel.setToolbarMode(MainToolbarMode.FULL, destination = MainNavigationDestination.CHATS)
toolbarViewModel.setToolbarMode(MainToolbarMode.FULL, destination = MainNavigationListLocation.CHATS)
}
private fun presentToolbarForConversationListArchiveFragment() {
toolbarViewModel.setToolbarMode(MainToolbarMode.BASIC, destination = MainNavigationDestination.CHATS)
toolbarViewModel.setToolbarMode(MainToolbarMode.BASIC, destination = MainNavigationListLocation.CHATS)
}
private fun presentToolbarForStoriesLandingFragment() {
toolbarViewModel.setToolbarMode(MainToolbarMode.FULL, destination = MainNavigationDestination.STORIES)
toolbarViewModel.setToolbarMode(MainToolbarMode.FULL, destination = MainNavigationListLocation.STORIES)
}
private fun presentToolbarForCallLogFragment() {
toolbarViewModel.setToolbarMode(MainToolbarMode.FULL, destination = MainNavigationDestination.CALLS)
toolbarViewModel.setToolbarMode(MainToolbarMode.FULL, destination = MainNavigationListLocation.CALLS)
}
private fun presentToolbarForMultiselect() {
@@ -152,7 +167,6 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
override fun onMultiSelectStarted() {
presentToolbarForMultiselect()
conversationListTabsViewModel.onMultiSelectStarted()
}
override fun onMultiSelectFinished() {
@@ -160,8 +174,6 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
if (currentDestination != null) {
presentToolbarForDestination(currentDestination)
}
conversationListTabsViewModel.onMultiSelectFinished()
}
override fun updateProxyStatus(state: WebSocketConnectionState) {
@@ -218,22 +230,18 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
private fun presentToolbarForDestination(destination: NavDestination) {
when (destination.id) {
R.id.conversationListFragment -> {
conversationListTabsViewModel.isShowingArchived(false)
presentToolbarForConversationListFragment()
}
R.id.conversationListArchiveFragment -> {
conversationListTabsViewModel.isShowingArchived(true)
presentToolbarForConversationListArchiveFragment()
}
R.id.storiesLandingFragment -> {
conversationListTabsViewModel.isShowingArchived(false)
presentToolbarForStoriesLandingFragment()
}
R.id.callLogFragment -> {
conversationListTabsViewModel.isShowingArchived(false)
presentToolbarForCallLogFragment()
}
}

View File

@@ -46,21 +46,21 @@ data class SnackbarState(
interface MainBottomChromeCallback {
fun onNewChatClick()
fun onNewCallClick()
fun onCameraClick(destination: MainNavigationDestination)
fun onCameraClick(destination: MainNavigationListLocation)
fun onMegaphoneVisible(megaphone: Megaphone)
fun onSnackbarDismissed()
object Empty : MainBottomChromeCallback {
override fun onNewChatClick() = Unit
override fun onNewCallClick() = Unit
override fun onCameraClick(destination: MainNavigationDestination) = Unit
override fun onCameraClick(destination: MainNavigationListLocation) = Unit
override fun onMegaphoneVisible(megaphone: Megaphone) = Unit
override fun onSnackbarDismissed() = Unit
}
}
data class MainBottomChromeState(
val destination: MainNavigationDestination = MainNavigationDestination.CHATS,
val destination: MainNavigationListLocation = MainNavigationListLocation.CHATS,
val megaphoneState: MainMegaphoneState = MainMegaphoneState(),
val snackbarState: SnackbarState? = null,
val mainToolbarMode: MainToolbarMode = MainToolbarMode.FULL

View File

@@ -46,9 +46,9 @@ private val ACTION_BUTTON_SPACING = 16.dp
@Composable
fun MainFloatingActionButtons(
destination: MainNavigationDestination,
destination: MainNavigationListLocation,
onNewChatClick: () -> Unit = {},
onCameraClick: (MainNavigationDestination) -> Unit = {},
onCameraClick: (MainNavigationListLocation) -> Unit = {},
onNewCallClick: () -> Unit = {}
) {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
@@ -67,7 +67,7 @@ fun MainFloatingActionButtons(
.height(boxHeightDp)
) {
AnimatedVisibility(
visible = destination == MainNavigationDestination.CHATS,
visible = destination == MainNavigationListLocation.CHATS,
modifier = Modifier.align(Alignment.TopCenter),
enter = slideInVertically(initialOffsetY = { boxHeightPx - it }),
exit = slideOutVertically(targetOffsetY = { boxHeightPx - it })
@@ -79,7 +79,7 @@ fun MainFloatingActionButtons(
containerColor = SignalTheme.colors.colorSurface1
),
onClick = {
onCameraClick(MainNavigationDestination.CHATS)
onCameraClick(MainNavigationListLocation.CHATS)
},
shadowElevation = elevation
)
@@ -100,16 +100,16 @@ fun MainFloatingActionButtons(
@Composable
private fun PrimaryActionButton(
destination: MainNavigationDestination,
destination: MainNavigationListLocation,
onNewChatClick: () -> Unit = {},
onCameraClick: (MainNavigationDestination) -> Unit = {},
onCameraClick: (MainNavigationListLocation) -> Unit = {},
onNewCallClick: () -> Unit = {}
) {
val onClick = remember(destination) {
when (destination) {
MainNavigationDestination.CHATS -> onNewChatClick
MainNavigationDestination.CALLS -> onNewCallClick
MainNavigationDestination.STORIES -> {
MainNavigationListLocation.CHATS -> onNewChatClick
MainNavigationListLocation.CALLS -> onNewCallClick
MainNavigationListLocation.STORIES -> {
{ onCameraClick(destination) }
}
}
@@ -120,9 +120,9 @@ private fun PrimaryActionButton(
icon = {
AnimatedContent(destination) { targetState ->
val icon = when (targetState) {
MainNavigationDestination.CHATS -> R.drawable.symbol_edit_24
MainNavigationDestination.CALLS -> R.drawable.symbol_phone_plus_24
MainNavigationDestination.STORIES -> R.drawable.symbol_camera_24
MainNavigationListLocation.CHATS -> R.drawable.symbol_edit_24
MainNavigationListLocation.CALLS -> R.drawable.symbol_phone_plus_24
MainNavigationListLocation.STORIES -> R.drawable.symbol_camera_24
}
Icon(
@@ -179,14 +179,14 @@ private fun MainFloatingActionButton(
@SignalPreview
@Composable
private fun MainFloatingActionButtonsPreview() {
var destination by remember { mutableStateOf(MainNavigationDestination.CHATS) }
var destination by remember { mutableStateOf(MainNavigationListLocation.CHATS) }
Previews.Preview {
MainFloatingActionButtons(
destination = destination,
onCameraClick = { destination = MainNavigationDestination.CALLS },
onNewChatClick = { destination = MainNavigationDestination.STORIES },
onNewCallClick = { destination = MainNavigationDestination.CHATS }
onCameraClick = { destination = MainNavigationListLocation.CALLS },
onNewChatClick = { destination = MainNavigationListLocation.STORIES },
onNewCallClick = { destination = MainNavigationListLocation.CHATS }
)
}
}

View File

@@ -65,7 +65,7 @@ import org.thoughtcrime.securesms.R
private val LOTTIE_SIZE = 28.dp
enum class MainNavigationDestination(
enum class MainNavigationListLocation(
@StringRes val label: Int,
@RawRes val icon: Int,
@StringRes val contentDescription: Int = label
@@ -90,7 +90,7 @@ data class MainNavigationState(
val storiesCount: Int = 0,
val storyFailure: Boolean = false,
val isStoriesFeatureEnabled: Boolean = true,
val selectedDestination: MainNavigationDestination = MainNavigationDestination.CHATS,
val selectedDestination: MainNavigationListLocation = MainNavigationListLocation.CHATS,
val compact: Boolean = false
)
@@ -100,7 +100,7 @@ data class MainNavigationState(
@Composable
fun MainNavigationBar(
state: MainNavigationState,
onDestinationSelected: (MainNavigationDestination) -> Unit
onDestinationSelected: (MainNavigationListLocation) -> Unit
) {
Column(modifier = Modifier.background(color = SignalTheme.colors.colorSurface2)) {
NavigationBar(
@@ -111,18 +111,18 @@ fun MainNavigationBar(
) {
val entries = remember(state.isStoriesFeatureEnabled) {
if (state.isStoriesFeatureEnabled) {
MainNavigationDestination.entries
MainNavigationListLocation.entries
} else {
MainNavigationDestination.entries.filterNot { it == MainNavigationDestination.STORIES }
MainNavigationListLocation.entries.filterNot { it == MainNavigationListLocation.STORIES }
}
}
entries.forEach { destination ->
val badgeCount = when (destination) {
MainNavigationDestination.CHATS -> state.chatsCount
MainNavigationDestination.CALLS -> state.callsCount
MainNavigationDestination.STORIES -> state.storiesCount
MainNavigationListLocation.CHATS -> state.chatsCount
MainNavigationListLocation.CALLS -> state.callsCount
MainNavigationListLocation.STORIES -> state.storiesCount
}
val selected = state.selectedDestination == destination
@@ -214,7 +214,7 @@ private fun Modifier.drawNavigationBarBadge(count: Int, compact: Boolean): Modif
@Composable
fun MainNavigationRail(
state: MainNavigationState,
onDestinationSelected: (MainNavigationDestination) -> Unit
onDestinationSelected: (MainNavigationListLocation) -> Unit
) {
NavigationRail(
containerColor = SignalTheme.colors.colorSurface1,
@@ -260,9 +260,9 @@ fun MainNavigationRail(
) {
val entries = remember(state.isStoriesFeatureEnabled) {
if (state.isStoriesFeatureEnabled) {
MainNavigationDestination.entries
MainNavigationListLocation.entries
} else {
MainNavigationDestination.entries.filterNot { it == MainNavigationDestination.STORIES }
MainNavigationListLocation.entries.filterNot { it == MainNavigationListLocation.STORIES }
}
}
@@ -271,7 +271,7 @@ fun MainNavigationRail(
Box {
NavigationRailItem(
modifier = Modifier.padding(bottom = if (MainNavigationDestination.entries.lastIndex == idx) 0.dp else 16.dp),
modifier = Modifier.padding(bottom = if (MainNavigationListLocation.entries.lastIndex == idx) 0.dp else 16.dp),
icon = {
NavigationDestinationIcon(
destination = destination,
@@ -299,13 +299,13 @@ fun MainNavigationRail(
@Composable
private fun BoxScope.NavigationRailCountIndicator(
state: MainNavigationState,
destination: MainNavigationDestination
destination: MainNavigationListLocation
) {
val count = remember(state, destination) {
when (destination) {
MainNavigationDestination.CHATS -> state.chatsCount
MainNavigationDestination.CALLS -> state.callsCount
MainNavigationDestination.STORIES -> state.storiesCount
MainNavigationListLocation.CHATS -> state.chatsCount
MainNavigationListLocation.CALLS -> state.callsCount
MainNavigationListLocation.STORIES -> state.storiesCount
}
}
@@ -332,7 +332,7 @@ private fun BoxScope.NavigationRailCountIndicator(
@Composable
private fun NavigationDestinationIcon(
destination: MainNavigationDestination,
destination: MainNavigationListLocation,
selected: Boolean
) {
val dynamicProperties = rememberLottieDynamicProperties(
@@ -358,7 +358,7 @@ private fun NavigationDestinationIcon(
}
@Composable
private fun NavigationDestinationLabel(destination: MainNavigationDestination) {
private fun NavigationDestinationLabel(destination: MainNavigationListLocation) {
Text(stringResource(destination.label))
}
@@ -374,7 +374,7 @@ private fun formatCount(count: Int): String {
@Composable
private fun MainNavigationRailPreview() {
Previews.Preview {
var selected by remember { mutableStateOf(MainNavigationDestination.CHATS) }
var selected by remember { mutableStateOf(MainNavigationListLocation.CHATS) }
MainNavigationRail(
state = MainNavigationState(
@@ -392,7 +392,7 @@ private fun MainNavigationRailPreview() {
@Composable
private fun MainNavigationBarPreview() {
Previews.Preview {
var selected by remember { mutableStateOf(MainNavigationDestination.CHATS) }
var selected by remember { mutableStateOf(MainNavigationListLocation.CHATS) }
MainNavigationBar(
state = MainNavigationState(

View File

@@ -1,17 +1,18 @@
package org.thoughtcrime.securesms.stories.tabs
package org.thoughtcrime.securesms.main
import io.reactivex.rxjava3.core.Flowable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.reactive.asFlow
import org.thoughtcrime.securesms.database.RxDatabaseObserver
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.Recipient
class ConversationListTabRepository {
object MainNavigationRepository {
fun getNumberOfUnreadMessages(): Flowable<Long> {
return RxDatabaseObserver.conversationList.map { SignalDatabase.threads.getUnreadMessageCount() }
fun getNumberOfUnreadMessages(): Flow<Long> {
return RxDatabaseObserver.conversationList.map { SignalDatabase.threads.getUnreadMessageCount() }.asFlow()
}
fun getNumberOfUnseenStories(): Flowable<Long> {
fun getNumberOfUnseenStories(): Flow<Long> {
return RxDatabaseObserver.conversationList.map {
SignalDatabase
.messages
@@ -20,14 +21,14 @@ class ConversationListTabRepository {
.filterNot { it.shouldHideStory }
.size
.toLong()
}
}.asFlow()
}
fun getHasFailedOutgoingStories(): Flowable<Boolean> {
return RxDatabaseObserver.conversationList.map { SignalDatabase.messages.hasFailedOutgoingStory() }
fun getHasFailedOutgoingStories(): Flow<Boolean> {
return RxDatabaseObserver.conversationList.map { SignalDatabase.messages.hasFailedOutgoingStory() }.asFlow()
}
fun getNumberOfUnseenCalls(): Flowable<Long> {
return RxDatabaseObserver.conversationList.map { SignalDatabase.calls.getUnreadMissedCallCount() }
fun getNumberOfUnseenCalls(): Flow<Long> {
return RxDatabaseObserver.conversationList.map { SignalDatabase.calls.getUnreadMissedCallCount() }.asFlow()
}
}

View File

@@ -7,18 +7,27 @@ package org.thoughtcrime.securesms.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.reactivex.rxjava3.core.Observable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.asFlow
import kotlinx.coroutines.rx3.asObservable
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.megaphone.Megaphone
import org.thoughtcrime.securesms.megaphone.Megaphones
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.stories.Stories
class MainNavigationViewModel : ViewModel() {
class MainNavigationViewModel(initialListLocation: MainNavigationListLocation = MainNavigationListLocation.CHATS) : ViewModel() {
private val megaphoneRepository = AppDependencies.megaphoneRepository
private val detailLocationFlow = MutableSharedFlow<MainNavigationDetailLocation>()
@@ -33,6 +42,35 @@ class MainNavigationViewModel : ViewModel() {
private val internalNavigationEvents = MutableSharedFlow<NavigationEvent>()
val navigationEvents: Flow<NavigationEvent> = internalNavigationEvents
private val notificationProfilesRepository: NotificationProfilesRepository = NotificationProfilesRepository()
private val internalMainNavigationState = MutableStateFlow(MainNavigationState(selectedDestination = initialListLocation))
val mainNavigationState: StateFlow<MainNavigationState> = internalMainNavigationState
/**
* This is Rx because these are still accessed from Java.
*/
private val internalTabClickEvents: MutableSharedFlow<MainNavigationListLocation> = MutableSharedFlow()
val tabClickEvents: Observable<MainNavigationListLocation> = internalTabClickEvents.filter { Stories.isFeatureEnabled() }.asObservable()
init {
performStoreUpdate(MainNavigationRepository.getNumberOfUnreadMessages()) { unreadChats, state ->
state.copy(chatsCount = unreadChats.toInt())
}
performStoreUpdate(MainNavigationRepository.getNumberOfUnseenCalls()) { unseenCalls, state ->
state.copy(callsCount = unseenCalls.toInt())
}
performStoreUpdate(MainNavigationRepository.getNumberOfUnseenStories()) { unseenStories, state ->
state.copy(storiesCount = unseenStories.toInt())
}
performStoreUpdate(MainNavigationRepository.getHasFailedOutgoingStories()) { hasFailedStories, state ->
state.copy(storyFailure = hasFailedStories)
}
}
fun goTo(location: MainNavigationDetailLocation) {
viewModelScope.launch {
detailLocationFlow.emit(location)
@@ -69,6 +107,43 @@ class MainNavigationViewModel : ViewModel() {
megaphoneRepository.markVisible(visible.event)
}
fun refreshNavigationBarState() {
internalMainNavigationState.update { it.copy(compact = SignalStore.settings.useCompactNavigationBar, isStoriesFeatureEnabled = Stories.isFeatureEnabled()) }
}
fun getNotificationProfiles(): Flow<List<NotificationProfile>> {
return notificationProfilesRepository.getProfiles().asFlow()
}
fun onChatsSelected() {
internalTabClickEvents.tryEmit(MainNavigationListLocation.CHATS)
internalMainNavigationState.update {
it.copy(selectedDestination = MainNavigationListLocation.CHATS)
}
}
fun onCallsSelected() {
internalTabClickEvents.tryEmit(MainNavigationListLocation.CALLS)
internalMainNavigationState.update {
it.copy(selectedDestination = MainNavigationListLocation.CALLS)
}
}
fun onStoriesSelected() {
internalTabClickEvents.tryEmit(MainNavigationListLocation.STORIES)
internalMainNavigationState.update {
it.copy(selectedDestination = MainNavigationListLocation.STORIES)
}
}
private fun <T : Any> performStoreUpdate(flow: Flow<T>, fn: (T, MainNavigationState) -> MainNavigationState) {
viewModelScope.launch {
flow.collectLatest { item ->
internalMainNavigationState.update { state -> fn(item, state) }
}
}
}
enum class NavigationEvent {
STORY_CAMERA_FIRST
}

View File

@@ -131,7 +131,7 @@ data class MainToolbarState(
val toolbarColor: Color? = null,
val self: Recipient = Recipient.self(),
val mode: MainToolbarMode = MainToolbarMode.FULL,
val destination: MainNavigationDestination = MainNavigationDestination.CHATS,
val destination: MainNavigationListLocation = MainNavigationListLocation.CHATS,
val chatFilter: ConversationFilter = ConversationFilter.OFF,
val callFilter: CallLogFilter = CallLogFilter.ALL,
val hasUnreadPayments: Boolean = false,
@@ -373,9 +373,9 @@ private fun PrimaryToolbar(
controller = controller
) {
when (state.destination) {
MainNavigationDestination.CHATS -> ChatDropdownItems(state, callback, dismiss)
MainNavigationDestination.CALLS -> CallDropdownItems(state.callFilter, callback, dismiss)
MainNavigationDestination.STORIES -> StoryDropDownItems(callback, dismiss)
MainNavigationListLocation.CHATS -> ChatDropdownItems(state, callback, dismiss)
MainNavigationListLocation.CALLS -> CallDropdownItems(state.callFilter, callback, dismiss)
MainNavigationListLocation.STORIES -> StoryDropDownItems(callback, dismiss)
}
}
}
@@ -671,7 +671,7 @@ private fun FullMainToolbarPreview() {
state = MainToolbarState(
self = Recipient(isResolving = false),
mode = mode,
destination = MainNavigationDestination.CHATS,
destination = MainNavigationListLocation.CHATS,
hasEnabledNotificationProfile = true,
proxyState = MainToolbarState.ProxyState.CONNECTED,
hasFailedBackups = true

View File

@@ -63,7 +63,7 @@ class MainToolbarViewModel : ViewModel() {
}
@JvmOverloads
fun setToolbarMode(mode: MainToolbarMode, destination: MainNavigationDestination? = null) {
fun setToolbarMode(mode: MainToolbarMode, destination: MainNavigationListLocation? = null) {
val previousMode = internalStateFlow.value.mode
internalStateFlow.update {

View File

@@ -31,7 +31,7 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.main.MainNavigationDestination
import org.thoughtcrime.securesms.main.MainNavigationListLocation
import org.thoughtcrime.securesms.main.MainNavigationViewModel
import org.thoughtcrime.securesms.main.MainToolbarMode
import org.thoughtcrime.securesms.main.MainToolbarViewModel
@@ -44,7 +44,6 @@ import org.thoughtcrime.securesms.stories.StoryViewerArgs
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs
import org.thoughtcrime.securesms.stories.my.MyStoriesActivity
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel
import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
@@ -73,7 +72,6 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
}
)
private val tabsViewModel: ConversationListTabsViewModel by viewModels(ownerProducer = { requireActivity() })
private val mainToolbarViewModel: MainToolbarViewModel by activityViewModels()
private val mainNavigationViewModel: MainNavigationViewModel by activityViewModels()
@@ -156,14 +154,14 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (!closeSearchIfOpen()) {
tabsViewModel.onChatsSelected()
mainNavigationViewModel.onChatsSelected()
}
}
}
)
lifecycleDisposable += tabsViewModel.tabClickEvents
.filter { it == MainNavigationDestination.STORIES }
lifecycleDisposable += mainNavigationViewModel.tabClickEvents
.filter { it == MainNavigationListLocation.STORIES }
.subscribeBy(onNext = {
val layoutManager = recyclerView?.layoutManager as? LinearLayoutManager ?: return@subscribeBy
if (layoutManager.findFirstVisibleItemPosition() <= LIST_SMOOTH_SCROLL_TO_TOP_THRESHOLD) {

View File

@@ -1,116 +0,0 @@
package org.thoughtcrime.securesms.stories.tabs
import android.os.Bundle
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rxjava3.subscribeAsState
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.main.MainNavigationBar
import org.thoughtcrime.securesms.main.MainNavigationDestination
import org.thoughtcrime.securesms.main.MainNavigationRail
import org.thoughtcrime.securesms.main.MainNavigationState
import org.thoughtcrime.securesms.main.MainToolbarMode
import org.thoughtcrime.securesms.main.MainToolbarViewModel
import org.thoughtcrime.securesms.window.Navigation
import org.thoughtcrime.securesms.window.WindowSizeClass
/**
* Displays the "Chats" and "Stories" tab to a user.
*/
class ConversationListTabsFragment : ComposeFragment() {
private val viewModel: ConversationListTabsViewModel by viewModels(ownerProducer = { requireActivity() })
private val mainToolbarViewModel: MainToolbarViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mainToolbarViewModel.state.map { it.mode }.collectLatest {
when (it) {
MainToolbarMode.ACTION_MODE -> {
viewModel.onMultiSelectStarted()
viewModel.onSearchClosed()
}
MainToolbarMode.FULL -> {
viewModel.onMultiSelectFinished()
viewModel.onSearchClosed()
viewModel.isShowingArchived(false)
}
MainToolbarMode.BASIC -> {
viewModel.onMultiSelectFinished()
viewModel.onSearchClosed()
viewModel.isShowingArchived(true)
}
MainToolbarMode.SEARCH -> {
viewModel.onMultiSelectFinished()
viewModel.onSearchOpened()
}
}
}
}
}
}
@Composable
override fun FragmentContent() {
val state by viewModel.state.subscribeAsState(ConversationListTabsState())
val navState = remember(state) {
MainNavigationState(
chatsCount = state.unreadMessagesCount.toInt(),
callsCount = state.unreadCallsCount.toInt(),
storiesCount = state.unreadStoriesCount.toInt(),
storyFailure = state.hasFailedStory,
selectedDestination = when (state.tab) {
MainNavigationDestination.CHATS -> MainNavigationDestination.CHATS
MainNavigationDestination.CALLS -> MainNavigationDestination.CALLS
MainNavigationDestination.STORIES -> MainNavigationDestination.STORIES
},
compact = state.compact,
isStoriesFeatureEnabled = state.isStoriesFeatureEnabled
)
}
if (state.visibilityState.isVisible()) {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val onDestinationSelected: (MainNavigationDestination) -> Unit = remember {
{
when (it) {
MainNavigationDestination.CHATS -> viewModel.onChatsSelected()
MainNavigationDestination.CALLS -> viewModel.onCallsSelected()
MainNavigationDestination.STORIES -> viewModel.onStoriesSelected()
}
}
}
if (windowSizeClass.navigation == Navigation.BAR) {
MainNavigationBar(
state = navState,
onDestinationSelected = onDestinationSelected
)
} else {
MainNavigationRail(
state = navState,
onDestinationSelected = onDestinationSelected
)
}
}
}
override fun onResume() {
super.onResume()
viewModel.refreshNavigationBarState()
}
}

View File

@@ -1,27 +0,0 @@
package org.thoughtcrime.securesms.stories.tabs
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.main.MainNavigationDestination
import org.thoughtcrime.securesms.stories.Stories
data class ConversationListTabsState(
val tab: MainNavigationDestination = MainNavigationDestination.CHATS,
val prevTab: MainNavigationDestination = if (tab == MainNavigationDestination.CHATS) MainNavigationDestination.STORIES else MainNavigationDestination.CHATS,
val unreadMessagesCount: Long = 0L,
val unreadCallsCount: Long = 0L,
val unreadStoriesCount: Long = 0L,
val hasFailedStory: Boolean = false,
val visibilityState: VisibilityState = VisibilityState(),
val compact: Boolean = SignalStore.settings.useCompactNavigationBar,
val isStoriesFeatureEnabled: Boolean = Stories.isFeatureEnabled()
) {
data class VisibilityState(
val isSearchOpen: Boolean = false,
val isMultiSelectOpen: Boolean = false,
val isShowingArchived: Boolean = false
) {
fun isVisible(): Boolean {
return !isSearchOpen && !isMultiSelectOpen && !isShowingArchived
}
}
}

View File

@@ -1,127 +0,0 @@
package org.thoughtcrime.securesms.stories.tabs
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.main.MainNavigationDestination
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.rx.RxStore
class ConversationListTabsViewModel(startingTab: MainNavigationDestination, repository: ConversationListTabRepository) : ViewModel() {
private val notificationProfilesRepository: NotificationProfilesRepository = NotificationProfilesRepository()
private val store = RxStore(ConversationListTabsState(tab = startingTab))
val stateSnapshot: ConversationListTabsState
get() = store.state
val state: Flowable<ConversationListTabsState> = store.stateFlowable.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread())
val disposables = CompositeDisposable()
private val internalTabClickEvents: Subject<MainNavigationDestination> = PublishSubject.create()
val tabClickEvents: Observable<MainNavigationDestination> = internalTabClickEvents.filter { Stories.isFeatureEnabled() }
init {
disposables += performStoreUpdate(repository.getNumberOfUnreadMessages()) { unreadChats, state ->
state.copy(unreadMessagesCount = unreadChats)
}
disposables += performStoreUpdate(repository.getNumberOfUnseenCalls()) { unseenCalls, state ->
state.copy(unreadCallsCount = unseenCalls)
}
disposables += performStoreUpdate(repository.getNumberOfUnseenStories()) { unseenStories, state ->
state.copy(unreadStoriesCount = unseenStories)
}
disposables += performStoreUpdate(repository.getHasFailedOutgoingStories()) { hasFailedStories, state ->
state.copy(hasFailedStory = hasFailedStories)
}
}
fun refreshNavigationBarState() {
store.update { it.copy(compact = SignalStore.settings.useCompactNavigationBar, isStoriesFeatureEnabled = Stories.isFeatureEnabled()) }
}
override fun onCleared() {
disposables.clear()
}
fun getNotificationProfiles(): Flowable<List<NotificationProfile>> {
return notificationProfilesRepository.getProfiles()
.observeOn(AndroidSchedulers.mainThread())
}
fun onChatsSelected() {
internalTabClickEvents.onNext(MainNavigationDestination.CHATS)
performStoreUpdate { it.copy(tab = MainNavigationDestination.CHATS) }
}
fun onCallsSelected() {
internalTabClickEvents.onNext(MainNavigationDestination.CALLS)
performStoreUpdate { it.copy(tab = MainNavigationDestination.CALLS) }
}
fun onStoriesSelected() {
internalTabClickEvents.onNext(MainNavigationDestination.STORIES)
performStoreUpdate { it.copy(tab = MainNavigationDestination.STORIES) }
}
fun onSearchOpened() {
performStoreUpdate { it.copy(visibilityState = it.visibilityState.copy(isSearchOpen = true)) }
}
fun onSearchClosed() {
performStoreUpdate { it.copy(visibilityState = it.visibilityState.copy(isSearchOpen = false)) }
}
fun onMultiSelectStarted() {
performStoreUpdate { it.copy(visibilityState = it.visibilityState.copy(isMultiSelectOpen = true)) }
}
fun isMultiSelectOpen(): Boolean {
return store.state.visibilityState.isMultiSelectOpen
}
fun onMultiSelectFinished() {
performStoreUpdate { it.copy(visibilityState = it.visibilityState.copy(isMultiSelectOpen = false)) }
}
fun isShowingArchived(isShowingArchived: Boolean) {
performStoreUpdate { it.copy(visibilityState = it.visibilityState.copy(isShowingArchived = isShowingArchived)) }
}
private fun performStoreUpdate(fn: (ConversationListTabsState) -> ConversationListTabsState) {
store.update {
fn(it.copy(prevTab = it.tab))
}
}
private fun <T : Any> performStoreUpdate(flowable: Flowable<T>, fn: (T, ConversationListTabsState) -> ConversationListTabsState): Disposable {
return store.update(flowable) { t, state ->
fn(t, state.copy(prevTab = state.tab))
}
}
class Factory(private val startingTab: MainNavigationDestination?, private val repository: ConversationListTabRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val tab = if (startingTab == null || (startingTab == MainNavigationDestination.STORIES && !Stories.isFeatureEnabled())) {
MainNavigationDestination.CHATS
} else {
startingTab
}
return modelClass.cast(ConversationListTabsViewModel(tab, repository)) as T
}
}
}