diff --git a/app/src/firebaseCommon/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushService.kt b/app/src/firebaseCommon/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushService.kt index 03ffc104b3..eb9560b9de 100644 --- a/app/src/firebaseCommon/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushService.kt +++ b/app/src/firebaseCommon/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushService.kt @@ -24,7 +24,7 @@ class FirebasePushService : FirebaseMessagingService() { } override fun onMessageReceived(message: RemoteMessage) { - Log.d(TAG, "Received a firebase push notification: $message - Priority received: ${message.priority} (Priority expected: ${message.originalPriority}) - Sent time: ${dateUtils.getLocaleFormattedDate(message.sentTime, "HH:mm:ss.SSS")}") + Log.d(TAG, "Received a firebase push notification: $message - Priority received: ${message.priority} (Priority expected: ${message.originalPriority}) - Sent time: ${DateUtils.getLocaleFormattedDate(message.sentTime, "HH:mm:ss.SSS")}") pushReceiver.onPushDataReceived(message.data) } } diff --git a/app/src/main/java/org/session/libsession/utilities/StringSubKeys.kt b/app/src/main/java/org/session/libsession/utilities/StringSubKeys.kt index 09a9d9fee2..09858a3ecd 100644 --- a/app/src/main/java/org/session/libsession/utilities/StringSubKeys.kt +++ b/app/src/main/java/org/session/libsession/utilities/StringSubKeys.kt @@ -50,12 +50,12 @@ object StringSubstitutionConstants { const val APP_PRO_KEY: StringSubKey = "app_pro" const val PRO_KEY: StringSubKey = "pro" const val CURRENT_PLAN_KEY: StringSubKey = "current_plan" - const val SELECTED_PLAN_KEY: StringSubKey = "selected_plan" + const val SELECTED_PLAN_KEY: StringSubKey = "selected_plan" const val PLATFORM_STORE_KEY: StringSubKey = "platform_store" const val PLATFORM_ACCOUNT_KEY: StringSubKey = "platform_account" const val MONTHLY_PRICE_KEY: StringSubKey = "monthly_price" const val PRICE_KEY: StringSubKey = "price" const val PERCENT_KEY: StringSubKey = "percent" - + const val DEVICE_TYPE_KEY: StringSubKey = "device_type" const val SESSION_FOUNDATION_KEY: StringSubKey = "session_foundation" } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/notification/NotificationSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/notification/NotificationSettingsViewModel.kt index 782ff35c55..2a04b3a6e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/notification/NotificationSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/notification/NotificationSettingsViewModel.kt @@ -195,7 +195,7 @@ class NotificationSettingsViewModel @AssistedInject constructor( } private fun formatTime(timestamp: Long): String{ - return dateUtils.formatTime(timestamp, "HH:mm dd/MM/yy") + return DateUtils.formatTime(timestamp, "HH:mm dd/MM/yy") } private fun shouldEnableSetButton(): Boolean { 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 14797eff51..a637fdf270 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt @@ -85,10 +85,9 @@ class DebugMenuViewModel @Inject constructor( debugSubscriptionStatuses = setOf( DebugSubscriptionStatus.AUTO_GOOGLE, DebugSubscriptionStatus.EXPIRING_GOOGLE, - DebugSubscriptionStatus.EXPIRED_GOOGLE, //todo PRO uncomment below once we know how to differentiate store providers -// DebugSubscriptionStatus.AUTO_APPLE, -// DebugSubscriptionStatus.EXPIRING_APPLE, -// DebugSubscriptionStatus.EXPIRED_APPLE, + DebugSubscriptionStatus.AUTO_APPLE, + DebugSubscriptionStatus.EXPIRING_APPLE, + DebugSubscriptionStatus.EXPIRED, ), selectedDebugSubscriptionStatus = textSecurePreferences.getDebugSubscriptionType() ?: DebugSubscriptionStatus.AUTO_GOOGLE, ) @@ -412,10 +411,9 @@ class DebugMenuViewModel @Inject constructor( enum class DebugSubscriptionStatus(val label: String) { AUTO_GOOGLE("Auto Renewing (Google, 3 months)"), EXPIRING_GOOGLE("Expiring/Cancelled (Google, 12 months)"), - EXPIRED_GOOGLE("Expired (Google)"), AUTO_APPLE("Auto Renewing (Apple, 1 months)"), EXPIRING_APPLE("Expiring/Cancelled (Apple, 1 months)"), - EXPIRED_APPLE("Expired (Apple)") + EXPIRED("Expired"), } sealed class Commands { 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 f65be99c35..24c167148a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -12,8 +12,10 @@ import android.view.ViewGroup.MarginLayoutParams import android.widget.Toast import androidx.activity.viewModels import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.view.isVisible @@ -216,7 +218,11 @@ class HomeActivity : ScreenLockActionBarActivity(), Avatar( size = LocalDimensions.current.iconMediumAvatar, data = avatarUtils.getUIDataFromRecipient(recipient), - modifier = Modifier.clickable(onClick = ::openSettings) + modifier = Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = ::openSettings + ) ) } 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 7bd289eb79..7e6d4147db 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 @@ -1,7 +1,11 @@ package org.thoughtcrime.securesms.preferences.prosettings +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides @@ -11,17 +15,22 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -31,12 +40,16 @@ import org.thoughtcrime.securesms.ui.SessionProSettingsHeader import org.thoughtcrime.securesms.ui.components.AccentFillButtonRect import org.thoughtcrime.securesms.ui.components.BackAppBar import org.thoughtcrime.securesms.ui.components.DangerFillButtonRect +import org.thoughtcrime.securesms.ui.components.annotatedStringResource import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors +import org.thoughtcrime.securesms.ui.theme.bold +import network.loki.messenger.R +import org.thoughtcrime.securesms.ui.DialogBg /** * Base structure used in most Pro Settings screen @@ -92,7 +105,7 @@ fun BaseCellButtonProSettingsScreen( buttonText: String, dangerButton: Boolean, onButtonClick: () -> Unit, - title: String? = null, + title: CharSequence? = null, content: @Composable () -> Unit ) { BaseProSettingsScreen( @@ -104,7 +117,7 @@ fun BaseCellButtonProSettingsScreen( if(!title.isNullOrEmpty()) { Text( modifier = Modifier.fillMaxWidth(), - text = title, + text = annotatedStringResource(title), textAlign = TextAlign.Center, style = LocalType.current.base, color = LocalColors.current.text, @@ -114,7 +127,14 @@ fun BaseCellButtonProSettingsScreen( Spacer(Modifier.height(LocalDimensions.current.smallSpacing)) - Cell(content = content) + Cell { + Column( + modifier = Modifier.fillMaxWidth() + .padding(LocalDimensions.current.smallSpacing) + ) { + content() + } + } Spacer(Modifier.height(LocalDimensions.current.smallSpacing)) @@ -159,3 +179,155 @@ private fun PreviewBaseCellButton( ) } } + +/** + * A reusable structure for Pro Settings screens for non originating steps + */ +@Composable +fun BaseNonOriginatingProSettingsScreen( + disabled: Boolean, + onBack: () -> Unit, + buttonText: String, + dangerButton: Boolean, + onButtonClick: () -> Unit, + headerTitle: CharSequence?, + contentTitle: String?, + contentDescription: CharSequence?, + linkCellsInfo: String?, + linkCells: List = emptyList(), +) { + BaseCellButtonProSettingsScreen( + disabled = disabled, + onBack = onBack, + buttonText = buttonText, + dangerButton = dangerButton, + onButtonClick = onButtonClick, + title = headerTitle, + ){ + if (contentTitle != null) { + Text( + text = contentTitle, + style = LocalType.current.h7, + color = LocalColors.current.text, + ) + } + + if (contentDescription != null) { + Spacer(Modifier.height(LocalDimensions.current.xxxsSpacing)) + Text( + text = annotatedStringResource(contentDescription), + style = LocalType.current.base, + color = LocalColors.current.text, + ) + } + + if (linkCellsInfo != null) { + Spacer(Modifier.height(LocalDimensions.current.smallSpacing)) + Text( + text = linkCellsInfo, + style = LocalType.current.base, + color = LocalColors.current.textSecondary, + ) + } + + Spacer(Modifier.height(LocalDimensions.current.xsSpacing)) + + linkCells.forEachIndexed { index, data -> + if (index > 0) { + Spacer(Modifier.height(LocalDimensions.current.xsSpacing)) + } + NonOriginatingLinkCell(data) + } + } +} + +@Composable +fun NonOriginatingLinkCell( + data: NonOriginatingLinkCellData +) { + DialogBg( + bgColor = LocalColors.current.backgroundTertiary + ) { + Row( + modifier = Modifier.fillMaxWidth() + .padding(LocalDimensions.current.smallSpacing), + horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) + ) { + // icon + Box(modifier = Modifier + .background( + color = LocalColors.current.accent.copy(alpha = 0.2f), + shape = MaterialTheme.shapes.small + ) + .padding(10.dp) + ){ + Icon( + modifier = Modifier.align(Center) + .size(LocalDimensions.current.iconMedium), + painter = painterResource(id = data.iconRes), + tint = LocalColors.current.accent, + contentDescription = null + ) + } + + // text content + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = annotatedStringResource(data.title), + style = LocalType.current.base.bold(), + color = LocalColors.current.text, + ) + + Spacer(Modifier.height(LocalDimensions.current.xxxsSpacing)) + + Text( + text = annotatedStringResource(data.info), + style = LocalType.current.base, + color = LocalColors.current.text, + ) + } + } + } +} + + +data class NonOriginatingLinkCellData( + val title: CharSequence, + val info: CharSequence, + @DrawableRes val iconRes: Int, + val onClick: (() -> Unit)? = null +) + +@Preview +@Composable +private fun PreviewBaseNonOrig( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + BaseNonOriginatingProSettingsScreen( + disabled = false, + onBack = {}, + headerTitle = "This is a title", + buttonText = "This is a button", + dangerButton = false, + onButtonClick = {}, + contentTitle = "This is a content title", + contentDescription = "This is a content description", + linkCellsInfo = "This is a link cells info", + linkCells = listOf( + NonOriginatingLinkCellData( + title = "This is a title", + info = "This is some info", + iconRes = R.drawable.ic_globe + ), + NonOriginatingLinkCellData( + title = "This is another title", + info = "This is some different info", + iconRes = R.drawable.ic_phone + ) + ) + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/CancelSubscriptionScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/CancelSubscriptionScreen.kt new file mode 100644 index 0000000000..12e62db262 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/CancelSubscriptionScreen.kt @@ -0,0 +1,2 @@ +package org.thoughtcrime.securesms.preferences.prosettings + diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ChoosePlanNonOriginating.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ChoosePlanNonOriginating.kt new file mode 100644 index 0000000000..9c8d2460d9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ChoosePlanNonOriginating.kt @@ -0,0 +1,130 @@ +package org.thoughtcrime.securesms.preferences.prosettings + +import android.icu.util.MeasureUnit +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import com.squareup.phrase.Phrase +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.session.libsession.utilities.StringSubstitutionConstants.CURRENT_PLAN_KEY +import org.session.libsession.utilities.StringSubstitutionConstants.DATE_KEY +import org.session.libsession.utilities.StringSubstitutionConstants.DEVICE_TYPE_KEY +import org.session.libsession.utilities.StringSubstitutionConstants.PLATFORM_ACCOUNT_KEY +import org.session.libsession.utilities.StringSubstitutionConstants.PLATFORM_STORE_KEY +import org.session.libsession.utilities.recipients.ProStatus +import org.thoughtcrime.securesms.pro.SubscriptionState +import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration +import org.thoughtcrime.securesms.pro.subscription.expiryFromNow +import org.thoughtcrime.securesms.ui.theme.PreviewTheme +import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider +import org.thoughtcrime.securesms.ui.theme.ThemeColors +import org.thoughtcrime.securesms.util.DateUtils +import java.time.Duration +import java.time.Instant + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class) +@Composable +fun ChoosePlanNonOriginating( + subscription: SubscriptionState.Active, + sendCommand: (ProSettingsViewModel.Commands) -> Unit, + onBack: () -> Unit, +){ + val nonOriginatingData = subscription.nonOriginatingSubscription ?: return + val context = LocalContext.current + + val headerTitle = when(subscription) { + is SubscriptionState.Active.Expiring -> Phrase.from(context.getText(R.string.proPlanExpireDate)) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .put(DATE_KEY, subscription.type.expiryFromNow()) + .format() + + else -> Phrase.from(context.getText(R.string.proPlanActivatedAutoShort)) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .put(CURRENT_PLAN_KEY, DateUtils.getLocalisedTimeDuration( + context = context, + amount = subscription.type.duration.months, + unit = MeasureUnit.MONTH + )) + .put(DATE_KEY, subscription.type.expiryFromNow()) + .format() + } + + BaseNonOriginatingProSettingsScreen( + disabled = false, + onBack = onBack, + headerTitle = headerTitle, + buttonText = Phrase.from(context.getText(R.string.openStoreWebsite)) + .put(PLATFORM_STORE_KEY, nonOriginatingData.store) + .format().toString(), + dangerButton = false, + onButtonClick = { + //todo PRO implement + }, + contentTitle = stringResource(R.string.updatePlan), + contentDescription = Phrase.from(context.getText(R.string.proPlanSignUp)) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .put(PLATFORM_STORE_KEY, nonOriginatingData.store) + .put(PLATFORM_ACCOUNT_KEY, nonOriginatingData.platformAccount) + .format(), + linkCellsInfo = stringResource(R.string.updatePlanTwo), + linkCells = listOf( + NonOriginatingLinkCellData( + title = Phrase.from(context.getText(R.string.onDevice)) + .put(DEVICE_TYPE_KEY, nonOriginatingData.device) + .format(), + info = Phrase.from(context.getText(R.string.onDeviceDescription)) + .put(APP_NAME_KEY, NonTranslatableStringConstants.APP_NAME) + .put(DEVICE_TYPE_KEY, nonOriginatingData.device) + .put(PLATFORM_ACCOUNT_KEY, nonOriginatingData.platformAccount) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .format(), + iconRes = R.drawable.ic_smartphone + ), + NonOriginatingLinkCellData( + title = Phrase.from(context.getText(R.string.viaStoreWebsite)) + .put(PLATFORM_STORE_KEY, nonOriginatingData.store) + .format(), + info = Phrase.from(context.getText(R.string.viaStoreWebsiteDescription)) + .put(PLATFORM_ACCOUNT_KEY, nonOriginatingData.platformAccount) + .put(PLATFORM_STORE_KEY, nonOriginatingData.store) + .format(), + iconRes = R.drawable.ic_globe + ) + ) + ) +} + +@Preview +@Composable +private fun PreviewUpdatePlan( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + val context = LocalContext.current + ChoosePlanNonOriginating ( + subscription = SubscriptionState.Active.AutoRenewing( + proStatus = ProStatus.Pro( + visible = true, + validUntil = Instant.now() + Duration.ofDays(14), + ), + type = ProSubscriptionDuration.THREE_MONTHS, + nonOriginatingSubscription = SubscriptionState.Active.NonOriginatingSubscription( + device = "iPhone", + store = "Apple App Store", + platform = "Apple", + platformAccount = "Apple Account", + urlSubscription = "https://www.apple.com/account/subscriptions", + ) + ), + sendCommand = {}, + onBack = {}, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/UpdatePlanScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ChoosePlanScreen.kt similarity index 83% rename from app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/UpdatePlanScreen.kt rename to app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ChoosePlanScreen.kt index 19f754c34e..83dad9ab8c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/UpdatePlanScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ChoosePlanScreen.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.preferences.prosettings +import android.icu.util.MeasureUnit import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -36,13 +37,18 @@ import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.utilities.NonTranslatableStringConstants import org.session.libsession.utilities.StringSubstitutionConstants.APP_PRO_KEY +import org.session.libsession.utilities.StringSubstitutionConstants.CURRENT_PLAN_KEY +import org.session.libsession.utilities.StringSubstitutionConstants.DATE_KEY import org.session.libsession.utilities.StringSubstitutionConstants.ICON_KEY import org.session.libsession.utilities.StringSubstitutionConstants.MONTHLY_PRICE_KEY import org.session.libsession.utilities.StringSubstitutionConstants.PRICE_KEY +import org.session.libsession.utilities.StringSubstitutionConstants.PRO_KEY import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.ProPlan import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.ProPlanBadge import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.* +import org.thoughtcrime.securesms.pro.SubscriptionState import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration +import org.thoughtcrime.securesms.pro.subscription.expiryFromNow import org.thoughtcrime.securesms.ui.SpeechBubbleTooltip import org.thoughtcrime.securesms.ui.components.AccentFillButtonRect import org.thoughtcrime.securesms.ui.components.RadioButtonIndicator @@ -58,27 +64,42 @@ import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors import org.thoughtcrime.securesms.ui.theme.bold +import org.thoughtcrime.securesms.util.DateUtils @OptIn(ExperimentalSharedTransitionApi::class) @Composable -fun UpdatePlanScreen( +fun ChoosePlanScreen( viewModel: ProSettingsViewModel, onBack: () -> Unit, ) { - val planData by viewModel.proPlanUIState.collectAsState() + val planData by viewModel.choosePlanState.collectAsState() - UpdatePlan( - planData = planData, - sendCommand = viewModel::onCommand, - onBack = onBack, - ) + // there are different UI depending on the state + when { + // there is an active subscription but from a different platform + (planData.subscriptionState as? SubscriptionState.Active)?.nonOriginatingSubscription != null -> + ChoosePlanNonOriginating( + subscription = planData.subscriptionState as SubscriptionState.Active, + sendCommand = viewModel::onCommand, + onBack = onBack, + ) + + //todo PRO handle the case here when there are no SubscriptionManager available (for example fdroid builds) + + // default plan chooser + else -> ChoosePlan( + planData = planData, + sendCommand = viewModel::onCommand, + onBack = onBack, + ) + } } @OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class) @Composable -fun UpdatePlan( - planData: ProSettingsViewModel.ProPlanUIState, +fun ChoosePlan( + planData: ProSettingsViewModel.ChoosePlanState, sendCommand: (ProSettingsViewModel.Commands) -> Unit, onBack: () -> Unit, ) { @@ -93,9 +114,36 @@ fun UpdatePlan( Spacer(Modifier.height(LocalDimensions.current.spacing)) + val context = LocalContext.current + val title = when(planData.subscriptionState) { + is SubscriptionState.Expired -> + Phrase.from(context.getText(R.string.proPlanRenewStart)) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .format() + + is SubscriptionState.Active.Expiring -> Phrase.from(context.getText(R.string.proPlanActivatedNotAuto)) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .put(DATE_KEY, planData.subscriptionState.type.expiryFromNow()) + .format() + + is SubscriptionState.Active.AutoRenewing -> Phrase.from(context.getText(R.string.proPlanActivatedAuto)) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .put(CURRENT_PLAN_KEY, DateUtils.getLocalisedTimeDuration( + context = context, + amount = planData.subscriptionState.type.duration.months, + unit = MeasureUnit.MONTH + )) + .put(DATE_KEY, planData.subscriptionState.type.expiryFromNow()) + .put(PRO_KEY, NonTranslatableStringConstants.PRO) + .format() + + else -> "" + } + Text( modifier = Modifier.fillMaxWidth(), - text = annotatedStringResource(planData.title), + text = annotatedStringResource(title), textAlign = TextAlign.Center, style = LocalType.current.base, color = LocalColors.current.text, @@ -123,10 +171,16 @@ fun UpdatePlan( Spacer(Modifier.height(LocalDimensions.current.contentSpacing)) + val buttonLabel = when(planData.subscriptionState) { + is SubscriptionState.Expired -> context.getString(R.string.renew) + is SubscriptionState.Active.Expiring -> context.getString(R.string.updatePlan) + else -> context.getString(R.string.updatePlan) + } + AccentFillButtonRect( modifier = Modifier.fillMaxWidth() .widthIn(max = LocalDimensions.current.maxContentWidth), - text = planData.buttonLabel, + text = buttonLabel, enabled = planData.enableButton, onClick = { sendCommand(GetProPlan) @@ -377,9 +431,8 @@ private fun PreviewUpdatePlan( ) { PreviewTheme(colors) { val context = LocalContext.current - UpdatePlan( - planData = ProSettingsViewModel.ProPlanUIState( - title = "This is a title", + ChoosePlan( + planData = ProSettingsViewModel.ChoosePlanState( enableButton = true, plans = listOf( ProPlan( 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 8e32367fce..4954a96b77 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 @@ -96,7 +96,7 @@ fun PlanConfirmationScreen( @OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class) @Composable fun PlanConfirmation( - proData: ProSettingsViewModel.ProSettingsUIState, + proData: ProSettingsViewModel.ProSettingsState, sendCommand: (ProSettingsViewModel.Commands) -> Unit, onBack: () -> Unit, ) { @@ -149,7 +149,7 @@ fun PlanConfirmation( color = LocalColors.current.text, ) - Spacer(Modifier.height(LocalDimensions.current.smallSpacing)) + Spacer(Modifier.height(LocalDimensions.current.spacing)) //todo PRO the button text can change if the user was renewing vs expiring and/or/auto-renew AccentFillButtonRect( @@ -172,13 +172,14 @@ private fun PreviewPlanConfirmation( ) { PreviewTheme(colors) { PlanConfirmation( - proData = ProSettingsViewModel.ProSettingsUIState( + proData = ProSettingsViewModel.ProSettingsState( subscriptionState = SubscriptionState.Active.AutoRenewing( proStatus = ProStatus.Pro( visible = true, validUntil = Instant.now() + Duration.ofDays(14), ), - type = ProSubscriptionDuration.THREE_MONTHS + type = ProSubscriptionDuration.THREE_MONTHS, + nonOriginatingSubscription = null ), ), sendCommand = {}, 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 4a9a939d34..1e10196db4 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 @@ -101,7 +101,7 @@ fun ProSettingsHomeScreen( @OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class) @Composable fun ProSettingsHome( - data: ProSettingsViewModel.ProSettingsUIState, + data: ProSettingsViewModel.ProSettingsState, sendCommand: (ProSettingsViewModel.Commands) -> Unit, onBack: () -> Unit, ) { @@ -289,7 +289,8 @@ fun ProStats( title = pluralStringResource( R.plurals.proBadgesSent, data.proBadges, - NumberUtil.getFormattedNumber(data.proBadges.toLong()) + NumberUtil.getFormattedNumber(data.proBadges.toLong()), + NonTranslatableStringConstants.PRO ), icon = R.drawable.ic_rectangle_ellipsis @@ -640,13 +641,14 @@ fun PreviewProSettingsPro( ) { PreviewTheme(colors) { ProSettingsHome( - data = ProSettingsViewModel.ProSettingsUIState( + data = ProSettingsViewModel.ProSettingsState( subscriptionState = SubscriptionState.Active.AutoRenewing( proStatus = ProStatus.Pro( visible = true, validUntil = Instant.now() + Duration.ofDays(14), ), - type = ProSubscriptionDuration.THREE_MONTHS + type = ProSubscriptionDuration.THREE_MONTHS, + nonOriginatingSubscription = null ), ), sendCommand = {}, @@ -662,7 +664,7 @@ fun PreviewProSettingsExpired( ) { PreviewTheme(colors) { ProSettingsHome( - data = ProSettingsViewModel.ProSettingsUIState( + data = ProSettingsViewModel.ProSettingsState( subscriptionState = SubscriptionState.Expired, ), sendCommand = {}, @@ -678,7 +680,7 @@ fun PreviewProSettingsNonPro( ) { PreviewTheme(colors) { ProSettingsHome( - data = ProSettingsViewModel.ProSettingsUIState( + data = ProSettingsViewModel.ProSettingsState( subscriptionState = SubscriptionState.NeverSubscribed, ), sendCommand = {}, 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 357df71f55..540656cdd5 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 @@ -24,10 +24,16 @@ sealed interface ProSettingsDestination { data object Home: ProSettingsDestination @Serializable - data object UpdatePlan: ProSettingsDestination + data object ChoosePlan: ProSettingsDestination @Serializable data object PlanConfirmation: ProSettingsDestination + + @Serializable + data object CancelSubscription: ProSettingsDestination + + @Serializable + data object RefundSubscription: ProSettingsDestination } @SuppressLint("RestrictedApi") @@ -64,7 +70,7 @@ fun ProSettingsNavHost( } } - NavHost(navController = navController, startDestination = PlanConfirmation) { + NavHost(navController = navController, startDestination = Home) { // Home horizontalSlideComposable { ProSettingsHomeScreen( @@ -74,8 +80,8 @@ fun ProSettingsNavHost( } // Subscription plan selection - horizontalSlideComposable { - UpdatePlanScreen( + horizontalSlideComposable { + ChoosePlanScreen( viewModel = viewModel, onBack = { scope.launch { navigator.navigateUp() }}, ) 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 e9414ce29c..4aae9028e9 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 @@ -4,7 +4,6 @@ import android.content.Context import android.icu.util.MeasureUnit import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.media3.common.Label import com.squareup.phrase.Phrase import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -23,13 +22,13 @@ import org.session.libsession.utilities.StringSubstitutionConstants.MONTHLY_PRIC import org.session.libsession.utilities.StringSubstitutionConstants.PERCENT_KEY import org.session.libsession.utilities.StringSubstitutionConstants.PRICE_KEY import org.session.libsession.utilities.StringSubstitutionConstants.PRO_KEY -import org.session.libsession.utilities.StringSubstitutionConstants.RELATIVE_TIME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.SELECTED_PLAN_KEY import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY import org.thoughtcrime.securesms.pro.SubscriptionState import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration import org.thoughtcrime.securesms.pro.subscription.SubscriptionCoordinator +import org.thoughtcrime.securesms.pro.subscription.expiryFromNow import org.thoughtcrime.securesms.ui.SimpleDialogData import org.thoughtcrime.securesms.ui.UINavigator import org.thoughtcrime.securesms.util.DateUtils @@ -47,16 +46,14 @@ class ProSettingsViewModel @Inject constructor( private val dateUtils: DateUtils ) : ViewModel() { - private val _proSettingsUIState: MutableStateFlow = MutableStateFlow(ProSettingsUIState()) - val proSettingsUIState: StateFlow = _proSettingsUIState + private val _proSettingsUIState: MutableStateFlow = MutableStateFlow(ProSettingsState()) + val proSettingsUIState: StateFlow = _proSettingsUIState private val _dialogState: MutableStateFlow = MutableStateFlow(DialogsState()) val dialogState: StateFlow = _dialogState - private val _proPlanUIState: MutableStateFlow = MutableStateFlow(ProPlanUIState()) - val proPlanUIState: StateFlow = _proPlanUIState - - private val proSettingsDateFormat = "MMMM d, yyyy" + private val _choosePlanState: MutableStateFlow = MutableStateFlow(ChoosePlanState()) + val choosePlanState: StateFlow = _choosePlanState init { generateState() @@ -67,10 +64,8 @@ class ProSettingsViewModel @Inject constructor( val subscriptionState = proStatusManager.getCurrentSubscriptionState() _proSettingsUIState.update { - ProSettingsUIState( - subscriptionState = if(proStatusManager.isCurrentUserPro()) - subscriptionState - else SubscriptionState.NeverSubscribed, + ProSettingsState( + subscriptionState = subscriptionState, subscriptionExpiryLabel = when(subscriptionState){ is SubscriptionState.Active.AutoRenewing -> Phrase.from(context, R.string.proAutoRenewTime) @@ -87,52 +82,20 @@ class ProSettingsViewModel @Inject constructor( else -> "" }, subscriptionExpiryDate = when(subscriptionState){ - is SubscriptionState.Active -> { - val newSubscriptionExpiryDate = ZonedDateTime.now() - .plus(subscriptionState.type.duration) - .toInstant() - .toEpochMilli() - - dateUtils.getLocaleFormattedDate(newSubscriptionExpiryDate, proSettingsDateFormat) - } - + is SubscriptionState.Active -> subscriptionState.type.expiryFromNow() else -> "" } ) } - _proPlanUIState.update { - // sort out the title and button label for the plan screen based on subscription status - val (title, buttonLabel) = when(subscriptionState) { - is SubscriptionState.Expired -> - Phrase.from(context.getText(R.string.proPlanRenewStart)) - .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) - .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) - .format() to - context.getString(R.string.renew) - - is SubscriptionState.Active.Expiring -> Phrase.from(context.getText(R.string.proPlanActivatedNotAuto)) - .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) - .put(DATE_KEY, "May 21st, 2025") //todo PRO implement properly - .format() to - context.getString(R.string.updatePlan) - - else -> Phrase.from(context.getText(R.string.proPlanActivatedAuto)) - .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) - .put(CURRENT_PLAN_KEY, "3 months") //todo PRO implement properly - .put(DATE_KEY, "May 21st, 2025") //todo PRO implement properly - .put(PRO_KEY, NonTranslatableStringConstants.PRO) - .format() to - context.getString(R.string.updatePlan) - } + _choosePlanState.update { val isActive = subscriptionState is SubscriptionState.Active val currentPlan12Months = isActive && subscriptionState.type == ProSubscriptionDuration.TWELVE_MONTHS val currentPlan3Months = isActive && subscriptionState.type == ProSubscriptionDuration.THREE_MONTHS val currentPlan1Month = isActive && subscriptionState.type == ProSubscriptionDuration.ONE_MONTH - ProPlanUIState( - title = title, - buttonLabel = buttonLabel, + ChoosePlanState( + subscriptionState = subscriptionState, enableButton = subscriptionState !is SubscriptionState.Active.AutoRenewing, // only the auto-renew can have a disabled state plans = listOf( ProPlan( @@ -222,7 +185,7 @@ class ProSettingsViewModel @Inject constructor( } Commands.ShowPlanUpdate -> { - navigateTo(ProSettingsDestination.UpdatePlan) + navigateTo(ProSettingsDestination.ChoosePlan) } is Commands.SetShowProBadge -> { @@ -230,7 +193,7 @@ class ProSettingsViewModel @Inject constructor( } is Commands.SelectProPlan -> { - _proPlanUIState.update { data -> + _choosePlanState.update { data -> data.copy( plans = data.plans.map { it.copy(selected = it == command.plan) @@ -256,20 +219,16 @@ class ProSettingsViewModel @Inject constructor( Commands.GetProPlan -> { // if we already have a current plan, ask for confirmation first if(_proSettingsUIState.value.subscriptionState is SubscriptionState.Active){ - val newSubscriptionExpiryDate = ZonedDateTime.now() - .plus(getSelectedPlan().durationType.duration) - .toInstant() - .toEpochMilli() - val newSubscriptionExpiryString = dateUtils.getLocaleFormattedDate( - newSubscriptionExpiryDate, proSettingsDateFormat - ) + val newSubscriptionExpiryString = getSelectedPlan().durationType.expiryFromNow() - val currentSubscriptionDuration = dateUtils.getLocalisedTimeDuration( + val currentSubscriptionDuration = DateUtils.getLocalisedTimeDuration( + context = context, amount = (_proSettingsUIState.value.subscriptionState as SubscriptionState.Active).type.duration.months, unit = MeasureUnit.MONTH ) - val selectedSubscriptionDuration = dateUtils.getLocalisedTimeDuration( + val selectedSubscriptionDuration = DateUtils.getLocalisedTimeDuration( + context = context, amount = getSelectedPlan().durationType.duration.months, unit = MeasureUnit.MONTH ) @@ -319,7 +278,7 @@ class ProSettingsViewModel @Inject constructor( } private fun getSelectedPlan(): ProPlan { - return _proPlanUIState.value.plans.first { it.selected } + return _choosePlanState.value.plans.first { it.selected } } private fun getPlanFromProvider(){ @@ -348,13 +307,19 @@ class ProSettingsViewModel @Inject constructor( data object ConfirmProPlan: Commands } - data class ProSettingsUIState( + data class ProSettingsState( val subscriptionState: SubscriptionState = SubscriptionState.NeverSubscribed, val proStats: ProStats = ProStats(), val subscriptionExpiryLabel: CharSequence = "", // eg: "Pro auto renewing in 3 days" val subscriptionExpiryDate: CharSequence = "" // eg: "May 21st, 2025" ) + data class ChoosePlanState( + val subscriptionState: SubscriptionState = SubscriptionState.NeverSubscribed, + val plans: List = emptyList(), + val enableButton: Boolean = false, + ) + data class ProStats( val groupsUpdated: Int = 0, val pinnedConversations: Int = 0, @@ -362,13 +327,6 @@ class ProSettingsViewModel @Inject constructor( val longMessages: Int = 0 ) - data class ProPlanUIState( - val plans: List = emptyList(), - val enableButton: Boolean = false, - val title: CharSequence = "", - val buttonLabel: String = "", - ) - data class ProPlan( val title: String, val subtitle: String, 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 f23f9c6ba7..b13b353b1f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt @@ -122,7 +122,8 @@ class ProStatusManager @Inject constructor( visible = true, validUntil = Instant.now() + Duration.ofDays(14), ), - type = ProSubscriptionDuration.THREE_MONTHS + type = ProSubscriptionDuration.THREE_MONTHS, + nonOriginatingSubscription = null ) DebugMenuViewModel.DebugSubscriptionStatus.EXPIRING_GOOGLE -> SubscriptionState.Active.Expiring( @@ -130,10 +131,41 @@ class ProStatusManager @Inject constructor( visible = true, validUntil = Instant.now() + Duration.ofDays(2), ), - type = ProSubscriptionDuration.TWELVE_MONTHS + type = ProSubscriptionDuration.TWELVE_MONTHS, + nonOriginatingSubscription = null ) - else -> SubscriptionState.Expired + DebugMenuViewModel.DebugSubscriptionStatus.AUTO_APPLE -> SubscriptionState.Active.AutoRenewing( + proStatus = ProStatus.Pro( + visible = true, + validUntil = Instant.now() + Duration.ofDays(14), + ), + type = ProSubscriptionDuration.ONE_MONTH, + nonOriginatingSubscription = SubscriptionState.Active.NonOriginatingSubscription( + device = "iPhone", + store = "Apple App Store", + platform = "Apple", + platformAccount = "Apple Account", + urlSubscription = "https://www.apple.com/account/subscriptions", + ) + ) + + DebugMenuViewModel.DebugSubscriptionStatus.EXPIRING_APPLE -> SubscriptionState.Active.Expiring( + proStatus = ProStatus.Pro( + visible = true, + validUntil = Instant.now() + Duration.ofDays(2), + ), + type = ProSubscriptionDuration.ONE_MONTH, + nonOriginatingSubscription = SubscriptionState.Active.NonOriginatingSubscription( + device = "iPhone", + store = "Apple App Store", + platform = "Apple", + platformAccount = "Apple Account", + urlSubscription = "https://www.apple.com/account/subscriptions", + ) + ) + + DebugMenuViewModel.DebugSubscriptionStatus.EXPIRED -> SubscriptionState.Expired } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/SubscriptionState.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/SubscriptionState.kt index 40059a9f07..be1e2892d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/SubscriptionState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/SubscriptionState.kt @@ -10,18 +10,33 @@ sealed interface SubscriptionState{ sealed interface Active: SubscriptionState{ val proStatus: ProStatus.Pro val type: ProSubscriptionDuration - - //todo PRO we need a way to know which store the subscription is from + val nonOriginatingSubscription: NonOriginatingSubscription? // null if the current subscription is from the current platform data class AutoRenewing( override val proStatus: ProStatus.Pro, - override val type: ProSubscriptionDuration + override val type: ProSubscriptionDuration, + override val nonOriginatingSubscription: NonOriginatingSubscription? ): Active data class Expiring( override val proStatus: ProStatus.Pro, - override val type: ProSubscriptionDuration + override val type: ProSubscriptionDuration, + override val nonOriginatingSubscription: NonOriginatingSubscription? ): Active + + /** + * A structure representing a non-originating subscription + * For example if a user bought Pro on their iOS device through the Apple Store + * This will help us direct them to their original subscription platform if they want + * to update or cancel Pro + */ + data class NonOriginatingSubscription( + val device: String, + val store: String, + val platform: String, + val platformAccount: String, + val urlSubscription: String, + ) } data object Expired: SubscriptionState diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/ProSubscriptionDuration.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/ProSubscriptionDuration.kt index 6eb77907d3..5f1b7b1ace 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/ProSubscriptionDuration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/ProSubscriptionDuration.kt @@ -1,10 +1,24 @@ package org.thoughtcrime.securesms.pro.subscription +import org.thoughtcrime.securesms.util.DateUtils import java.time.Duration import java.time.Period +import java.time.ZonedDateTime enum class ProSubscriptionDuration(val duration: Period) { ONE_MONTH(Period.ofMonths(1)), THREE_MONTHS(Period.ofMonths(3)), TWELVE_MONTHS(Period.ofMonths(12)) +} + +private val proSettingsDateFormat = "MMMM d, yyyy" + +fun ProSubscriptionDuration.expiryFromNow(): String { + val newSubscriptionExpiryDate = ZonedDateTime.now() + .plus(duration) + .toInstant() + .toEpochMilli() + return DateUtils.getLocaleFormattedDate( + newSubscriptionExpiryDate, proSettingsDateFormat + ) } \ No newline at end of file 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 ec2047a44e..3e7df9f5fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/AlertDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/AlertDialog.kt @@ -292,12 +292,13 @@ fun DialogButton( @Composable fun DialogBg( modifier: Modifier = Modifier, + bgColor: Color = LocalColors.current.backgroundSecondary, content: @Composable BoxScope.() -> Unit ){ Box( modifier = modifier .background( - color = LocalColors.current.backgroundSecondary, + color = bgColor, shape = MaterialTheme.shapes.small ) .border( diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt index d3dbb513ff..4d2259e7a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt @@ -116,22 +116,13 @@ class DateUtils @Inject constructor( ).toString() } - // Format a given timestamp with a specific pattern - fun formatTime(timestamp: Long, pattern: String, locale: Locale = Locale.getDefault()): String { - val formatter = DateTimeFormatter.ofPattern(pattern, locale) - - return Instant.ofEpochMilli(timestamp) - .atZone(ZoneId.systemDefault()) - .format(formatter) - } + // Method to get a date in a locale-aware fashion or with a specific pattern + fun getLocaleFormattedDate(timestamp: Long): String = + formatTime(timestamp, userDateFormat) fun getLocaleFormattedDateTime(timestamp: Long): String = formatTime(timestamp, defaultDateTimeFormat) - // Method to get a date in a locale-aware fashion or with a specific pattern - fun getLocaleFormattedDate(timestamp: Long, specificPattern: String = ""): String = - formatTime(timestamp, specificPattern.takeIf { it.isNotEmpty() } ?: userDateFormat) - // Method to get a time in a locale-aware fashion (i.e., 13:25 or 1:25 PM) fun getLocaleFormattedTime(timestamp: Long): String = formatTime(timestamp, userTimeFormat) @@ -231,12 +222,6 @@ class DateUtils @Inject constructor( } } - fun getLocalisedTimeDuration(amount: Int, unit: MeasureUnit): String { - val locale = context.resources.configuration.locales[0] - val format = MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.WIDE) - return format.format(Measure(amount, unit)) - } - // Helper methods private fun toLocalDate(timestamp: Long): LocalDate = Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDate() @@ -284,5 +269,24 @@ class DateUtils @Inject constructor( fun Instant.toEpochSeconds(): Long { return this.toEpochMilli() / 1000 } + + fun getLocalisedTimeDuration(context: Context, amount: Int, unit: MeasureUnit): String { + val locale = context.resources.configuration.locales[0] + val format = MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.WIDE) + return format.format(Measure(amount, unit)) + } + + // Format a given timestamp with a specific pattern + fun formatTime(timestamp: Long, pattern: String, locale: Locale = Locale.getDefault()): String { + val formatter = DateTimeFormatter.ofPattern(pattern, locale) + + return Instant.ofEpochMilli(timestamp) + .atZone(ZoneId.systemDefault()) + .format(formatter) + } + + // Method to get a date in a locale-aware fashion or with a specific pattern + fun getLocaleFormattedDate(timestamp: Long, specificPattern: String): String = + formatTime(timestamp, specificPattern) } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_smartphone.xml b/app/src/main/res/drawable/ic_smartphone.xml new file mode 100644 index 0000000000..60855ec987 --- /dev/null +++ b/app/src/main/res/drawable/ic_smartphone.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/DateUtilsTimeSpanTests.kt b/app/src/test/java/org/thoughtcrime/securesms/util/DateUtilsTimeSpanTests.kt index 274a360f44..185f1b686a 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/util/DateUtilsTimeSpanTests.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/util/DateUtilsTimeSpanTests.kt @@ -111,7 +111,7 @@ class DateUtilsTest { assertEquals("02/04/2021", result) // Test with specific format - val customResult = dateUtils.getLocaleFormattedDate(testTimestamp, "yyyy.MM.dd") + val customResult = DateUtils.getLocaleFormattedDate(testTimestamp, "yyyy.MM.dd") assertEquals("2021.04.02", customResult) }