diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0ddf065ec3..cce3a226fb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -190,9 +190,6 @@ android:screenOrientation="portrait" /> - diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt deleted file mode 100644 index b8784a3d44..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.thoughtcrime.securesms.conversation.disappearingmessages - -import androidx.compose.runtime.Composable -import androidx.core.content.IntentCompat -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import dagger.hilt.android.AndroidEntryPoint -import network.loki.messenger.BuildConfig -import org.session.libsession.messaging.messages.ExpirationConfiguration -import org.session.libsession.utilities.Address -import org.thoughtcrime.securesms.FullComposeScreenLockActivity -import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.DisappearingMessagesScreen - -@AndroidEntryPoint -class DisappearingMessagesActivity: FullComposeScreenLockActivity() { - - @Composable - override fun ComposeContent() { - val viewModel: DisappearingMessagesViewModel = - hiltViewModel { factory -> - factory.create( - address = requireNotNull( - IntentCompat.getParcelableExtra(intent, ARG_ADDRESS, Address::class.java) - ) { - "DisappearingMessagesActivity requires an Address to be passed in via the intent." - }, - isNewConfigEnabled = ExpirationConfiguration.isNewConfigEnabled, - showDebugOptions = BuildConfig.BUILD_TYPE != "release" - ) - } - - DisappearingMessagesScreen( - viewModel = viewModel, - onBack = { finish() }, - ) - } - - companion object { - const val ARG_ADDRESS = "address" - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt index 1ba68f20a6..cb9a40ca23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt @@ -31,9 +31,9 @@ class DisappearingMessagesViewModel @AssistedInject constructor( @Assisted private val address: Address, @Assisted("isNewConfigEnabled") private val isNewConfigEnabled: Boolean, @Assisted("showDebugOptions") private val showDebugOptions: Boolean, + @Assisted private val navigator: UINavigator, @param:ApplicationContext private val context: Context, private val disappearingMessages: DisappearingMessages, - private val navigator: UINavigator, private val recipientRepository: RecipientRepository, ) : ViewModel() { @@ -96,7 +96,8 @@ class DisappearingMessagesViewModel @AssistedInject constructor( fun create( address: Address, @Assisted("isNewConfigEnabled") isNewConfigEnabled: Boolean, - @Assisted("showDebugOptions") showDebugOptions: Boolean + @Assisted("showDebugOptions") showDebugOptions: Boolean, + navigator: UINavigator ): DisappearingMessagesViewModel } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 68139a8268..f69b716d6c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -124,7 +124,6 @@ import org.thoughtcrime.securesms.audio.AudioRecorderHandle import org.thoughtcrime.securesms.audio.recordAudio import org.thoughtcrime.securesms.components.TypingStatusSender import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel -import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesActivity import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.ShowOpenUrlDialog @@ -150,6 +149,7 @@ import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDel import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsActivity +import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination import org.thoughtcrime.securesms.conversation.v2.settings.notification.NotificationSettingsActivity import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities @@ -692,9 +692,11 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } is ConversationUiEvent.ShowDisappearingMessages -> { - val intent = Intent(this@ConversationActivityV2, DisappearingMessagesActivity::class.java).apply { - putExtra(DisappearingMessagesActivity.ARG_ADDRESS, event.address) - } + val intent = ConversationSettingsActivity.createIntent( + context = this@ConversationActivityV2, + address = event.address, + startDestination = ConversationSettingsDestination.RouteDisappearingMessages + ) startActivity(intent) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 89c980c0c5..21113c8bc3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -1293,7 +1293,7 @@ class ConversationViewModel @AssistedInject constructor( } } - _uiEvents.tryEmit(ConversationUiEvent.ShowDisappearingMessages(convo.address)) + _uiEvents.tryEmit(ConversationUiEvent.ShowDisappearingMessages(address)) } } @@ -1455,7 +1455,7 @@ data class UiMessage(val id: Long, val message: String) sealed interface ConversationUiEvent { data class NavigateToConversation(val address: Address.Conversable) : ConversationUiEvent - data class ShowDisappearingMessages(val address: Address) : ConversationUiEvent + data class ShowDisappearingMessages(val address: Address.Conversable) : ConversationUiEvent data class ShowNotificationSettings(val address: Address) : ConversationUiEvent data class ShowGroupMembers(val groupAddress: Address.Group) : ConversationUiEvent data class ShowConversationSettings(val threadAddress: Address.Conversable) : ConversationUiEvent diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsActivity.kt index 9e6058c79a..fedeadd188 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsActivity.kt @@ -7,32 +7,39 @@ import androidx.core.content.IntentCompat import dagger.hilt.android.AndroidEntryPoint import org.session.libsession.utilities.Address import org.thoughtcrime.securesms.FullComposeScreenLockActivity -import org.thoughtcrime.securesms.ui.UINavigator -import javax.inject.Inject @AndroidEntryPoint class ConversationSettingsActivity: FullComposeScreenLockActivity() { companion object { - const val THREAD_ADDRESS = "conversation_settings_thread_address" + private const val THREAD_ADDRESS = "conversation_settings_thread_address" + private const val EXTRA_START_DESTINATION = "start_destination" - fun createIntent(context: Context, address: Address.Conversable): Intent { + fun createIntent( + context: Context, + address: Address.Conversable, + startDestination: ConversationSettingsDestination = ConversationSettingsDestination.RouteConversationSettings + ): Intent { return Intent(context, ConversationSettingsActivity::class.java).apply { putExtra(THREAD_ADDRESS, address) + putExtra(EXTRA_START_DESTINATION, startDestination) } } } - @Inject - lateinit var navigator: UINavigator - @Composable override fun ComposeContent() { + val startDestination = IntentCompat.getParcelableExtra( + intent, + EXTRA_START_DESTINATION, + ConversationSettingsDestination::class.java + ) ?: ConversationSettingsDestination.RouteConversationSettings + ConversationSettingsNavHost( address = requireNotNull(IntentCompat.getParcelableExtra(intent, THREAD_ADDRESS, Address.Conversable::class.java)) { "ConversationSettingsActivity requires an Address to be passed in the intent." }, - navigator = navigator, + startDestination = startDestination, returnResult = { code, value -> setResult(RESULT_OK, Intent().putExtra(code, value)) finish() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsNavHost.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsNavHost.kt index 02c288e96b..5a540d1d0c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsNavHost.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsNavHost.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.conversation.v2.settings import android.annotation.SuppressLint +import android.os.Parcelable import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout import androidx.compose.runtime.Composable @@ -14,6 +15,7 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute +import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable import network.loki.messenger.BuildConfig import org.session.libsession.messaging.messages.ExpirationConfiguration @@ -39,11 +41,13 @@ import org.thoughtcrime.securesms.ui.UINavigator import org.thoughtcrime.securesms.ui.horizontalSlideComposable // Destinations -sealed interface ConversationSettingsDestination { +sealed interface ConversationSettingsDestination: Parcelable { @Serializable + @Parcelize data object RouteConversationSettings: ConversationSettingsDestination @Serializable + @Parcelize data class RouteGroupMembers private constructor( private val address: String ): ConversationSettingsDestination { @@ -53,6 +57,7 @@ sealed interface ConversationSettingsDestination { } @Serializable + @Parcelize data class RouteManageMembers private constructor( private val address: String ): ConversationSettingsDestination { @@ -62,6 +67,7 @@ sealed interface ConversationSettingsDestination { } @Serializable + @Parcelize data class RouteInviteToGroup private constructor( private val address: String, val excludingAccountIDs: List @@ -73,15 +79,19 @@ sealed interface ConversationSettingsDestination { } @Serializable + @Parcelize data object RouteDisappearingMessages: ConversationSettingsDestination @Serializable + @Parcelize data object RouteAllMedia: ConversationSettingsDestination @Serializable + @Parcelize data object RouteNotifications: ConversationSettingsDestination @Serializable + @Parcelize data class RouteInviteToCommunity( val communityUrl: String ): ConversationSettingsDestination @@ -92,12 +102,21 @@ sealed interface ConversationSettingsDestination { @Composable fun ConversationSettingsNavHost( address: Address.Conversable, - navigator: UINavigator, + startDestination: ConversationSettingsDestination = RouteConversationSettings, returnResult: (String, Boolean) -> Unit, onBack: () -> Unit ){ SharedTransitionLayout { val navController = rememberNavController() + val navigator: UINavigator = remember { UINavigator() } + + val handleBack: () -> Unit = { + if (navController.previousBackStackEntry != null) { + navController.navigateUp() + } else { + onBack() // Finish activity if at root + } + } ObserveAsEvents(flow = navigator.navigationActions) { action -> when (action) { @@ -107,7 +126,7 @@ fun ConversationSettingsNavHost( action.navOptions(this) } - NavigationAction.NavigateUp -> navController.navigateUp() + NavigationAction.NavigateUp -> handleBack() is NavigationAction.NavigateToIntent -> { navController.context.startActivity(action.intent) @@ -119,12 +138,12 @@ fun ConversationSettingsNavHost( } } - NavHost(navController = navController, startDestination = RouteConversationSettings) { + NavHost(navController = navController, startDestination = startDestination) { // Conversation Settings horizontalSlideComposable { val viewModel = hiltViewModel { factory -> - factory.create(address) + factory.create(address, navigator) } val lifecycleOwner = LocalLifecycleOwner.current @@ -154,7 +173,7 @@ fun ConversationSettingsNavHost( GroupMembersScreen( viewModel = viewModel, onBack = dropUnlessResumed { - navController.popBackStack() + handleBack() }, ) } @@ -178,7 +197,7 @@ fun ConversationSettingsNavHost( ) }, onBack = dropUnlessResumed { - navController.popBackStack() + handleBack() }, ) } @@ -208,10 +227,10 @@ fun ConversationSettingsNavHost( //send invites from the manage group screen editGroupViewModel.onContactSelected(viewModel.currentSelected) - navController.popBackStack() + handleBack() }, onBack = dropUnlessResumed { - navController.popBackStack() + handleBack() }, banner = { GroupMinimumVersionBanner() @@ -244,7 +263,7 @@ fun ConversationSettingsNavHost( viewModel.clearSelection() }, onBack = dropUnlessResumed { - navController.popBackStack() + handleBack() }, ) } @@ -256,14 +275,15 @@ fun ConversationSettingsNavHost( factory.create( address = address, isNewConfigEnabled = ExpirationConfiguration.isNewConfigEnabled, - showDebugOptions = BuildConfig.BUILD_TYPE != "release" + showDebugOptions = BuildConfig.BUILD_TYPE != "release", + navigator = navigator ) } DisappearingMessagesScreen( viewModel = viewModel, onBack = dropUnlessResumed { - navController.popBackStack() + handleBack() }, ) } @@ -278,7 +298,7 @@ fun ConversationSettingsNavHost( MediaOverviewScreen( viewModel = viewModel, onClose = dropUnlessResumed { - navController.popBackStack() + handleBack() }, ) } @@ -293,7 +313,7 @@ fun ConversationSettingsNavHost( NotificationSettingsScreen( viewModel = viewModel, onBack = dropUnlessResumed { - navController.popBackStack() + handleBack() } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt index de86d4cec5..0d98c5c126 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt @@ -72,13 +72,13 @@ import org.thoughtcrime.securesms.util.AvatarUtils @HiltViewModel(assistedFactory = ConversationSettingsViewModel.Factory::class) class ConversationSettingsViewModel @AssistedInject constructor( @Assisted private val address: Address.Conversable, + @Assisted private val navigator: UINavigator, @param:ApplicationContext private val context: Context, private val avatarUtils: AvatarUtils, private val repository: ConversationRepository, private val configFactory: ConfigFactoryProtocol, private val storage: StorageProtocol, private val conversationRepository: ConversationRepository, - private val navigator: UINavigator, private val groupManagerV2: GroupManagerV2, private val groupManager: GroupManagerV2, private val openGroupManager: OpenGroupManager, @@ -1385,7 +1385,10 @@ class ConversationSettingsViewModel @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(address: Address.Conversable): ConversationSettingsViewModel + fun create( + address: Address.Conversable, + navigator: UINavigator + ): ConversationSettingsViewModel } data class UIState( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientRepository.kt index 70d252701e..e10d248e0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientRepository.kt @@ -47,8 +47,10 @@ import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.database.model.NotifyType import org.thoughtcrime.securesms.database.model.RecipientSettings +import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.groups.GroupMemberComparator +import org.thoughtcrime.securesms.pro.SubscriptionType import org.thoughtcrime.securesms.util.DateUtils.Companion.secondsToInstant import java.lang.ref.WeakReference import java.time.Duration @@ -147,7 +149,10 @@ class RecipientRepository @Inject constructor( value = createLocalRecipient(address, recipientData) changeSource = merge( configFactory.userConfigsChanged(onlyConfigTypes = EnumSet.of(UserConfigType.USER_PROFILE)), - TextSecurePreferences.events.filter { it == TextSecurePreferences.SET_FORCE_CURRENT_USER_PRO } + TextSecurePreferences.events.filter { + it == TextSecurePreferences.SET_FORCE_CURRENT_USER_PRO + || it == TextSecurePreferences.DEBUG_SUBSCRIPTION_STATUS + } ) } @@ -407,7 +412,11 @@ class RecipientRepository @Inject constructor( expiryMode = configs.userProfile.getNtsExpiry(), priority = configs.userProfile.getNtsPriority(), proStatus = if (preferences.forceCurrentUserAsPro()) { - ProStatus.Pro() + // take into account the fact that we can be expired via the debug menu - which is no longer pro + if(preferences.getDebugSubscriptionType() == DebugMenuViewModel.DebugSubscriptionStatus.EXPIRED + || preferences.getDebugSubscriptionType() == DebugMenuViewModel.DebugSubscriptionStatus.EXPIRED_APPLE + || preferences.getDebugSubscriptionType() == DebugMenuViewModel.DebugSubscriptionStatus.EXPIRED_EARLIER) ProStatus.None + else ProStatus.Pro() } else { // TODO: Get pro status from config ProStatus.None diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt index ad8a6100e5..6be7bb2d63 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt @@ -277,28 +277,28 @@ fun DebugMenu( ) } ) + } + } - Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing)) - Text( - modifier = Modifier.padding(top = LocalDimensions.current.xxsSpacing), - text = "Pro Data Status", - style = LocalType.current.base - ) - DropDown( - modifier = Modifier.fillMaxWidth() - .padding(top = LocalDimensions.current.xxsSpacing), - selectedText = uiState.selectedDebugProPlanStatus.label, - values = uiState.debugProPlanStatus.map { it.label }, - onValueSelected = { selection -> - sendCommand( - DebugMenuViewModel.Commands.SetDebugProPlanStatus( - uiState.debugProPlanStatus.first { it.label == selection } - ) - ) - } + Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing)) + Text( + modifier = Modifier.padding(top = LocalDimensions.current.xxsSpacing), + text = "Pro Data Status", + style = LocalType.current.base + ) + DropDown( + modifier = Modifier.fillMaxWidth() + .padding(top = LocalDimensions.current.xxsSpacing), + selectedText = uiState.selectedDebugProPlanStatus.label, + values = uiState.debugProPlanStatus.map { it.label }, + onValueSelected = { selection -> + sendCommand( + DebugMenuViewModel.Commands.SetDebugProPlanStatus( + uiState.debugProPlanStatus.first { it.label == selection } + ) ) } - } + ) Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing)) DebugSwitchRow( diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt index 5fc01a9cdd..8688a484ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt @@ -87,9 +87,11 @@ class DebugMenuViewModel @Inject constructor( debugSubscriptionStatuses = setOf( DebugSubscriptionStatus.AUTO_GOOGLE, DebugSubscriptionStatus.EXPIRING_GOOGLE, + DebugSubscriptionStatus.EXPIRING_GOOGLE_LATER, DebugSubscriptionStatus.AUTO_APPLE, DebugSubscriptionStatus.EXPIRING_APPLE, DebugSubscriptionStatus.EXPIRED, + DebugSubscriptionStatus.EXPIRED_EARLIER, DebugSubscriptionStatus.EXPIRED_APPLE, ), selectedDebugSubscriptionStatus = textSecurePreferences.getDebugSubscriptionType() ?: DebugSubscriptionStatus.AUTO_GOOGLE, @@ -445,11 +447,13 @@ class DebugMenuViewModel @Inject constructor( enum class DebugSubscriptionStatus(val label: String) { AUTO_GOOGLE("Auto Renewing (Google, 3 months)"), - EXPIRING_GOOGLE("Expiring/Cancelled (Google, 12 months)"), + EXPIRING_GOOGLE("Expiring/Cancelled (Expires in 14 days, Google, 12 months)"), + EXPIRING_GOOGLE_LATER("Expiring/Cancelled (Expires in 40 days, Google, 12 months)"), AUTO_APPLE("Auto Renewing (Apple, 1 months)"), - EXPIRING_APPLE("Expiring/Cancelled (Apple, 1 months)"), - EXPIRED("Expired (Google)"), - EXPIRED_APPLE("Expired (Apple)"), + EXPIRING_APPLE("Expiring/Cancelled (Expires in 14 days, Apple, 1 months)"), + EXPIRED("Expired (Expired 2 days ago, Google)"), + EXPIRED_EARLIER("Expired (Expired 60 days ago, Google)"), + EXPIRED_APPLE("Expired (Expired 2 days ago, Apple)"), } enum class DebugProPlanStatus(val label: String){ diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 3318d50bf1..33f91e5a8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -135,7 +135,6 @@ class HomeActivity : ScreenLockActionBarActivity(), @Inject lateinit var proStatusManager: ProStatusManager @Inject lateinit var recipientRepository: RecipientRepository @Inject lateinit var avatarUtils: AvatarUtils - @Inject lateinit var startConversationNavigator: UINavigator private val globalSearchViewModel by viewModels() private val homeViewModel by viewModels() @@ -294,7 +293,6 @@ class HomeActivity : ScreenLockActionBarActivity(), val dialogsState by homeViewModel.dialogsState.collectAsState() HomeDialogs( dialogsState = dialogsState, - startConversationNavigator = startConversationNavigator, sendCommand = homeViewModel::onCommand ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDialogs.kt index 45ab8a9a8d..31e7c693e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDialogs.kt @@ -2,12 +2,18 @@ package org.thoughtcrime.securesms.home import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import com.squareup.phrase.Phrase +import kotlinx.coroutines.delay import network.loki.messenger.R import org.session.libsession.utilities.NonTranslatableStringConstants import org.session.libsession.utilities.NonTranslatableStringConstants.PRO @@ -32,7 +38,6 @@ import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme @Composable fun HomeDialogs( dialogsState: HomeViewModel.DialogsState, - startConversationNavigator: UINavigator, sendCommand: (HomeViewModel.Commands) -> Unit ) { SessionMaterialTheme { @@ -61,14 +66,24 @@ fun HomeDialogs( if(dialogsState.showStartConversationSheet != null){ StartConversationSheet( accountId = dialogsState.showStartConversationSheet.accountId, - navigator = startConversationNavigator, onDismissRequest = { sendCommand(HomeViewModel.Commands.HideStartConversationSheet) } ) } - if(dialogsState.proExpiringCTA != null){ + // we need a delay before displaying this. + // Setting the delay in the VM does not account for render and it seems to appear immediately + var showExpiring by remember { mutableStateOf(false) } + LaunchedEffect(dialogsState.proExpiringCTA) { + showExpiring = false + if (dialogsState.proExpiringCTA != null) { + delay(1500) + showExpiring = true + } + } + + if(showExpiring && dialogsState.proExpiringCTA != null){ val context = LocalContext.current AnimatedSessionProCTA( heroImageBg = R.drawable.cta_hero_generic_bg, @@ -82,9 +97,9 @@ fun HomeDialogs( .format() .toString(), features = listOf( - CTAFeature.Icon(stringResource(R.string.proFeatureListLargerGroups)), CTAFeature.Icon(stringResource(R.string.proFeatureListLongerMessages)), CTAFeature.Icon(stringResource(R.string.proFeatureListPinnedConversations)), + CTAFeature.Icon(stringResource(R.string.proFeatureListAnimatedDisplayPicture)), ), positiveButtonText = stringResource(R.string.update), negativeButtonText = stringResource(R.string.close), @@ -98,7 +113,18 @@ fun HomeDialogs( ) } - if(dialogsState.proExpiredCTA){ + // we need a delay before displaying this. + // Setting the delay in the VM does not account for render and it seems to appear immediately + var showExpired by remember { mutableStateOf(false) } + LaunchedEffect(dialogsState.proExpiredCTA) { + showExpired = false + if (dialogsState.proExpiredCTA) { + delay(1500) + showExpired = true + } + } + + if (showExpired && dialogsState.proExpiredCTA) { val context = LocalContext.current AnimatedSessionProCTA( heroImageBg = R.drawable.cta_hero_generic_bg, @@ -112,9 +138,9 @@ fun HomeDialogs( .format() .toString(), features = listOf( - CTAFeature.Icon(stringResource(R.string.proFeatureListLargerGroups)), CTAFeature.Icon(stringResource(R.string.proFeatureListLongerMessages)), CTAFeature.Icon(stringResource(R.string.proFeatureListPinnedConversations)), + CTAFeature.Icon(stringResource(R.string.proFeatureListAnimatedDisplayPicture)), ), positiveButtonText = stringResource(R.string.renew), negativeButtonText = stringResource(R.string.cancel), diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt index 4ecd116b9f..6906b3c7ed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt @@ -9,7 +9,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -50,7 +49,6 @@ import org.thoughtcrime.securesms.util.UserProfileModalData import org.thoughtcrime.securesms.util.UserProfileUtils import org.thoughtcrime.securesms.webrtc.CallManager import org.thoughtcrime.securesms.webrtc.data.State -import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit import javax.inject.Inject @@ -172,7 +170,6 @@ class HomeViewModel @Inject constructor( val validUntil = subscription.type.proStatus.validUntil ?: return@collect if (validUntil.isBefore(now.plus(7, ChronoUnit.DAYS))) { - delay(2000) prefs.setHasSeenProExpiring() _dialogsState.update { state -> state.copy( @@ -191,7 +188,6 @@ class HomeViewModel @Inject constructor( // Check if now is within 30 days after expiry if (now.isBefore(validUntil.plus(30, ChronoUnit.DAYS))) { - delay(2000) prefs.setHasSeenProExpired() _dialogsState.update { state -> state.copy(proExpiredCTA = true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt index 6a9c7cc4f0..fc3652f386 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -48,7 +49,6 @@ import org.thoughtcrime.securesms.ui.theme.PreviewTheme fun StartConversationSheet( modifier: Modifier = Modifier, accountId: String, - navigator: UINavigator, onDismissRequest: () -> Unit, ){ val sheetState = rememberModalBottomSheetState( @@ -71,7 +71,6 @@ fun StartConversationSheet( ) { StartConversationNavHost( accountId = accountId, - navigator = navigator, onClose = { scope.launch { sheetState.hide() @@ -107,11 +106,12 @@ sealed interface StartConversationDestination { @Composable fun StartConversationNavHost( accountId: String, - navigator: UINavigator, onClose: () -> Unit ){ SharedTransitionLayout { val navController = rememberNavController() + val navigator: UINavigator = + remember { UINavigator() } ObserveAsEvents(flow = navigator.navigationActions) { action -> when (action) { @@ -235,7 +235,6 @@ fun PreviewStartConversationSheet(){ StartConversationSheet( accountId = "", onDismissRequest = {}, - navigator = UINavigator() ) } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt index 62cbb09cbc..693d109e32 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt @@ -561,15 +561,7 @@ fun Buttons( modifier = Modifier.qaTag(R.string.qa_settings_item_pro), colors = accentTextButtonColors() ) { - // there is a special case when we have a subscription error or loading - // but also no pro account - if(subscriptionState.refreshState !is State.Success && - subscriptionState.type is SubscriptionType.NeverSubscribed - ){ - sendCommand(ShowProErrorOrLoading) - } else { - activity?.push() - } + activity?.push() } Divider() @@ -1079,7 +1071,7 @@ private fun SettingsScreenPreview() { ), duration = ProSubscriptionDuration.THREE_MONTHS, subscriptionDetails = SubscriptionDetails( - device = "iPhone", + device = "iOS", store = "Apple App Store", platform = "Apple", platformAccount = "Apple Account", diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt index d272784213..1c609572be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt @@ -585,53 +585,6 @@ class SettingsViewModel @Inject constructor( showUrlDialog( "https://session.foundation/donate#app") } - is Commands.ShowProErrorOrLoading -> { - when(_uiState.value.subscriptionState.refreshState){ - // if we are in a loading or refresh state we should show a dialog instead - is State.Loading -> { - _uiState.update { - it.copy( - showSimpleDialog = SimpleDialogData( - title = Phrase.from(context.getText(R.string.proStatusLoading)) - .put(PRO_KEY, NonTranslatableStringConstants.PRO) - .format().toString(), - message = Phrase.from(context.getText(R.string.proStatusLoadingDescription)) - .put(PRO_KEY, NonTranslatableStringConstants.PRO) - .format(), - positiveText = context.getString(R.string.okay), - positiveStyleDanger = false, - ) - ) - } - } - - is State.Error -> { - _uiState.update { - it.copy( - showSimpleDialog = SimpleDialogData( - title = Phrase.from(context.getText(R.string.proStatusError)) - .put(PRO_KEY, NonTranslatableStringConstants.PRO) - .format().toString(), - message = Phrase.from(context.getText(R.string.proStatusRefreshNetworkError)) - .put(PRO_KEY, NonTranslatableStringConstants.PRO) - .format(), - positiveText = context.getString(R.string.retry), - negativeText = context.getString(R.string.helpSupport), - positiveStyleDanger = false, - showXIcon = true, - onPositive = { refreshSubscriptionData() }, - onNegative = { - showUrlDialog(ProStatusManager.URL_PRO_SUPPORT) - } - ) - ) - } - } - - else -> {} - } - } - is Commands.HideSimpleDialog -> { _uiState.update { it.copy(showSimpleDialog = null) } } @@ -718,8 +671,6 @@ class SettingsViewModel @Inject constructor( data object OnDonateClicked: Commands - data object ShowProErrorOrLoading: Commands - data class ClearData(val clearNetwork: Boolean): Commands } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt index 71cf886ad2..ec7c688348 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt @@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.preferences.prosettings import androidx.annotation.DrawableRes import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -27,6 +29,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier @@ -60,26 +63,29 @@ import org.thoughtcrime.securesms.ui.components.inlineContentMap @Composable fun BaseProSettingsScreen( disabled: Boolean, + hideHomeAppBar: Boolean = false, onBack: () -> Unit, onHeaderClick: (() -> Unit)? = null, extraHeaderContent: @Composable (() -> Unit)? = null, content: @Composable () -> Unit ){ Scaffold( - topBar = { - BackAppBar( - title = "", - backgroundColor = Color.Transparent, - onBack = onBack, - ) - }, + topBar = if(!hideHomeAppBar){{ + BackAppBar( + title = "", + backgroundColor = Color.Transparent, + onBack = onBack, + ) + }} else {{}}, contentWindowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal), ) { paddings -> Column( modifier = Modifier .fillMaxSize() - .padding(top = paddings.calculateTopPadding() - LocalDimensions.current.appBarHeight) + .padding(top = + (paddings.calculateTopPadding() - LocalDimensions.current.appBarHeight) + .coerceAtLeast(0.dp)) .consumeWindowInsets(paddings) .padding( horizontal = LocalDimensions.current.spacing, @@ -204,6 +210,7 @@ fun BaseNonOriginatingProSettingsScreen( headerTitle: CharSequence?, contentTitle: String?, contentDescription: CharSequence?, + contentClick: (() -> Unit)? = null, linkCellsInfo: String?, linkCells: List = emptyList(), ) { @@ -226,6 +233,15 @@ fun BaseNonOriginatingProSettingsScreen( if (contentDescription != null) { Spacer(Modifier.height(LocalDimensions.current.xxxsSpacing)) Text( + modifier = Modifier.then( + // make the component clickable is there is an action + if (contentClick != null) Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = contentClick + ) + else Modifier + ), text = annotatedStringResource(contentDescription), style = LocalType.current.base, color = LocalColors.current.text, diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/CancelPlanNonOriginating.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/CancelPlanNonOriginating.kt index 792c8957bb..e8f190426f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/CancelPlanNonOriginating.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/CancelPlanNonOriginating.kt @@ -89,7 +89,7 @@ private fun PreviewUpdatePlan( val context = LocalContext.current CancelPlanNonOriginating ( subscriptionDetails = SubscriptionDetails( - device = "iPhone", + device = "iOS", store = "Apple App Store", platform = "Apple", platformAccount = "Apple Account", diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/PlanConfirmationScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/PlanConfirmationScreen.kt index e2246b8281..5e8c6c0b13 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/PlanConfirmationScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/PlanConfirmationScreen.kt @@ -160,7 +160,7 @@ private fun PreviewPlanConfirmation( ), duration = ProSubscriptionDuration.THREE_MONTHS, subscriptionDetails = SubscriptionDetails( - device = "iPhone", + device = "iOS", store = "Apple App Store", platform = "Apple", platformAccount = "Apple Account", diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsActivity.kt index da7e5cbd20..710d638c2f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsActivity.kt @@ -3,17 +3,13 @@ package org.thoughtcrime.securesms.preferences.prosettings import android.content.Context import android.content.Intent import androidx.compose.runtime.Composable +import androidx.core.content.IntentCompat import dagger.hilt.android.AndroidEntryPoint import org.thoughtcrime.securesms.FullComposeScreenLockActivity -import org.thoughtcrime.securesms.ui.UINavigator -import javax.inject.Inject @AndroidEntryPoint class ProSettingsActivity: FullComposeScreenLockActivity() { - @Inject - lateinit var navigator: UINavigator - companion object { private const val EXTRA_START_DESTINATION = "start_destination" @@ -29,12 +25,14 @@ class ProSettingsActivity: FullComposeScreenLockActivity() { @Composable override fun ComposeContent() { - val startDestination = intent.getParcelableExtra( - EXTRA_START_DESTINATION + val startDestination = IntentCompat.getParcelableExtra( + intent, + EXTRA_START_DESTINATION, + ProSettingsDestination::class.java ) ?: ProSettingsDestination.Home ProSettingsNavHost( - navigator = navigator, + hideHomeAppBar = false, startDestination = startDestination, onBack = this::finish ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsHomeScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsHomeScreen.kt index 6cd615cbf8..d22f35c78c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsHomeScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsHomeScreen.kt @@ -99,12 +99,14 @@ import java.time.Instant @Composable fun ProSettingsHomeScreen( viewModel: ProSettingsViewModel, + hideHomeAppBar: Boolean, onBack: () -> Unit, ) { val data by viewModel.proSettingsUIState.collectAsState() ProSettingsHome( data = data, + hideHomeAppBar = hideHomeAppBar, sendCommand = viewModel::onCommand, onBack = onBack, ) @@ -114,6 +116,7 @@ fun ProSettingsHomeScreen( @Composable fun ProSettingsHome( data: ProSettingsViewModel.ProSettingsState, + hideHomeAppBar: Boolean, sendCommand: (ProSettingsViewModel.Commands) -> Unit, onBack: () -> Unit, ) { @@ -122,11 +125,13 @@ fun ProSettingsHome( BaseProSettingsScreen( disabled = subscriptionType is SubscriptionType.Expired, + hideHomeAppBar = hideHomeAppBar, onBack = onBack, onHeaderClick = { // add a click handling if the subscription state is loading or errored if(data.subscriptionState.refreshState !is State.Success<*>){ sendCommand(OnHeaderClicked) + //todo PRO double check if KEE is ok to not have two different dialogs for the header vs the action button. If yes then I need to simplify the logic, if not I need to fix the never-subscribed case } else null }, extraHeaderContent = { @@ -142,7 +147,6 @@ fun ProSettingsHome( when(subscriptionType){ is SubscriptionType.Active -> R.string.proStatusLoadingSubtitle else -> R.string.checkingProStatus - //todo PRO will need to handle never subscribed here })) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format().toString(), @@ -189,6 +193,10 @@ fun ProSettingsHome( ) { // Header for non-pro users if(subscriptionType is SubscriptionType.NeverSubscribed) { + if(data.subscriptionState.refreshState !is State.Success){ + Spacer(Modifier.height(LocalDimensions.current.smallSpacing)) + } + Text( text = Phrase.from(context.getText(R.string.proFullestPotential)) .put(APP_NAME_KEY, stringResource(R.string.app_name)) @@ -203,6 +211,7 @@ fun ProSettingsHome( AccentFillButtonRect( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.theContinue), + enabled = data.subscriptionState.refreshState is State.Success, onClick = { sendCommand(GoToChoosePlan) } ) } @@ -631,7 +640,8 @@ fun ProFeatures( // Longer messages ProFeatureItem( title = stringResource(R.string.proLongerMessages), - subtitle = annotatedStringResource(R.string.proLongerMessagesDescription), + subtitle = if(data is SubscriptionType.Active) annotatedStringResource(R.string.proLongerMessagesDescription) + else annotatedStringResource(R.string.nonProLongerMessagesDescription), icon = R.drawable.ic_message_square, iconGradientStart = primaryBlue, iconGradientEnd = primaryPurple, @@ -816,21 +826,20 @@ fun ProManage( is SubscriptionType.Expired -> { // the details depend on the loading/error state - val renewIcon: @Composable BoxScope.() -> Unit = { + fun renewIcon(color: Color): @Composable BoxScope.() -> Unit = { Icon( modifier = Modifier.align(Alignment.Center) .size(LocalDimensions.current.iconMedium) .qaTag(R.string.qa_action_item_icon), painter = painterResource(id = R.drawable.ic_circle_plus), contentDescription = null, - tint = LocalColors.current.text + tint = color ) } val (subtitle, subColor, icon) = when(subscriptionRefreshState){ is State.Loading -> Triple Unit>( - //todo PRO need the ellipsis version of this string - Phrase.from(LocalContext.current, R.string.checkingProStatus) + Phrase.from(LocalContext.current, R.string.checkingProStatusEllipsis) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format().toString(), LocalColors.current.text, @@ -841,12 +850,12 @@ fun ProManage( Phrase.from(LocalContext.current, R.string.errorCheckingProStatus) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format().toString(), - LocalColors.current.warning, renewIcon + LocalColors.current.warning, renewIcon(LocalColors.current.text) ) is State.Success<*> -> Triple Unit>( null, - LocalColors.current.text, renewIcon + LocalColors.current.text, renewIcon(LocalColors.current.accent) ) } @@ -856,6 +865,8 @@ fun ProManage( .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format().toString() ), + titleColor = if(subscriptionRefreshState is State.Success ) LocalColors.current.accent + else LocalColors.current.text, subtitle = if(subtitle == null) null else annotatedStringResource(subtitle), subtitleColor = subColor, endContent = { @@ -906,7 +917,7 @@ fun PreviewProSettingsPro( ), duration = ProSubscriptionDuration.THREE_MONTHS, subscriptionDetails = SubscriptionDetails( - device = "iPhone", + device = "iOS", store = "Apple App Store", platform = "Apple", platformAccount = "Apple Account", @@ -917,6 +928,7 @@ fun PreviewProSettingsPro( refreshState = State.Success(Unit), ), ), + hideHomeAppBar = false, sendCommand = {}, onBack = {}, ) @@ -939,7 +951,7 @@ fun PreviewProSettingsProLoading( ), duration = ProSubscriptionDuration.THREE_MONTHS, subscriptionDetails = SubscriptionDetails( - device = "iPhone", + device = "iOS", store = "Apple App Store", platform = "Apple", platformAccount = "Apple Account", @@ -950,6 +962,7 @@ fun PreviewProSettingsProLoading( refreshState = State.Loading, ), ), + hideHomeAppBar = false, sendCommand = {}, onBack = {}, ) @@ -972,7 +985,7 @@ fun PreviewProSettingsProError( ), duration = ProSubscriptionDuration.THREE_MONTHS, subscriptionDetails = SubscriptionDetails( - device = "iPhone", + device = "iOS", store = "Apple App Store", platform = "Apple", platformAccount = "Apple Account", @@ -983,6 +996,7 @@ fun PreviewProSettingsProError( refreshState = State.Error(Exception()), ), ), + hideHomeAppBar = false, sendCommand = {}, onBack = {}, ) @@ -1001,7 +1015,7 @@ fun PreviewProSettingsExpired( type = SubscriptionType.Expired( expiredAt = Instant.now() - Duration.ofDays(14), SubscriptionDetails( - device = "iPhone", + device = "iOS", store = "Apple App Store", platform = "Apple", platformAccount = "Apple Account", @@ -1011,6 +1025,7 @@ fun PreviewProSettingsExpired( refreshState = State.Success(Unit), ) ), + hideHomeAppBar = false, sendCommand = {}, onBack = {}, ) @@ -1029,7 +1044,7 @@ fun PreviewProSettingsExpiredLoading( type = SubscriptionType.Expired( expiredAt = Instant.now() - Duration.ofDays(14), SubscriptionDetails( - device = "iPhone", + device = "iOS", store = "Apple App Store", platform = "Apple", platformAccount = "Apple Account", @@ -1039,6 +1054,7 @@ fun PreviewProSettingsExpiredLoading( refreshState = State.Loading, ) ), + hideHomeAppBar = false, sendCommand = {}, onBack = {}, ) @@ -1057,7 +1073,7 @@ fun PreviewProSettingsExpiredError( type = SubscriptionType.Expired( expiredAt = Instant.now() - Duration.ofDays(14), SubscriptionDetails( - device = "iPhone", + device = "iOS", store = "Apple App Store", platform = "Apple", platformAccount = "Apple Account", @@ -1067,6 +1083,7 @@ fun PreviewProSettingsExpiredError( refreshState = State.Error(Exception()), ) ), + hideHomeAppBar = false, sendCommand = {}, onBack = {}, ) @@ -1086,6 +1103,7 @@ fun PreviewProSettingsNonPro( refreshState = State.Success(Unit), ) ), + hideHomeAppBar = false, sendCommand = {}, onBack = {}, ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsNavHost.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsNavHost.kt index efb35fe9f2..3123ee4292 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsNavHost.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsNavHost.kt @@ -7,11 +7,13 @@ import androidx.compose.animation.SharedTransitionLayout import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.remember import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation.NavController import androidx.navigation.compose.NavHost +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController -import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsDestination.CancelSubscription @@ -53,26 +55,25 @@ sealed interface ProSettingsDestination: Parcelable { data object RefundSubscription: ProSettingsDestination } +@Serializable object ProSettingsGraph + @SuppressLint("RestrictedApi") @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun ProSettingsNavHost( - navigator: UINavigator, startDestination: ProSettingsDestination = Home, + hideHomeAppBar: Boolean, onBack: () -> Unit ){ SharedTransitionLayout { val navController = rememberNavController() - val scope = rememberCoroutineScope() - - // all screens within the Pro Flow can share the same VM - val viewModel = hiltViewModel() - - val dialogsState by viewModel.dialogState.collectAsState() + val navigator: UINavigator = remember { + UINavigator() + } val handleBack: () -> Unit = { if (navController.previousBackStackEntry != null) { - scope.launch { navigator.navigateUp() } + navController.navigateUp() } else { onBack() // Finish activity if at root } @@ -87,7 +88,7 @@ fun ProSettingsNavHost( action.navOptions(this) } - NavigationAction.NavigateUp -> navController.navigateUp() + NavigationAction.NavigateUp -> handleBack() is NavigationAction.NavigateToIntent -> { navController.context.startActivity(action.intent) @@ -97,58 +98,84 @@ fun ProSettingsNavHost( } } - NavHost(navController = navController, startDestination = startDestination) { - // Home - horizontalSlideComposable { - ProSettingsHomeScreen( - viewModel = viewModel, - onBack = onBack, - ) - } + NavHost(navController = navController, startDestination = ProSettingsGraph) { + navigation(startDestination = startDestination) { + // Home + horizontalSlideComposable { entry -> + val viewModel = navController.proGraphViewModel(entry, navigator) + ProSettingsHomeScreen( + viewModel = viewModel, + hideHomeAppBar = hideHomeAppBar, + onBack = onBack, + ) + } - // Subscription plan selection - horizontalSlideComposable { - UpdatePlanScreen( - viewModel = viewModel, - onBack = handleBack, - ) - } - horizontalSlideComposable { - GetOrRenewPlanScreen( - viewModel = viewModel, - onBack = handleBack, - ) - } + // Subscription plan selection + horizontalSlideComposable { entry -> + val viewModel = navController.proGraphViewModel(entry, navigator) + UpdatePlanScreen( + viewModel = viewModel, + onBack = handleBack, + ) + } + horizontalSlideComposable { entry -> + val viewModel = navController.proGraphViewModel(entry, navigator) + GetOrRenewPlanScreen( + viewModel = viewModel, + onBack = handleBack, + ) + } - // Subscription plan confirmation - horizontalSlideComposable { - PlanConfirmationScreen( - viewModel = viewModel, - onBack = handleBack, - ) - } + // Subscription plan confirmation + horizontalSlideComposable { entry -> + val viewModel = navController.proGraphViewModel(entry, navigator) + PlanConfirmationScreen( + viewModel = viewModel, + onBack = handleBack, + ) + } - // Refund - horizontalSlideComposable { - RefundPlanScreen( - viewModel = viewModel, - onBack = handleBack, - ) - } + // Refund + horizontalSlideComposable { entry -> + val viewModel = navController.proGraphViewModel(entry, navigator) + RefundPlanScreen( + viewModel = viewModel, + onBack = handleBack, + ) + } - // Cancellation - horizontalSlideComposable { - CancelPlanScreen( - viewModel = viewModel, - onBack = handleBack, - ) + // Cancellation + horizontalSlideComposable { entry -> + val viewModel = navController.proGraphViewModel(entry, navigator) + CancelPlanScreen( + viewModel = viewModel, + onBack = handleBack, + ) + } } } // Dialogs - ProSettingsDialogs( - dialogsState = dialogsState, - sendCommand = viewModel::onCommand, - ) + // the composable need to wait until the graph has been rendered + val graphReady = remember(navController.currentBackStackEntryAsState().value) { + runCatching { navController.getBackStackEntry(ProSettingsGraph) }.getOrNull() + } + graphReady?.let { entry -> + val vm = navController.proGraphViewModel(entry, navigator) + val dialogsState by vm.dialogState.collectAsState() + ProSettingsDialogs(dialogsState = dialogsState, sendCommand = vm::onCommand) + } } +} + +@Composable +fun NavController.proGraphViewModel( + entry: androidx.navigation.NavBackStackEntry, + navigator: UINavigator +): ProSettingsViewModel { + val graphEntry = remember(entry) { getBackStackEntry(ProSettingsGraph) } + return hiltViewModel< + ProSettingsViewModel, + ProSettingsViewModel.Factory + >(graphEntry) { factory -> factory.create(navigator) } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt index 3464985b08..982a7afb3b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt @@ -8,6 +8,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavOptionsBuilder import com.squareup.phrase.Phrase +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -31,6 +34,7 @@ import org.session.libsession.utilities.StringSubstitutionConstants.PRO_KEY import org.session.libsession.utilities.StringSubstitutionConstants.SELECTED_PLAN_LENGTH_KEY import org.session.libsession.utilities.StringSubstitutionConstants.SELECTED_PLAN_LENGTH_SINGULAR_KEY import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY +import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.ShowOpenUrlDialog import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.pro.SubscriptionState @@ -48,15 +52,20 @@ import javax.inject.Inject @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) -@HiltViewModel -class ProSettingsViewModel @Inject constructor( - @ApplicationContext private val context: Context, - private val navigator: UINavigator, +@HiltViewModel(assistedFactory = ProSettingsViewModel.Factory::class) +class ProSettingsViewModel @AssistedInject constructor( + @Assisted private val navigator: UINavigator, + @param:ApplicationContext private val context: Context, private val proStatusManager: ProStatusManager, private val subscriptionCoordinator: SubscriptionCoordinator, private val dateUtils: DateUtils ) : ViewModel() { + @AssistedFactory + interface Factory { + fun create(navigator: UINavigator): ProSettingsViewModel + } + private val _proSettingsUIState: MutableStateFlow = MutableStateFlow(ProSettingsState()) val proSettingsUIState: StateFlow = _proSettingsUIState @@ -284,6 +293,7 @@ class ProSettingsViewModel @Inject constructor( .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format().toString() to Phrase.from(context.getText(R.string.proStatusRenewError)) + .put(PRO_KEY, NonTranslatableStringConstants.PRO) .put(APP_NAME_KEY, context.getString(R.string.app_name)) .format() //todo PRO will need to handle never subscribed here diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanNonOriginating.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanNonOriginating.kt index 80ae0b0f84..11b77926b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanNonOriginating.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanNonOriginating.kt @@ -100,7 +100,7 @@ private fun PreviewUpdatePlan( ), duration = ProSubscriptionDuration.THREE_MONTHS, subscriptionDetails = SubscriptionDetails( - device = "iPhone", + device = "iOS", store = "Apple App Store", platform = "Apple", platformAccount = "Apple Account", diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNoBilling.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNoBilling.kt index bc16f1a34d..6096804e94 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNoBilling.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNoBilling.kt @@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.C import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.pro.SubscriptionDetails import org.thoughtcrime.securesms.pro.SubscriptionType +import org.thoughtcrime.securesms.ui.OpenURLAlertDialog import org.thoughtcrime.securesms.ui.components.iconExternalLink import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider @@ -203,6 +204,9 @@ fun ChoosePlanNoBilling( }, contentTitle = contentTitle, contentDescription = contentDescription, + contentClick = { + sendCommand(ShowOpenUrlDialog("https://getsession.org/pro-roadmap")) + }, linkCellsInfo = cellsInfo, linkCells = cells ) @@ -220,7 +224,7 @@ private fun PreviewNonOrigExpiredUpdatePlan( subscription = SubscriptionType.Expired( expiredAt = Instant.now() - Duration.ofDays(14), SubscriptionDetails( - device = "iPhone", + device = "iOS", store = "Apple App Store", platform = "Apple", platformAccount = "Apple Account", diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNonOriginating.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNonOriginating.kt index 8cb7861a4c..ab0947d8ed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNonOriginating.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNonOriginating.kt @@ -132,7 +132,7 @@ private fun PreviewUpdatePlan( ), duration = ProSubscriptionDuration.THREE_MONTHS, subscriptionDetails = SubscriptionDetails( - device = "iPhone", + device = "iOS", store = "Apple App Store", platform = "Apple", platformAccount = "Apple Account", diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt index ba363f5d9d..a71d352678 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt @@ -46,7 +46,10 @@ class ProStatusManager @Inject constructor( (TextSecurePreferences.events.filter { it == TextSecurePreferences.DEBUG_PRO_PLAN_STATUS } as Flow<*>) .onStart { emit(Unit) } .map { prefs.getDebugProPlanStatus() }, - ){ selfRecipient, debugSubscription, debugProPlanStatus -> + (TextSecurePreferences.events.filter { it == TextSecurePreferences.SET_FORCE_CURRENT_USER_PRO } as Flow<*>) + .onStart { emit(Unit) } + .map { prefs.forceCurrentUserAsPro() }, + ){ selfRecipient, debugSubscription, debugProPlanStatus, forceCurrentUserAsPro -> //todo PRO implement properly val subscriptionState = debugSubscription ?: DebugMenuViewModel.DebugSubscriptionStatus.AUTO_GOOGLE @@ -56,7 +59,8 @@ class ProStatusManager @Inject constructor( else -> State.Success(Unit) } - if(selfRecipient.proStatus is ProStatus.None){ + if(!forceCurrentUserAsPro){ + //todo PRO this is where we should get the real state SubscriptionState( type = SubscriptionType.NeverSubscribed, refreshState = proDataStatus @@ -96,6 +100,22 @@ class ProStatusManager @Inject constructor( ) ) + DebugMenuViewModel.DebugSubscriptionStatus.EXPIRING_GOOGLE_LATER -> SubscriptionType.Active.Expiring( + proStatus = ProStatus.Pro( + visible = true, + validUntil = Instant.now() + Duration.ofDays(40), + ), + duration = ProSubscriptionDuration.TWELVE_MONTHS, + subscriptionDetails = SubscriptionDetails( + device = "Android", + store = "Google Play Store", + platform = "Google", + platformAccount = "Google account", + subscriptionUrl = "https://play.google.com/store/account/subscriptions?package=network.loki.messenger&sku=SESSION_PRO_MONTHLY", + refundUrl = "https://getsession.org/android-refund", + ) + ) + DebugMenuViewModel.DebugSubscriptionStatus.AUTO_APPLE -> SubscriptionType.Active.AutoRenewing( proStatus = ProStatus.Pro( visible = true, @@ -103,7 +123,7 @@ class ProStatusManager @Inject constructor( ), duration = ProSubscriptionDuration.ONE_MONTH, subscriptionDetails = SubscriptionDetails( - device = "iPhone", + device = "iOS", store = "Apple App Store", platform = "Apple", platformAccount = "Apple Account", @@ -119,7 +139,7 @@ class ProStatusManager @Inject constructor( ), duration = ProSubscriptionDuration.ONE_MONTH, subscriptionDetails = SubscriptionDetails( - device = "iPhone", + device = "iOS", store = "Apple App Store", platform = "Apple", platformAccount = "Apple Account", @@ -139,10 +159,21 @@ class ProStatusManager @Inject constructor( refundUrl = "https://getsession.org/android-refund", ) ) + DebugMenuViewModel.DebugSubscriptionStatus.EXPIRED_EARLIER -> SubscriptionType.Expired( + expiredAt = Instant.now() - Duration.ofDays(60), + subscriptionDetails = SubscriptionDetails( + device = "Android", + store = "Google Play Store", + platform = "Google", + platformAccount = "Google account", + subscriptionUrl = "https://play.google.com/store/account/subscriptions?package=network.loki.messenger&sku=SESSION_PRO_MONTHLY", + refundUrl = "https://getsession.org/android-refund", + ) + ) DebugMenuViewModel.DebugSubscriptionStatus.EXPIRED_APPLE -> SubscriptionType.Expired( expiredAt = Instant.now() - Duration.ofDays(14), subscriptionDetails = SubscriptionDetails( - device = "iPhone", + device = "iOS", store = "Apple App Store", platform = "Apple", platformAccount = "Apple Account", diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/AlertDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/AlertDialog.kt index e0290112aa..ea1077065e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/AlertDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/AlertDialog.kt @@ -151,7 +151,7 @@ fun AlertDialogContent( Icon( painter = painterResource(id = R.drawable.ic_x), tint = LocalColors.current.text, - contentDescription = "back" + contentDescription = stringResource(R.string.close) ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt index c19697a5d9..7dfd639686 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt @@ -15,29 +15,38 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -51,6 +60,7 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics @@ -62,17 +72,22 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideSubcomposition import com.bumptech.glide.integration.compose.RequestState import com.squareup.phrase.Phrase +import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.utilities.NonTranslatableStringConstants import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.APP_PRO_KEY +import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsDestination +import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsNavHost import org.thoughtcrime.securesms.ui.components.AccentFillButtonRect import org.thoughtcrime.securesms.ui.components.Avatar +import org.thoughtcrime.securesms.ui.components.BaseBottomSheet import org.thoughtcrime.securesms.ui.components.FillButtonRect import org.thoughtcrime.securesms.ui.components.QrImage import org.thoughtcrime.securesms.ui.components.TertiaryFillButtonRect @@ -205,6 +220,10 @@ private fun PreviewProBadgeText( } } +/** + * This composable comprises of the CTA itself + * and the bottom sheet with the whole pro settings content + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun SessionProCTA( @@ -212,100 +231,164 @@ fun SessionProCTA( textContent: @Composable ColumnScope.() -> Unit, modifier: Modifier = Modifier, title: String = stringResource(R.string.upgradeTo), + titleColor: Color = LocalColors.current.text, badgeAtStart: Boolean = false, disabled: Boolean = false, features: List = emptyList(), positiveButtonText: String? = stringResource(R.string.theContinue), negativeButtonText: String? = stringResource(R.string.cancel), - onUpgrade: () -> Unit = {}, + onUpgrade: (() -> Unit)? = null, onCancel: () -> Unit = {}, ){ - BasicAlertDialog( - modifier = modifier, - onDismissRequest = onCancel, - content = { - DialogBg { - Column(modifier = Modifier.fillMaxWidth()) { - // hero image - BottomFadingEdgeBox( - fadingEdgeHeight = 70.dp, - fadingColor = LocalColors.current.backgroundSecondary, - content = { _ -> - content() - }, - ) + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + val scope = rememberCoroutineScope() - // content - Column( - modifier = Modifier - .fillMaxWidth() - .padding(LocalDimensions.current.smallSpacing) - ) { - // title - ProBadgeText( - modifier = Modifier.align(Alignment.CenterHorizontally), - text = title, - badgeAtStart = badgeAtStart, - badgeColors = if(disabled) proBadgeColorDisabled() else proBadgeColorStandard(), - ) + // We should avoid internal state in a composable but having the bottom sheet + // here avoids re-defining the sheet in multiple places in the app + var showDialog by remember { mutableStateOf(true) } + var showProSheet by remember { mutableStateOf(false) } - Spacer(Modifier.height(LocalDimensions.current.contentSpacing)) + // default handling of the upgrade button + val defaultUpgrade: () -> Unit = { + showProSheet = true + showDialog = false + } - // main message - textContent() + if(showDialog) { + BasicAlertDialog( + modifier = modifier, + onDismissRequest = onCancel, + content = { + DialogBg { + Column(modifier = Modifier.fillMaxWidth()) { + // hero image + BottomFadingEdgeBox( + fadingEdgeHeight = 70.dp, + fadingColor = LocalColors.current.backgroundSecondary, + content = { _ -> + content() + }, + ) - Spacer(Modifier.height(LocalDimensions.current.contentSpacing)) + // content + Column( + modifier = Modifier + .fillMaxWidth() + .padding(LocalDimensions.current.smallSpacing) + ) { + // title + ProBadgeText( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = title, + textStyle = LocalType.current.h5.copy(color = titleColor), + badgeAtStart = badgeAtStart, + badgeColors = if (disabled) proBadgeColorDisabled() else proBadgeColorStandard(), + ) - // features - if(features.isNotEmpty()) { - features.forEachIndexed { index, feature -> - ProCTAFeature(data = feature) - if (index < features.size - 1) { - Spacer(Modifier.height(LocalDimensions.current.xsSpacing)) - } - } + Spacer(Modifier.height(LocalDimensions.current.contentSpacing)) + + // main message + textContent() Spacer(Modifier.height(LocalDimensions.current.contentSpacing)) - } - // buttons - Row( - Modifier.height(IntrinsicSize.Min) - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy( - LocalDimensions.current.xsSpacing, - Alignment.CenterHorizontally - ), - ) { - positiveButtonText?.let { - AccentFillButtonRect( - modifier = Modifier.then( - if(negativeButtonText != null) - Modifier.weight(1f) - else Modifier - ).shimmerOverlay(), - text = it, - onClick = onUpgrade - ) + // features + if (features.isNotEmpty()) { + features.forEachIndexed { index, feature -> + ProCTAFeature(data = feature) + if (index < features.size - 1) { + Spacer(Modifier.height(LocalDimensions.current.xsSpacing)) + } + } + + Spacer(Modifier.height(LocalDimensions.current.contentSpacing)) } - negativeButtonText?.let { - TertiaryFillButtonRect( - modifier = Modifier.then( - if(positiveButtonText != null) - Modifier.weight(1f) - else Modifier - ), - text = it, - onClick = onCancel - ) + // buttons + Row( + Modifier.height(IntrinsicSize.Min) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy( + LocalDimensions.current.xsSpacing, + Alignment.CenterHorizontally + ), + ) { + positiveButtonText?.let { + AccentFillButtonRect( + modifier = Modifier.then( + if (negativeButtonText != null) + Modifier.weight(1f) + else Modifier + ).shimmerOverlay(), + text = it, + onClick = onUpgrade ?: defaultUpgrade + ) + } + + negativeButtonText?.let { + TertiaryFillButtonRect( + modifier = Modifier.then( + if (positiveButtonText != null) + Modifier.weight(1f) + else Modifier + ), + text = it, + onClick = onCancel + ) + } } } } } } + ) + } + + if(showProSheet) { + val dismissSheet: () -> Unit = { + scope.launch { + sheetState.hide() + onCancel() + } + } + + BaseBottomSheet( + modifier = modifier, + sheetState = sheetState, + dragHandle = null, + onDismissRequest = dismissSheet + ) { + BoxWithConstraints(modifier = modifier) { + val topInset = WindowInsets.safeDrawing.asPaddingValues().calculateTopPadding() + val targetHeight = + (this.maxHeight - topInset) * 0.94f // sheet should take up 94% of the height, without the status bar + Box( + modifier = Modifier.height(targetHeight), + contentAlignment = Alignment.TopCenter + ) { + ProSettingsNavHost( + startDestination = ProSettingsDestination.Home, + hideHomeAppBar = true, + onBack = dismissSheet + ) + + IconButton( + onClick = dismissSheet, + modifier = Modifier.align(Alignment.TopEnd) + .padding(10.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_x), + tint = LocalColors.current.text, + contentDescription = stringResource(R.string.close) + ) + } + } + } } - ) + } } sealed interface CTAFeature { @@ -332,7 +415,7 @@ fun SimpleSessionProCTA( features: List = emptyList(), positiveButtonText: String? = stringResource(R.string.theContinue), negativeButtonText: String? = stringResource(R.string.cancel), - onUpgrade: () -> Unit = {}, + onUpgrade: (() -> Unit)? = null, onCancel: () -> Unit = {}, ){ SessionProCTA( @@ -385,7 +468,7 @@ fun AnimatedSessionProCTA( features: List = emptyList(), positiveButtonText: String? = stringResource(R.string.theContinue), negativeButtonText: String? = stringResource(R.string.cancel), - onUpgrade: () -> Unit = {}, + onUpgrade: (() -> Unit)? = null, onCancel: () -> Unit = {}, ){ SessionProCTA( @@ -413,6 +496,7 @@ fun AnimatedSessionProCTA( positiveButtonText = positiveButtonText, negativeButtonText = negativeButtonText, title = title, + titleColor = if(disabled) LocalColors.current.disabled else LocalColors.current.text, badgeAtStart = badgeAtStart, disabled = disabled ) @@ -465,7 +549,6 @@ fun CTAAnimatedImages( @Composable fun GenericProCTA( onDismissRequest: () -> Unit, - onPostAction: (() -> Unit)? = null // a function for optional code once an action has been taken ){ val context = LocalContext.current AnimatedSessionProCTA( @@ -481,11 +564,6 @@ fun GenericProCTA( CTAFeature.Icon(stringResource(R.string.proFeatureListLongerMessages)), CTAFeature.RainbowIcon(stringResource(R.string.proFeatureListLoadsMore)), ), - onUpgrade = { - onDismissRequest() - onPostAction?.invoke() - //todo PRO go to screen once it exists - }, onCancel = { onDismissRequest() } @@ -508,10 +586,6 @@ fun LongMessageProCTA( CTAFeature.Icon(stringResource(R.string.proFeatureListLargerGroups)), CTAFeature.RainbowIcon(stringResource(R.string.proFeatureListLoadsMore)), ), - onUpgrade = { - onDismissRequest() - //todo PRO go to screen once it exists - }, onCancel = { onDismissRequest() } @@ -535,10 +609,6 @@ fun AnimatedProfilePicProCTA( CTAFeature.Icon(stringResource(R.string.proFeatureListLargerGroups)), CTAFeature.RainbowIcon(stringResource(R.string.proFeatureListLoadsMore)), ), - onUpgrade = { - onDismissRequest() - //todo PRO go to screen once it exists - }, onCancel = { onDismissRequest() } @@ -574,10 +644,6 @@ fun PinProCTA( CTAFeature.Icon(stringResource(R.string.proFeatureListLargerGroups)), CTAFeature.RainbowIcon(stringResource(R.string.proFeatureListLoadsMore)), ), - onUpgrade = { - onDismissRequest() - //todo PRO go to screen once it exists - }, onCancel = { onDismissRequest() } @@ -953,22 +1019,25 @@ fun SessionProSettingsHeader( Spacer(Modifier.height(LocalDimensions.current.xsSpacing)) - Row( - modifier = Modifier.height(LocalDimensions.current.smallSpacing) - ) { - Image( - painter = painterResource(R.drawable.ic_session), - contentDescription = null, - colorFilter = ColorFilter.tint(LocalColors.current.text) - ) + // Force the row to remain in LTR to preserve the image+icon order + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + Row( + modifier = Modifier.height(LocalDimensions.current.smallSpacing) + ) { + Image( + painter = painterResource(R.drawable.ic_session), + contentDescription = null, + colorFilter = ColorFilter.tint(LocalColors.current.text) + ) - Spacer(Modifier.width(LocalDimensions.current.xxxsSpacing)) + Spacer(Modifier.width(LocalDimensions.current.xxxsSpacing)) - ProBadge( - colors = proBadgeColorStandard().copy( - backgroundColor = color + ProBadge( + colors = proBadgeColorStandard().copy( + backgroundColor = color + ) ) - ) + } } extraContent?.let{ diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/UINavigator.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/UINavigator.kt index 599873491d..6fdcf5ce90 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/UINavigator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/UINavigator.kt @@ -5,10 +5,10 @@ import androidx.navigation.NavOptionsBuilder import dagger.hilt.android.scopes.ActivityRetainedScoped import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow +import org.session.libsignal.utilities.Log import javax.inject.Inject -@ActivityRetainedScoped -class UINavigator @Inject constructor() { +class UINavigator () { private val _navigationActions = Channel>() val navigationActions = _navigationActions.receiveAsFlow() diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/BottomSheets.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/BottomSheets.kt index 1cfb7701cb..8100afa6f0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/BottomSheets.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/BottomSheets.kt @@ -62,8 +62,8 @@ fun BaseBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, shape = RoundedCornerShape( - topStart = LocalDimensions.current.xsSpacing, - topEnd = LocalDimensions.current.xsSpacing + topStart = LocalDimensions.current.contentSpacing, + topEnd = LocalDimensions.current.contentSpacing ), dragHandle = dragHandle, containerColor = LocalColors.current.backgroundSecondary diff --git a/app/src/main/res/drawable/ic_circle_help.xml b/app/src/main/res/drawable/ic_circle_help.xml index 3b3b13768b..1fba16ac74 100644 --- a/app/src/main/res/drawable/ic_circle_help.xml +++ b/app/src/main/res/drawable/ic_circle_help.xml @@ -1,4 +1,4 @@ - + diff --git a/app/src/main/res/drawable/ic_question_custom.xml b/app/src/main/res/drawable/ic_question_custom.xml index 148e732324..1f9cfc20e0 100644 --- a/app/src/main/res/drawable/ic_question_custom.xml +++ b/app/src/main/res/drawable/ic_question_custom.xml @@ -1,4 +1,4 @@ - +