mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-25 12:17:22 +00:00
Add proper adaptive material app scaffolding.
This commit is contained in:
@@ -5,35 +5,59 @@
|
||||
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
|
||||
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.compose.AndroidFragment
|
||||
import androidx.fragment.compose.rememberFragmentState
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getSerializableCompat
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
import org.thoughtcrime.securesms.calls.new.NewCallActivity
|
||||
import org.thoughtcrime.securesms.components.ConnectivityWarningBottomSheet
|
||||
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment
|
||||
import org.thoughtcrime.securesms.components.DeviceSpecificNotificationBottomSheet
|
||||
@@ -43,36 +67,55 @@ import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity.Co
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.manual.NotificationProfileSelectionFragment
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationFragment
|
||||
import org.thoughtcrime.securesms.conversation.v2.MotionEventRelay
|
||||
import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.conversationlist.RestoreCompleteBottomSheetDialog
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
|
||||
import org.thoughtcrime.securesms.main.MainActivityListHostFragment
|
||||
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.MainNavigationDetailLocation
|
||||
import org.thoughtcrime.securesms.main.MainNavigationViewModel
|
||||
import org.thoughtcrime.securesms.main.MainToolbar
|
||||
import org.thoughtcrime.securesms.main.MainToolbarCallback
|
||||
import org.thoughtcrime.securesms.main.MainToolbarMode
|
||||
import org.thoughtcrime.securesms.main.MainToolbarViewModel
|
||||
import org.thoughtcrime.securesms.main.SnackbarState
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneActionController
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
|
||||
import org.thoughtcrime.securesms.notifications.VitalsViewModel
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
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
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.SplashScreenUtil
|
||||
import org.thoughtcrime.securesms.util.WindowUtil
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import org.thoughtcrime.securesms.window.AppScaffold
|
||||
import org.thoughtcrime.securesms.window.WindowSizeClass
|
||||
|
||||
class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider {
|
||||
|
||||
@@ -119,57 +162,153 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
private val toolbarViewModel: MainToolbarViewModel by viewModels()
|
||||
private val toolbarCallback = ToolbarCallback()
|
||||
|
||||
private val motionEventRelay: MotionEventRelay by viewModels()
|
||||
|
||||
private var onFirstRender = false
|
||||
|
||||
private val mainBottomChromeCallback = BottomChromeCallback()
|
||||
private val megaphoneActionController = MainMegaphoneActionController()
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||
return motionEventRelay.offer(ev) || super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
enableEdgeToEdge()
|
||||
|
||||
AppStartup.getInstance().onCriticalRenderEventStart()
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
conversationListTabsViewModel
|
||||
|
||||
AppForegroundObserver.addListener(object : AppForegroundObserver.Listener {
|
||||
override fun onForeground() {
|
||||
navigator.viewModel.getNextMegaphone()
|
||||
}
|
||||
})
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
navigator.viewModel.navigationEvents.collectLatest {
|
||||
when (it) {
|
||||
MainNavigationViewModel.NavigationEvent.STORY_CAMERA_FIRST -> {
|
||||
mainBottomChromeCallback.onCameraClick(MainNavigationDestination.STORIES)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val navState = rememberFragmentState()
|
||||
val listHostState = rememberFragmentState()
|
||||
val detailLocation by navigator.viewModel.detailLocation.collectAsStateWithLifecycle(MainNavigationDetailLocation.Empty)
|
||||
val snackbar by navigator.viewModel.snackbar.collectAsStateWithLifecycle()
|
||||
val mainToolbarState by toolbarViewModel.state.collectAsStateWithLifecycle()
|
||||
val megaphone by navigator.viewModel.megaphone.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(detailLocation) {
|
||||
if (detailLocation is MainNavigationDetailLocation.Conversation) {
|
||||
startActivity((detailLocation as MainNavigationDetailLocation.Conversation).intent)
|
||||
overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out)
|
||||
val mainBottomChromeState = remember(mainToolbarState.destination, snackbar, mainToolbarState.mode, megaphone) {
|
||||
MainBottomChromeState(
|
||||
destination = mainToolbarState.destination,
|
||||
snackbarState = snackbar,
|
||||
mainToolbarMode = mainToolbarState.mode,
|
||||
megaphoneState = MainMegaphoneState(
|
||||
megaphone = megaphone,
|
||||
mainToolbarMode = mainToolbarState.mode
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<Any>()
|
||||
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
|
||||
|
||||
val contentClip: Shape = remember(windowSizeClass) {
|
||||
if (windowSizeClass.isExtended()) {
|
||||
RoundedCornerShape(18.dp)
|
||||
} else {
|
||||
RectangleShape
|
||||
}
|
||||
}
|
||||
|
||||
AppScaffold(
|
||||
bottomNavContent = {
|
||||
AndroidFragment(
|
||||
clazz = ConversationListTabsFragment::class.java,
|
||||
fragmentState = navState
|
||||
)
|
||||
},
|
||||
navRailContent = {
|
||||
AndroidFragment(
|
||||
clazz = ConversationListTabsFragment::class.java,
|
||||
fragmentState = navState
|
||||
)
|
||||
}
|
||||
) {
|
||||
Column {
|
||||
val state by toolbarViewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(LocalContext.current)) {
|
||||
MainToolbar(
|
||||
state = state,
|
||||
callback = toolbarCallback
|
||||
)
|
||||
LaunchedEffect(detailLocation) {
|
||||
if (detailLocation is MainNavigationDetailLocation.Conversation) {
|
||||
if (RemoteConfig.largeScreenUi) {
|
||||
scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Primary, detailLocation)
|
||||
} else {
|
||||
startActivity((detailLocation as MainNavigationDetailLocation.Conversation).intent)
|
||||
}
|
||||
|
||||
AndroidFragment(
|
||||
clazz = MainActivityListHostFragment::class.java,
|
||||
fragmentState = listHostState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(LocalContext.current)) {
|
||||
AppScaffold(
|
||||
navigator = scaffoldNavigator,
|
||||
bottomNavContent = {
|
||||
AndroidFragment(
|
||||
clazz = ConversationListTabsFragment::class.java,
|
||||
fragmentState = navState
|
||||
)
|
||||
},
|
||||
navRailContent = {
|
||||
AndroidFragment(
|
||||
clazz = ConversationListTabsFragment::class.java,
|
||||
fragmentState = navState
|
||||
)
|
||||
},
|
||||
listContent = {
|
||||
val listContainerColor = if (windowSizeClass.isMedium()) {
|
||||
SignalTheme.colors.colorSurface1
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surface
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(listContainerColor)
|
||||
.clip(contentClip)
|
||||
) {
|
||||
MainToolbar(
|
||||
state = mainToolbarState,
|
||||
callback = toolbarCallback
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
AndroidFragment(
|
||||
clazz = MainActivityListHostFragment::class.java,
|
||||
fragmentState = listHostState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
MainBottomChrome(
|
||||
state = mainBottomChromeState,
|
||||
callback = mainBottomChromeCallback,
|
||||
megaphoneActionController = megaphoneActionController,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
detailContent = {
|
||||
when (val destination = scaffoldNavigator.currentDestination?.contentKey) {
|
||||
is MainNavigationDetailLocation.Conversation -> {
|
||||
val fragmentState = key(destination) { rememberFragmentState() }
|
||||
AndroidFragment(
|
||||
clazz = ConversationFragment::class.java,
|
||||
fragmentState = fragmentState,
|
||||
arguments = requireNotNull(destination.intent.extras) { "Handed null Conversation intent arguments." },
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.surface)
|
||||
.fillMaxSize()
|
||||
.clip(contentClip)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val content: View = findViewById(android.R.id.content)
|
||||
@@ -260,11 +399,29 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
SplashScreenUtil.setSplashScreenThemeIfNecessary(this, SignalStore.settings.theme)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray, deviceId: Int) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == MainNavigator.REQUEST_CONFIG_CHANGES && resultCode == RESULT_CONFIG_CHANGED) {
|
||||
recreate()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if (resultCode == RESULT_OK && requestCode == UsernameEditFragment.REQUEST_CODE) {
|
||||
val snackbarString = getString(R.string.ConversationListFragment_username_recovered_toast, SignalStore.account.username)
|
||||
getNavigator().getViewModel().setSnackbar(
|
||||
SnackbarState(
|
||||
message = snackbarString
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFirstRender() {
|
||||
@@ -412,4 +569,86 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
toolbarViewModel.setShowNotificationProfilesTooltip(false)
|
||||
}
|
||||
}
|
||||
|
||||
inner class BottomChromeCallback : MainBottomChromeCallback {
|
||||
override fun onNewChatClick() {
|
||||
startActivity(Intent(this@MainActivity, NewConversationActivity::class.java))
|
||||
}
|
||||
|
||||
override fun onNewCallClick() {
|
||||
startActivity(NewCallActivity.createIntent(this@MainActivity))
|
||||
}
|
||||
|
||||
override fun onCameraClick(destination: MainNavigationDestination) {
|
||||
val onGranted = {
|
||||
startActivity(
|
||||
MediaSelectionActivity.camera(
|
||||
context = this@MainActivity,
|
||||
isStory = destination == MainNavigationDestination.STORIES
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (CameraXUtil.isSupported()) {
|
||||
onGranted()
|
||||
} else {
|
||||
Permissions.with(this@MainActivity)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24)
|
||||
.withPermanentDenialDialog(
|
||||
getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos),
|
||||
null,
|
||||
R.string.CameraXFragment_allow_access_camera,
|
||||
R.string.CameraXFragment_to_capture_photos_videos,
|
||||
supportFragmentManager
|
||||
)
|
||||
.onAllGranted(onGranted)
|
||||
.onAnyDenied { Toast.makeText(this@MainActivity, R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() }
|
||||
.execute()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMegaphoneVisible(megaphone: Megaphone) {
|
||||
navigator.viewModel.onMegaphoneVisible(megaphone)
|
||||
}
|
||||
|
||||
override fun onSnackbarDismissed() {
|
||||
navigator.viewModel.setSnackbar(null)
|
||||
}
|
||||
}
|
||||
|
||||
inner class MainMegaphoneActionController : MegaphoneActionController {
|
||||
override fun onMegaphoneNavigationRequested(intent: Intent) {
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onMegaphoneNavigationRequested(intent: Intent, requestCode: Int) {
|
||||
startActivityForResult(intent, requestCode)
|
||||
}
|
||||
|
||||
override fun onMegaphoneToastRequested(string: String) {
|
||||
getNavigator().viewModel.setSnackbar(
|
||||
SnackbarState(
|
||||
message = string
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getMegaphoneActivity(): Activity {
|
||||
return this@MainActivity
|
||||
}
|
||||
|
||||
override fun onMegaphoneSnooze(event: Megaphones.Event) {
|
||||
getNavigator().viewModel.onMegaphoneSnoozed(event)
|
||||
}
|
||||
|
||||
override fun onMegaphoneCompleted(event: Megaphones.Event) {
|
||||
getNavigator().viewModel.onMegaphoneCompleted(event)
|
||||
}
|
||||
|
||||
override fun onMegaphoneDialogFragmentRequested(dialogFragment: DialogFragment) {
|
||||
dialogFragment.show(supportFragmentManager, "megaphone_dialog")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,20 +9,16 @@ import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.app.SharedElementCallback
|
||||
import androidx.core.view.ViewCompat
|
||||
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 androidx.transition.TransitionInflater
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.kotlin.Flowables
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.DimensionUnit
|
||||
@@ -31,9 +27,7 @@ import org.signal.core.util.concurrent.addTo
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.MainNavigator
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
|
||||
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
|
||||
import org.thoughtcrime.securesms.calls.new.NewCallActivity
|
||||
import org.thoughtcrime.securesms.components.ProgressCardDialogFragment
|
||||
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
@@ -51,9 +45,11 @@ 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.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
|
||||
@@ -62,7 +58,6 @@ import org.thoughtcrime.securesms.util.doAfterNextLayout
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.Objects
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Call Log tab.
|
||||
@@ -91,10 +86,9 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
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()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
initializeSharedElementTransition()
|
||||
|
||||
viewLifecycleOwner.lifecycle.addObserver(conversationUpdateTick)
|
||||
viewLifecycleOwner.lifecycle.addObserver(viewModel.callLogPeekHelper)
|
||||
|
||||
@@ -150,9 +144,6 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
this.callLogAdapter = callLogAdapter
|
||||
|
||||
requireListener<Material3OnScrollHelperBinder>().bindScrollHelper(binding.recycler, viewLifecycleOwner)
|
||||
binding.fab.setOnClickListener {
|
||||
startActivity(NewCallActivity.createIntent(requireContext()))
|
||||
}
|
||||
|
||||
binding.pullView.setPillText(R.string.CallLogFragment__filtered_by_missed)
|
||||
|
||||
@@ -204,26 +195,6 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
callLogAdapter?.onTimestampTick()
|
||||
}
|
||||
|
||||
private fun initializeSharedElementTransition() {
|
||||
ViewCompat.setTransitionName(binding.fab, "new_convo_fab")
|
||||
ViewCompat.setTransitionName(binding.fabSharedElementTarget, "camera_fab")
|
||||
|
||||
sharedElementEnterTransition = TransitionInflater.from(requireContext()).inflateTransition(R.transition.change_transform_fabs)
|
||||
setEnterSharedElementCallback(object : SharedElementCallback() {
|
||||
override fun onSharedElementStart(sharedElementNames: MutableList<String>?, sharedElements: MutableList<View>?, sharedElementSnapshots: MutableList<View>?) {
|
||||
if (sharedElementNames?.contains("camera_fab") == true) {
|
||||
this@CallLogFragment.binding.fab.setImageResource(R.drawable.symbol_edit_24)
|
||||
disposables += Single.timer(200, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
this@CallLogFragment.binding.fab.setImageResource(R.drawable.symbol_phone_plus_24)
|
||||
this@CallLogFragment.binding.fabSharedElementTarget.alpha = 0f
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initializeTapToScrollToTop(scrollToPositionDelegate: ScrollToPositionDelegate) {
|
||||
disposables += tabsViewModel.tabClickEvents
|
||||
.filter { it == MainNavigationDestination.CALLS }
|
||||
@@ -363,14 +334,22 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
|
||||
override fun onStartAudioCallClicked(recipient: Recipient) {
|
||||
CommunicationActions.startVoiceCall(this, recipient) {
|
||||
YouAreAlreadyInACallSnackbar.show(requireView())
|
||||
mainNavigationViewModel.setSnackbar(
|
||||
SnackbarState(
|
||||
getString(R.string.CommunicationActions__you_are_already_in_a_call)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartVideoCallClicked(recipient: Recipient, canUserBeginCall: Boolean) {
|
||||
if (canUserBeginCall) {
|
||||
CommunicationActions.startVideoCall(this, recipient) {
|
||||
YouAreAlreadyInACallSnackbar.show(requireView())
|
||||
mainNavigationViewModel.setSnackbar(
|
||||
SnackbarState(
|
||||
getString(R.string.CommunicationActions__you_are_already_in_a_call)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ConversationDialogs.displayCannotStartGroupCallDueToPermissionsDialog(requireContext())
|
||||
@@ -461,13 +440,12 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
}
|
||||
|
||||
CallLogDeletionResult.Success -> {
|
||||
Snackbar
|
||||
.make(
|
||||
binding.root,
|
||||
snackbarMessage,
|
||||
Snackbar.LENGTH_SHORT
|
||||
mainNavigationViewModel.setSnackbar(
|
||||
SnackbarState(
|
||||
message = snackbarMessage,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
.show()
|
||||
)
|
||||
}
|
||||
|
||||
is CallLogDeletionResult.UnknownFailure -> {
|
||||
@@ -488,14 +466,12 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
val actionMode = (requireActivity() as AppCompatActivity).startSupportActionMode(callback)
|
||||
requireListener<Callback>().onMultiSelectStarted()
|
||||
signalBottomActionBarController.setVisibility(true)
|
||||
binding.fab.visible = false
|
||||
return actionMode
|
||||
}
|
||||
|
||||
override fun onActionModeWillEnd() {
|
||||
requireListener<Callback>().onMultiSelectFinished()
|
||||
signalBottomActionBarController.setVisibility(false)
|
||||
binding.fab.visible = true
|
||||
}
|
||||
|
||||
override fun getResources(): Resources = resources
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Surface
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.Guideline
|
||||
import androidx.core.content.withStyledAttributes
|
||||
@@ -63,25 +64,34 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
private val displayMetrics = DisplayMetrics()
|
||||
private var overridingKeyboard: Boolean = false
|
||||
private var previousKeyboardHeight: Int = 0
|
||||
private var applyRootInsets: Boolean = false
|
||||
|
||||
val isKeyboardShowing: Boolean
|
||||
get() = previousKeyboardHeight > 0
|
||||
|
||||
init {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { _, windowInsetsCompat ->
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(insetTarget()) { _, windowInsetsCompat ->
|
||||
applyInsets(windowInsets = windowInsetsCompat.getInsets(windowTypes), keyboardInsets = windowInsetsCompat.getInsets(keyboardType))
|
||||
windowInsetsCompat
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
if (attrs != null) {
|
||||
context.withStyledAttributes(attrs, R.styleable.InsetAwareConstraintLayout) {
|
||||
applyRootInsets = getBoolean(R.styleable.InsetAwareConstraintLayout_applyRootInsets, false)
|
||||
|
||||
if (getBoolean(R.styleable.InsetAwareConstraintLayout_animateKeyboardChanges, false)) {
|
||||
ViewCompat.setWindowInsetsAnimationCallback(this@InsetAwareConstraintLayout, keyboardAnimator)
|
||||
ViewCompat.setWindowInsetsAnimationCallback(insetTarget(), keyboardAnimator)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun insetTarget(): View = if (applyRootInsets) rootView else this
|
||||
|
||||
fun addKeyboardStateListener(listener: KeyboardStateListener) {
|
||||
keyboardStateListeners += listener
|
||||
}
|
||||
|
||||
@@ -346,6 +346,7 @@ import org.thoughtcrime.securesms.util.visible
|
||||
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil
|
||||
import org.thoughtcrime.securesms.window.WindowSizeClass.Companion.getWindowSizeClass
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
@@ -589,7 +590,10 @@ class ConversationFragment :
|
||||
binding.toolbar.isBackInvokedCallbackEnabled = false
|
||||
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
FullscreenHelper(requireActivity()).showSystemUI()
|
||||
|
||||
if (requireActivity() is ConversationActivity) {
|
||||
FullscreenHelper(requireActivity()).showSystemUI()
|
||||
}
|
||||
|
||||
markReadHelper = MarkReadHelper(ConversationId.forConversation(args.threadId), requireContext(), viewLifecycleOwner)
|
||||
markReadHelper.ignoreViewReveals()
|
||||
@@ -1361,10 +1365,17 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
private fun presentNavigationIconForNormal() {
|
||||
binding.toolbar.setNavigationIcon(R.drawable.ic_arrow_left_24)
|
||||
binding.toolbar.setNavigationContentDescription(R.string.ConversationFragment__content_description_back_button)
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
requireActivity().finishAfterTransition()
|
||||
val windowSizeClass = resources.getWindowSizeClass()
|
||||
|
||||
if (windowSizeClass.isCompact()) {
|
||||
binding.toolbar.setNavigationIcon(R.drawable.ic_arrow_left_24)
|
||||
binding.toolbar.setNavigationContentDescription(R.string.ConversationFragment__content_description_back_button)
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
requireActivity().finishAfterTransition()
|
||||
}
|
||||
} else {
|
||||
binding.toolbar.navigationIcon = null
|
||||
binding.toolbar.contentInsetStartWithNavigation = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ class ConversationToolbarOnScrollHelper(
|
||||
) : Material3OnScrollHelper(
|
||||
activity = activity,
|
||||
views = listOf(toolbarBackground),
|
||||
lifecycleOwner = lifecycleOwner
|
||||
lifecycleOwner = lifecycleOwner,
|
||||
setStatusBarColor = {}
|
||||
) {
|
||||
override val activeColorSet: ColorSet
|
||||
get() = ColorSet(getActiveToolbarColor(wallpaperProvider() != null))
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
@@ -27,19 +26,24 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.compose.material3.SnackbarDuration;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.main.SnackbarState;
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil;
|
||||
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Completable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import kotlin.Unit;
|
||||
|
||||
|
||||
public class ConversationListArchiveFragment extends ConversationListFragment implements ActionMode.Callback
|
||||
{
|
||||
@@ -47,8 +51,7 @@ public class ConversationListArchiveFragment extends ConversationListFragment im
|
||||
private RecyclerView list;
|
||||
private RecyclerView foldersList;
|
||||
private Stub<View> emptyState;
|
||||
private PulsingFloatingActionButton fab;
|
||||
private PulsingFloatingActionButton cameraFab;
|
||||
private LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
|
||||
|
||||
public static ConversationListArchiveFragment newInstance() {
|
||||
return new ConversationListArchiveFragment();
|
||||
@@ -64,15 +67,13 @@ public class ConversationListArchiveFragment extends ConversationListFragment im
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
lifecycleDisposable.bindTo(getViewLifecycleOwner());
|
||||
|
||||
coordinator = view.findViewById(R.id.coordinator);
|
||||
list = view.findViewById(R.id.list);
|
||||
emptyState = new Stub<>(view.findViewById(R.id.empty_state));
|
||||
fab = view.findViewById(R.id.fab);
|
||||
cameraFab = view.findViewById(R.id.camera_fab);
|
||||
foldersList = view.findViewById(R.id.chat_folder_list);
|
||||
|
||||
fab.hide();
|
||||
cameraFab.hide();
|
||||
foldersList.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@@ -118,26 +119,34 @@ public class ConversationListArchiveFragment extends ConversationListFragment im
|
||||
archiveDecoration.onArchiveStarted();
|
||||
itemAnimator.enable();
|
||||
|
||||
new SnackbarAsyncTask<Long>(getViewLifecycleOwner().getLifecycle(),
|
||||
coordinator,
|
||||
getResources().getQuantityString(R.plurals.ConversationListFragment_moved_conversations_to_inbox, 1, 1),
|
||||
getString(R.string.ConversationListFragment_undo),
|
||||
getResources().getColor(R.color.amber_500),
|
||||
Snackbar.LENGTH_LONG,
|
||||
false)
|
||||
{
|
||||
@Override
|
||||
protected void executeAction(@Nullable Long parameter) {
|
||||
SignalDatabase.threads().unarchiveConversation(threadId);
|
||||
ConversationUtil.refreshRecipientShortcuts();
|
||||
}
|
||||
lifecycleDisposable.add(
|
||||
Completable
|
||||
.fromAction(() -> {
|
||||
SignalDatabase.threads().unarchiveConversation(threadId);
|
||||
ConversationUtil.refreshRecipientShortcuts();
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(() -> {
|
||||
getNavigator().getViewModel().setSnackbar(new SnackbarState(
|
||||
getResources().getQuantityString(R.plurals.ConversationListFragment_moved_conversations_to_inbox, 1, 1),
|
||||
new SnackbarState.ActionState(
|
||||
getString(R.string.ConversationListFragment_undo),
|
||||
R.color.amber_500,
|
||||
() -> {
|
||||
SignalExecutors.BOUNDED_IO.execute(() -> {
|
||||
SignalDatabase.threads().archiveConversation(threadId);
|
||||
ConversationUtil.refreshRecipientShortcuts();
|
||||
});
|
||||
|
||||
@Override
|
||||
protected void reverseAction(@Nullable Long parameter) {
|
||||
SignalDatabase.threads().archiveConversation(threadId);
|
||||
ConversationUtil.refreshRecipientShortcuts();
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
),
|
||||
false,
|
||||
SnackbarDuration.Long
|
||||
));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,11 +16,8 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
@@ -38,7 +35,6 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.ColorInt;
|
||||
@@ -50,11 +46,11 @@ import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.compose.material3.SnackbarDuration;
|
||||
import androidx.compose.ui.platform.ComposeView;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
@@ -70,7 +66,6 @@ import com.google.android.material.animation.ArgbEvaluatorCompat;
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
@@ -84,7 +79,6 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.MainFragment;
|
||||
import org.thoughtcrime.securesms.MainNavigator;
|
||||
import org.thoughtcrime.securesms.MuteDialog;
|
||||
import org.thoughtcrime.securesms.NewConversationActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert;
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertBottomSheet;
|
||||
@@ -110,7 +104,6 @@ import org.thoughtcrime.securesms.components.SignalProgressDialog;
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
||||
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar;
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu;
|
||||
import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord;
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.InAppPaymentsBottomSheetDelegate;
|
||||
@@ -139,19 +132,13 @@ 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.lock.v2.CreateSvrPinActivity;
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDestination;
|
||||
import org.thoughtcrime.securesms.main.MainToolbarMode;
|
||||
import org.thoughtcrime.securesms.main.MainToolbarViewModel;
|
||||
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneActionController;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneViewBuilder;
|
||||
import org.thoughtcrime.securesms.main.SnackbarState;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment;
|
||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -171,7 +158,6 @@ import org.thoughtcrime.securesms.util.SignalProxyUtil;
|
||||
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||
@@ -188,14 +174,15 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Completable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import kotlin.Unit;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
|
||||
public class ConversationListFragment extends MainFragment implements ActionMode.Callback,
|
||||
ConversationListAdapter.OnConversationClickListener,
|
||||
MegaphoneActionController,
|
||||
ClearFilterViewHolder.OnClearFilterClickListener,
|
||||
ChatFolderAdapter.Callbacks,
|
||||
ConversationListAdapter.EmptyFolderViewHolder.OnFolderSettingsClickListener
|
||||
@@ -217,15 +204,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
private RecyclerView chatFolderList;
|
||||
private RecyclerView list;
|
||||
private Stub<ComposeView> bannerView;
|
||||
private PulsingFloatingActionButton fab;
|
||||
private PulsingFloatingActionButton cameraFab;
|
||||
private ConversationListFilterPullView pullView;
|
||||
private AppBarLayout pullViewAppBarLayout;
|
||||
private ConversationListViewModel viewModel;
|
||||
private RecyclerView.Adapter activeAdapter;
|
||||
private ConversationListAdapter defaultAdapter;
|
||||
private PagingMappingAdapter<ContactSearchKey> searchAdapter;
|
||||
private Stub<ViewGroup> megaphoneContainer;
|
||||
private SnapToTopDataObserver snapToTopDataObserver;
|
||||
private Drawable archiveDrawable;
|
||||
private AppForegroundObserver.Listener appForegroundObserver;
|
||||
@@ -283,21 +267,14 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
lifecycleDisposable = new LifecycleDisposable();
|
||||
lifecycleDisposable.bindTo(getViewLifecycleOwner());
|
||||
|
||||
coordinator = view.findViewById(R.id.coordinator);
|
||||
chatFolderList = view.findViewById(R.id.chat_folder_list);
|
||||
list = view.findViewById(R.id.list);
|
||||
bottomActionBar = view.findViewById(R.id.conversation_list_bottom_action_bar);
|
||||
bannerView = new Stub<>(view.findViewById(R.id.banner_compose_view));
|
||||
megaphoneContainer = new Stub<>(view.findViewById(R.id.megaphone_container));
|
||||
voiceNotePlayerViewStub = new Stub<>(view.findViewById(R.id.voice_note_player));
|
||||
fab = view.findViewById(R.id.fab);
|
||||
cameraFab = view.findViewById(R.id.camera_fab);
|
||||
pullView = view.findViewById(R.id.pull_view);
|
||||
pullViewAppBarLayout = view.findViewById(R.id.recycler_coordinator_app_bar);
|
||||
|
||||
fab.setVisibility(View.VISIBLE);
|
||||
cameraFab.setVisibility(View.VISIBLE);
|
||||
|
||||
contactSearchMediator = new ContactSearchMediator(this,
|
||||
Collections.emptySet(),
|
||||
SelectionLimits.NO_LIMITS,
|
||||
@@ -381,9 +358,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
pullView.onUserDrag(progress);
|
||||
});
|
||||
|
||||
fab.show();
|
||||
cameraFab.show();
|
||||
|
||||
archiveDecoration = new ConversationListArchiveItemDecoration(new ColorDrawable(getResources().getColor(R.color.conversation_list_archive_background_end)));
|
||||
itemAnimator = new ConversationListItemAnimator();
|
||||
|
||||
@@ -404,22 +378,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
new ItemTouchHelper(new ArchiveListenerCallback(getResources().getColor(R.color.conversation_list_archive_background_start),
|
||||
getResources().getColor(R.color.conversation_list_archive_background_end))).attachToRecyclerView(list);
|
||||
|
||||
fab.setOnClickListener(v -> startActivity(new Intent(getActivity(), NewConversationActivity.class)));
|
||||
cameraFab.setOnClickListener(v -> {
|
||||
if (CameraXUtil.isSupported()) {
|
||||
startActivity(MediaSelectionActivity.camera(requireContext()));
|
||||
} else {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24)
|
||||
.withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, getParentFragmentManager())
|
||||
.onAllGranted(() -> startActivity(MediaSelectionActivity.camera(requireContext())))
|
||||
.onAnyDenied(() -> Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show())
|
||||
.execute();
|
||||
}
|
||||
});
|
||||
|
||||
initializeViewModel();
|
||||
initializeListAdapters();
|
||||
initializeTypingObserver();
|
||||
@@ -479,10 +437,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
coordinator = null;
|
||||
list = null;
|
||||
bottomActionBar = null;
|
||||
megaphoneContainer = null;
|
||||
voiceNotePlayerViewStub = null;
|
||||
fab = null;
|
||||
cameraFab = null;
|
||||
snapToTopDataObserver = null;
|
||||
itemAnimator = null;
|
||||
|
||||
@@ -569,8 +524,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
fab.stopPulse();
|
||||
cameraFab.stopPulse();
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@@ -583,7 +536,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
onMegaphoneChanged(viewModel.getMegaphone());
|
||||
}
|
||||
|
||||
private ContactSearchConfiguration mapSearchStateToConfiguration(@NonNull ContactSearchState state) {
|
||||
@@ -654,31 +606,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
private void closeSearchIfOpen() {
|
||||
if (isSearchOpen()) {
|
||||
setAdapter(defaultAdapter);
|
||||
fadeInButtonsAndMegaphone(250);
|
||||
mainToolbarViewModel.setToolbarMode(MainToolbarMode.FULL);
|
||||
chatListBackHandler.setEnabled(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (resultCode == RESULT_OK && requestCode == CreateSvrPinActivity.REQUEST_NEW_PIN) {
|
||||
Snackbar.make(fab, R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show();
|
||||
viewModel.onMegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL);
|
||||
}
|
||||
|
||||
if (resultCode == RESULT_OK && requestCode == UsernameEditFragment.REQUEST_CODE) {
|
||||
String snackbarString = getString(R.string.ConversationListFragment_username_recovered_toast, SignalStore.account().getUsername());
|
||||
Snackbar.make(fab, snackbarString, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void onConversationClicked(@NonNull ThreadRecord threadRecord) {
|
||||
hideKeyboard();
|
||||
getNavigator().goToConversation(threadRecord.getRecipient().getId(),
|
||||
@@ -720,41 +653,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMegaphoneNavigationRequested(@NonNull Intent intent) {
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMegaphoneNavigationRequested(@NonNull Intent intent, int requestCode) {
|
||||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMegaphoneToastRequested(@NonNull String string) {
|
||||
Snackbar.make(fab, string, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Activity getMegaphoneActivity() {
|
||||
return requireActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMegaphoneSnooze(@NonNull Megaphones.Event event) {
|
||||
viewModel.onMegaphoneSnoozed(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMegaphoneCompleted(@NonNull Megaphones.Event event) {
|
||||
viewModel.onMegaphoneCompleted(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMegaphoneDialogFragmentRequested(@NonNull DialogFragment dialogFragment) {
|
||||
dialogFragment.show(getChildFragmentManager(), "megaphone_dialog");
|
||||
}
|
||||
|
||||
private void hideKeyboard() {
|
||||
InputMethodManager imm = ServiceUtil.getInputMethodManager(requireContext());
|
||||
imm.hideSoftInputFromWindow(requireView().getWindowToken(), 0);
|
||||
@@ -772,7 +670,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
mainToolbarViewModel.getSearchEventsFlowable().subscribe(event -> {
|
||||
if (event instanceof MainToolbarViewModel.Event.Search.Open) {
|
||||
onSearchOpen();
|
||||
} else if (event instanceof MainToolbarViewModel.Event.Search.Close) {
|
||||
} if (event instanceof MainToolbarViewModel.Event.Search.Close) {
|
||||
onSearchClose();
|
||||
} else if (event instanceof MainToolbarViewModel.Event.Search.Query) {
|
||||
onSearchQueryUpdated(((MainToolbarViewModel.Event.Search.Query) event).getQuery());
|
||||
@@ -939,7 +837,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
private void initializeViewModel() {
|
||||
viewModel = new ViewModelProvider(this, new ConversationListViewModel.Factory(isArchived())).get(ConversationListViewModel.class);
|
||||
|
||||
lifecycleDisposable.add(viewModel.getMegaphoneState().subscribe(this::onMegaphoneChanged));
|
||||
lifecycleDisposable.add(viewModel.getConversationsState().subscribe(this::onConversationListChanged));
|
||||
lifecycleDisposable.add(viewModel.getHasNoConversations().subscribe(this::updateEmptyState));
|
||||
lifecycleDisposable.add(viewModel.getWebSocketState().subscribe(pipeState -> requireCallback().updateProxyStatus(pipeState)));
|
||||
@@ -1008,33 +905,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
chatFolderAdapter.submitList(new ArrayList<>(folders));
|
||||
}
|
||||
|
||||
private void onMegaphoneChanged(@NonNull Megaphone megaphone) {
|
||||
if (megaphone == Megaphone.NONE || isArchived() || getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
if (megaphoneContainer.resolved()) {
|
||||
megaphoneContainer.get().setVisibility(View.GONE);
|
||||
megaphoneContainer.get().removeAllViews();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
View view = MegaphoneViewBuilder.build(requireContext(), megaphone, this);
|
||||
|
||||
megaphoneContainer.get().removeAllViews();
|
||||
|
||||
if (view != null) {
|
||||
megaphoneContainer.get().addView(view);
|
||||
if (isSearchOpen() || actionMode != null) {
|
||||
megaphoneContainer.get().setVisibility(View.GONE);
|
||||
} else {
|
||||
megaphoneContainer.get().setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
megaphoneContainer.get().setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
viewModel.onMegaphoneVisible(megaphone);
|
||||
}
|
||||
|
||||
private void handleMarkAsRead(@NonNull Collection<Long> ids) {
|
||||
Context context = requireContext();
|
||||
Stopwatch stopwatch = new Stopwatch("mark-read");
|
||||
@@ -1078,31 +948,27 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
int count = selectedConversations.size();
|
||||
String snackBarTitle = getResources().getQuantityString(getArchivedSnackbarTitleRes(), count, count);
|
||||
|
||||
new SnackbarAsyncTask<Void>(getViewLifecycleOwner().getLifecycle(),
|
||||
coordinator,
|
||||
snackBarTitle,
|
||||
getString(R.string.ConversationListFragment_undo),
|
||||
getResources().getColor(R.color.amber_500),
|
||||
Snackbar.LENGTH_LONG,
|
||||
showProgress)
|
||||
{
|
||||
lifecycleDisposable.add(Completable
|
||||
.fromAction(() -> archiveThreads(selectedConversations))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(() -> {
|
||||
endActionModeIfActive();
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
super.onPostExecute(result);
|
||||
endActionModeIfActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void executeAction(@Nullable Void parameter) {
|
||||
archiveThreads(selectedConversations);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reverseAction(@Nullable Void parameter) {
|
||||
reverseArchiveThreads(selectedConversations);
|
||||
}
|
||||
}.executeOnExecutor(SignalExecutors.BOUNDED);
|
||||
getNavigator().getViewModel().setSnackbar(new SnackbarState(
|
||||
snackBarTitle,
|
||||
new SnackbarState.ActionState(
|
||||
getString(R.string.ConversationListFragment_undo),
|
||||
R.color.amber_500,
|
||||
() -> {
|
||||
SignalExecutors.BOUNDED_IO.execute(() -> reverseArchiveThreads(selectedConversations));
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
),
|
||||
showProgress,
|
||||
SnackbarDuration.Long
|
||||
));
|
||||
}));
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@@ -1178,10 +1044,13 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
.toList());
|
||||
|
||||
if (toPin.size() + viewModel.getPinnedCount() > MAXIMUM_PINNED_CONVERSATIONS) {
|
||||
Snackbar.make(fab,
|
||||
getString(R.string.conversation_list__you_can_only_pin_up_to_d_chats, MAXIMUM_PINNED_CONVERSATIONS),
|
||||
Snackbar.LENGTH_LONG)
|
||||
.show();
|
||||
getNavigator().getViewModel().setSnackbar(new SnackbarState(
|
||||
getString(R.string.conversation_list__you_can_only_pin_up_to_d_chats, MAXIMUM_PINNED_CONVERSATIONS),
|
||||
null,
|
||||
false,
|
||||
SnackbarDuration.Long
|
||||
));
|
||||
|
||||
endActionModeIfActive();
|
||||
return;
|
||||
}
|
||||
@@ -1250,38 +1119,9 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
});
|
||||
}
|
||||
|
||||
private void fadeOutButtonsAndMegaphone(int fadeDuration) {
|
||||
if (fab != null) {
|
||||
ViewUtil.fadeOut(fab, fadeDuration);
|
||||
}
|
||||
if (cameraFab != null) {
|
||||
ViewUtil.fadeOut(cameraFab, fadeDuration);
|
||||
}
|
||||
if (megaphoneContainer != null && megaphoneContainer.resolved()) {
|
||||
ViewUtil.fadeOut(megaphoneContainer.get(), fadeDuration);
|
||||
}
|
||||
}
|
||||
|
||||
private void fadeInButtonsAndMegaphone(int fadeDuration) {
|
||||
if (fab != null) {
|
||||
ViewUtil.fadeIn(fab, fadeDuration);
|
||||
}
|
||||
if (cameraFab != null) {
|
||||
ViewUtil.fadeIn(cameraFab, fadeDuration);
|
||||
}
|
||||
if (megaphoneContainer != null && megaphoneContainer.resolved()) {
|
||||
ViewUtil.fadeIn(megaphoneContainer.get(), fadeDuration);
|
||||
}
|
||||
}
|
||||
|
||||
private void startActionMode() {
|
||||
actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(ConversationListFragment.this);
|
||||
ViewUtil.animateIn(bottomActionBar, bottomActionBar.getEnterAnimation());
|
||||
ViewUtil.fadeOut(fab, 250);
|
||||
ViewUtil.fadeOut(cameraFab, 250);
|
||||
if (megaphoneContainer.resolved()) {
|
||||
ViewUtil.fadeOut(megaphoneContainer.get(), 250);
|
||||
}
|
||||
requireCallback().onMultiSelectStarted();
|
||||
}
|
||||
|
||||
@@ -1295,25 +1135,15 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
actionMode.finish();
|
||||
actionMode = null;
|
||||
ViewUtil.animateOut(bottomActionBar, bottomActionBar.getExitAnimation());
|
||||
ViewUtil.fadeIn(fab, 250);
|
||||
ViewUtil.fadeIn(cameraFab, 250);
|
||||
if (megaphoneContainer.resolved()) {
|
||||
ViewUtil.fadeIn(megaphoneContainer.get(), 250);
|
||||
}
|
||||
requireCallback().onMultiSelectFinished();
|
||||
}
|
||||
|
||||
void updateEmptyState(boolean isConversationEmpty) {
|
||||
if (isConversationEmpty) {
|
||||
Log.i(TAG, "Received an empty data set.");
|
||||
fab.startPulse(3 * 1000);
|
||||
cameraFab.startPulse(3 * 1000);
|
||||
|
||||
SignalStore.onboarding().setShowNewGroup(true);
|
||||
SignalStore.onboarding().setShowInviteFriends(true);
|
||||
} else {
|
||||
fab.stopPulse();
|
||||
cameraFab.stopPulse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1566,34 +1396,40 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
archiveDecoration.onArchiveStarted();
|
||||
itemAnimator.enable();
|
||||
|
||||
new SnackbarAsyncTask<Long>(getViewLifecycleOwner().getLifecycle(),
|
||||
coordinator,
|
||||
getResources().getQuantityString(R.plurals.ConversationListFragment_conversations_archived, 1, 1),
|
||||
getString(R.string.ConversationListFragment_undo),
|
||||
getResources().getColor(R.color.amber_500),
|
||||
Snackbar.LENGTH_LONG,
|
||||
false)
|
||||
{
|
||||
private final ThreadTable threadTable = SignalDatabase.threads();
|
||||
lifecycleDisposable.add(
|
||||
Single
|
||||
.fromCallable(() -> {
|
||||
List<Long> pinnedThreadIds = SignalDatabase.threads().getPinnedThreadIds();
|
||||
SignalDatabase.threads().archiveConversation(threadId);
|
||||
|
||||
private List<Long> pinnedThreadIds;
|
||||
ConversationUtil.refreshRecipientShortcuts();
|
||||
|
||||
@Override
|
||||
protected void executeAction(@Nullable Long parameter) {
|
||||
pinnedThreadIds = threadTable.getPinnedThreadIds();
|
||||
threadTable.archiveConversation(threadId);
|
||||
return pinnedThreadIds;
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(pinnedThreadIds -> {
|
||||
getNavigator().getViewModel().setSnackbar(new SnackbarState(
|
||||
getResources().getQuantityString(R.plurals.ConversationListFragment_conversations_archived, 1, 1),
|
||||
new SnackbarState.ActionState(
|
||||
getString(R.string.ConversationListFragment_undo),
|
||||
R.color.amber_500,
|
||||
() -> {
|
||||
SignalExecutors.BOUNDED_IO.execute(() -> {
|
||||
SignalDatabase.threads().unarchiveConversation(threadId);
|
||||
SignalDatabase.threads().restorePins(pinnedThreadIds);
|
||||
|
||||
ConversationUtil.refreshRecipientShortcuts();
|
||||
}
|
||||
ConversationUtil.refreshRecipientShortcuts();
|
||||
});
|
||||
|
||||
@Override
|
||||
protected void reverseAction(@Nullable Long parameter) {
|
||||
threadTable.unarchiveConversation(threadId);
|
||||
threadTable.restorePins(pinnedThreadIds);
|
||||
|
||||
ConversationUtil.refreshRecipientShortcuts();
|
||||
}
|
||||
}.executeOnExecutor(SignalExecutors.BOUNDED, threadId);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
),
|
||||
false,
|
||||
SnackbarDuration.Long
|
||||
));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1683,7 +1519,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
|
||||
private void onSearchOpen() {
|
||||
chatListBackHandler.setEnabled(true);
|
||||
fadeOutButtonsAndMegaphone(250);
|
||||
}
|
||||
|
||||
private void onSearchClose() {
|
||||
@@ -1692,7 +1527,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
}
|
||||
|
||||
chatListBackHandler.setEnabled(false);
|
||||
fadeInButtonsAndMegaphone(250);
|
||||
}
|
||||
|
||||
private void onSearchQueryUpdated(@NonNull String query) {
|
||||
|
||||
@@ -26,9 +26,6 @@ import org.thoughtcrime.securesms.database.RxDatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -37,8 +34,7 @@ import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ConversationListViewModel(
|
||||
private val isArchived: Boolean,
|
||||
private val megaphoneRepository: MegaphoneRepository = AppDependencies.megaphoneRepository
|
||||
private val isArchived: Boolean
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
@@ -55,7 +51,6 @@ class ConversationListViewModel(
|
||||
.build()
|
||||
|
||||
val conversationsState: Flowable<List<Conversation>> = store.mapDistinctForUi { it.conversations }
|
||||
val megaphoneState: Flowable<Megaphone> = store.mapDistinctForUi { it.megaphone }
|
||||
val selectedState: Flowable<ConversationSet> = store.mapDistinctForUi { it.selectedConversations }
|
||||
val filterRequestState: Flowable<ConversationFilterRequest> = store.mapDistinctForUi { it.filterRequest }
|
||||
val chatFolderState: Flowable<List<ChatFolderMappingModel>> = store.mapDistinctForUi { it.chatFolders }
|
||||
@@ -69,8 +64,6 @@ class ConversationListViewModel(
|
||||
get() = store.state.currentFolder
|
||||
val conversationFilterRequest: ConversationFilterRequest
|
||||
get() = store.state.filterRequest
|
||||
val megaphone: Megaphone
|
||||
get() = store.state.megaphone
|
||||
val pinnedCount: Int
|
||||
get() = store.state.pinnedCount
|
||||
val webSocketState: Observable<WebSocketConnectionState>
|
||||
@@ -154,10 +147,6 @@ class ConversationListViewModel(
|
||||
}
|
||||
|
||||
fun onVisible() {
|
||||
megaphoneRepository.getNextMegaphone { next ->
|
||||
store.update { it.copy(megaphone = next ?: Megaphone.NONE) }
|
||||
}
|
||||
|
||||
if (!coldStart) {
|
||||
AppDependencies.databaseObserver.notifyConversationListListeners()
|
||||
}
|
||||
@@ -202,20 +191,6 @@ class ConversationListViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun onMegaphoneCompleted(event: Megaphones.Event) {
|
||||
store.update { it.copy(megaphone = Megaphone.NONE) }
|
||||
megaphoneRepository.markFinished(event)
|
||||
}
|
||||
|
||||
fun onMegaphoneSnoozed(event: Megaphones.Event) {
|
||||
megaphoneRepository.markSeen(event)
|
||||
store.update { it.copy(megaphone = Megaphone.NONE) }
|
||||
}
|
||||
|
||||
fun onMegaphoneVisible(visible: Megaphone) {
|
||||
megaphoneRepository.markVisible(visible.event)
|
||||
}
|
||||
|
||||
private fun loadCurrentFolders() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val folders = ChatFoldersRepository.getCurrentFolders()
|
||||
@@ -303,7 +278,6 @@ class ConversationListViewModel(
|
||||
val chatFolders: List<ChatFolderMappingModel> = emptyList(),
|
||||
val currentFolder: ChatFolderRecord = ChatFolderRecord(),
|
||||
val conversations: List<Conversation> = emptyList(),
|
||||
val megaphone: Megaphone = Megaphone.NONE,
|
||||
val selectedConversations: ConversationSet = ConversationSet(),
|
||||
val internalSelection: Set<Conversation> = emptySet(),
|
||||
val filterRequest: ConversationFilterRequest = ConversationFilterRequest(ConversationFilter.OFF, ConversationFilterSource.DRAG),
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.main
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -11,7 +10,6 @@ import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.FragmentNavigatorExtras
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
@@ -75,23 +73,6 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
|
||||
if (state.tab == MainNavigationDestination.CHATS) {
|
||||
return
|
||||
} else {
|
||||
val cameraFab = requireView().findViewById<View?>(R.id.camera_fab)
|
||||
val newConvoFab = requireView().findViewById<View?>(R.id.fab)
|
||||
|
||||
val extras = when {
|
||||
cameraFab != null && newConvoFab != null -> {
|
||||
ViewCompat.setTransitionName(cameraFab, "camera_fab")
|
||||
ViewCompat.setTransitionName(newConvoFab, "new_convo_fab")
|
||||
|
||||
FragmentNavigatorExtras(
|
||||
cameraFab to "camera_fab",
|
||||
newConvoFab to "new_convo_fab"
|
||||
)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
val destination = if (state.tab == MainNavigationDestination.STORIES) {
|
||||
R.id.action_conversationListFragment_to_storiesLandingFragment
|
||||
} else {
|
||||
@@ -101,8 +82,7 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
|
||||
navController.navigate(
|
||||
destination,
|
||||
null,
|
||||
null,
|
||||
extras
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -283,11 +263,11 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
|
||||
activity = requireActivity(),
|
||||
views = listOf(chatFolders),
|
||||
viewStubs = listOf(),
|
||||
setStatusBarColor = {},
|
||||
onSetToolbarColor = {
|
||||
toolbarViewModel.setToolbarColor(it)
|
||||
},
|
||||
lifecycleOwner = lifecycleOwner,
|
||||
setStatusBarColor = {},
|
||||
setChatFolderColor = setChatFolder
|
||||
).attach(recyclerView)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||
|
||||
data class SnackbarState(
|
||||
val message: String,
|
||||
val actionState: ActionState?,
|
||||
val actionState: ActionState? = null,
|
||||
val showProgress: Boolean = false,
|
||||
val duration: SnackbarDuration = SnackbarDuration.Long
|
||||
) {
|
||||
@@ -59,7 +59,8 @@ interface MainBottomChromeCallback {
|
||||
data class MainBottomChromeState(
|
||||
val destination: MainNavigationDestination = MainNavigationDestination.CHATS,
|
||||
val megaphoneState: MainMegaphoneState = MainMegaphoneState(),
|
||||
val snackbarState: SnackbarState? = null
|
||||
val snackbarState: SnackbarState? = null,
|
||||
val mainToolbarMode: MainToolbarMode = MainToolbarMode.FULL
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -72,31 +73,34 @@ data class MainBottomChromeState(
|
||||
fun MainBottomChrome(
|
||||
state: MainBottomChromeState,
|
||||
callback: MainBottomChromeCallback,
|
||||
megaphoneActionController: MegaphoneActionController
|
||||
megaphoneActionController: MegaphoneActionController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize()
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
MainFloatingActionButtons(
|
||||
destination = state.destination,
|
||||
onCameraClick = callback::onCameraClick,
|
||||
onNewCallClick = callback::onNewCallClick,
|
||||
onNewChatClick = callback::onNewChatClick
|
||||
if (state.mainToolbarMode == MainToolbarMode.FULL) {
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
MainFloatingActionButtons(
|
||||
destination = state.destination,
|
||||
onCameraClick = callback::onCameraClick,
|
||||
onNewCallClick = callback::onNewCallClick,
|
||||
onNewChatClick = callback::onNewChatClick
|
||||
)
|
||||
}
|
||||
|
||||
MainMegaphoneContainer(
|
||||
state = state.megaphoneState,
|
||||
controller = megaphoneActionController,
|
||||
onMegaphoneVisible = callback::onMegaphoneVisible
|
||||
)
|
||||
}
|
||||
|
||||
MainMegaphoneContainer(
|
||||
state = state.megaphoneState,
|
||||
controller = megaphoneActionController,
|
||||
onMegaphoneVisible = callback::onMegaphoneVisible
|
||||
)
|
||||
|
||||
MainSnackbar(
|
||||
snackbarState = state.snackbarState,
|
||||
onDismissed = callback::onSnackbarDismissed
|
||||
@@ -116,12 +120,13 @@ private fun MainSnackbar(
|
||||
LaunchedEffect(snackbarState) {
|
||||
if (snackbarState != null) {
|
||||
val result = hostState.showSnackbar(
|
||||
message = snackbarState.message
|
||||
message = snackbarState.message,
|
||||
actionLabel = snackbarState.actionState?.action
|
||||
)
|
||||
|
||||
when (result) {
|
||||
SnackbarResult.Dismissed -> Unit
|
||||
SnackbarResult.ActionPerformed -> snackbarState.actionState
|
||||
SnackbarResult.ActionPerformed -> snackbarState.actionState?.onActionClick?.invoke()
|
||||
}
|
||||
|
||||
onDismissed()
|
||||
|
||||
@@ -7,11 +7,10 @@ package org.thoughtcrime.securesms.main
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.EnterExitState
|
||||
import androidx.compose.animation.core.animateDp
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -32,11 +31,14 @@ import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.window.Navigation
|
||||
import org.thoughtcrime.securesms.window.WindowSizeClass
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val ACTION_BUTTON_SIZE = 56.dp
|
||||
@@ -49,6 +51,11 @@ fun MainFloatingActionButtons(
|
||||
onCameraClick: (MainNavigationDestination) -> Unit = {},
|
||||
onNewCallClick: () -> Unit = {}
|
||||
) {
|
||||
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
|
||||
if (windowSizeClass.navigation == Navigation.RAIL) {
|
||||
return
|
||||
}
|
||||
|
||||
val boxHeightDp = (ACTION_BUTTON_SIZE * 2 + ACTION_BUTTON_SPACING)
|
||||
val boxHeightPx = with(LocalDensity.current) {
|
||||
boxHeightDp.toPx().roundToInt()
|
||||
@@ -65,38 +72,65 @@ fun MainFloatingActionButtons(
|
||||
enter = slideInVertically(initialOffsetY = { boxHeightPx - it }),
|
||||
exit = slideOutVertically(targetOffsetY = { boxHeightPx - it })
|
||||
) {
|
||||
val elevation by transition.animateDp(targetValueByState = { if (it == EnterExitState.Visible) 4.dp else 0.dp })
|
||||
|
||||
CameraButton(
|
||||
colors = IconButtonDefaults.filledTonalIconButtonColors().copy(
|
||||
containerColor = SignalTheme.colors.colorSurface1
|
||||
),
|
||||
onClick = {
|
||||
onCameraClick(MainNavigationDestination.CHATS)
|
||||
}
|
||||
},
|
||||
shadowElevation = elevation
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedContent(
|
||||
targetState = destination,
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
transitionSpec = { EnterTransition.None togetherWith ExitTransition.None }
|
||||
) { targetState ->
|
||||
when (targetState) {
|
||||
MainNavigationDestination.CHATS -> NewChatButton(onNewChatClick)
|
||||
MainNavigationDestination.CALLS -> NewCallButton(onNewCallClick)
|
||||
MainNavigationDestination.STORIES -> CameraButton(onClick = { onCameraClick(MainNavigationDestination.STORIES) })
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
) {
|
||||
PrimaryActionButton(
|
||||
destination = destination,
|
||||
onNewChatClick = onNewChatClick,
|
||||
onCameraClick = onCameraClick,
|
||||
onNewCallClick = onNewCallClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewChatButton(
|
||||
onClick: () -> Unit
|
||||
private fun PrimaryActionButton(
|
||||
destination: MainNavigationDestination,
|
||||
onNewChatClick: () -> Unit = {},
|
||||
onCameraClick: (MainNavigationDestination) -> Unit = {},
|
||||
onNewCallClick: () -> Unit = {}
|
||||
) {
|
||||
val onClick = remember(destination) {
|
||||
when (destination) {
|
||||
MainNavigationDestination.CHATS -> onNewChatClick
|
||||
MainNavigationDestination.CALLS -> onNewCallClick
|
||||
MainNavigationDestination.STORIES -> {
|
||||
{ onCameraClick(destination) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MainFloatingActionButton(
|
||||
onClick = onClick,
|
||||
contentDescription = "",
|
||||
icon = ImageVector.vectorResource(R.drawable.symbol_edit_24)
|
||||
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
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(icon),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -104,34 +138,29 @@ private fun NewChatButton(
|
||||
private fun CameraButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
shadowElevation: Dp = 4.dp,
|
||||
colors: IconButtonColors = IconButtonDefaults.filledTonalIconButtonColors()
|
||||
) {
|
||||
MainFloatingActionButton(
|
||||
onClick = onClick,
|
||||
contentDescription = "",
|
||||
icon = ImageVector.vectorResource(R.drawable.symbol_camera_24),
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.symbol_camera_24),
|
||||
contentDescription = ""
|
||||
)
|
||||
},
|
||||
colors = colors,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewCallButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
MainFloatingActionButton(
|
||||
onClick = onClick,
|
||||
contentDescription = "",
|
||||
icon = ImageVector.vectorResource(R.drawable.symbol_phone_plus_24)
|
||||
modifier = modifier,
|
||||
shadowElevation = shadowElevation
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainFloatingActionButton(
|
||||
onClick: () -> Unit,
|
||||
icon: ImageVector,
|
||||
contentDescription: String,
|
||||
icon: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
shadowElevation: Dp = 4.dp,
|
||||
colors: IconButtonColors = IconButtonDefaults.filledTonalIconButtonColors()
|
||||
) {
|
||||
FilledTonalIconButton(
|
||||
@@ -139,14 +168,11 @@ private fun MainFloatingActionButton(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
modifier = modifier
|
||||
.size(ACTION_BUTTON_SIZE)
|
||||
.shadow(4.dp, RoundedCornerShape(18.dp)),
|
||||
.shadow(shadowElevation, RoundedCornerShape(18.dp)),
|
||||
enabled = true,
|
||||
colors = colors
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
icon()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,33 +190,3 @@ private fun MainFloatingActionButtonsPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun NewChatButtonPreview() {
|
||||
Previews.Preview {
|
||||
NewChatButton(
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun CameraButtonPreview() {
|
||||
Previews.Preview {
|
||||
CameraButton(
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun NewCallButtonPreview() {
|
||||
Previews.Preview {
|
||||
NewCallButton(
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,8 @@ import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||
|
||||
data class MainMegaphoneState(
|
||||
val megaphone: Megaphone = Megaphone.NONE,
|
||||
val isDisplayingArchivedChats: Boolean = false,
|
||||
private val isSearchOpen: Boolean = false,
|
||||
private val isInActionMode: Boolean = false
|
||||
) {
|
||||
fun isVisible(): Boolean = !isDisplayingArchivedChats && !isSearchOpen && !isInActionMode
|
||||
}
|
||||
val mainToolbarMode: MainToolbarMode = MainToolbarMode.FULL
|
||||
)
|
||||
|
||||
object EmptyMegaphoneActionController : MegaphoneActionController {
|
||||
override fun onMegaphoneNavigationRequested(intent: Intent) = Unit
|
||||
@@ -51,7 +47,7 @@ fun MainMegaphoneContainer(
|
||||
) {
|
||||
val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
val visible = remember(isLandscape, state) {
|
||||
!isLandscape && state.isVisible()
|
||||
!(state.megaphone == Megaphone.NONE || state.mainToolbarMode != MainToolbarMode.FULL || isLandscape)
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = visible) {
|
||||
@@ -61,10 +57,12 @@ fun MainMegaphoneContainer(
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(state.megaphone, state.isDisplayingArchivedChats, isLandscape) {
|
||||
if (!(state.megaphone == Megaphone.NONE || state.isDisplayingArchivedChats || isLandscape)) {
|
||||
onMegaphoneVisible(state.megaphone)
|
||||
LaunchedEffect(state, isLandscape) {
|
||||
if (state.megaphone == Megaphone.NONE || state.mainToolbarMode == MainToolbarMode.BASIC || isLandscape) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
onMegaphoneVisible(state.megaphone)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -103,7 +104,9 @@ fun MainNavigationBar(
|
||||
NavigationBar(
|
||||
containerColor = SignalTheme.colors.colorSurface2,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.height(if (state.compact) 48.dp else 80.dp)
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.height(if (state.compact) 48.dp else 80.dp)
|
||||
) {
|
||||
val entries = remember(state.isStoriesFeatureEnabled) {
|
||||
if (state.isStoriesFeatureEnabled) {
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
package org.thoughtcrime.securesms.main
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Describes which content to display in the detail view.
|
||||
*/
|
||||
sealed interface MainNavigationDetailLocation {
|
||||
@Parcelize
|
||||
sealed interface MainNavigationDetailLocation : Parcelable {
|
||||
data object Empty : MainNavigationDetailLocation
|
||||
data class Conversation(val intent: Intent) : MainNavigationDetailLocation
|
||||
}
|
||||
|
||||
@@ -7,18 +7,69 @@ package org.thoughtcrime.securesms.main
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||
|
||||
class MainNavigationViewModel : ViewModel() {
|
||||
private val detailLocationFlow = MutableSharedFlow<MainNavigationDetailLocation>()
|
||||
private val megaphoneRepository = AppDependencies.megaphoneRepository
|
||||
|
||||
private val detailLocationFlow = MutableSharedFlow<MainNavigationDetailLocation>()
|
||||
val detailLocation: SharedFlow<MainNavigationDetailLocation> = detailLocationFlow
|
||||
|
||||
private val internalMegaphone = MutableStateFlow(Megaphone.NONE)
|
||||
val megaphone: StateFlow<Megaphone> = internalMegaphone
|
||||
|
||||
private val internalSnackbar = MutableStateFlow<SnackbarState?>(null)
|
||||
val snackbar: StateFlow<SnackbarState?> = internalSnackbar
|
||||
|
||||
private val internalNavigationEvents = MutableSharedFlow<NavigationEvent>()
|
||||
val navigationEvents: Flow<NavigationEvent> = internalNavigationEvents
|
||||
|
||||
fun goTo(location: MainNavigationDetailLocation) {
|
||||
viewModelScope.launch {
|
||||
detailLocationFlow.emit(location)
|
||||
}
|
||||
}
|
||||
|
||||
fun goToCameraFirstStoryCapture() {
|
||||
viewModelScope.launch {
|
||||
internalNavigationEvents.emit(NavigationEvent.STORY_CAMERA_FIRST)
|
||||
}
|
||||
}
|
||||
|
||||
fun getNextMegaphone() {
|
||||
megaphoneRepository.getNextMegaphone { next ->
|
||||
internalMegaphone.update { next ?: Megaphone.NONE }
|
||||
}
|
||||
}
|
||||
|
||||
fun setSnackbar(snackbarState: SnackbarState?) {
|
||||
internalSnackbar.update { snackbarState }
|
||||
}
|
||||
|
||||
fun onMegaphoneSnoozed(event: Megaphones.Event) {
|
||||
megaphoneRepository.markSeen(event)
|
||||
internalMegaphone.update { Megaphone.NONE }
|
||||
}
|
||||
|
||||
fun onMegaphoneCompleted(event: Megaphones.Event) {
|
||||
internalMegaphone.update { Megaphone.NONE }
|
||||
megaphoneRepository.markFinished(event)
|
||||
}
|
||||
|
||||
fun onMegaphoneVisible(visible: Megaphone) {
|
||||
megaphoneRepository.markVisible(visible.event)
|
||||
}
|
||||
|
||||
enum class NavigationEvent {
|
||||
STORY_CAMERA_FIRST
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -244,6 +245,7 @@ private fun SearchToolbar(
|
||||
Text(text = stringResource(state.searchHint))
|
||||
},
|
||||
modifier = modifier
|
||||
.systemBarsPadding()
|
||||
.background(color = state.toolbarColor ?: MaterialTheme.colorScheme.surface)
|
||||
.height(dimensionResource(R.dimen.signal_m3_toolbar_height))
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp)
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
package org.thoughtcrime.securesms.stories.landing
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.app.SharedElementCallback
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
@@ -40,11 +32,11 @@ 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.MainNavigationViewModel
|
||||
import org.thoughtcrime.securesms.main.MainToolbarMode
|
||||
import org.thoughtcrime.securesms.main.MainToolbarViewModel
|
||||
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||
import org.thoughtcrime.securesms.main.SnackbarState
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostModel
|
||||
@@ -59,7 +51,6 @@ import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.views.Stub
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* The "landing page" for Stories.
|
||||
@@ -71,7 +62,6 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
|
||||
}
|
||||
|
||||
private lateinit var emptyNotice: View
|
||||
private lateinit var cameraFab: FloatingActionButton
|
||||
|
||||
private lateinit var bannerView: Stub<ComposeView>
|
||||
|
||||
@@ -85,6 +75,7 @@ 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()
|
||||
|
||||
private lateinit var adapter: MappingAdapter
|
||||
|
||||
@@ -152,47 +143,6 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
emptyNotice = requireView().findViewById(R.id.empty_notice)
|
||||
cameraFab = requireView().findViewById(R.id.camera_fab)
|
||||
val sharedElementTarget: View = requireView().findViewById(R.id.camera_fab_shared_element_target)
|
||||
|
||||
ViewCompat.setTransitionName(cameraFab, "new_convo_fab")
|
||||
ViewCompat.setTransitionName(sharedElementTarget, "camera_fab")
|
||||
|
||||
sharedElementEnterTransition = TransitionInflater.from(requireContext()).inflateTransition(R.transition.change_transform_fabs)
|
||||
setEnterSharedElementCallback(object : SharedElementCallback() {
|
||||
override fun onSharedElementStart(sharedElementNames: MutableList<String>?, sharedElements: MutableList<View>?, sharedElementSnapshots: MutableList<View>?) {
|
||||
if (sharedElementNames?.contains("camera_fab") == true) {
|
||||
cameraFab.setImageResource(R.drawable.symbol_edit_24)
|
||||
lifecycleDisposable += Single.timer(200, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
cameraFab.setImageResource(R.drawable.symbol_camera_24)
|
||||
sharedElementTarget.alpha = 0f
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
cameraFab.setOnClickListener {
|
||||
if (CameraXUtil.isSupported()) {
|
||||
startActivityIfAble(MediaSelectionActivity.camera(requireContext(), isStory = true))
|
||||
} else {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.onAllGranted { startActivityIfAble(MediaSelectionActivity.camera(requireContext(), isStory = true)) }
|
||||
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24)
|
||||
.withPermanentDenialDialog(
|
||||
getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos),
|
||||
null,
|
||||
R.string.CameraXFragment_allow_access_camera,
|
||||
R.string.CameraXFragment_to_capture_photos_videos,
|
||||
getParentFragmentManager()
|
||||
)
|
||||
.onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() }
|
||||
.execute()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
if (it.loadingState == StoriesLandingState.LoadingState.LOADED) {
|
||||
@@ -253,7 +203,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
|
||||
MyStoriesItem.Model(
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
onClick = {
|
||||
cameraFab.performClick()
|
||||
mainNavigationViewModel.goToCameraFirstStoryCapture()
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -323,7 +273,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
|
||||
openStoryViewer(model, preview, true)
|
||||
},
|
||||
onAvatarClick = {
|
||||
cameraFab.performClick()
|
||||
mainNavigationViewModel.goToCameraFirstStoryCapture()
|
||||
},
|
||||
onLockList = {
|
||||
recyclerView?.suppressLayout(true)
|
||||
@@ -385,8 +335,11 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
|
||||
private fun handleHideStory(model: StoriesLandingItem.Model) {
|
||||
StoryDialogs.hideStory(requireContext(), model.data.storyRecipient.getShortDisplayName(requireContext())) {
|
||||
viewModel.setHideStory(model.data.storyRecipient, true).subscribe {
|
||||
Snackbar.make(cameraFab, R.string.StoriesLandingFragment__story_hidden, Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
mainNavigationViewModel.setSnackbar(
|
||||
SnackbarState(
|
||||
message = getString(R.string.StoriesLandingFragment__story_hidden)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util.task;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.SignalProgressDialog;
|
||||
|
||||
public abstract class SnackbarAsyncTask<Params>
|
||||
extends AsyncTask<Params, Void, Void>
|
||||
implements View.OnClickListener
|
||||
{
|
||||
private static final String TAG = Log.tag(SnackbarAsyncTask.class);
|
||||
|
||||
private final Lifecycle lifecycle;
|
||||
private final View view;
|
||||
private final String snackbarText;
|
||||
private final String snackbarActionText;
|
||||
private final int snackbarActionColor;
|
||||
private final int snackbarDuration;
|
||||
private final boolean showProgress;
|
||||
|
||||
private @Nullable Params reversibleParameter;
|
||||
private @Nullable SignalProgressDialog progressDialog;
|
||||
|
||||
public SnackbarAsyncTask(@NonNull Lifecycle lifecycle,
|
||||
@NonNull View view,
|
||||
String snackbarText,
|
||||
String snackbarActionText,
|
||||
int snackbarActionColor,
|
||||
int snackbarDuration,
|
||||
boolean showProgress)
|
||||
{
|
||||
this.lifecycle = lifecycle;
|
||||
this.view = view;
|
||||
this.snackbarText = snackbarText;
|
||||
this.snackbarActionText = snackbarActionText;
|
||||
this.snackbarActionColor = snackbarActionColor;
|
||||
this.snackbarDuration = snackbarDuration;
|
||||
this.showProgress = showProgress;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
if (this.showProgress) this.progressDialog = SignalProgressDialog.show(view.getContext(), "", "", true);
|
||||
else this.progressDialog = null;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
@Override
|
||||
protected final Void doInBackground(Params... params) {
|
||||
this.reversibleParameter = params != null && params.length > 0 ?params[0] : null;
|
||||
executeAction(reversibleParameter);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
if (this.showProgress && this.progressDialog != null) {
|
||||
this.progressDialog.dismiss();
|
||||
this.progressDialog = null;
|
||||
}
|
||||
|
||||
if (!lifecycle.getCurrentState().isAtLeast(Lifecycle.State.CREATED)) {
|
||||
Log.w(TAG, "Not in at least created state. Refusing to show snack bar.");
|
||||
return;
|
||||
}
|
||||
|
||||
Snackbar.make(view, snackbarText, snackbarDuration)
|
||||
.setAction(snackbarActionText, this)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
if (showProgress) progressDialog = SignalProgressDialog.show(view.getContext(), "", "", true);
|
||||
else progressDialog = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
reverseAction(reversibleParameter);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
if (showProgress && progressDialog != null) {
|
||||
progressDialog.dismiss();
|
||||
progressDialog = null;
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
protected abstract void executeAction(@Nullable Params parameter);
|
||||
protected abstract void reverseAction(@Nullable Params parameter);
|
||||
|
||||
}
|
||||
@@ -6,16 +6,20 @@
|
||||
package org.thoughtcrime.securesms.window
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.material3.adaptive.layout.AnimatedPane
|
||||
import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold
|
||||
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
|
||||
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -25,7 +29,7 @@ import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.window.core.ExperimentalWindowCoreApi
|
||||
import androidx.window.core.layout.WindowHeightSizeClass
|
||||
import androidx.window.core.layout.WindowWidthSizeClass
|
||||
import org.signal.core.ui.compose.Previews
|
||||
@@ -58,43 +62,76 @@ enum class WindowSizeClass(
|
||||
EXTENDED_PORTRAIT(Navigation.RAIL),
|
||||
EXTENDED_LANDSCAPE(Navigation.RAIL);
|
||||
|
||||
fun isCompact(): Boolean = this == COMPACT_PORTRAIT || this == COMPACT_LANDSCAPE
|
||||
fun isMedium(): Boolean = this == MEDIUM_PORTRAIT || this == MEDIUM_LANDSCAPE
|
||||
fun isExtended(): Boolean = this == EXTENDED_PORTRAIT || this == EXTENDED_LANDSCAPE
|
||||
|
||||
companion object {
|
||||
|
||||
@OptIn(ExperimentalWindowCoreApi::class)
|
||||
fun Resources.getWindowSizeClass(): WindowSizeClass {
|
||||
val orientation = configuration.orientation
|
||||
|
||||
if (!RemoteConfig.largeScreenUi) {
|
||||
return getCompactSizeClassForOrientation(orientation)
|
||||
}
|
||||
|
||||
val windowSizeClass = androidx.window.core.layout.WindowSizeClass.compute(
|
||||
displayMetrics.widthPixels,
|
||||
displayMetrics.heightPixels,
|
||||
displayMetrics.density
|
||||
)
|
||||
|
||||
return getSizeClassForOrientationAndSystemSizeClass(orientation, windowSizeClass)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberWindowSizeClass(): WindowSizeClass {
|
||||
val orientation = LocalConfiguration.current.orientation
|
||||
|
||||
if (!LocalInspectionMode.current && !RemoteConfig.largeScreenUi) {
|
||||
return when (orientation) {
|
||||
Configuration.ORIENTATION_PORTRAIT, Configuration.ORIENTATION_UNDEFINED, Configuration.ORIENTATION_SQUARE -> {
|
||||
COMPACT_PORTRAIT
|
||||
}
|
||||
Configuration.ORIENTATION_LANDSCAPE -> COMPACT_LANDSCAPE
|
||||
else -> error("Unexpected orientation: $orientation")
|
||||
}
|
||||
return getCompactSizeClassForOrientation(orientation)
|
||||
}
|
||||
|
||||
val wsc = currentWindowAdaptiveInfo().windowSizeClass
|
||||
|
||||
return remember(orientation, wsc) {
|
||||
when (orientation) {
|
||||
Configuration.ORIENTATION_PORTRAIT, Configuration.ORIENTATION_UNDEFINED, Configuration.ORIENTATION_SQUARE -> {
|
||||
when (wsc.windowWidthSizeClass) {
|
||||
WindowWidthSizeClass.COMPACT -> COMPACT_PORTRAIT
|
||||
WindowWidthSizeClass.MEDIUM -> MEDIUM_PORTRAIT
|
||||
WindowWidthSizeClass.EXPANDED -> EXTENDED_PORTRAIT
|
||||
else -> error("Unsupported.")
|
||||
}
|
||||
}
|
||||
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||
when (wsc.windowHeightSizeClass) {
|
||||
WindowHeightSizeClass.COMPACT -> COMPACT_LANDSCAPE
|
||||
WindowHeightSizeClass.MEDIUM -> MEDIUM_LANDSCAPE
|
||||
WindowHeightSizeClass.EXPANDED -> EXTENDED_LANDSCAPE
|
||||
else -> error("Unsupported.")
|
||||
}
|
||||
}
|
||||
else -> error("Unexpected orientation: $orientation")
|
||||
getSizeClassForOrientationAndSystemSizeClass(orientation, wsc)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCompactSizeClassForOrientation(orientation: Int): WindowSizeClass {
|
||||
return when (orientation) {
|
||||
Configuration.ORIENTATION_PORTRAIT, Configuration.ORIENTATION_UNDEFINED, Configuration.ORIENTATION_SQUARE -> {
|
||||
COMPACT_PORTRAIT
|
||||
}
|
||||
|
||||
Configuration.ORIENTATION_LANDSCAPE -> COMPACT_LANDSCAPE
|
||||
else -> error("Unexpected orientation: $orientation")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSizeClassForOrientationAndSystemSizeClass(orientation: Int, windowSizeClass: androidx.window.core.layout.WindowSizeClass): WindowSizeClass {
|
||||
return when (orientation) {
|
||||
Configuration.ORIENTATION_PORTRAIT, Configuration.ORIENTATION_UNDEFINED, Configuration.ORIENTATION_SQUARE -> {
|
||||
when (windowSizeClass.windowWidthSizeClass) {
|
||||
WindowWidthSizeClass.COMPACT -> COMPACT_PORTRAIT
|
||||
WindowWidthSizeClass.MEDIUM -> MEDIUM_PORTRAIT
|
||||
WindowWidthSizeClass.EXPANDED -> EXTENDED_PORTRAIT
|
||||
else -> error("Unsupported.")
|
||||
}
|
||||
}
|
||||
|
||||
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||
when (windowSizeClass.windowHeightSizeClass) {
|
||||
WindowHeightSizeClass.COMPACT -> COMPACT_LANDSCAPE
|
||||
WindowHeightSizeClass.MEDIUM -> MEDIUM_LANDSCAPE
|
||||
WindowHeightSizeClass.EXPANDED -> EXTENDED_LANDSCAPE
|
||||
else -> error("Unsupported.")
|
||||
}
|
||||
}
|
||||
|
||||
else -> error("Unexpected orientation: $orientation")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,8 +141,10 @@ enum class WindowSizeClass(
|
||||
* Composable who's precise layout will depend on the window size class of the device it is being utilized on.
|
||||
* This is built to be generic so that we can use it throughout the application to support different device classes.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun AppScaffold(
|
||||
navigator: ThreePaneScaffoldNavigator<Any> = rememberListDetailPaneScaffoldNavigator<Any>(),
|
||||
detailContent: @Composable () -> Unit = {},
|
||||
navRailContent: @Composable () -> Unit = {},
|
||||
bottomNavContent: @Composable () -> Unit = {},
|
||||
@@ -113,49 +152,68 @@ fun AppScaffold(
|
||||
) {
|
||||
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
|
||||
|
||||
Row {
|
||||
if (windowSizeClass.isMedium()) {
|
||||
Row {
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
ListAndNavigation(
|
||||
listContent = listContent,
|
||||
navRailContent = navRailContent,
|
||||
bottomNavContent = bottomNavContent,
|
||||
windowSizeClass = windowSizeClass
|
||||
)
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
detailContent()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NavigableListDetailPaneScaffold(
|
||||
navigator = navigator,
|
||||
listPane = {
|
||||
AnimatedPane {
|
||||
ListAndNavigation(
|
||||
listContent = listContent,
|
||||
navRailContent = navRailContent,
|
||||
bottomNavContent = bottomNavContent,
|
||||
windowSizeClass = windowSizeClass
|
||||
)
|
||||
}
|
||||
},
|
||||
detailPane = {
|
||||
AnimatedPane {
|
||||
detailContent()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListAndNavigation(
|
||||
listContent: @Composable () -> Unit,
|
||||
navRailContent: @Composable () -> Unit,
|
||||
bottomNavContent: @Composable () -> Unit,
|
||||
windowSizeClass: WindowSizeClass
|
||||
) {
|
||||
Row(modifier = Modifier.navigationBarsPadding()) {
|
||||
if (windowSizeClass.navigation == Navigation.RAIL) {
|
||||
navRailContent()
|
||||
}
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
val listWidth = when (windowSizeClass) {
|
||||
WindowSizeClass.COMPACT_PORTRAIT -> maxWidth
|
||||
WindowSizeClass.COMPACT_LANDSCAPE -> maxWidth
|
||||
WindowSizeClass.MEDIUM_PORTRAIT -> maxWidth * 0.5f
|
||||
WindowSizeClass.MEDIUM_LANDSCAPE -> 360.dp
|
||||
WindowSizeClass.EXTENDED_PORTRAIT -> 360.dp
|
||||
WindowSizeClass.EXTENDED_LANDSCAPE -> 360.dp
|
||||
Column {
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
listContent()
|
||||
}
|
||||
|
||||
val detailWidth = maxWidth - listWidth
|
||||
|
||||
Row {
|
||||
Column(
|
||||
modifier = Modifier.width(listWidth).navigationBarsPadding()
|
||||
) {
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
listContent()
|
||||
}
|
||||
|
||||
if (windowSizeClass.navigation == Navigation.BAR) {
|
||||
bottomNavContent()
|
||||
}
|
||||
}
|
||||
|
||||
if (detailWidth > 0.dp) {
|
||||
// TODO -- slider to divide sizing?
|
||||
Box(modifier = Modifier.width(detailWidth)) {
|
||||
detailContent()
|
||||
}
|
||||
}
|
||||
if (windowSizeClass.navigation == Navigation.BAR) {
|
||||
bottomNavContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Preview(device = "spec:width=360dp,height=640dp,orientation=portrait")
|
||||
@Preview(device = "spec:width=640dp,height=360dp,orientation=landscape")
|
||||
@Preview(device = "spec:width=600dp,height=1024dp,orientation=portrait")
|
||||
|
||||
Reference in New Issue
Block a user