Replace main activity xml with AppScaffold.

This commit is contained in:
Alex Hart
2025-03-25 14:01:20 -03:00
committed by Cody Henthorne
parent 8053d567f2
commit 276285ebef
8 changed files with 149 additions and 39 deletions

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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
}

View File

@@ -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 }
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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>