mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 20:48:43 +00:00
Replace main activity xml with AppScaffold.
This commit is contained in:
committed by
Cody Henthorne
parent
8053d567f2
commit
276285ebef
@@ -12,7 +12,15 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.compose.AndroidFragment
|
||||
import androidx.fragment.compose.rememberFragmentState
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getSerializableCompat
|
||||
@@ -29,11 +37,14 @@ import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSh
|
||||
import org.thoughtcrime.securesms.conversationlist.RestoreCompleteBottomSheetDialog
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.main.MainActivityListHostFragment
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
|
||||
import org.thoughtcrime.securesms.notifications.VitalsViewModel
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTab
|
||||
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.AppStartup
|
||||
import org.thoughtcrime.securesms.util.CachedInflater
|
||||
@@ -42,6 +53,7 @@ import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.SplashScreenUtil
|
||||
import org.thoughtcrime.securesms.util.WindowUtil
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import org.thoughtcrime.securesms.window.AppScaffold
|
||||
|
||||
class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider {
|
||||
|
||||
@@ -85,7 +97,39 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
AppStartup.getInstance().onCriticalRenderEventStart()
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
|
||||
setContentView(R.layout.main_activity)
|
||||
setContent {
|
||||
val navState = rememberFragmentState()
|
||||
val listHostState = rememberFragmentState()
|
||||
val detailLocation by navigator.viewModel.detailLocation.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)
|
||||
}
|
||||
}
|
||||
|
||||
AppScaffold(
|
||||
bottomNavContent = {
|
||||
AndroidFragment(
|
||||
clazz = ConversationListTabsFragment::class.java,
|
||||
fragmentState = navState
|
||||
)
|
||||
},
|
||||
navRailContent = {
|
||||
AndroidFragment(
|
||||
clazz = ConversationListTabsFragment::class.java,
|
||||
fragmentState = navState
|
||||
)
|
||||
}
|
||||
) {
|
||||
AndroidFragment(
|
||||
clazz = MainActivityListHostFragment::class.java,
|
||||
fragmentState = listHostState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val content: View = findViewById(android.R.id.content)
|
||||
content.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
|
||||
|
||||
@@ -3,15 +3,20 @@ package org.thoughtcrime.securesms;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.lifecycle.viewmodel.internal.ViewModelProviders;
|
||||
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation;
|
||||
import org.thoughtcrime.securesms.main.MainNavigationViewModel;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
@@ -20,8 +25,10 @@ public class MainNavigator {
|
||||
|
||||
public static final int REQUEST_CONFIG_CHANGES = 901;
|
||||
|
||||
private final AppCompatActivity activity;
|
||||
private final LifecycleDisposable lifecycleDisposable;
|
||||
private final AppCompatActivity activity;
|
||||
private final LifecycleDisposable lifecycleDisposable;
|
||||
|
||||
private MainNavigationViewModel viewModel;
|
||||
|
||||
public MainNavigator(@NonNull AppCompatActivity activity) {
|
||||
this.activity = activity;
|
||||
@@ -30,6 +37,15 @@ public class MainNavigator {
|
||||
lifecycleDisposable.bindTo(activity);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public @NonNull MainNavigationViewModel getViewModel() {
|
||||
if (viewModel == null) {
|
||||
viewModel = new ViewModelProvider(activity).get(MainNavigationViewModel.class);
|
||||
}
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public static MainNavigator get(@NonNull Activity activity) {
|
||||
if (!(activity instanceof MainActivity)) {
|
||||
throw new IllegalArgumentException("Activity must be an instance of MainActivity!");
|
||||
@@ -57,10 +73,7 @@ public class MainNavigator {
|
||||
.map(builder -> builder.withDistributionType(distributionType)
|
||||
.withStartingPosition(startingPosition)
|
||||
.build())
|
||||
.subscribe(intent -> {
|
||||
activity.startActivity(intent);
|
||||
activity.overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out);
|
||||
});
|
||||
.subscribe(intent -> viewModel.goTo(new MainNavigationDetailLocation.Conversation(intent)));
|
||||
|
||||
lifecycleDisposable.add(disposable);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.main
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* Describes which content to display in the detail view.
|
||||
*/
|
||||
sealed interface MainNavigationDetailLocation {
|
||||
data object Empty : MainNavigationDetailLocation
|
||||
data class Conversation(val intent: Intent) : MainNavigationDetailLocation
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.main
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class MainNavigationViewModel : ViewModel() {
|
||||
private val detailLocationFlow = MutableStateFlow<MainNavigationDetailLocation>(MainNavigationDetailLocation.Empty)
|
||||
|
||||
val detailLocation: StateFlow<MainNavigationDetailLocation> = detailLocationFlow
|
||||
|
||||
fun goTo(location: MainNavigationDetailLocation) {
|
||||
detailLocationFlow.update { location }
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,10 @@ import androidx.fragment.app.viewModels
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.main.MainNavigationBar
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDestination
|
||||
import org.thoughtcrime.securesms.main.MainNavigationRail
|
||||
import org.thoughtcrime.securesms.main.MainNavigationState
|
||||
import org.thoughtcrime.securesms.window.Navigation
|
||||
import org.thoughtcrime.securesms.window.WindowSizeClass
|
||||
|
||||
/**
|
||||
* Displays the "Chats" and "Stories" tab to a user.
|
||||
@@ -37,16 +40,28 @@ class ConversationListTabsFragment : ComposeFragment() {
|
||||
}
|
||||
|
||||
if (state.visibilityState.isVisible()) {
|
||||
MainNavigationBar(
|
||||
state = navState,
|
||||
onDestinationSelected = {
|
||||
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
|
||||
val onDestinationSelected: (MainNavigationDestination) -> Unit = remember {
|
||||
{
|
||||
when (it) {
|
||||
MainNavigationDestination.CHATS -> viewModel.onChatsSelected()
|
||||
MainNavigationDestination.CALLS -> viewModel.onCallsSelected()
|
||||
MainNavigationDestination.STORIES -> viewModel.onStoriesSelected()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (windowSizeClass.navigation == Navigation.BAR) {
|
||||
MainNavigationBar(
|
||||
state = navState,
|
||||
onDestinationSelected = onDestinationSelected
|
||||
)
|
||||
} else {
|
||||
MainNavigationRail(
|
||||
state = navState,
|
||||
onDestinationSelected = onDestinationSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1121,5 +1121,12 @@ object RemoteConfig {
|
||||
hotSwappable = true
|
||||
)
|
||||
|
||||
/** Whether to allow different WindowSizeClasses to be used to determine screen layout */
|
||||
val largeScreenUi: Boolean by remoteBoolean(
|
||||
key = "android.largeScreenUI",
|
||||
defaultValue = false,
|
||||
hotSwappable = false
|
||||
)
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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
|
||||
@@ -30,6 +31,7 @@ import org.signal.core.ui.compose.Previews
|
||||
import org.thoughtcrime.securesms.main.MainNavigationBar
|
||||
import org.thoughtcrime.securesms.main.MainNavigationRail
|
||||
import org.thoughtcrime.securesms.main.MainNavigationState
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
|
||||
enum class Navigation {
|
||||
RAIL,
|
||||
@@ -39,6 +41,10 @@ enum class Navigation {
|
||||
/**
|
||||
* Describes the size of screen we are displaying, and what components should be displayed.
|
||||
*
|
||||
* Screens should utilize this class by convention instead of calling [currentWindowAdaptiveInfo]
|
||||
* themselves, as this class includes checks with [RemoteConfig] to ensure we're allowed to display
|
||||
* content in different screen sizes.
|
||||
*
|
||||
* https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes
|
||||
*/
|
||||
enum class WindowSizeClass(
|
||||
@@ -54,9 +60,20 @@ enum class WindowSizeClass(
|
||||
companion object {
|
||||
@Composable
|
||||
fun rememberWindowSizeClass(): WindowSizeClass {
|
||||
val wsc = currentWindowAdaptiveInfo().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")
|
||||
}
|
||||
}
|
||||
|
||||
val wsc = currentWindowAdaptiveInfo().windowSizeClass
|
||||
|
||||
return remember(orientation, wsc) {
|
||||
when (orientation) {
|
||||
Configuration.ORIENTATION_PORTRAIT, Configuration.ORIENTATION_UNDEFINED, Configuration.ORIENTATION_SQUARE -> {
|
||||
@@ -88,10 +105,10 @@ enum class WindowSizeClass(
|
||||
*/
|
||||
@Composable
|
||||
fun AppScaffold(
|
||||
listContent: @Composable () -> Unit,
|
||||
detailContent: @Composable () -> Unit,
|
||||
navRailContent: @Composable () -> Unit,
|
||||
bottomNavContent: @Composable () -> Unit
|
||||
detailContent: @Composable () -> Unit = {},
|
||||
navRailContent: @Composable () -> Unit = {},
|
||||
bottomNavContent: @Composable () -> Unit = {},
|
||||
listContent: @Composable () -> Unit
|
||||
) {
|
||||
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:name="org.thoughtcrime.securesms.main.MainActivityListHostFragment"
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
tools:context=".MainActivity" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/conversation_list_tabs"
|
||||
android:name="org.thoughtcrime.securesms.stories.tabs.ConversationListTabsFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
Reference in New Issue
Block a user