diff options
| author | David Göransson <david.goransson@mullvad.net> | 2025-01-09 10:55:29 +0100 |
|---|---|---|
| committer | David Göransson <david.goransson@mullvad.net> | 2025-01-09 10:55:29 +0100 |
| commit | af606551507a95671264979c8a5ca537b33e8a3c (patch) | |
| tree | 4cf45c753e523389bca114f24fbefe576cc53427 | |
| parent | dce74bb3a8c983cfc55f46f989e93f762b772e36 (diff) | |
| parent | 412040218f3d965aca121a47bd5ca954dd9916e5 (diff) | |
| download | mullvadvpn-af606551507a95671264979c8a5ca537b33e8a3c.tar.xz mullvadvpn-af606551507a95671264979c8a5ca537b33e8a3c.zip | |
Merge branch 'update-changelog-presentation-droid-1634'
50 files changed, 584 insertions, 281 deletions
diff --git a/android/BuildInstructions.md b/android/BuildInstructions.md index e89d1b3102..cfff1b3d73 100644 --- a/android/BuildInstructions.md +++ b/android/BuildInstructions.md @@ -219,13 +219,6 @@ rm ./gradle/verification-metadata.xml ## Gradle properties Some gradle properties can be set to simplify development. These are listed below. -### Always show changelog -For development purposes, `ALWAYS_SHOW_CHANGELOG` can be set in `local.properties` to always show -the changelog dialog on each app start. For example: -``` -ALWAYS_SHOW_CHANGELOG=true -``` - ### Override version code and version name To avoid or override the rust based version generation, the `OVERRIDE_VERSION_CODE` and `OVERRIDE_VERSION_NAME` properties can be set in `local.properties`. For example: diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index dd1444302c..024a5fba7f 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -184,12 +184,6 @@ android { } applicationVariants.configureEach { - val alwaysShowChangelog = - gradleLocalProperties(rootProject.projectDir, providers) - .getProperty("ALWAYS_SHOW_CHANGELOG") ?: "false" - - buildConfigField("boolean", "ALWAYS_SHOW_CHANGELOG", alwaysShowChangelog) - val enableInAppVersionNotifications = gradleLocalProperties(rootProject.projectDir, providers) .getProperty("ENABLE_IN_APP_VERSION_NOTIFICATIONS") ?: "true" diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogScreenTest.kt index b811209d1c..968f16eb29 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialogTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogScreenTest.kt @@ -1,8 +1,7 @@ -package net.mullvad.mullvadvpn.compose.dialog +package net.mullvad.mullvadvpn.compose.screen import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick import de.mannodermaus.junit5.compose.ComposeContext import io.mockk.MockKAnnotations import io.mockk.impl.annotations.MockK @@ -15,7 +14,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @OptIn(ExperimentalTestApi::class) -class ChangelogDialogTest { +class ChangelogScreenTest { @JvmField @RegisterExtension val composeExtension = createEdgeToEdgeComposeExtension() @MockK lateinit var mockedViewModel: AppInfoViewModel @@ -25,29 +24,38 @@ class ChangelogDialogTest { MockKAnnotations.init(this) } - private fun ComposeContext.initDialog(state: ChangelogUiState, onDismiss: () -> Unit = {}) { - setContentWithTheme { ChangelogDialog(state = state, onDismiss = onDismiss) } + private fun ComposeContext.initScreen( + state: ChangelogUiState, + onSeeFullChangelog: () -> Unit = {}, + onBackClick: () -> Unit = {}, + ) { + setContentWithTheme { + ChangelogScreen( + state = state, + onSeeFullChangelog = onSeeFullChangelog, + onBackClick = onBackClick, + ) + } } @Test fun testShowChangeLogWhenNeeded() = composeExtension.use { // Arrange - initDialog( + initScreen( state = ChangelogUiState(changes = listOf(CHANGELOG_ITEM), version = CHANGELOG_VERSION), - onDismiss = {}, + onBackClick = {}, ) + // Check changelog version shown + onNodeWithText(CHANGELOG_VERSION).assertExists() + // Check changelog content showed within dialog onNodeWithText(CHANGELOG_ITEM).assertExists() - - // perform click on Got It button to check if dismiss occur - onNodeWithText(CHANGELOG_BUTTON_TEXT).performClick() } companion object { - private const val CHANGELOG_BUTTON_TEXT = "Got it!" private const val CHANGELOG_ITEM = "Changelog item" private const val CHANGELOG_VERSION = "1234.5" } diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt index 59357d0527..0cdc8b8fe7 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt @@ -17,6 +17,7 @@ import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR import net.mullvad.mullvadvpn.compose.test.CONNECT_BUTTON_TEST_TAG import net.mullvad.mullvadvpn.compose.test.CONNECT_CARD_HEADER_TEST_TAG import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER_ACTION +import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER_TEXT_ACTION import net.mullvad.mullvadvpn.compose.test.RECONNECT_BUTTON_TEST_TAG import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_BUTTON_TEST_TAG import net.mullvad.mullvadvpn.compose.test.TOP_BAR_ACCOUNT_BUTTON @@ -53,6 +54,7 @@ class ConnectScreenTest { unmockkAll() } + @Suppress("LongParameterList") private fun ComposeContext.initScreen( state: ConnectUiState = ConnectUiState.INITIAL, onDisconnectClick: () -> Unit = {}, @@ -65,6 +67,8 @@ class ConnectScreenTest { onSettingsClick: () -> Unit = {}, onAccountClick: () -> Unit = {}, onDismissNewDeviceClick: () -> Unit = {}, + onChangelogClick: () -> Unit = {}, + onDismissChangelogClick: () -> Unit = {}, ) { setContentWithTheme { ConnectScreen( @@ -79,6 +83,8 @@ class ConnectScreenTest { onSettingsClick = onSettingsClick, onAccountClick = onAccountClick, onDismissNewDeviceClick = onDismissNewDeviceClick, + onChangelogClick = onChangelogClick, + onDismissChangelogClick = onDismissChangelogClick, ) } } @@ -632,6 +638,34 @@ class ConnectScreenTest { } @Test + fun testOnNewChangelogMessageClick() { + composeExtension.use { + // Arrange + val mockedClickHandler: () -> Unit = mockk(relaxed = true) + initScreen( + onChangelogClick = mockedClickHandler, + state = + ConnectUiState( + location = null, + selectedRelayItemTitle = null, + tunnelState = TunnelState.Connecting(null, null, emptyList()), + showLocation = false, + deviceName = "", + daysLeftUntilExpiry = null, + inAppNotification = InAppNotification.NewVersionChangelog, + isPlayBuild = false, + ), + ) + + // Act + onNodeWithTag(NOTIFICATION_BANNER_TEXT_ACTION).performClick() + + // Assert + verify { mockedClickHandler.invoke() } + } + } + + @Test fun testOpenAccountView() { composeExtension.use { // Arrange diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt index bc1aaeb641..90bdbca1be 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt @@ -5,21 +5,25 @@ import androidx.compose.animation.animateContentSize import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.toUpperCase import androidx.compose.ui.tooling.preview.Preview @@ -29,6 +33,7 @@ import androidx.constraintlayout.compose.Dimension import net.mullvad.mullvadvpn.compose.component.MullvadTopBar import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER_ACTION +import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER_TEXT_ACTION import net.mullvad.mullvadvpn.compose.util.rememberPrevious import net.mullvad.mullvadvpn.lib.model.ErrorState import net.mullvad.mullvadvpn.lib.model.ErrorStateCause @@ -56,8 +61,9 @@ private fun PreviewNotificationBanner() { InAppNotification.TunnelStateError( error = ErrorState(ErrorStateCause.FirewallPolicyError.Generic, true) ), + InAppNotification.NewVersionChangelog, ) - .map { it.toNotificationData(false, {}, {}, {}) } + .map { it.toNotificationData(false, {}, {}, {}, {}, {}) } bannerDataList.forEach { MullvadTopBar( @@ -80,6 +86,8 @@ fun NotificationBanner( isPlayBuild: Boolean, openAppListing: () -> Unit, onClickShowAccount: () -> Unit, + onClickShowChangelog: () -> Unit, + onClickDismissChangelog: () -> Unit, onClickDismissNewDevice: () -> Unit, ) { // Fix for animating to invisible state @@ -97,6 +105,8 @@ fun NotificationBanner( isPlayBuild = isPlayBuild, openAppListing, onClickShowAccount, + onClickShowChangelog, + onClickDismissChangelog, onClickDismissNewDevice, ) ) @@ -153,21 +163,38 @@ private fun Notification(notificationBannerData: NotificationData) { maxLines = 1, overflow = TextOverflow.Ellipsis, ) - message?.let { + message?.let { message -> Text( - text = message, + text = message.text, modifier = Modifier.constrainAs(textMessage) { top.linkTo(textTitle.bottom) start.linkTo(textTitle.start) if (action != null) { end.linkTo(actionIcon.start) + bottom.linkTo(actionIcon.bottom) } else { end.linkTo(parent.end) + bottom.linkTo(parent.bottom) } width = Dimension.fillToConstraints + height = Dimension.fillToConstraints } - .padding(start = Dimens.smallPadding, top = Dimens.tinyPadding), + .padding(start = Dimens.smallPadding, top = Dimens.tinyPadding) + .wrapContentWidth(Alignment.Start) + .let { + if (message is NotificationMessage.ClickableText) { + it.clickable( + onClickLabel = message.contentDescription, + role = Role.Button, + ) { + message.onClick() + } + .testTag(NOTIFICATION_BANNER_TEXT_ACTION) + } else { + it + } + }, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.labelMedium, ) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt index e6f9d3ea69..58798978bc 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt @@ -10,7 +10,10 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle import androidx.core.text.HtmlCompat import java.net.InetAddress import net.mullvad.mullvadvpn.R @@ -25,7 +28,7 @@ import net.mullvad.mullvadvpn.ui.notification.StatusLevel data class NotificationData( val title: AnnotatedString, - val message: AnnotatedString? = null, + val message: NotificationMessage? = null, val statusLevel: StatusLevel, val action: NotificationAction? = null, ) { @@ -34,7 +37,31 @@ data class NotificationData( message: String? = null, statusLevel: StatusLevel, action: NotificationAction? = null, - ) : this(AnnotatedString(title), message?.let { AnnotatedString(it) }, statusLevel, action) + ) : this( + AnnotatedString(title), + message?.let { NotificationMessage.Text(AnnotatedString(it)) }, + statusLevel, + action, + ) + + constructor( + title: String, + message: NotificationMessage, + statusLevel: StatusLevel, + action: NotificationAction? = null, + ) : this(AnnotatedString(title), message, statusLevel, action) +} + +sealed interface NotificationMessage { + val text: AnnotatedString + + data class Text(override val text: AnnotatedString) : NotificationMessage + + data class ClickableText( + override val text: AnnotatedString, + val onClick: () -> Unit, + val contentDescription: String, + ) : NotificationMessage } data class NotificationAction( @@ -48,7 +75,9 @@ fun InAppNotification.toNotificationData( isPlayBuild: Boolean, openAppListing: () -> Unit, onClickShowAccount: () -> Unit, - onDismissNewDevice: () -> Unit, + onClickShowChangelog: () -> Unit, + onClickDismissChangelog: () -> Unit, + onClickDismissNewDevice: () -> Unit, ) = when (this) { is InAppNotification.NewDevice -> @@ -56,13 +85,15 @@ fun InAppNotification.toNotificationData( title = AnnotatedString(stringResource(id = R.string.new_device_notification_title)), message = - stringResource(id = R.string.new_device_notification_message, deviceName) - .formatWithHtml(), + NotificationMessage.Text( + stringResource(id = R.string.new_device_notification_message, deviceName) + .formatWithHtml() + ), statusLevel = StatusLevel.Info, action = NotificationAction( Icons.Default.Clear, - onDismissNewDevice, + onClickDismissNewDevice, stringResource(id = R.string.dismiss), ), ) @@ -98,13 +129,40 @@ fun InAppNotification.toNotificationData( stringResource(id = R.string.open_url), ), ) + is InAppNotification.NewVersionChangelog -> + NotificationData( + title = stringResource(id = R.string.new_changelog_notification_title), + message = + NotificationMessage.ClickableText( + text = + buildAnnotatedString { + withStyle(SpanStyle(textDecoration = TextDecoration.Underline)) { + append( + stringResource( + id = R.string.new_changelog_notification_message + ) + ) + } + }, + onClick = onClickShowChangelog, + contentDescription = + stringResource(id = R.string.new_changelog_notification_message), + ), + statusLevel = StatusLevel.Info, + action = + NotificationAction( + Icons.Default.Clear, + onClickDismissChangelog, + stringResource(id = R.string.dismiss), + ), + ) } @Composable private fun errorMessageBannerData(error: ErrorState) = NotificationData( title = error.title().formatWithHtml(), - message = error.message().formatWithHtml(), + message = NotificationMessage.Text(error.message().formatWithHtml()), statusLevel = StatusLevel.Error, ) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialog.kt deleted file mode 100644 index e5afe6d9bc..0000000000 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialog.kt +++ /dev/null @@ -1,152 +0,0 @@ -package net.mullvad.mullvadvpn.compose.dialog - -import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.spec.DestinationStyle -import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.compose.button.PrimaryButton -import net.mullvad.mullvadvpn.lib.theme.AppTheme -import net.mullvad.mullvadvpn.lib.theme.Dimens -import net.mullvad.mullvadvpn.viewmodel.ChangelogUiState -import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel -import org.koin.androidx.compose.koinViewModel - -@Destination<RootGraph>(style = DestinationStyle.Dialog::class) -@Composable -fun Changelog(navController: NavController) { - val viewModel = koinViewModel<ChangelogViewModel>() - val uiState = viewModel.uiState.collectAsStateWithLifecycle() - - ChangelogDialog(uiState.value, onDismiss = { navController.navigateUp() }) -} - -@Composable -fun ChangelogDialog(state: ChangelogUiState, onDismiss: () -> Unit) { - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text( - text = state.version, - style = MaterialTheme.typography.headlineLarge, - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - ) - }, - text = { - val scrollState: ScrollState = rememberScrollState() - Column( - modifier = Modifier.fillMaxWidth().verticalScroll(scrollState), - verticalArrangement = Arrangement.spacedBy(Dimens.smallPadding), - ) { - Text( - text = stringResource(R.string.changes_dialog_subtitle), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.fillMaxWidth(), - ) - - state.changes.forEach { changeItem -> ChangeListItem(text = changeItem) } - } - }, - confirmButton = { - PrimaryButton(text = stringResource(R.string.got_it), onClick = onDismiss) - }, - containerColor = MaterialTheme.colorScheme.surface, - titleContentColor = MaterialTheme.colorScheme.onSurface, - ) -} - -@Composable -private fun ChangeListItem(text: String) { - Column { - Row { - Text( - text = "•", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.width(Dimens.buttonSpacing), - textAlign = TextAlign.Center, - ) - Text( - text = text, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } - } -} - -@Preview -@Composable -private fun PreviewChangelogDialogWithSingleShortItem() { - AppTheme { - ChangelogDialog( - ChangelogUiState(changes = listOf("Item 1"), version = "1111.1"), - onDismiss = {}, - ) - } -} - -@Preview -@Composable -private fun PreviewChangelogDialogWithTwoLongItems() { - val longPreviewText = - "This is a sample changelog item of a Compose Preview visualization. " + - "The purpose of this specific sample text is to visualize a long text that will result " + - "in multiple lines in the changelog dialog." - - AppTheme { - ChangelogDialog( - ChangelogUiState( - changes = listOf(longPreviewText, longPreviewText), - version = "1111.1", - ), - onDismiss = {}, - ) - } -} - -@Preview -@Composable -private fun PreviewChangelogDialogWithTenShortItems() { - AppTheme { - ChangelogDialog( - ChangelogUiState( - changes = - listOf( - "Item 1", - "Item 2", - "Item 3", - "Item 4", - "Item 5", - "Item 6", - "Item 7", - "Item 8", - "Item 9", - "Item 10", - ), - version = "1111.1", - ), - onDismiss = {}, - ) - } -} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/UriHandlerExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/UriHandlerExtensions.kt index a642dc72fe..3e0ae8f898 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/UriHandlerExtensions.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/UriHandlerExtensions.kt @@ -17,6 +17,12 @@ fun UriHandler.createOpenAccountPageHook(): (WebsiteAuthToken?) -> Unit { } } +@Composable +fun UriHandler.createOpenFullChangeLogHook(): () -> Unit { + val changelogUrl = stringResource(id = R.string.changelog_url) + return { safeOpenUri(changelogUrl) } +} + fun UriHandler.createUriHook(uri: String): () -> Unit = { safeOpenUri(uri) } private fun UriHandler.safeOpenUri(uri: String) { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/AppInfoUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/AppInfoUiStatePreviewParameterProvider.kt new file mode 100644 index 0000000000..42d23a1d03 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/AppInfoUiStatePreviewParameterProvider.kt @@ -0,0 +1,19 @@ +package net.mullvad.mullvadvpn.compose.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import net.mullvad.mullvadvpn.ui.VersionInfo +import net.mullvad.mullvadvpn.viewmodel.AppInfoUiState + +class AppInfoUiStatePreviewParameterProvider : PreviewParameterProvider<AppInfoUiState> { + override val values: Sequence<AppInfoUiState> = + sequenceOf( + AppInfoUiState( + version = VersionInfo(currentVersion = "2024.9", isSupported = true), + isPlayBuild = true, + ), + AppInfoUiState( + version = VersionInfo(currentVersion = "2024.9", isSupported = false), + isPlayBuild = true, + ), + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AppInfoScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AppInfoScreen.kt index e65dc2c8d8..3c57587637 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AppInfoScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AppInfoScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.Error -import androidx.compose.material.icons.filled.Info import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -17,9 +16,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed import com.ramcosta.composedestinations.annotation.Destination @@ -31,8 +31,10 @@ import net.mullvad.mullvadvpn.compose.cell.NavigationComposeCell import net.mullvad.mullvadvpn.compose.cell.TwoRowCell import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.compose.preview.AppInfoUiStatePreviewParameterProvider import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle +import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.viewmodel.AppInfoSideEffect import net.mullvad.mullvadvpn.viewmodel.AppInfoUiState @@ -40,6 +42,17 @@ import net.mullvad.mullvadvpn.viewmodel.AppInfoViewModel import org.koin.androidx.compose.koinViewModel @OptIn(ExperimentalMaterial3Api::class) +@Preview("Initial|Unsupported") +@Composable +private fun PreviewAppInfoScreen( + @PreviewParameter(AppInfoUiStatePreviewParameterProvider::class) state: AppInfoUiState +) { + AppTheme { + AppInfo(state = state, onBackClick = {}, navigateToChangelog = {}, openAppListing = {}) + } +} + +@OptIn(ExperimentalMaterial3Api::class) @Destination<RootGraph>(style = SlideInFromRightTransition::class) @Composable fun AppInfo(navigator: DestinationsNavigator) { @@ -57,7 +70,8 @@ fun AppInfo(navigator: DestinationsNavigator) { AppInfo( state = state, onBackClick = dropUnlessResumed { navigator.navigateUp() }, - navigateToChangelog = dropUnlessResumed { navigator.navigate(ChangelogDestination) }, + navigateToChangelog = + dropUnlessResumed { navigator.navigate(ChangelogDestination(ChangelogNavArgs())) }, openAppListing = dropUnlessResumed { vm.openAppListing() }, ) } @@ -87,9 +101,9 @@ fun AppInfoContent( openAppListing: () -> Unit, ) { Column(modifier = Modifier.padding(bottom = Dimens.smallPadding).animateContentSize()) { - AppVersionRow(state, openAppListing) - ChangelogRow(navigateToChangelog) + HorizontalDivider() + AppVersionRow(state, openAppListing) } } @@ -133,8 +147,6 @@ private fun AppVersionRow(state: AppInfoUiState, openAppListing: () -> Unit) { bottom = Dimens.mediumPadding, ), ) - } else { - HorizontalDivider(color = Color.Transparent) } } } @@ -144,12 +156,5 @@ private fun ChangelogRow(navigateToChangelog: () -> Unit) { NavigationComposeCell( title = stringResource(R.string.changelog_title), onClick = navigateToChangelog, - bodyView = { - Icon( - imageVector = Icons.Default.Info, - contentDescription = stringResource(R.string.changelog_title), - tint = MaterialTheme.colorScheme.onPrimary, - ) - }, ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogScreen.kt new file mode 100644 index 0000000000..53bf2113a6 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogScreen.kt @@ -0,0 +1,211 @@ +package net.mullvad.mullvadvpn.compose.screen + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.button.PrimaryButton +import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton +import net.mullvad.mullvadvpn.compose.component.NavigateCloseIconButton +import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar +import net.mullvad.mullvadvpn.compose.extensions.createOpenFullChangeLogHook +import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition +import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle +import net.mullvad.mullvadvpn.lib.theme.AppTheme +import net.mullvad.mullvadvpn.lib.theme.Dimens +import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar +import net.mullvad.mullvadvpn.viewmodel.ChangeLogSideEffect +import net.mullvad.mullvadvpn.viewmodel.ChangelogUiState +import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel +import org.koin.androidx.compose.koinViewModel + +@Destination<RootGraph>( + style = SlideInFromRightTransition::class, + navArgs = ChangelogNavArgs::class, +) +@Composable +fun Changelog(navController: NavController) { + val viewModel = koinViewModel<ChangelogViewModel>() + + val uiState = viewModel.uiState.collectAsStateWithLifecycle() + + val openAccountPage = LocalUriHandler.current.createOpenFullChangeLogHook() + CollectSideEffectWithLifecycle(viewModel.uiSideEffect) { + when (it) { + is ChangeLogSideEffect.OpenFullChangelog -> openAccountPage() + } + } + LaunchedEffect(Unit) { viewModel.dismissChangelogNotification() } + + ChangelogScreen( + uiState.value, + onBackClick = navController::navigateUp, + onSeeFullChangelog = viewModel::onSeeFullChangelog, + ) +} + +data class ChangelogNavArgs(val isModal: Boolean = false) + +@Composable +fun ChangelogScreen( + state: ChangelogUiState, + onBackClick: () -> Unit, + onSeeFullChangelog: () -> Unit, +) { + + ScaffoldWithMediumTopBar( + appBarTitle = stringResource(id = R.string.changelog_title), + navigationIcon = { + if (state.isModal) { + NavigateCloseIconButton(onBackClick) + } else { + NavigateBackIconButton(onNavigateBack = onBackClick) + } + }, + ) { modifier -> + Column(modifier = modifier.padding(horizontal = Dimens.mediumPadding)) { + val scrollState = rememberScrollState() + Column( + Modifier.weight(1f) + .fillMaxWidth() + .drawVerticalScrollbar( + scrollState, + MaterialTheme.colorScheme.onSurface.copy(alpha = AlphaScrollbar), + ) + .verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(Dimens.mediumPadding), + ) { + Text( + text = state.version, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + + if (state.changes.isEmpty()) { + Text( + text = stringResource(R.string.changelog_empty), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } else { + state.changes.forEach { changeItem -> ChangeListItem(text = changeItem) } + } + } + Box(modifier = Modifier.padding(Dimens.mediumPadding).fillMaxWidth()) { + PrimaryButton( + onClick = onSeeFullChangelog, + text = stringResource(R.string.see_full_changelog), + trailingIcon = { + Icon( + imageVector = Icons.AutoMirrored.Default.OpenInNew, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + }, + ) + } + } + } +} + +@Composable +private fun ChangeListItem(text: String) { + Column { + Row { + Text( + text = "•", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.width(Dimens.buttonSpacing), + textAlign = TextAlign.Center, + ) + Text( + text = text, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + +@Preview +@Composable +private fun PreviewChangelogDialogWithSingleShortItem() { + AppTheme { + ChangelogScreen( + ChangelogUiState(changes = listOf("Item 1"), version = "1111.1"), + onBackClick = {}, + onSeeFullChangelog = {}, + ) + } +} + +@Preview +@Composable +private fun PreviewChangelogDialogWithTwoLongItems() { + val longPreviewText = + "This is a sample changelog item of a Compose Preview visualization. " + + "The purpose of this specific sample text is to visualize a long text that will result " + + "in multiple lines in the changelog dialog." + + AppTheme { + ChangelogScreen( + ChangelogUiState( + changes = listOf(longPreviewText, longPreviewText), + version = "1111.1", + ), + onBackClick = {}, + onSeeFullChangelog = {}, + ) + } +} + +@Preview +@Composable +private fun PreviewChangelogDialogWithTenShortItems() { + AppTheme { + ChangelogScreen( + ChangelogUiState( + changes = + listOf( + "Item 1", + "Item 2", + "Item 3", + "Item 4", + "Item 5", + "Item 6", + "Item 7", + "Item 8", + "Item 9", + "Item 10", + ), + version = "1111.1", + ), + onBackClick = {}, + onSeeFullChangelog = {}, + ) + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt index 0ce574d8c1..7c4bdbd3b3 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt @@ -57,6 +57,7 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.NavGraphs import com.ramcosta.composedestinations.generated.destinations.AccountDestination +import com.ramcosta.composedestinations.generated.destinations.ChangelogDestination import com.ramcosta.composedestinations.generated.destinations.DeviceRevokedDestination import com.ramcosta.composedestinations.generated.destinations.OutOfTimeDestination import com.ramcosta.composedestinations.generated.destinations.SelectLocationDestination @@ -142,10 +143,13 @@ private fun PreviewAccountScreen( {}, {}, {}, + {}, + {}, ) } } +@Suppress("LongMethod") @Destination<RootGraph>(style = HomeTransition::class) @Composable fun Connect( @@ -237,6 +241,9 @@ fun Connect( onSwitchLocationClick = dropUnlessResumed { navigator.navigate(SelectLocationDestination) }, onOpenAppListing = connectViewModel::openAppListing, onManageAccountClick = connectViewModel::onManageAccountClick, + onChangelogClick = + dropUnlessResumed { navigator.navigate(ChangelogDestination(ChangelogNavArgs(true))) }, + onDismissChangelogClick = connectViewModel::dismissNewChangelogNotification, onSettingsClick = dropUnlessResumed { navigator.navigate(SettingsDestination) }, onAccountClick = dropUnlessResumed { navigator.navigate(AccountDestination) }, onDismissNewDeviceClick = connectViewModel::dismissNewDeviceNotification, @@ -254,6 +261,8 @@ fun ConnectScreen( onSwitchLocationClick: () -> Unit, onOpenAppListing: () -> Unit, onManageAccountClick: () -> Unit, + onChangelogClick: () -> Unit, + onDismissChangelogClick: () -> Unit, onSettingsClick: () -> Unit, onAccountClick: () -> Unit, onDismissNewDeviceClick: () -> Unit, @@ -309,6 +318,8 @@ fun ConnectScreen( isPlayBuild = state.isPlayBuild, openAppListing = onOpenAppListing, onClickShowAccount = onManageAccountClick, + onClickShowChangelog = onChangelogClick, + onClickDismissChangelog = onDismissChangelogClick, onClickDismissNewDevice = onDismissNewDeviceClick, ) ConnectionCard( diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt index eef89c5ea2..bdc85b1d6f 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt @@ -46,6 +46,7 @@ const val LOCATION_INFO_CONNECTION_OUT_TEST_TAG = "location_info_connection_out_ // ConnectScreen - Notification banner const val NOTIFICATION_BANNER = "notification_banner" const val NOTIFICATION_BANNER_ACTION = "notification_banner_action" +const val NOTIFICATION_BANNER_TEXT_ACTION = "notification_banner_text_action" // PlayPayment const val PLAY_PAYMENT_INFO_ICON_TEST_TAG = "play_payment_info_icon_test_tag" diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt index bc236cc792..a56541ee1a 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt @@ -43,6 +43,7 @@ import net.mullvad.mullvadvpn.usecase.FilterChipUseCase import net.mullvad.mullvadvpn.usecase.FilteredRelayListUseCase import net.mullvad.mullvadvpn.usecase.InternetAvailableUseCase import net.mullvad.mullvadvpn.usecase.LastKnownLocationUseCase +import net.mullvad.mullvadvpn.usecase.NewChangelogNotificationUseCase import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase import net.mullvad.mullvadvpn.usecase.PaymentUseCase @@ -127,8 +128,8 @@ val uiModule = module { single { androidContext().assets } single { androidContext().contentResolver } - single { ChangelogRepository(get()) } - single { UserPreferencesRepository(get()) } + single { ChangelogRepository(get(), get(), get()) } + single { UserPreferencesRepository(get(), get()) } single { SettingsRepository(get()) } single { MullvadProblemReport(get(), get<DaemonConfig>().apiEndpointOverride, get()) } single { RelayOverridesRepository(get()) } @@ -152,6 +153,7 @@ val uiModule = module { single { TunnelStateNotificationUseCase(get()) } single { VersionNotificationUseCase(get(), BuildConfig.ENABLE_IN_APP_VERSION_NOTIFICATIONS) } single { NewDeviceNotificationUseCase(get(), get()) } + single { NewChangelogNotificationUseCase(get()) } single { OutOfTimeUseCase(get(), get(), MainScope()) } single { InternetAvailableUseCase(get()) } single { SystemVpnSettingsAvailableUseCase(androidContext()) } @@ -166,7 +168,7 @@ val uiModule = module { single { SelectedLocationUseCase(get(), get()) } single { FilterChipUseCase(get(), get(), get(), get()) } - single { InAppNotificationController(get(), get(), get(), get(), MainScope()) } + single { InAppNotificationController(get(), get(), get(), get(), get(), MainScope()) } single<IChangelogDataProvider> { ChangelogDataProvider(get()) } @@ -188,7 +190,7 @@ val uiModule = module { // View models viewModel { AccountViewModel(get(), get(), get(), IS_PLAY_BUILD) } - viewModel { ChangelogViewModel(get(), get()) } + viewModel { ChangelogViewModel(get(), get(), get()) } viewModel { AppInfoViewModel(get(), get(), get(), IS_PLAY_BUILD, get(named(SELF_PACKAGE_NAME))) } @@ -204,6 +206,7 @@ val uiModule = module { get(), get(), get(), + get(), IS_PLAY_BUILD, get(named(SELF_PACKAGE_NAME)), ) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepository.kt index 5267f52271..171eb116b1 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepository.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepository.kt @@ -1,12 +1,40 @@ package net.mullvad.mullvadvpn.repository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import net.mullvad.mullvadvpn.lib.model.BuildVersion import net.mullvad.mullvadvpn.util.IChangelogDataProvider import net.mullvad.mullvadvpn.util.trimAll private const val NEWLINE_CHAR = '\n' private const val BULLET_POINT_CHAR = '-' -class ChangelogRepository(private val dataProvider: IChangelogDataProvider) { +class ChangelogRepository( + private val dataProvider: IChangelogDataProvider, + private val userPreferencesRepository: UserPreferencesRepository, + private val buildVersion: BuildVersion, + dispatcher: CoroutineDispatcher = Dispatchers.IO, +) { + val hasUnreadChangelog: StateFlow<Boolean> = + userPreferencesRepository.preferencesFlow + .map { + getLastVersionChanges().isNotEmpty() && + buildVersion.code > it.lastShownChangelogVersionCode + } + .stateIn( + CoroutineScope(dispatcher), + started = SharingStarted.Eagerly, + initialValue = false, + ) + + suspend fun setDismissNewChangelogNotification() { + userPreferencesRepository.setHasDisplayedChangelogNotification() + } fun getLastVersionChanges(): List<String> = // Prepend with a new line char so each entry consists of NEWLINE_CHAR + BULLET_POINT_CHAR diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/InAppNotificationController.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/InAppNotificationController.kt index 1608e3689e..0fcee60bed 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/InAppNotificationController.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/InAppNotificationController.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.stateIn import net.mullvad.mullvadvpn.lib.model.ErrorState import net.mullvad.mullvadvpn.ui.VersionInfo import net.mullvad.mullvadvpn.usecase.AccountExpiryInAppNotificationUseCase +import net.mullvad.mullvadvpn.usecase.NewChangelogNotificationUseCase import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase import net.mullvad.mullvadvpn.usecase.TunnelStateNotificationUseCase import net.mullvad.mullvadvpn.usecase.VersionNotificationUseCase @@ -47,11 +48,17 @@ sealed class InAppNotification { override val statusLevel = StatusLevel.Info override val priority: Long = 1001 } + + data object NewVersionChangelog : InAppNotification() { + override val statusLevel = StatusLevel.Info + override val priority: Long = 1001 + } } class InAppNotificationController( accountExpiryInAppNotificationUseCase: AccountExpiryInAppNotificationUseCase, newDeviceNotificationUseCase: NewDeviceNotificationUseCase, + newChangelogNotificationUseCase: NewChangelogNotificationUseCase, versionNotificationUseCase: VersionNotificationUseCase, tunnelStateNotificationUseCase: TunnelStateNotificationUseCase, scope: CoroutineScope, @@ -63,8 +70,9 @@ class InAppNotificationController( versionNotificationUseCase(), accountExpiryInAppNotificationUseCase(), newDeviceNotificationUseCase(), - ) { a, b, c, d -> - a + b + c + d + newChangelogNotificationUseCase(), + ) { a, b, c, d, e -> + a + b + c + d + e } .map { it.sortedWith( diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesRepository.kt index f3e6a72b64..8a6dfd59a6 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesRepository.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesRepository.kt @@ -6,13 +6,17 @@ import java.io.IOException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first +import net.mullvad.mullvadvpn.lib.model.BuildVersion -class UserPreferencesRepository(private val userPreferences: DataStore<UserPreferences>) { +class UserPreferencesRepository( + private val userPreferencesStore: DataStore<UserPreferences>, + private val buildVersion: BuildVersion, +) { // Note: this should not be made into a StateFlow. See: // https://developer.android.com/reference/kotlin/androidx/datastore/core/DataStore#data() val preferencesFlow: Flow<UserPreferences> = - userPreferences.data.catch { exception -> + userPreferencesStore.data.catch { exception -> // dataStore.data throws an IOException when an error is encountered when reading data if (exception is IOException) { Logger.e("Error reading user preferences file, falling back to default.", exception) @@ -25,8 +29,14 @@ class UserPreferencesRepository(private val userPreferences: DataStore<UserPrefe suspend fun preferences(): UserPreferences = preferencesFlow.first() suspend fun setPrivacyDisclosureAccepted() { - userPreferences.updateData { prefs -> + userPreferencesStore.updateData { prefs -> prefs.toBuilder().setIsPrivacyDisclosureAccepted(true).build() } } + + suspend fun setHasDisplayedChangelogNotification() { + userPreferencesStore.updateData { prefs -> + prefs.toBuilder().setLastShownChangelogVersionCode(buildVersion.code).build() + } + } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/NewChangelogNotificationUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/NewChangelogNotificationUseCase.kt new file mode 100644 index 0000000000..157de67013 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/NewChangelogNotificationUseCase.kt @@ -0,0 +1,19 @@ +package net.mullvad.mullvadvpn.usecase + +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import net.mullvad.mullvadvpn.repository.ChangelogRepository +import net.mullvad.mullvadvpn.repository.InAppNotification + +class NewChangelogNotificationUseCase(private val changelogRepository: ChangelogRepository) { + operator fun invoke() = + changelogRepository.hasUnreadChangelog + .map { + buildList { + if (it) { + add(InAppNotification.NewVersionChangelog) + } + } + } + .distinctUntilChanged() +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt index 662cbdc4a1..8e5ec24a14 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt @@ -34,16 +34,12 @@ class AppInfoViewModel( flowOf(changelogRepository.getLastVersionChanges()), flowOf(isPlayBuild), ) { versionInfo, changes, isPlayBuild -> - AppInfoUiState(versionInfo, changes, isPlayBuild) + AppInfoUiState(versionInfo, isPlayBuild) } .stateIn( viewModelScope, SharingStarted.WhileSubscribed(), - AppInfoUiState( - appVersionInfoRepository.versionInfo.value, - changelogRepository.getLastVersionChanges(), - true, - ), + AppInfoUiState(appVersionInfoRepository.versionInfo.value, true), ) fun openAppListing() = @@ -58,11 +54,7 @@ class AppInfoViewModel( } } -data class AppInfoUiState( - val version: VersionInfo, - val changes: List<String>, - val isPlayBuild: Boolean, -) +data class AppInfoUiState(val version: VersionInfo, val isPlayBuild: Boolean) sealed interface AppInfoSideEffect { data class OpenUri(val uri: Uri) : AppInfoSideEffect diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt index 571d5da3e3..0feb6ecbd3 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt @@ -1,19 +1,51 @@ package net.mullvad.mullvadvpn.viewmodel import android.os.Parcelable +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ramcosta.composedestinations.generated.destinations.ChangelogDestination +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import net.mullvad.mullvadvpn.lib.model.BuildVersion import net.mullvad.mullvadvpn.repository.ChangelogRepository -class ChangelogViewModel(changelogRepository: ChangelogRepository, buildVersion: BuildVersion) : - ViewModel() { +class ChangelogViewModel( + private val changelogRepository: ChangelogRepository, + savedStateHandle: SavedStateHandle, + buildVersion: BuildVersion, +) : ViewModel() { + private val navArgs = ChangelogDestination.argsFrom(savedStateHandle) + private val _uiSideEffect = Channel<ChangeLogSideEffect>() + val uiSideEffect = _uiSideEffect.receiveAsFlow() + val uiState: StateFlow<ChangelogUiState> = MutableStateFlow( - ChangelogUiState(buildVersion.name, changelogRepository.getLastVersionChanges()) + ChangelogUiState( + navArgs.isModal, + buildVersion.name, + changelogRepository.getLastVersionChanges(), + ) ) + + fun dismissChangelogNotification() = + viewModelScope.launch { changelogRepository.setDismissNewChangelogNotification() } + + fun onSeeFullChangelog() = + viewModelScope.launch { _uiSideEffect.send(ChangeLogSideEffect.OpenFullChangelog) } +} + +sealed interface ChangeLogSideEffect { + object OpenFullChangelog : ChangeLogSideEffect } -@Parcelize data class ChangelogUiState(val version: String, val changes: List<String>) : Parcelable +@Parcelize +data class ChangelogUiState( + val isModal: Boolean = false, + val version: String, + val changes: List<String>, +) : Parcelable diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt index 5fb08bcc48..0ddbd7d724 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt @@ -26,6 +26,7 @@ import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken import net.mullvad.mullvadvpn.lib.shared.AccountRepository import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy import net.mullvad.mullvadvpn.lib.shared.DeviceRepository +import net.mullvad.mullvadvpn.repository.ChangelogRepository import net.mullvad.mullvadvpn.repository.InAppNotificationController import net.mullvad.mullvadvpn.repository.NewDeviceRepository import net.mullvad.mullvadvpn.usecase.LastKnownLocationUseCase @@ -41,6 +42,7 @@ import net.mullvad.mullvadvpn.util.withPrev class ConnectViewModel( private val accountRepository: AccountRepository, private val deviceRepository: DeviceRepository, + private val changelogRepository: ChangelogRepository, inAppNotificationController: InAppNotificationController, private val newDeviceRepository: NewDeviceRepository, selectedLocationTitleUseCase: SelectedLocationTitleUseCase, @@ -192,6 +194,9 @@ class ConnectViewModel( newDeviceRepository.clearNewDeviceCreatedNotification() } + fun dismissNewChangelogNotification() = + viewModelScope.launch { changelogRepository.setDismissNewChangelogNotification() } + private fun outOfTimeEffect() = outOfTimeUseCase.isOutOfTime.filter { it == true }.map { UiSideEffect.OutOfTime } diff --git a/android/app/src/main/proto/user_prefs.proto b/android/app/src/main/proto/user_prefs.proto index 3a7e79285f..6f9661970f 100644 --- a/android/app/src/main/proto/user_prefs.proto +++ b/android/app/src/main/proto/user_prefs.proto @@ -3,4 +3,7 @@ syntax = "proto3"; option java_package = "net.mullvad.mullvadvpn.repository"; option java_multiple_files = true; -message UserPreferences { bool is_privacy_disclosure_accepted = 1; } +message UserPreferences { + bool is_privacy_disclosure_accepted = 1; + int32 last_shown_changelog_version_code = 2; +} diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/InAppNotificationControllerTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/InAppNotificationControllerTest.kt index 74b599da97..c8b27f2e6f 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/InAppNotificationControllerTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/InAppNotificationControllerTest.kt @@ -17,6 +17,7 @@ import net.mullvad.mullvadvpn.lib.model.ErrorState import net.mullvad.mullvadvpn.repository.InAppNotification import net.mullvad.mullvadvpn.repository.InAppNotificationController import net.mullvad.mullvadvpn.usecase.AccountExpiryInAppNotificationUseCase +import net.mullvad.mullvadvpn.usecase.NewChangelogNotificationUseCase import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase import net.mullvad.mullvadvpn.usecase.TunnelStateNotificationUseCase import net.mullvad.mullvadvpn.usecase.VersionNotificationUseCase @@ -33,6 +34,8 @@ class InAppNotificationControllerTest { private lateinit var inAppNotificationController: InAppNotificationController private val accountExpiryNotifications = MutableStateFlow(emptyList<InAppNotification>()) private val newDeviceNotifications = MutableStateFlow(emptyList<InAppNotification.NewDevice>()) + private val newVersionChangelogNotifications = + MutableStateFlow(emptyList<InAppNotification.NewVersionChangelog>()) private val versionNotifications = MutableStateFlow(emptyList<InAppNotification>()) private val tunnelStateNotifications = MutableStateFlow(emptyList<InAppNotification>()) @@ -44,10 +47,13 @@ class InAppNotificationControllerTest { val accountExpiryInAppNotificationUseCase: AccountExpiryInAppNotificationUseCase = mockk() val newDeviceNotificationUseCase: NewDeviceNotificationUseCase = mockk() + val newVersionChangelogUseCase: NewChangelogNotificationUseCase = mockk() val versionNotificationUseCase: VersionNotificationUseCase = mockk() val tunnelStateNotificationUseCase: TunnelStateNotificationUseCase = mockk() every { accountExpiryInAppNotificationUseCase.invoke() } returns accountExpiryNotifications every { newDeviceNotificationUseCase.invoke() } returns newDeviceNotifications + every { newVersionChangelogUseCase.invoke() } returns newVersionChangelogNotifications + every { versionNotificationUseCase.invoke() } returns versionNotifications every { versionNotificationUseCase.invoke() } returns versionNotifications every { tunnelStateNotificationUseCase.invoke() } returns tunnelStateNotifications job = Job() @@ -56,6 +62,7 @@ class InAppNotificationControllerTest { InAppNotificationController( accountExpiryInAppNotificationUseCase, newDeviceNotificationUseCase, + newVersionChangelogUseCase, versionNotificationUseCase, tunnelStateNotificationUseCase, CoroutineScope(job + UnconfinedTestDispatcher()), @@ -73,6 +80,9 @@ class InAppNotificationControllerTest { val newDevice = InAppNotification.NewDevice("") newDeviceNotifications.value = listOf(newDevice) + val newVersionChangelog = InAppNotification.NewVersionChangelog + newVersionChangelogNotifications.value = listOf(newVersionChangelog) + val errorState: ErrorState = mockk() val tunnelStateBlocked = InAppNotification.TunnelStateBlocked val tunnelStateError = InAppNotification.TunnelStateError(errorState) @@ -94,6 +104,7 @@ class InAppNotificationControllerTest { unsupportedVersion, accountExpiry, newDevice, + newVersionChangelog, ), notifications, ) diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepositoryTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepositoryTest.kt index 1524549e57..4d608b7231 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepositoryTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepositoryTest.kt @@ -2,15 +2,24 @@ package net.mullvad.mullvadvpn.repository import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher import net.mullvad.mullvadvpn.lib.common.test.assertLists import net.mullvad.mullvadvpn.util.IChangelogDataProvider import org.junit.jupiter.api.Test +@ExperimentalCoroutinesApi class ChangelogRepositoryTest { private val mockDataProvider: IChangelogDataProvider = mockk() - private val changelogRepository = ChangelogRepository(dataProvider = mockDataProvider) + private val changelogRepository = + ChangelogRepository( + mockDataProvider, + mockk(relaxed = true), + mockk(), + UnconfinedTestDispatcher(), + ) @Test fun `when given a changelog text should return a list of correctly formatted strings`() { diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt index 1dab9a4565..1206af7152 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt @@ -30,6 +30,7 @@ import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken import net.mullvad.mullvadvpn.lib.shared.AccountRepository import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy import net.mullvad.mullvadvpn.lib.shared.DeviceRepository +import net.mullvad.mullvadvpn.repository.ChangelogRepository import net.mullvad.mullvadvpn.repository.InAppNotification import net.mullvad.mullvadvpn.repository.InAppNotificationController import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager @@ -65,6 +66,9 @@ class ConnectViewModelTest { // Device Repository private val mockDeviceRepository: DeviceRepository = mockk() + // Changelog Repository + private val mockChangelogRepository: ChangelogRepository = mockk() + // In App Notifications private val mockInAppNotificationController: InAppNotificationController = mockk() @@ -113,6 +117,7 @@ class ConnectViewModelTest { ConnectViewModel( accountRepository = mockAccountRepository, deviceRepository = mockDeviceRepository, + changelogRepository = mockChangelogRepository, inAppNotificationController = mockInAppNotificationController, newDeviceRepository = mockk(), outOfTimeUseCase = outOfTimeUseCase, diff --git a/android/lib/resource/src/main/res/values-da/strings.xml b/android/lib/resource/src/main/res/values-da/strings.xml index 13aaf95f08..33adf30dc7 100644 --- a/android/lib/resource/src/main/res/values-da/strings.xml +++ b/android/lib/resource/src/main/res/values-da/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">Blokering af internet (enhed offline)</string> <string name="buy_credit">Køb kredit</string> <string name="cancel">Annuller</string> - <string name="changelog_title">Ændringslog</string> - <string name="changes_dialog_subtitle">Ændringer i denne version:</string> <string name="cipher">Chiffer</string> <string name="clear_input">Ryd input</string> <string name="close">Luk</string> diff --git a/android/lib/resource/src/main/res/values-de/strings.xml b/android/lib/resource/src/main/res/values-de/strings.xml index 24001a5e1e..0de987fd67 100644 --- a/android/lib/resource/src/main/res/values-de/strings.xml +++ b/android/lib/resource/src/main/res/values-de/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">Internet sperren (Gerät offline)</string> <string name="buy_credit">Guthaben erwerben</string> <string name="cancel">Abbrechen</string> - <string name="changelog_title">Changelog</string> - <string name="changes_dialog_subtitle">Änderungen in dieser Version:</string> <string name="cipher">Chiffre</string> <string name="clear_input">Eingabe löschen</string> <string name="close">Schließen</string> diff --git a/android/lib/resource/src/main/res/values-es/strings.xml b/android/lib/resource/src/main/res/values-es/strings.xml index c181585b8e..591c4669bb 100644 --- a/android/lib/resource/src/main/res/values-es/strings.xml +++ b/android/lib/resource/src/main/res/values-es/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">Bloqueo de Internet (dispositivo sin conexión)</string> <string name="buy_credit">Comprar créditos</string> <string name="cancel">Cancelar</string> - <string name="changelog_title">Registro de cambios</string> - <string name="changes_dialog_subtitle">Cambios en esta versión:</string> <string name="cipher">Cifrado</string> <string name="clear_input">Borrar entrada</string> <string name="close">Cerrar</string> diff --git a/android/lib/resource/src/main/res/values-fi/strings.xml b/android/lib/resource/src/main/res/values-fi/strings.xml index e61ba4dbb4..80507a0613 100644 --- a/android/lib/resource/src/main/res/values-fi/strings.xml +++ b/android/lib/resource/src/main/res/values-fi/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">Internetyhteys on estetty (laite on offline-tilassa)</string> <string name="buy_credit">Osta käyttöaikaa</string> <string name="cancel">Peruuta</string> - <string name="changelog_title">Muutosloki</string> - <string name="changes_dialog_subtitle">Muutokset tässä versiossa:</string> <string name="cipher">Salaus</string> <string name="clear_input">Tyhjennä syöte</string> <string name="close">Sulje</string> diff --git a/android/lib/resource/src/main/res/values-fr/strings.xml b/android/lib/resource/src/main/res/values-fr/strings.xml index 57c276a287..cd36838c4a 100644 --- a/android/lib/resource/src/main/res/values-fr/strings.xml +++ b/android/lib/resource/src/main/res/values-fr/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">Internet bloqué (appareil hors ligne)</string> <string name="buy_credit">Acheter des crédits</string> <string name="cancel">Annuler</string> - <string name="changelog_title">Journal des modifications</string> - <string name="changes_dialog_subtitle">Modifications dans cette version :</string> <string name="cipher">Chiffre</string> <string name="clear_input">Effacer la saisie</string> <string name="close">Fermer</string> diff --git a/android/lib/resource/src/main/res/values-it/strings.xml b/android/lib/resource/src/main/res/values-it/strings.xml index 85d399b3f7..ab13bbfdbb 100644 --- a/android/lib/resource/src/main/res/values-it/strings.xml +++ b/android/lib/resource/src/main/res/values-it/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">Blocco di Internet (dispositivo offline)</string> <string name="buy_credit">Acquista credito</string> <string name="cancel">Annulla</string> - <string name="changelog_title">Changelog</string> - <string name="changes_dialog_subtitle">Modifiche in questa versione:</string> <string name="cipher">Codice</string> <string name="clear_input">Cancella inserimento</string> <string name="close">Chiudi</string> diff --git a/android/lib/resource/src/main/res/values-ja/strings.xml b/android/lib/resource/src/main/res/values-ja/strings.xml index e3922b12e9..9685400528 100644 --- a/android/lib/resource/src/main/res/values-ja/strings.xml +++ b/android/lib/resource/src/main/res/values-ja/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">インターネットをブロック中 (デバイスがオフライン)</string> <string name="buy_credit">クレジットを購入</string> <string name="cancel">キャンセル</string> - <string name="changelog_title">変更履歴</string> - <string name="changes_dialog_subtitle">このバージョンでの変更内容:</string> <string name="cipher">暗号化</string> <string name="clear_input">入力をクリア</string> <string name="close">閉じる</string> diff --git a/android/lib/resource/src/main/res/values-ko/strings.xml b/android/lib/resource/src/main/res/values-ko/strings.xml index 09f899d083..04bad20602 100644 --- a/android/lib/resource/src/main/res/values-ko/strings.xml +++ b/android/lib/resource/src/main/res/values-ko/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">인터넷 차단(장치 오프라인)</string> <string name="buy_credit">크레딧 구매</string> <string name="cancel">취소</string> - <string name="changelog_title">변경 로그</string> - <string name="changes_dialog_subtitle">이 버전의 변경 사항:</string> <string name="cipher">암호</string> <string name="clear_input">입력 지우기</string> <string name="close">닫기</string> diff --git a/android/lib/resource/src/main/res/values-my/strings.xml b/android/lib/resource/src/main/res/values-my/strings.xml index 789af576cf..544f44cb20 100644 --- a/android/lib/resource/src/main/res/values-my/strings.xml +++ b/android/lib/resource/src/main/res/values-my/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">အင်တာနက် ပိတ်ဆို့နေဆဲ (စက် အော့ဖ်လိုင်း)</string> <string name="buy_credit">ခရက်ဒစ် ဝယ်ရန်</string> <string name="cancel">မလုပ်တော့ပါ</string> - <string name="changelog_title">ပြောင်းလဲမှုမှတ်တမ်း</string> - <string name="changes_dialog_subtitle">ဤဗားရှင်းတွင် ပြောင်းလဲမှုများ-</string> <string name="cipher">ဝှက်စာ</string> <string name="clear_input">ထည့်သွင်းမှုကို ရှင်းရန်</string> <string name="close">ပိတ်ရန်</string> diff --git a/android/lib/resource/src/main/res/values-nb/strings.xml b/android/lib/resource/src/main/res/values-nb/strings.xml index e842f655a6..4436bd166a 100644 --- a/android/lib/resource/src/main/res/values-nb/strings.xml +++ b/android/lib/resource/src/main/res/values-nb/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">Blokkerer internett (enhet frakoblet)</string> <string name="buy_credit">Kjøp kreditt</string> <string name="cancel">Avbryt</string> - <string name="changelog_title">Changelog</string> - <string name="changes_dialog_subtitle">Endringer i denne versjonen:</string> <string name="cipher">Chiffer</string> <string name="clear_input">Fjern inndata</string> <string name="close">Lukk</string> diff --git a/android/lib/resource/src/main/res/values-nl/strings.xml b/android/lib/resource/src/main/res/values-nl/strings.xml index a7a5732eda..5d0d05f1df 100644 --- a/android/lib/resource/src/main/res/values-nl/strings.xml +++ b/android/lib/resource/src/main/res/values-nl/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">Internet wordt geblokkeerd (apparaat offline)</string> <string name="buy_credit">Krediet kopen</string> <string name="cancel">Annuleren</string> - <string name="changelog_title">Changelog</string> - <string name="changes_dialog_subtitle">Wijzigingen in deze versie:</string> <string name="cipher">Versleuteling</string> <string name="clear_input">Invoer wissen</string> <string name="close">Sluiten</string> diff --git a/android/lib/resource/src/main/res/values-pl/strings.xml b/android/lib/resource/src/main/res/values-pl/strings.xml index 80fa36e5bd..19d95b1c9a 100644 --- a/android/lib/resource/src/main/res/values-pl/strings.xml +++ b/android/lib/resource/src/main/res/values-pl/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">Blokowanie Internetu (urządzenie rozłączone)</string> <string name="buy_credit">Kup doładowanie</string> <string name="cancel">Anuluj</string> - <string name="changelog_title">Dziennik zmian</string> - <string name="changes_dialog_subtitle">Zmiany w tej wersji:</string> <string name="cipher">Szyfrowanie</string> <string name="clear_input">Wyczyść dane wejściowe</string> <string name="close">Zamknij</string> diff --git a/android/lib/resource/src/main/res/values-pt/strings.xml b/android/lib/resource/src/main/res/values-pt/strings.xml index d1c940560c..00dcd663d4 100644 --- a/android/lib/resource/src/main/res/values-pt/strings.xml +++ b/android/lib/resource/src/main/res/values-pt/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">Bloqueio de Internet (dispositivo offline)</string> <string name="buy_credit">Comprar crédito</string> <string name="cancel">Cancelar</string> - <string name="changelog_title">Registo de alterações</string> - <string name="changes_dialog_subtitle">Alterações nesta versão:</string> <string name="cipher">Cifra</string> <string name="clear_input">Limpar entrada</string> <string name="close">Fechar</string> diff --git a/android/lib/resource/src/main/res/values-ru/strings.xml b/android/lib/resource/src/main/res/values-ru/strings.xml index b93e21062d..0df1d79a3f 100644 --- a/android/lib/resource/src/main/res/values-ru/strings.xml +++ b/android/lib/resource/src/main/res/values-ru/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">Блокируется доступ в Интернет (устройство офлайн)</string> <string name="buy_credit">Пополнить баланс</string> <string name="cancel">Отмена</string> - <string name="changelog_title">История изменений</string> - <string name="changes_dialog_subtitle">Изменения в этой версии:</string> <string name="cipher">Шифр</string> <string name="clear_input">Очистить поле ввода</string> <string name="close">Закрыть</string> diff --git a/android/lib/resource/src/main/res/values-sv/strings.xml b/android/lib/resource/src/main/res/values-sv/strings.xml index 215df1e8e1..3890a8f2aa 100644 --- a/android/lib/resource/src/main/res/values-sv/strings.xml +++ b/android/lib/resource/src/main/res/values-sv/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">Blockerar internet (enheten är offline)</string> <string name="buy_credit">Köp kredit</string> <string name="cancel">Avbryt</string> - <string name="changelog_title">Ändringslogg</string> - <string name="changes_dialog_subtitle">Ändringar i den här versionen:</string> <string name="cipher">Chiffrering</string> <string name="clear_input">Rensa inmatning</string> <string name="close">Stäng</string> diff --git a/android/lib/resource/src/main/res/values-th/strings.xml b/android/lib/resource/src/main/res/values-th/strings.xml index 7aafd7be05..ad5e98ede7 100644 --- a/android/lib/resource/src/main/res/values-th/strings.xml +++ b/android/lib/resource/src/main/res/values-th/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">กำลังบล็อกอินเทอร์เน็ต (อุปกรณ์ออฟไลน์)</string> <string name="buy_credit">ซื้อเครดิต</string> <string name="cancel">ยกเลิก</string> - <string name="changelog_title">บันทึกการเปลี่ยนแปลง</string> - <string name="changes_dialog_subtitle">การเปลี่ยนแปลงในเวอร์ชันนี้:</string> <string name="cipher">เข้ารหัส</string> <string name="clear_input">ล้างข้อมูลอินพุต</string> <string name="close">ปิด</string> diff --git a/android/lib/resource/src/main/res/values-tr/strings.xml b/android/lib/resource/src/main/res/values-tr/strings.xml index b076839b10..38b157f0b1 100644 --- a/android/lib/resource/src/main/res/values-tr/strings.xml +++ b/android/lib/resource/src/main/res/values-tr/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">İnternet bağlantısı engelleniyor (cihaz çevrimdışı)</string> <string name="buy_credit">Kredi satın alın</string> <string name="cancel">İptal et</string> - <string name="changelog_title">Değişiklik günlüğü</string> - <string name="changes_dialog_subtitle">Bu sürümdeki değişiklikler:</string> <string name="cipher">Şifre</string> <string name="clear_input">Girişi temizle</string> <string name="close">Kapat</string> diff --git a/android/lib/resource/src/main/res/values-zh-rCN/strings.xml b/android/lib/resource/src/main/res/values-zh-rCN/strings.xml index 5d253f81ba..744c1cac33 100644 --- a/android/lib/resource/src/main/res/values-zh-rCN/strings.xml +++ b/android/lib/resource/src/main/res/values-zh-rCN/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">正在阻止网络(设备离线)</string> <string name="buy_credit">购买额度</string> <string name="cancel">取消</string> - <string name="changelog_title">变更日志</string> - <string name="changes_dialog_subtitle">此版本中的变更:</string> <string name="cipher">加密方式</string> <string name="clear_input">清除输入</string> <string name="close">关闭</string> diff --git a/android/lib/resource/src/main/res/values-zh-rTW/strings.xml b/android/lib/resource/src/main/res/values-zh-rTW/strings.xml index 15b40db2ed..68ccea2405 100644 --- a/android/lib/resource/src/main/res/values-zh-rTW/strings.xml +++ b/android/lib/resource/src/main/res/values-zh-rTW/strings.xml @@ -60,8 +60,6 @@ <string name="blocking_internet_device_offline">封鎖網際網路 (裝置離線)</string> <string name="buy_credit">購買點數</string> <string name="cancel">取消</string> - <string name="changelog_title">變更日誌</string> - <string name="changes_dialog_subtitle">此版本中的變更:</string> <string name="cipher">加密方式</string> <string name="clear_input">清除輸入</string> <string name="close">關閉</string> diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index 49b5481a7a..1352648630 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -161,7 +161,6 @@ <string name="copy_account_number">Copy account number</string> <string name="hide_account_number">Hide account number</string> <string name="failed_to_remove_device">Failed to remove device</string> - <string name="changes_dialog_subtitle">Changes in this version:</string> <string name="always_on_vpn_error_notification_title">Always-on VPN assigned to %s</string> <string name="always_on_vpn_error_notification_content"> <![CDATA[Unable to start tunnel connection. Please disable Always-on VPN for <b>%s</b> before using Mullvad VPN.]]> @@ -175,6 +174,8 @@ <string name="new_device_notification_message"> <![CDATA[Welcome, this device is now called <b>%s</b>. For more details see the info button in Account.]]> </string> + <string name="new_changelog_notification_title">NEW VERSION INSTALLED</string> + <string name="new_changelog_notification_message">Click here to see what’s new.</string> <string name="agree_and_continue">Agree and continue</string> <string name="privacy_disclaimer_title">Privacy</string> <string name="privacy_disclaimer_body_first_paragraph">To make sure that you have the most secure version and to inform you of any issues with the current version that is running, the app performs version checks automatically. This sends the app version and Android system version to Mullvad servers. Mullvad keeps counters on number of used app versions and Android versions. The data is never stored or used in any way that can identify you.</string> @@ -387,7 +388,7 @@ <string name="copy">Copy</string> <string name="share">Share…</string> <string name="app_info">App info</string> - <string name="changelog_title">Changelog</string> + <string name="changelog_title">What’s new</string> <string name="version">Version</string> <string name="local_network_sharing_info_block_connections_warning">Attention: If \"Block connections without VPN\" is enabled, \"Local network sharing\" will not work.</string> <string name="encrypted_dns_proxy_info_message_part1">With the “Encrypted DNS proxy” method, the app will communicate with our Mullvad API through a proxy address. It does this by retrieving an address from a DNS over HTTPS (DoH) server and then using that to reach our API servers.</string> @@ -417,4 +418,6 @@ <string name="open_feature_settings">Open %1$s settings</string> <string name="search">Search</string> <string name="obfuscation_info_shadowsocks_batteryusage">Attention: Shadowsocks can increase battery consumption depending on data usage, such as streaming a video.</string> + <string name="see_full_changelog">See full changelog</string> + <string name="changelog_empty">No changelog was added for this version</string> </resources> diff --git a/android/lib/resource/src/main/res/values/strings_non_translatable.xml b/android/lib/resource/src/main/res/values/strings_non_translatable.xml index 837740aa47..f04cabd78e 100644 --- a/android/lib/resource/src/main/res/values/strings_non_translatable.xml +++ b/android/lib/resource/src/main/res/values/strings_non_translatable.xml @@ -6,6 +6,7 @@ <string name="market_uri" translatable="false">market://details?id=%s</string> <string name="faqs_and_guides_url" translatable="false">https://mullvad.net/help/tag/mullvad-app/</string> <string name="privacy_policy_url" translatable="false">https://mullvad.net/help/privacy-policy/</string> + <string name="changelog_url" translatable="false">https://github.com/mullvad/mullvadvpn-app/blob/main/android/CHANGELOG.md</string> <string name="lockdown_url" translatable="false">https://mullvad.net/l/android-lockdown</string> <string name="split_tunneling" translatable="false">Split tunneling</string> <string name="wireguard" translatable="false">WireGuard</string> diff --git a/desktop/packages/mullvad-vpn/locales/messages.pot b/desktop/packages/mullvad-vpn/locales/messages.pot index 2a50198bf0..0801445c51 100644 --- a/desktop/packages/mullvad-vpn/locales/messages.pot +++ b/desktop/packages/mullvad-vpn/locales/messages.pot @@ -621,7 +621,7 @@ msgid "No updates or changes were made in this release for this platform." msgstr "" msgctxt "changelog-view" -msgid "What's new" +msgid "What’s new" msgstr "" #. The selected location label displayed on the main view, when a user selected a specific host to connect to. @@ -911,7 +911,7 @@ msgid "BLOCKING INTERNET" msgstr "" msgctxt "in-app-notifications" -msgid "Click here to see what's new." +msgid "Click here to see what’s new." msgstr "" #. The in-app banner displayed to the user when the app update is available. @@ -1574,7 +1574,7 @@ msgid "VPN settings" msgstr "" msgctxt "settings-view" -msgid "What's new" +msgid "What’s new" msgstr "" msgctxt "split-tunneling-view" @@ -2364,12 +2364,6 @@ msgstr "" msgid "Blocking..." msgstr "" -msgid "Changelog" -msgstr "" - -msgid "Changes in this version:" -msgstr "" - msgid "Changes to DNS related settings might not go into effect immediately due to cached results." msgstr "" @@ -2553,6 +2547,9 @@ msgstr "" msgid "New list" msgstr "" +msgid "No changelog was added for this version" +msgstr "" + msgid "No custom lists available" msgstr "" diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/app-info/components/ChangelogListItem.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/app-info/components/ChangelogListItem.tsx index c05b0286b6..9c861008d1 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/app-info/components/ChangelogListItem.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/app-info/components/ChangelogListItem.tsx @@ -11,7 +11,7 @@ export function ChangelogListItem() { return ( <Cell.CellNavigationButton onClick={navigate}> - <Cell.Label>{messages.pgettext('settings-view', "What's new")}</Cell.Label> + <Cell.Label>{messages.pgettext('settings-view', 'What’s new')}</Cell.Label> </Cell.CellNavigationButton> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/changelog/ChangelogView.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/changelog/ChangelogView.tsx index 6bfe45b7a9..4a93908e65 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/changelog/ChangelogView.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/changelog/ChangelogView.tsx @@ -51,13 +51,13 @@ export const ChangelogView = () => { <NavigationContainer> <NavigationBar> <NavigationItems> - <TitleBarItem>{messages.pgettext('changelog-view', "What's new")}</TitleBarItem> + <TitleBarItem>{messages.pgettext('changelog-view', 'What’s new')}</TitleBarItem> </NavigationItems> </NavigationBar> <NavigationScrollbars> <SettingsHeader> - <TitleBig as={'h1'}>{messages.pgettext('changelog-view', "What's new")}</TitleBig> + <TitleBig as={'h1'}>{messages.pgettext('changelog-view', 'What’s new')}</TitleBig> </SettingsHeader> <Flex $flexDirection="column" $gap={Spacings.spacing3}> <Container size="4"> diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/new-version.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/new-version.ts index 73ccf61525..49be1a2718 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/new-version.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/notifications/new-version.ts @@ -22,7 +22,7 @@ export class NewVersionNotificationProvider implements InAppNotificationProvider public getInAppNotification(): InAppNotification { const title = messages.pgettext('in-app-notifications', 'NEW VERSION INSTALLED'); - const subtitle = messages.pgettext('in-app-notifications', "Click here to see what's new."); + const subtitle = messages.pgettext('in-app-notifications', 'Click here to see what’s new.'); return { indicator: 'success', action: { type: 'close', close: this.context.close }, |
