Remove MainListHostFragment and rescope list vms to the activity.

This commit is contained in:
Alex Hart
2025-05-05 11:54:19 -03:00
committed by Michelle Tang
parent bc94a92f68
commit 6d04c8ba42
20 changed files with 293 additions and 423 deletions

View File

@@ -13,6 +13,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.widget.Toast import android.widget.Toast
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
@@ -37,11 +38,11 @@ import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -50,18 +51,24 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.compose.AndroidFragment import androidx.fragment.compose.AndroidFragment
import androidx.fragment.compose.rememberFragmentState import androidx.fragment.compose.rememberFragmentState
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.ui.compose.theme.SignalTheme import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.getSerializableCompat import org.signal.core.util.getSerializableCompat
import org.signal.core.util.logging.Log
import org.signal.donations.StripeApi import org.signal.donations.StripeApi
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show
import org.thoughtcrime.securesms.calls.log.CallLogFilter import org.thoughtcrime.securesms.calls.log.CallLogFilter
import org.thoughtcrime.securesms.calls.log.CallLogFragment
import org.thoughtcrime.securesms.calls.new.NewCallActivity import org.thoughtcrime.securesms.calls.new.NewCallActivity
import org.thoughtcrime.securesms.components.ConnectivityWarningBottomSheet import org.thoughtcrime.securesms.components.ConnectivityWarningBottomSheet
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment
@@ -76,14 +83,16 @@ import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.conversation.v2.ConversationFragment import org.thoughtcrime.securesms.conversation.v2.ConversationFragment
import org.thoughtcrime.securesms.conversation.v2.MotionEventRelay import org.thoughtcrime.securesms.conversation.v2.MotionEventRelay
import org.thoughtcrime.securesms.conversation.v2.ShareDataTimestampViewModel import org.thoughtcrime.securesms.conversation.v2.ShareDataTimestampViewModel
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment
import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSheetFragment import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSheetFragment
import org.thoughtcrime.securesms.conversationlist.RestoreCompleteBottomSheetDialog import org.thoughtcrime.securesms.conversationlist.RestoreCompleteBottomSheetDialog
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
import org.thoughtcrime.securesms.main.MainActivityListHostFragment
import org.thoughtcrime.securesms.main.MainBottomChrome import org.thoughtcrime.securesms.main.MainBottomChrome
import org.thoughtcrime.securesms.main.MainBottomChromeCallback import org.thoughtcrime.securesms.main.MainBottomChromeCallback
import org.thoughtcrime.securesms.main.MainBottomChromeState import org.thoughtcrime.securesms.main.MainBottomChromeState
@@ -97,7 +106,9 @@ import org.thoughtcrime.securesms.main.MainNavigationViewModel
import org.thoughtcrime.securesms.main.MainToolbar import org.thoughtcrime.securesms.main.MainToolbar
import org.thoughtcrime.securesms.main.MainToolbarCallback import org.thoughtcrime.securesms.main.MainToolbarCallback
import org.thoughtcrime.securesms.main.MainToolbarMode import org.thoughtcrime.securesms.main.MainToolbarMode
import org.thoughtcrime.securesms.main.MainToolbarState
import org.thoughtcrime.securesms.main.MainToolbarViewModel import org.thoughtcrime.securesms.main.MainToolbarViewModel
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
import org.thoughtcrime.securesms.main.NavigationBarSpacerCompat import org.thoughtcrime.securesms.main.NavigationBarSpacerCompat
import org.thoughtcrime.securesms.main.SnackbarState import org.thoughtcrime.securesms.main.SnackbarState
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
@@ -107,25 +118,35 @@ import org.thoughtcrime.securesms.megaphone.MegaphoneActionController
import org.thoughtcrime.securesms.megaphone.Megaphones import org.thoughtcrime.securesms.megaphone.Megaphones
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
import org.thoughtcrime.securesms.notifications.VitalsViewModel import org.thoughtcrime.securesms.notifications.VitalsViewModel
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment
import org.thoughtcrime.securesms.service.KeyCachingService import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.stories.landing.StoriesLandingFragment
import org.thoughtcrime.securesms.stories.settings.StorySettingsActivity import org.thoughtcrime.securesms.stories.settings.StorySettingsActivity
import org.thoughtcrime.securesms.util.AppForegroundObserver import org.thoughtcrime.securesms.util.AppForegroundObserver
import org.thoughtcrime.securesms.util.AppStartup import org.thoughtcrime.securesms.util.AppStartup
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.CachedInflater import org.thoughtcrime.securesms.util.CachedInflater
import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.DynamicTheme import org.thoughtcrime.securesms.util.DynamicTheme
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.SplashScreenUtil import org.thoughtcrime.securesms.util.SplashScreenUtil
import org.thoughtcrime.securesms.util.TopToastPopup
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.viewModel import org.thoughtcrime.securesms.util.viewModel
import org.thoughtcrime.securesms.window.AppScaffold import org.thoughtcrime.securesms.window.AppScaffold
import org.thoughtcrime.securesms.window.WindowSizeClass import org.thoughtcrime.securesms.window.WindowSizeClass
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider { class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider, Material3OnScrollHelperBinder, ConversationListFragment.Callback, CallLogFragment.Callback {
companion object { companion object {
private val TAG = Log.tag(MainActivity::class)
private const val KEY_STARTING_TAB = "STARTING_TAB" private const val KEY_STARTING_TAB = "STARTING_TAB"
const val RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901 const val RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901
@@ -172,6 +193,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
private val motionEventRelay: MotionEventRelay by viewModels() private val motionEventRelay: MotionEventRelay by viewModels()
private var onFirstRender = false private var onFirstRender = false
private var previousTopToastPopup: TopToastPopup? = null
private val mainBottomChromeCallback = BottomChromeCallback() private val mainBottomChromeCallback = BottomChromeCallback()
private val megaphoneActionController = MainMegaphoneActionController() private val megaphoneActionController = MainMegaphoneActionController()
@@ -202,27 +224,49 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
} }
}) })
UnreadPaymentsLiveData().observe(this) { unread ->
toolbarViewModel.setHasUnreadPayments(unread.isPresent)
}
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) { launch {
mainNavigationViewModel.navigationEvents.collectLatest { repeatOnLifecycle(Lifecycle.State.RESUMED) {
when (it) { mainNavigationViewModel.navigationEvents.collectLatest {
MainNavigationViewModel.NavigationEvent.STORY_CAMERA_FIRST -> { when (it) {
mainBottomChromeCallback.onCameraClick(MainNavigationListLocation.STORIES) MainNavigationViewModel.NavigationEvent.STORY_CAMERA_FIRST -> {
mainBottomChromeCallback.onCameraClick(MainNavigationListLocation.STORIES)
}
} }
} }
} }
} }
launch {
mainNavigationViewModel.getNotificationProfiles().collectLatest { profiles ->
withContext(Dispatchers.Main) {
updateNotificationProfileStatus(profiles)
}
}
}
} }
shareDataTimestampViewModel.setTimestampFromActivityCreation(savedInstanceState, intent) shareDataTimestampViewModel.setTimestampFromActivityCreation(savedInstanceState, intent)
setContent { setContent {
val listHostState = rememberFragmentState()
val snackbar by mainNavigationViewModel.snackbar.collectAsStateWithLifecycle() val snackbar by mainNavigationViewModel.snackbar.collectAsStateWithLifecycle()
val mainToolbarState by toolbarViewModel.state.collectAsStateWithLifecycle() val mainToolbarState by toolbarViewModel.state.collectAsStateWithLifecycle()
val megaphone by mainNavigationViewModel.megaphone.collectAsStateWithLifecycle() val megaphone by mainNavigationViewModel.megaphone.collectAsStateWithLifecycle()
val mainNavigationState by mainNavigationViewModel.mainNavigationState.collectAsStateWithLifecycle() val mainNavigationState by mainNavigationViewModel.mainNavigationState.collectAsStateWithLifecycle()
LaunchedEffect(mainNavigationState.selectedDestination) {
when (mainNavigationState.selectedDestination) {
MainNavigationListLocation.CHATS -> toolbarViewModel.presentToolbarForConversationListFragment()
MainNavigationListLocation.ARCHIVE -> toolbarViewModel.presentToolbarForConversationListArchiveFragment()
MainNavigationListLocation.CALLS -> toolbarViewModel.presentToolbarForCallLogFragment()
MainNavigationListLocation.STORIES -> toolbarViewModel.presentToolbarForStoriesLandingFragment()
}
}
val isNavigationVisible = remember(mainToolbarState.mode) { val isNavigationVisible = remember(mainToolbarState.mode) {
mainToolbarState.mode == MainToolbarMode.FULL mainToolbarState.mode == MainToolbarMode.FULL
} }
@@ -296,11 +340,40 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
Box( Box(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
AndroidFragment( when (val destination = mainNavigationState.selectedDestination) {
clazz = MainActivityListHostFragment::class.java, MainNavigationListLocation.CHATS -> {
fragmentState = listHostState, val state = key(destination) { rememberFragmentState() }
modifier = Modifier.fillMaxSize() AndroidFragment(
) clazz = ConversationListFragment::class.java,
fragmentState = state,
modifier = Modifier.fillMaxSize()
)
}
MainNavigationListLocation.ARCHIVE -> {
val state = key(destination) { rememberFragmentState() }
AndroidFragment(
clazz = ConversationListArchiveFragment::class.java,
fragmentState = state,
modifier = Modifier.fillMaxSize()
)
}
MainNavigationListLocation.CALLS -> {
val state = key(destination) { rememberFragmentState() }
AndroidFragment(
clazz = CallLogFragment::class.java,
fragmentState = state,
modifier = Modifier.fillMaxSize()
)
}
MainNavigationListLocation.STORIES -> {
val state = key(destination) { rememberFragmentState() }
AndroidFragment(
clazz = StoriesLandingFragment::class.java,
fragmentState = state,
modifier = Modifier.fillMaxSize()
)
}
}
MainBottomChrome( MainBottomChrome(
state = mainBottomChromeState, state = mainBottomChromeState,
@@ -433,6 +506,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
when (startingTab) { when (startingTab) {
MainNavigationListLocation.CHATS -> mainNavigationViewModel.onChatsSelected() MainNavigationListLocation.CHATS -> mainNavigationViewModel.onChatsSelected()
MainNavigationListLocation.ARCHIVE -> mainNavigationViewModel.onArchiveSelected()
MainNavigationListLocation.CALLS -> mainNavigationViewModel.onCallsSelected() MainNavigationListLocation.CALLS -> mainNavigationViewModel.onCallsSelected()
MainNavigationListLocation.STORIES -> { MainNavigationListLocation.STORIES -> {
if (Stories.isFeatureEnabled()) { if (Stories.isFeatureEnabled()) {
@@ -453,6 +527,8 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
super.onResume() super.onResume()
dynamicTheme.onResume(this) dynamicTheme.onResume(this)
toolbarViewModel.refresh()
if (SignalStore.misc.shouldShowLinkedDevicesReminder) { if (SignalStore.misc.shouldShowLinkedDevicesReminder) {
SignalStore.misc.shouldShowLinkedDevicesReminder = false SignalStore.misc.shouldShowLinkedDevicesReminder = false
RelinkDevicesReminderBottomSheetFragment.show(supportFragmentManager) RelinkDevicesReminderBottomSheetFragment.show(supportFragmentManager)
@@ -516,6 +592,56 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
return navigator return navigator
} }
override fun bindScrollHelper(recyclerView: RecyclerView, lifecycleOwner: LifecycleOwner) {
Material3OnScrollHelper(
activity = this,
views = listOf(),
viewStubs = listOf(),
onSetToolbarColor = {
toolbarViewModel.setToolbarColor(it)
},
setStatusBarColor = {},
lifecycleOwner = lifecycleOwner
).attach(recyclerView)
}
override fun bindScrollHelper(recyclerView: RecyclerView, lifecycleOwner: LifecycleOwner, chatFolders: RecyclerView, setChatFolder: (Int) -> Unit) {
Material3OnScrollHelper(
activity = this,
views = listOf(chatFolders),
viewStubs = listOf(),
setStatusBarColor = {},
onSetToolbarColor = {
toolbarViewModel.setToolbarColor(it)
},
lifecycleOwner = lifecycleOwner,
setChatFolderColor = setChatFolder
).attach(recyclerView)
}
override fun updateProxyStatus(state: WebSocketConnectionState) {
if (SignalStore.proxy.isProxyEnabled) {
val proxyState: MainToolbarState.ProxyState = when (state) {
WebSocketConnectionState.CONNECTING, WebSocketConnectionState.DISCONNECTING, WebSocketConnectionState.DISCONNECTED -> MainToolbarState.ProxyState.CONNECTING
WebSocketConnectionState.CONNECTED -> MainToolbarState.ProxyState.CONNECTED
WebSocketConnectionState.AUTHENTICATION_FAILED, WebSocketConnectionState.FAILED, WebSocketConnectionState.REMOTE_DEPRECATED -> MainToolbarState.ProxyState.FAILED
else -> MainToolbarState.ProxyState.NONE
}
toolbarViewModel.setProxyState(proxyState = proxyState)
} else {
toolbarViewModel.setProxyState(proxyState = MainToolbarState.ProxyState.NONE)
}
}
override fun onMultiSelectStarted() {
toolbarViewModel.presentToolbarForMultiselect()
}
override fun onMultiSelectFinished() {
toolbarViewModel.presentToolbarForCurrentDestination()
}
private fun handleDeepLinkIntent(intent: Intent) { private fun handleDeepLinkIntent(intent: Intent) {
handleConversationIntent(intent) handleConversationIntent(intent)
handleGroupLinkInIntent(intent) handleGroupLinkInIntent(intent)
@@ -578,6 +704,44 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
} }
} }
private fun updateNotificationProfileStatus(notificationProfiles: List<NotificationProfile>) {
val activeProfile = NotificationProfiles.getActiveProfile(notificationProfiles)
if (activeProfile != null) {
if (activeProfile.id != SignalStore.notificationProfile.lastProfilePopup) {
val view = findViewById<ViewGroup>(android.R.id.content)
view.postDelayed({
try {
var fragmentView = view ?: return@postDelayed
SignalStore.notificationProfile.lastProfilePopup = activeProfile.id
SignalStore.notificationProfile.lastProfilePopupTime = System.currentTimeMillis()
if (previousTopToastPopup?.isShowing == true) {
previousTopToastPopup?.dismiss()
}
val fragment = supportFragmentManager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
if (fragment != null && fragment.isAdded && fragment.view != null) {
fragmentView = fragment.requireView() as ViewGroup
}
previousTopToastPopup = TopToastPopup.show(fragmentView, 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)
}
toolbarViewModel.setNotificationProfileEnabled(true)
} else {
toolbarViewModel.setNotificationProfileEnabled(false)
}
if (!SignalStore.notificationProfile.hasSeenTooltip && Util.hasItems(notificationProfiles)) {
toolbarViewModel.setShowNotificationProfilesTooltip(true)
}
}
inner class ToolbarCallback : MainToolbarCallback { inner class ToolbarCallback : MainToolbarCallback {
override fun onNewGroupClick() { override fun onNewGroupClick() {
@@ -744,6 +908,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
MainNavigationListLocation.CHATS -> mainNavigationViewModel.onChatsSelected() MainNavigationListLocation.CHATS -> mainNavigationViewModel.onChatsSelected()
MainNavigationListLocation.CALLS -> mainNavigationViewModel.onCallsSelected() MainNavigationListLocation.CALLS -> mainNavigationViewModel.onCallsSelected()
MainNavigationListLocation.STORIES -> mainNavigationViewModel.onStoriesSelected() MainNavigationListLocation.STORIES -> mainNavigationViewModel.onStoriesSelected()
MainNavigationListLocation.ARCHIVE -> mainNavigationViewModel.onArchiveSelected()
} }
} }
} }

View File

@@ -9,13 +9,18 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.map import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.rx3.asFlow
import org.thoughtcrime.securesms.components.AvatarImageView import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails
import org.thoughtcrime.securesms.profiles.AvatarHelper import org.thoughtcrime.securesms.profiles.AvatarHelper
@@ -36,11 +41,21 @@ fun AvatarImage(
) )
} else { } else {
val context = LocalContext.current val context = LocalContext.current
val state = recipient.live().liveData.map { AvatarImageState(NameUtil.getAbbreviation(it.getDisplayName(context)), it, AvatarHelper.getAvatarFileDetails(context, it.id)) }.observeAsState().value ?: return var state: AvatarImageState by remember {
mutableStateOf(AvatarImageState(null, recipient, ProfileAvatarFileDetails.NO_DETAILS))
}
LaunchedEffect(recipient.id) {
Recipient.observable(recipient.id).asFlow()
.collectLatest {
state = AvatarImageState(NameUtil.getAbbreviation(it.getDisplayName(context)), it, AvatarHelper.getAvatarFileDetails(context, it.id))
}
}
AndroidView( AndroidView(
factory = { factory = {
AvatarImageView(context).apply { AvatarImageView(context).apply {
initialize(context, null)
this.contentDescription = contentDescription this.contentDescription = contentDescription
} }
}, },

View File

@@ -34,7 +34,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult import androidx.fragment.app.setFragmentResult
import androidx.navigation.fragment.navArgs
import org.signal.core.ui.compose.Buttons import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.Scaffolds import org.signal.core.ui.compose.Scaffolds
import org.signal.core.util.BreakIteratorCompat import org.signal.core.util.BreakIteratorCompat
@@ -45,11 +44,11 @@ class EditCallLinkNameDialogFragment : ComposeDialogFragment() {
companion object { companion object {
const val RESULT_KEY = "edit_call_link_name" const val RESULT_KEY = "edit_call_link_name"
const val ARG_NAME = "name"
private const val MAX_CHARACTER_COUNT = 32
} }
private val args: EditCallLinkNameDialogFragmentArgs by navArgs() private val argName: String
get() = requireArguments().getString(ARG_NAME)!!
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -69,8 +68,8 @@ class EditCallLinkNameDialogFragment : ComposeDialogFragment() {
var callName by remember { var callName by remember {
mutableStateOf( mutableStateOf(
TextFieldValue( TextFieldValue(
text = args.name, text = argName,
selection = TextRange(args.name.length) selection = TextRange(argName.length)
) )
) )
} }

View File

@@ -33,9 +33,9 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController
import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.ui.compose.BottomSheets import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons import org.signal.core.ui.compose.Buttons
@@ -127,9 +127,9 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
private fun onAddACallNameClicked() { private fun onAddACallNameClicked() {
val snapshot = viewModel.callLink.value val snapshot = viewModel.callLink.value
findNavController().navigate( EditCallLinkNameDialogFragment().apply {
CreateCallLinkBottomSheetDialogFragmentDirections.actionCreateCallLinkBottomSheetToEditCallLinkNameDialogFragment(snapshot.state.name) arguments = bundleOf(EditCallLinkNameDialogFragment.ARG_NAME to snapshot.state.name)
) }.show(parentFragmentManager, null)
} }
private fun onJoinClicked() { private fun onJoinClicked() {

View File

@@ -13,7 +13,6 @@ import androidx.compose.material3.SnackbarDuration
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -26,6 +25,7 @@ import org.signal.core.util.concurrent.addTo
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.MainNavigator import org.thoughtcrime.securesms.MainNavigator
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.links.create.CreateCallLinkBottomSheetDialogFragment
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
import org.thoughtcrime.securesms.components.ProgressCardDialogFragment import org.thoughtcrime.securesms.components.ProgressCardDialogFragment
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
@@ -50,6 +50,7 @@ import org.thoughtcrime.securesms.main.MainToolbarViewModel
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
import org.thoughtcrime.securesms.main.SnackbarState import org.thoughtcrime.securesms.main.SnackbarState
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.doAfterNextLayout import org.thoughtcrime.securesms.util.doAfterNextLayout
@@ -293,7 +294,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
} }
override fun onCreateACallLinkClicked() { override fun onCreateACallLinkClicked() {
findNavController().navigate(R.id.createCallLinkBottomSheet) CreateCallLinkBottomSheetDialogFragment().show(parentFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
} }
override fun onCallClicked(callLogRow: CallLogRow.Call) { override fun onCallClicked(callLogRow: CallLogRow.Call) {

View File

@@ -9,12 +9,12 @@ import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.widget.Toast import android.widget.Toast
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.os.bundleOf
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.thoughtcrime.securesms.BaseActivity import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.links.CallLinks import org.thoughtcrime.securesms.calls.links.CallLinks
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragmentArgs
import org.thoughtcrime.securesms.components.webrtc.controls.CallInfoView import org.thoughtcrime.securesms.components.webrtc.controls.CallInfoView
import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
@@ -44,7 +44,7 @@ class CallInfoCallbacks(
override fun onEditNameClicked(name: String) { override fun onEditNameClicked(name: String) {
EditCallLinkNameDialogFragment().apply { EditCallLinkNameDialogFragment().apply {
arguments = EditCallLinkNameDialogFragmentArgs.Builder(name).build().toBundle() arguments = bundleOf(EditCallLinkNameDialogFragment.ARG_NAME to name)
}.show(activity.supportFragmentManager, null) }.show(activity.supportFragmentManager, null)
} }

View File

@@ -937,6 +937,7 @@ class ConversationFragment :
firstRender = false firstRender = false
binding.conversationItemRecycler.doAfterNextLayout { binding.conversationItemRecycler.doAfterNextLayout {
SignalLocalMetrics.ConversationOpen.onRenderFinished() SignalLocalMetrics.ConversationOpen.onRenderFinished()
(requireActivity() as? MainActivity)?.onFirstRender()
doAfterFirstRender() doAfterFirstRender()
} }
} }

View File

@@ -32,7 +32,7 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels import androidx.fragment.app.activityViewModels
import org.signal.core.ui.compose.BottomSheets import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview import org.signal.core.ui.compose.SignalPreview
@@ -59,7 +59,7 @@ class AddToFolderBottomSheet private constructor(private val onDismissListener:
OTHER(3) OTHER(3)
} }
private val viewModel: ConversationListViewModel by viewModels( private val viewModel: ConversationListViewModel.UnarchivedConversationListViewModel by activityViewModels(
factoryProducer = { factoryProducer = {
ConversationListViewModel.Factory(isArchived = false) ConversationListViewModel.Factory(isArchived = false)
} }

View File

@@ -72,6 +72,8 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
this.onConversationClickListener = onConversationClickListener; this.onConversationClickListener = onConversationClickListener;
this.onClearFilterClicked = onClearFilterClicked; this.onClearFilterClicked = onClearFilterClicked;
this.onFolderSettingsClicked = onFolderSettingsClicked; this.onFolderSettingsClicked = onFolderSettingsClicked;
setStateRestorationPolicy(StateRestorationPolicy.PREVENT_WHEN_EMPTY);
} }
@Override @Override

View File

@@ -53,7 +53,6 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
@@ -643,8 +642,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
@Override @Override
public void onShowArchiveClick() { public void onShowArchiveClick() {
if (viewModel.currentSelectedConversations().isEmpty()) { if (viewModel.currentSelectedConversations().isEmpty()) {
NavHostFragment.findNavController(this) mainNavigationViewModel.goTo(MainNavigationListLocation.ARCHIVE);
.navigate(ConversationListFragmentDirections.actionConversationListFragmentToConversationListArchiveFragment());
} }
} }
@@ -707,7 +705,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
} else if (event instanceof MainToolbarViewModel.Event.Chats.ClearFilter) { } else if (event instanceof MainToolbarViewModel.Event.Chats.ClearFilter) {
onClearFilterClick(); onClearFilterClick();
} else if (event instanceof MainToolbarViewModel.Event.Chats.CloseArchive) { } else if (event instanceof MainToolbarViewModel.Event.Chats.CloseArchive) {
NavHostFragment.findNavController(this).popBackStack(R.id.conversationListFragment, false); mainNavigationViewModel.goTo(MainNavigationListLocation.CHATS);
} }
}) })
); );
@@ -855,7 +853,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
} }
private void initializeViewModel() { private void initializeViewModel() {
viewModel = new ViewModelProvider(this, new ConversationListViewModel.Factory(isArchived())).get(ConversationListViewModel.class); Class<? extends ConversationListViewModel> viewModelClass = isArchived() ? ConversationListViewModel.ArchivedConversationListViewModel.class : ConversationListViewModel.UnarchivedConversationListViewModel.class;
viewModel = new ViewModelProvider(requireActivity(), new ConversationListViewModel.Factory(isArchived())).get(viewModelClass);
lifecycleDisposable.add(viewModel.getConversationsState().subscribe(this::onConversationListChanged)); lifecycleDisposable.add(viewModel.getConversationsState().subscribe(this::onConversationListChanged));
lifecycleDisposable.add(viewModel.getHasNoConversations().subscribe(this::updateEmptyState)); lifecycleDisposable.add(viewModel.getHasNoConversations().subscribe(this::updateEmptyState));
@@ -1389,7 +1388,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
} }
protected Callback requireCallback() { protected Callback requireCallback() {
return ((Callback) getParentFragment().getParentFragment()); return ((Callback) requireActivity());
} }
protected @PluralsRes int getArchivedSnackbarTitleRes() { protected @PluralsRes int getArchivedSnackbarTitleRes() {

View File

@@ -42,7 +42,7 @@ import org.thoughtcrime.securesms.util.rx.RxStore
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class ConversationListViewModel( sealed class ConversationListViewModel(
private val isArchived: Boolean, private val isArchived: Boolean,
private val savedStateHandle: SavedStateHandle private val savedStateHandle: SavedStateHandle
) : ViewModel() { ) : ViewModel() {
@@ -317,13 +317,20 @@ class ConversationListViewModel(
val pinnedCount: Int = 0 val pinnedCount: Int = 0
) )
class UnarchivedConversationListViewModel(savedStateHandle: SavedStateHandle) : ConversationListViewModel(isArchived = false, savedStateHandle = savedStateHandle)
class ArchivedConversationListViewModel(savedStateHandle: SavedStateHandle) : ConversationListViewModel(isArchived = true, savedStateHandle = savedStateHandle)
class Factory( class Factory(
private val isArchived: Boolean private val isArchived: Boolean
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T { override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val savedStateHandle = extras.createSavedStateHandle() val savedStateHandle = extras.createSavedStateHandle()
return modelClass.cast(ConversationListViewModel(isArchived, savedStateHandle))!! return if (isArchived) {
ArchivedConversationListViewModel(savedStateHandle) as T
} else {
UnarchivedConversationListViewModel(savedStateHandle) as T
}
} }
} }
} }

View File

@@ -1,282 +0,0 @@
package org.thoughtcrime.securesms.main
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
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 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
import org.thoughtcrime.securesms.calls.log.CallLogFragment
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment
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.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.TopToastPopup
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_fragment), ConversationListFragment.Callback, Material3OnScrollHelperBinder, CallLogFragment.Callback {
companion object {
private val TAG = Log.tag(MainActivityListHostFragment::class.java)
}
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)
UnreadPaymentsLiveData().observe(viewLifecycleOwner) { unread ->
toolbarViewModel.setHasUnreadPayments(unread.isPresent)
}
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)
}
}
}
}
launch {
mainNavigationViewModel.getNotificationProfiles().collectLatest { profiles ->
withContext(Dispatchers.Main) {
updateNotificationProfileStatus(profiles)
}
}
}
}
}
}
private fun getChildNavController(): NavController {
return requireView().findViewById<View>(R.id.fragment_container).findNavController()
}
private fun goToStateFromConversationList(state: MainNavigationState, navController: NavController) {
if (state.selectedDestination == MainNavigationListLocation.CHATS) {
return
} else {
val destination = if (state.selectedDestination == MainNavigationListLocation.STORIES) {
R.id.action_conversationListFragment_to_storiesLandingFragment
} else {
R.id.action_conversationListFragment_to_callLogFragment
}
navController.navigate(
destination,
null,
null
)
}
}
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: 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)
}
}
override fun onResume() {
super.onResume()
toolbarViewModel.refresh()
requireView()
.findViewById<View>(R.id.fragment_container)
.findNavController()
.addOnDestinationChangedListener(destinationChangedListener)
if (toolbarViewModel.state.value.mode == MainToolbarMode.ACTION_MODE) {
presentToolbarForMultiselect()
}
}
override fun onPause() {
super.onPause()
requireView()
.findViewById<View>(R.id.fragment_container)
.findNavController()
.removeOnDestinationChangedListener(destinationChangedListener)
}
private fun presentToolbarForConversationListFragment() {
toolbarViewModel.setToolbarMode(MainToolbarMode.FULL, destination = MainNavigationListLocation.CHATS, overwriteSearchMode = false)
}
private fun presentToolbarForConversationListArchiveFragment() {
toolbarViewModel.setToolbarMode(MainToolbarMode.BASIC, destination = MainNavigationListLocation.CHATS)
}
private fun presentToolbarForStoriesLandingFragment() {
toolbarViewModel.setToolbarMode(MainToolbarMode.FULL, destination = MainNavigationListLocation.STORIES)
}
private fun presentToolbarForCallLogFragment() {
toolbarViewModel.setToolbarMode(MainToolbarMode.FULL, destination = MainNavigationListLocation.CALLS)
}
private fun presentToolbarForMultiselect() {
toolbarViewModel.setToolbarMode(MainToolbarMode.ACTION_MODE)
}
override fun onDestroyView() {
previousTopToastPopup = null
super.onDestroyView()
}
override fun onMultiSelectStarted() {
presentToolbarForMultiselect()
}
override fun onMultiSelectFinished() {
val currentDestination: NavDestination? = requireView().findViewById<View>(R.id.fragment_container).findNavController().currentDestination
if (currentDestination != null) {
presentToolbarForDestination(currentDestination)
}
}
override fun updateProxyStatus(state: WebSocketConnectionState) {
if (SignalStore.proxy.isProxyEnabled) {
val proxyState: MainToolbarState.ProxyState = when (state) {
WebSocketConnectionState.CONNECTING, WebSocketConnectionState.DISCONNECTING, WebSocketConnectionState.DISCONNECTED -> MainToolbarState.ProxyState.CONNECTING
WebSocketConnectionState.CONNECTED -> MainToolbarState.ProxyState.CONNECTED
WebSocketConnectionState.AUTHENTICATION_FAILED, WebSocketConnectionState.FAILED, WebSocketConnectionState.REMOTE_DEPRECATED -> MainToolbarState.ProxyState.FAILED
else -> MainToolbarState.ProxyState.NONE
}
toolbarViewModel.setProxyState(proxyState = proxyState)
} else {
toolbarViewModel.setProxyState(proxyState = MainToolbarState.ProxyState.NONE)
}
}
private fun updateNotificationProfileStatus(notificationProfiles: List<NotificationProfile>) {
val activeProfile = NotificationProfiles.getActiveProfile(notificationProfiles)
if (activeProfile != null) {
if (activeProfile.id != SignalStore.notificationProfile.lastProfilePopup) {
view?.postDelayed({
try {
var fragmentView = view as? ViewGroup ?: return@postDelayed
SignalStore.notificationProfile.lastProfilePopup = activeProfile.id
SignalStore.notificationProfile.lastProfilePopupTime = System.currentTimeMillis()
if (previousTopToastPopup?.isShowing == true) {
previousTopToastPopup?.dismiss()
}
val fragment = parentFragmentManager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
if (fragment != null && fragment.isAdded && fragment.view != null) {
fragmentView = fragment.requireView() as ViewGroup
}
previousTopToastPopup = TopToastPopup.show(fragmentView, 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)
}
toolbarViewModel.setNotificationProfileEnabled(true)
} else {
toolbarViewModel.setNotificationProfileEnabled(false)
}
if (!SignalStore.notificationProfile.hasSeenTooltip && Util.hasItems(notificationProfiles)) {
toolbarViewModel.setShowNotificationProfilesTooltip(true)
}
}
private fun presentToolbarForDestination(destination: NavDestination) {
when (destination.id) {
R.id.conversationListFragment -> {
presentToolbarForConversationListFragment()
}
R.id.conversationListArchiveFragment -> {
presentToolbarForConversationListArchiveFragment()
}
R.id.storiesLandingFragment -> {
presentToolbarForStoriesLandingFragment()
}
R.id.callLogFragment -> {
presentToolbarForCallLogFragment()
}
}
}
private inner class DestinationChangedListener : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(controller: NavController, destination: NavDestination, arguments: Bundle?) {
presentToolbarForDestination(destination)
}
}
override fun bindScrollHelper(recyclerView: RecyclerView, lifecycleOwner: LifecycleOwner) {
Material3OnScrollHelper(
activity = requireActivity(),
views = listOf(),
viewStubs = listOf(),
onSetToolbarColor = {
toolbarViewModel.setToolbarColor(it)
},
setStatusBarColor = {},
lifecycleOwner = lifecycleOwner
).attach(recyclerView)
}
override fun bindScrollHelper(recyclerView: RecyclerView, lifecycleOwner: LifecycleOwner, chatFolders: RecyclerView, setChatFolder: (Int) -> Unit) {
Material3OnScrollHelper(
activity = requireActivity(),
views = listOf(chatFolders),
viewStubs = listOf(),
setStatusBarColor = {},
onSetToolbarColor = {
toolbarViewModel.setToolbarColor(it)
},
lifecycleOwner = lifecycleOwner,
setChatFolderColor = setChatFolder
).attach(recyclerView)
}
}

View File

@@ -170,6 +170,7 @@ private fun PrimaryActionButton(
) { ) {
val onClick = remember(destination) { val onClick = remember(destination) {
when (destination) { when (destination) {
MainNavigationListLocation.ARCHIVE -> error("Not supported")
MainNavigationListLocation.CHATS -> onNewChatClick MainNavigationListLocation.CHATS -> onNewChatClick
MainNavigationListLocation.CALLS -> onNewCallClick MainNavigationListLocation.CALLS -> onNewCallClick
MainNavigationListLocation.STORIES -> { MainNavigationListLocation.STORIES -> {
@@ -184,6 +185,7 @@ private fun PrimaryActionButton(
icon = { icon = {
AnimatedContent(destination) { targetState -> AnimatedContent(destination) { targetState ->
val (icon, contentDescriptionId) = when (targetState) { val (icon, contentDescriptionId) = when (targetState) {
MainNavigationListLocation.ARCHIVE -> error("Not supported")
MainNavigationListLocation.CHATS -> R.drawable.symbol_edit_24 to R.string.conversation_list_fragment__fab_content_description MainNavigationListLocation.CHATS -> R.drawable.symbol_edit_24 to R.string.conversation_list_fragment__fab_content_description
MainNavigationListLocation.CALLS -> R.drawable.symbol_phone_plus_24 to R.string.CallLogFragment__start_a_new_call MainNavigationListLocation.CALLS -> R.drawable.symbol_phone_plus_24 to R.string.CallLogFragment__start_a_new_call
MainNavigationListLocation.STORIES -> R.drawable.symbol_camera_24 to R.string.conversation_list_fragment__open_camera_description MainNavigationListLocation.STORIES -> R.drawable.symbol_camera_24 to R.string.conversation_list_fragment__open_camera_description

View File

@@ -68,6 +68,10 @@ enum class MainNavigationListLocation(
label = R.string.ConversationListTabs__chats, label = R.string.ConversationListTabs__chats,
icon = R.raw.chats_28 icon = R.raw.chats_28
), ),
ARCHIVE(
label = R.string.ConversationListTabs__chats,
icon = R.raw.chats_28
),
CALLS( CALLS(
label = R.string.ConversationListTabs__calls, label = R.string.ConversationListTabs__calls,
icon = R.raw.calls_28 icon = R.raw.calls_28
@@ -104,15 +108,16 @@ fun MainNavigationBar(
) { ) {
val entries = remember(state.isStoriesFeatureEnabled) { val entries = remember(state.isStoriesFeatureEnabled) {
if (state.isStoriesFeatureEnabled) { if (state.isStoriesFeatureEnabled) {
MainNavigationListLocation.entries MainNavigationListLocation.entries.filterNot { it == MainNavigationListLocation.ARCHIVE }
} else { } else {
MainNavigationListLocation.entries.filterNot { it == MainNavigationListLocation.STORIES } MainNavigationListLocation.entries.filterNot { it == MainNavigationListLocation.STORIES || it == MainNavigationListLocation.ARCHIVE }
} }
} }
entries.forEach { destination -> entries.forEach { destination ->
val badgeCount = when (destination) { val badgeCount = when (destination) {
MainNavigationListLocation.ARCHIVE -> error("Not supported")
MainNavigationListLocation.CHATS -> state.chatsCount MainNavigationListLocation.CHATS -> state.chatsCount
MainNavigationListLocation.CALLS -> state.callsCount MainNavigationListLocation.CALLS -> state.callsCount
MainNavigationListLocation.STORIES -> state.storiesCount MainNavigationListLocation.STORIES -> state.storiesCount
@@ -219,9 +224,9 @@ fun MainNavigationRail(
) { ) {
val entries = remember(state.isStoriesFeatureEnabled) { val entries = remember(state.isStoriesFeatureEnabled) {
if (state.isStoriesFeatureEnabled) { if (state.isStoriesFeatureEnabled) {
MainNavigationListLocation.entries MainNavigationListLocation.entries.filterNot { it == MainNavigationListLocation.ARCHIVE }
} else { } else {
MainNavigationListLocation.entries.filterNot { it == MainNavigationListLocation.STORIES } MainNavigationListLocation.entries.filterNot { it == MainNavigationListLocation.STORIES || it == MainNavigationListLocation.ARCHIVE }
} }
} }
@@ -262,6 +267,7 @@ private fun BoxScope.NavigationRailCountIndicator(
) { ) {
val count = remember(state, destination) { val count = remember(state, destination) {
when (destination) { when (destination) {
MainNavigationListLocation.ARCHIVE -> error("Not supported")
MainNavigationListLocation.CHATS -> state.chatsCount MainNavigationListLocation.CHATS -> state.chatsCount
MainNavigationListLocation.CALLS -> state.callsCount MainNavigationListLocation.CALLS -> state.callsCount
MainNavigationListLocation.STORIES -> state.storiesCount MainNavigationListLocation.STORIES -> state.storiesCount

View File

@@ -95,6 +95,15 @@ class MainNavigationViewModel(
val wrapped = LegacyNavigator(composeScope, threePaneScaffoldNavigator, goToLegacyDetailLocation) val wrapped = LegacyNavigator(composeScope, threePaneScaffoldNavigator, goToLegacyDetailLocation)
this.navigator = wrapped this.navigator = wrapped
if (previous != null) {
val destination = previous.currentDestination?.contentKey ?: return wrapped
if (destination is MainNavigationListLocation) {
goTo(destination)
}
} else {
goTo(mainNavigationState.value.selectedDestination)
}
if (previous != null) { if (previous != null) {
val destination = previous.currentDestination?.contentKey ?: return wrapped val destination = previous.currentDestination?.contentKey ?: return wrapped
if (destination is MainNavigationDetailLocation) { if (destination is MainNavigationDetailLocation) {
@@ -123,6 +132,12 @@ class MainNavigationViewModel(
} }
} }
fun goTo(location: MainNavigationListLocation) {
internalMainNavigationState.update {
it.copy(selectedDestination = location)
}
}
fun goToCameraFirstStoryCapture() { fun goToCameraFirstStoryCapture() {
viewModelScope.launch { viewModelScope.launch {
internalNavigationEvents.emit(NavigationEvent.STORY_CAMERA_FIRST) internalNavigationEvents.emit(NavigationEvent.STORY_CAMERA_FIRST)
@@ -168,6 +183,13 @@ class MainNavigationViewModel(
} }
} }
fun onArchiveSelected() {
internalTabClickEvents.tryEmit(MainNavigationListLocation.ARCHIVE)
internalMainNavigationState.update {
it.copy(selectedDestination = MainNavigationListLocation.ARCHIVE)
}
}
fun onCallsSelected() { fun onCallsSelected() {
internalTabClickEvents.tryEmit(MainNavigationListLocation.CALLS) internalTabClickEvents.tryEmit(MainNavigationListLocation.CALLS)
internalMainNavigationState.update { internalMainNavigationState.update {

View File

@@ -395,6 +395,7 @@ private fun PrimaryToolbar(
controller = controller controller = controller
) { ) {
when (state.destination) { when (state.destination) {
MainNavigationListLocation.ARCHIVE -> Unit
MainNavigationListLocation.CHATS -> ChatDropdownItems(state, callback, dismiss) MainNavigationListLocation.CHATS -> ChatDropdownItems(state, callback, dismiss)
MainNavigationListLocation.CALLS -> CallDropdownItems(state.callFilter, callback, dismiss) MainNavigationListLocation.CALLS -> CallDropdownItems(state.callFilter, callback, dismiss)
MainNavigationListLocation.STORIES -> StoryDropDownItems(callback, dismiss) MainNavigationListLocation.STORIES -> StoryDropDownItems(callback, dismiss)

View File

@@ -73,6 +73,33 @@ class MainToolbarViewModel : ViewModel() {
} }
} }
fun presentToolbarForConversationListFragment() {
setToolbarMode(MainToolbarMode.FULL, destination = MainNavigationListLocation.CHATS, overwriteSearchMode = false)
}
fun presentToolbarForConversationListArchiveFragment() {
setToolbarMode(MainToolbarMode.BASIC, destination = MainNavigationListLocation.CHATS)
}
fun presentToolbarForStoriesLandingFragment() {
setToolbarMode(MainToolbarMode.FULL, destination = MainNavigationListLocation.STORIES)
}
fun presentToolbarForCallLogFragment() {
setToolbarMode(MainToolbarMode.FULL, destination = MainNavigationListLocation.CALLS)
}
fun presentToolbarForMultiselect() {
setToolbarMode(MainToolbarMode.ACTION_MODE)
}
fun presentToolbarForCurrentDestination() {
when (state.value.destination) {
MainNavigationListLocation.ARCHIVE -> setToolbarMode(MainToolbarMode.BASIC)
else -> setToolbarMode(MainToolbarMode.FULL)
}
}
@JvmOverloads @JvmOverloads
fun setToolbarMode( fun setToolbarMode(
mode: MainToolbarMode, mode: MainToolbarMode,

View File

@@ -9,7 +9,6 @@ import androidx.compose.ui.platform.ComposeView
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
@@ -66,7 +65,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
private val lifecycleDisposable = LifecycleDisposable() private val lifecycleDisposable = LifecycleDisposable()
private val viewModel: StoriesLandingViewModel by viewModels( private val viewModel: StoriesLandingViewModel by activityViewModels(
factoryProducer = { factoryProducer = {
StoriesLandingViewModel.Factory(StoriesLandingRepository(requireContext())) StoriesLandingViewModel.Factory(StoriesLandingRepository(requireContext()))
} }

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:viewBindingIgnore="true">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:navGraph="@navigation/main_activity_list" />
</LinearLayout>

View File

@@ -1,76 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_activity_list"
app:startDestination="@id/conversationListFragment">
<fragment
android:id="@+id/conversationListFragment"
android:name="org.thoughtcrime.securesms.conversationlist.ConversationListFragment"
android:label="conversation_list_fragment">
<action
android:id="@+id/action_conversationListFragment_to_conversationListArchiveFragment"
app:destination="@id/conversationListArchiveFragment"
app:enterAnim="@anim/slide_from_end"
app:exitAnim="@anim/slide_to_start"
app:popEnterAnim="@anim/slide_from_start"
app:popExitAnim="@anim/slide_to_end" />
<action
android:id="@+id/action_conversationListFragment_to_storiesLandingFragment"
app:destination="@id/storiesLandingFragment" />
<action
android:id="@+id/action_conversationListFragment_to_callLogFragment"
app:destination="@id/callLogFragment" />
</fragment>
<fragment
android:id="@+id/conversationListArchiveFragment"
android:name="org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment"
android:label="conversation_list_archive_fragment" />
<fragment
android:id="@+id/storiesLandingFragment"
android:name="org.thoughtcrime.securesms.stories.landing.StoriesLandingFragment"
android:label="stories_landing_fragment">
<action
android:id="@+id/action_storiesLandingFragment_to_callLogFragment"
app:destination="@id/callLogFragment" />
</fragment>
<fragment
android:id="@+id/callLogFragment"
android:name="org.thoughtcrime.securesms.calls.log.CallLogFragment"
android:label="call_log_fragment">
<action
android:id="@+id/action_callLogFragment_to_storiesLandingFragment"
app:destination="@id/storiesLandingFragment" />
<action
android:id="@+id/action_callLogFragment_to_createCallLinkBottomSheet"
app:destination="@id/createCallLinkBottomSheet" />
</fragment>
<dialog
android:id="@+id/createCallLinkBottomSheet"
android:name="org.thoughtcrime.securesms.calls.links.create.CreateCallLinkBottomSheetDialogFragment"
android:label="create_call_link_bottom_sheet">
<action
android:id="@+id/action_createCallLinkBottomSheet_to_editCallLinkNameDialogFragment"
app:destination="@id/editCallLinkNameDialogFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_close_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
</dialog>
<dialog
android:id="@+id/editCallLinkNameDialogFragment"
android:name="org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment"
android:label="edit_call_link_name_fragment">
<argument
android:name="name"
app:argType="string"
app:nullable="false" />
</dialog>
</navigation>