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.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewTreeObserver
|
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.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 com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||||
import org.signal.core.util.getSerializableCompat
|
import org.signal.core.util.getSerializableCompat
|
||||||
@@ -29,11 +37,14 @@ import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSh
|
|||||||
import org.thoughtcrime.securesms.conversationlist.RestoreCompleteBottomSheetDialog
|
import org.thoughtcrime.securesms.conversationlist.RestoreCompleteBottomSheetDialog
|
||||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity
|
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
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.net.DeviceTransferBlockingInterceptor
|
||||||
import org.thoughtcrime.securesms.notifications.VitalsViewModel
|
import org.thoughtcrime.securesms.notifications.VitalsViewModel
|
||||||
import org.thoughtcrime.securesms.stories.Stories
|
import org.thoughtcrime.securesms.stories.Stories
|
||||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTab
|
import org.thoughtcrime.securesms.stories.tabs.ConversationListTab
|
||||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository
|
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.stories.tabs.ConversationListTabsViewModel
|
||||||
import org.thoughtcrime.securesms.util.AppStartup
|
import org.thoughtcrime.securesms.util.AppStartup
|
||||||
import org.thoughtcrime.securesms.util.CachedInflater
|
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.SplashScreenUtil
|
||||||
import org.thoughtcrime.securesms.util.WindowUtil
|
import org.thoughtcrime.securesms.util.WindowUtil
|
||||||
import org.thoughtcrime.securesms.util.viewModel
|
import org.thoughtcrime.securesms.util.viewModel
|
||||||
|
import org.thoughtcrime.securesms.window.AppScaffold
|
||||||
|
|
||||||
class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider {
|
class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider {
|
||||||
|
|
||||||
@@ -85,7 +97,39 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
|||||||
AppStartup.getInstance().onCriticalRenderEventStart()
|
AppStartup.getInstance().onCriticalRenderEventStart()
|
||||||
super.onCreate(savedInstanceState, ready)
|
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)
|
val content: View = findViewById(android.R.id.content)
|
||||||
content.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
|
content.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
|
||||||
|
|||||||
@@ -3,15 +3,20 @@ package org.thoughtcrime.securesms;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import androidx.annotation.MainThread;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.lifecycle.viewmodel.internal.ViewModelProviders;
|
||||||
|
|
||||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
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 org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
@@ -20,8 +25,10 @@ public class MainNavigator {
|
|||||||
|
|
||||||
public static final int REQUEST_CONFIG_CHANGES = 901;
|
public static final int REQUEST_CONFIG_CHANGES = 901;
|
||||||
|
|
||||||
private final AppCompatActivity activity;
|
private final AppCompatActivity activity;
|
||||||
private final LifecycleDisposable lifecycleDisposable;
|
private final LifecycleDisposable lifecycleDisposable;
|
||||||
|
|
||||||
|
private MainNavigationViewModel viewModel;
|
||||||
|
|
||||||
public MainNavigator(@NonNull AppCompatActivity activity) {
|
public MainNavigator(@NonNull AppCompatActivity activity) {
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
@@ -30,6 +37,15 @@ public class MainNavigator {
|
|||||||
lifecycleDisposable.bindTo(activity);
|
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) {
|
public static MainNavigator get(@NonNull Activity activity) {
|
||||||
if (!(activity instanceof MainActivity)) {
|
if (!(activity instanceof MainActivity)) {
|
||||||
throw new IllegalArgumentException("Activity must be an instance of MainActivity!");
|
throw new IllegalArgumentException("Activity must be an instance of MainActivity!");
|
||||||
@@ -57,10 +73,7 @@ public class MainNavigator {
|
|||||||
.map(builder -> builder.withDistributionType(distributionType)
|
.map(builder -> builder.withDistributionType(distributionType)
|
||||||
.withStartingPosition(startingPosition)
|
.withStartingPosition(startingPosition)
|
||||||
.build())
|
.build())
|
||||||
.subscribe(intent -> {
|
.subscribe(intent -> viewModel.goTo(new MainNavigationDetailLocation.Conversation(intent)));
|
||||||
activity.startActivity(intent);
|
|
||||||
activity.overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out);
|
|
||||||
});
|
|
||||||
|
|
||||||
lifecycleDisposable.add(disposable);
|
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.compose.ComposeFragment
|
||||||
import org.thoughtcrime.securesms.main.MainNavigationBar
|
import org.thoughtcrime.securesms.main.MainNavigationBar
|
||||||
import org.thoughtcrime.securesms.main.MainNavigationDestination
|
import org.thoughtcrime.securesms.main.MainNavigationDestination
|
||||||
|
import org.thoughtcrime.securesms.main.MainNavigationRail
|
||||||
import org.thoughtcrime.securesms.main.MainNavigationState
|
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.
|
* Displays the "Chats" and "Stories" tab to a user.
|
||||||
@@ -37,16 +40,28 @@ class ConversationListTabsFragment : ComposeFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.visibilityState.isVisible()) {
|
if (state.visibilityState.isVisible()) {
|
||||||
MainNavigationBar(
|
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
|
||||||
state = navState,
|
val onDestinationSelected: (MainNavigationDestination) -> Unit = remember {
|
||||||
onDestinationSelected = {
|
{
|
||||||
when (it) {
|
when (it) {
|
||||||
MainNavigationDestination.CHATS -> viewModel.onChatsSelected()
|
MainNavigationDestination.CHATS -> viewModel.onChatsSelected()
|
||||||
MainNavigationDestination.CALLS -> viewModel.onCallsSelected()
|
MainNavigationDestination.CALLS -> viewModel.onCallsSelected()
|
||||||
MainNavigationDestination.STORIES -> viewModel.onStoriesSelected()
|
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
|
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
|
// endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
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.MainNavigationBar
|
||||||
import org.thoughtcrime.securesms.main.MainNavigationRail
|
import org.thoughtcrime.securesms.main.MainNavigationRail
|
||||||
import org.thoughtcrime.securesms.main.MainNavigationState
|
import org.thoughtcrime.securesms.main.MainNavigationState
|
||||||
|
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||||
|
|
||||||
enum class Navigation {
|
enum class Navigation {
|
||||||
RAIL,
|
RAIL,
|
||||||
@@ -39,6 +41,10 @@ enum class Navigation {
|
|||||||
/**
|
/**
|
||||||
* Describes the size of screen we are displaying, and what components should be displayed.
|
* 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
|
* https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes
|
||||||
*/
|
*/
|
||||||
enum class WindowSizeClass(
|
enum class WindowSizeClass(
|
||||||
@@ -54,9 +60,20 @@ enum class WindowSizeClass(
|
|||||||
companion object {
|
companion object {
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberWindowSizeClass(): WindowSizeClass {
|
fun rememberWindowSizeClass(): WindowSizeClass {
|
||||||
val wsc = currentWindowAdaptiveInfo().windowSizeClass
|
|
||||||
val orientation = LocalConfiguration.current.orientation
|
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) {
|
return remember(orientation, wsc) {
|
||||||
when (orientation) {
|
when (orientation) {
|
||||||
Configuration.ORIENTATION_PORTRAIT, Configuration.ORIENTATION_UNDEFINED, Configuration.ORIENTATION_SQUARE -> {
|
Configuration.ORIENTATION_PORTRAIT, Configuration.ORIENTATION_UNDEFINED, Configuration.ORIENTATION_SQUARE -> {
|
||||||
@@ -88,10 +105,10 @@ enum class WindowSizeClass(
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AppScaffold(
|
fun AppScaffold(
|
||||||
listContent: @Composable () -> Unit,
|
detailContent: @Composable () -> Unit = {},
|
||||||
detailContent: @Composable () -> Unit,
|
navRailContent: @Composable () -> Unit = {},
|
||||||
navRailContent: @Composable () -> Unit,
|
bottomNavContent: @Composable () -> Unit = {},
|
||||||
bottomNavContent: @Composable () -> Unit
|
listContent: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
|
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