Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
buildConfig = true
compose = true
}
packaging {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.kairo.reader.ui.settings

import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performScrollToNode
import androidx.compose.ui.unit.Density
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.kairo.reader.R
import com.kairo.reader.TestActivity
import com.kairo.reader.ui.theme.KairoTheme
import com.kairo.reader.ui.tutorial.StartingTutorialOverlay
import com.kairo.reader.ui.tutorial.StartingTutorialOverlayState
import com.kairo.reader.ui.updates.ChangelogScreen
import com.kairo.reader.ui.updates.ReleaseInvitationDialog
import com.kairo.reader.ui.updates.releaseNotes
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class WhatsNewDeviceTest {
@get:Rule
val composeRule = createAndroidComposeRule<TestActivity>()

@Test
fun invitationOffersTourChangelogAndSkip() {
var action = ""
composeRule.setContent {
KairoTheme {
ReleaseInvitationDialog(
version = "1.4.0",
hasTour = true,
onSkip = { action = "skip" },
onStartTour = { action = "tour" },
onViewChangelog = { action = "changelog" },
)
}
}
composeRule.onNodeWithText(text(R.string.whats_new_take_tour)).performClick()
composeRule.runOnIdle { assertEquals("tour", action) }
composeRule.onNodeWithText(text(R.string.whats_new_view_changelog)).performClick()
composeRule.runOnIdle { assertEquals("changelog", action) }
composeRule.onNodeWithText(text(R.string.action_skip)).performClick()
composeRule.runOnIdle { assertEquals("skip", action) }
}

@Test
fun changelogCanReplayTourAndReachOlderVersions() {
var tours = 0
composeRule.setContent {
KairoTheme { ChangelogScreen(onBack = {}, onStartTour = { tours++ }) }
}
composeRule.onNodeWithText(text(R.string.whats_new_take_tour)).performClick()
composeRule.runOnIdle { assertEquals(1, tours) }
composeRule.onNodeWithTag("changelog").performScrollToNode(
hasText(composeRule.activity.getString(R.string.whats_new_version, "1.0.8"))
)
}

@Test
fun tourActionsRemainReachableAtLargeFontSize() {
var action = ""
composeRule.setContent {
CompositionLocalProvider(LocalDensity provides Density(LocalDensity.current.density, fontScale = 2f)) {
KairoTheme {
StartingTutorialOverlay(
state = StartingTutorialOverlayState(releaseNotes.first().tour.first(), 0, 4) { action = "changelog" },
targetBounds = null,
onNext = { action = "next" },
onPrevious = {},
onSkip = { action = "skip" },
)
}
}
}
composeRule.onNodeWithText(text(R.string.whats_new_view_changelog)).performScrollTo().performClick()
composeRule.runOnIdle { assertEquals("changelog", action) }
composeRule.onNodeWithText(text(R.string.action_skip)).performScrollTo().performClick()
composeRule.runOnIdle { assertEquals("skip", action) }
}

private fun text(id: Int) = composeRule.activity.getString(id)
}
1 change: 1 addition & 0 deletions app/src/main/java/com/kairo/reader/core/model/Models.kt
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ data class UserPreferences(
val rsvpSelectedProfileId: String = RsvpProfileIds.builtIn(RsvpProfile.BALANCED),
val rsvpCustomProfiles: List<RsvpCustomProfile> = emptyList(),
val hasSeenStartingTutorial: Boolean = false,
val lastSeenReleaseVersionCode: Int = 0,
val readerFontSizeSp: Float = 18f,
val readerTheme: ReaderTheme = ReaderTheme.LIGHT,
val customTheme: CustomTheme = CustomTheme(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal object PrefKeys {
val rsvpPresetVersion = intPreferencesKey("rsvp_preset_version")
val rsvpProfile = stringPreferencesKey("rsvp_profile")
val customRsvpProfilesJson = stringPreferencesKey("custom_rsvp_profiles_json")
val lastSeenReleaseVersionCode = intPreferencesKey("last_seen_release_version_code")
val hasSeenStartingTutorial = booleanPreferencesKey("has_seen_starting_tutorial")
val minWordMs = longPreferencesKey("min_word_ms")
val longWordMinMs = longPreferencesKey("long_word_min_ms")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import kotlinx.coroutines.flow.Flow
interface PreferencesRepository {
val preferences: Flow<UserPreferences>

suspend fun acknowledgeRelease(versionCode: Int)

suspend fun updateHasSeenStartingTutorial(seen: Boolean)

suspend fun updateRsvpConfig(updater: (RsvpConfig) -> RsvpConfig)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ class PreferencesRepositoryImpl(private val context: Context,) : PreferencesRepo
}
}

override suspend fun acknowledgeRelease(versionCode: Int) {
context.dataStore.edit { prefs ->
prefs[keys.lastSeenReleaseVersionCode] = maxOf(prefs[keys.lastSeenReleaseVersionCode] ?: 0, versionCode)
}
}

override suspend fun updateHasSeenStartingTutorial(seen: Boolean) {
context.dataStore.edit { prefs -> prefs[keys.hasSeenStartingTutorial] = seen }
}
Expand Down Expand Up @@ -443,7 +449,11 @@ class PreferencesRepositoryImpl(private val context: Context,) : PreferencesRepo
}

override suspend fun reset() {
context.dataStore.edit { it.clear() }
context.dataStore.edit { prefs ->
val seenRelease = prefs[keys.lastSeenReleaseVersionCode]
prefs.clear()
if (seenRelease != null) prefs[keys.lastSeenReleaseVersionCode] = seenRelease
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ internal class UserPreferencesMapper(
defaults: UserPreferences,
): UserPreferences =
copy(
lastSeenReleaseVersionCode = prefs[keys.lastSeenReleaseVersionCode] ?: 0,
hasSeenStartingTutorial =
prefs.readOrDefault(
keys.hasSeenStartingTutorial,
Expand Down
19 changes: 19 additions & 0 deletions app/src/main/java/com/kairo/reader/ui/navigation/KairoNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import com.kairo.reader.KairoApplication
import com.kairo.reader.R
import com.kairo.reader.core.model.UserPreferences
import com.kairo.reader.data.books.SharedTextImport
import com.kairo.reader.ui.export.NoteExportPhase
import com.kairo.reader.ui.export.NoteExportProgressDialog
import com.kairo.reader.ui.export.rememberNoteExportCoordinator
import com.kairo.reader.ui.importing.rememberImportCoordinator
import com.kairo.reader.ui.tutorial.rememberStartingTutorialCoordinator
import com.kairo.reader.ui.updates.InAppUpdateCheckResult
import com.kairo.reader.ui.updates.InAppUpdateUiBindings
import com.kairo.reader.ui.updates.WhatsNewPrompt

@Suppress("FunctionNaming")
@Composable
Expand Down Expand Up @@ -74,6 +76,7 @@ internal fun KairoNavHost(
externalArticleUrl = externalArticleUrl,
externalSharedText = externalSharedText,
isImporting = importCoordinator.state.isImporting,
onViewChangelog = { navController.navigate(KairoRoutes.SETTINGS_CHANGELOG) },
)
KairoNavChrome(
prefs = prefs,
Expand Down Expand Up @@ -144,6 +147,7 @@ internal fun KairoNavHost(
}
},
onOpenStartingTutorial = tutorialCoordinator.start,
onOpenReleaseTour = tutorialCoordinator.startReleaseTour,
onTutorialNext = tutorialCoordinator.next,
onTutorialPrevious = tutorialCoordinator.previous,
onTutorialSkip = tutorialCoordinator.skip,
Expand All @@ -152,6 +156,21 @@ internal fun KairoNavHost(
)
}
}
WhatsNewPrompt(
preferences = prefs,
repository = container.preferencesRepository,
canShow = (currentRoute == KairoRoutes.LIBRARY || currentRoute == KairoRoutes.LIBRARY_WITH_TAB) &&
!tutorialCoordinator.active &&
!importCoordinator.state.isImporting &&
externalImportUri == null &&
externalArticleUrl == null &&
externalSharedText == null &&
inAppUpdateUi.prompt == null &&
noteExportUi.state.phase == NoteExportPhase.IDLE &&
noteExportUi.state.sheetOrigin == null,
onStartTour = tutorialCoordinator.startReleaseTour,
onViewChangelog = { navController.navigate(KairoRoutes.SETTINGS_CHANGELOG) },
)
NoteExportProgressDialog(
phase = noteExportUi.state.phase,
onCancelAwaiting = noteExportUi.cancelPending,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ internal object KairoRoutes {
const val LIBRARY_WITH_TAB = "library?tab={tab}"
const val SETTINGS = "settings"
const val SETTINGS_LANGUAGE = "settings/language"
const val SETTINGS_CHANGELOG = "settings/changelog"
const val SETTINGS_INFO = "settings/info"
const val SETTINGS_RSVP = "settings/rsvp"
const val SETTINGS_BIONIC = "settings/bionic"
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/java/com/kairo/reader/ui/navigation/SettingsRoutes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

package com.kairo.reader.ui.navigation

import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.kairo.reader.BuildConfig
import com.kairo.reader.KairoApplication
import com.kairo.reader.core.model.UserPreferences
import com.kairo.reader.ui.settings.BionicSettingsScreen
Expand All @@ -18,6 +20,7 @@ import com.kairo.reader.ui.settings.SettingsHomeActions
import com.kairo.reader.ui.settings.SettingsHomeScreen
import com.kairo.reader.ui.settings.SettingsTutorialActions
import com.kairo.reader.ui.tutorial.StartingTutorialOverlayState
import com.kairo.reader.ui.updates.ChangelogScreen
import kotlinx.coroutines.launch

internal data class SettingsRouteDependencies(
Expand All @@ -27,6 +30,7 @@ internal data class SettingsRouteDependencies(
val tutorialState: StartingTutorialOverlayState?,
val onCheckForUpdates: () -> Unit,
val onOpenStartingTutorial: () -> Unit,
val onOpenReleaseTour: () -> Unit,
val onTutorialNext: () -> Unit,
val onTutorialPrevious: () -> Unit,
val onTutorialSkip: () -> Unit,
Expand All @@ -48,6 +52,7 @@ internal fun NavGraphBuilder.settingsRoutes(dependencies: SettingsRouteDependenc
onOpenTheme = { dependencies.navController.navigate(KairoRoutes.SETTINGS_THEME) },
onOpenReader = { dependencies.navController.navigate(KairoRoutes.SETTINGS_READER) },
onOpenFocus = { dependencies.navController.navigate(KairoRoutes.SETTINGS_FOCUS) },
onOpenChangelog = { dependencies.navController.navigate(KairoRoutes.SETTINGS_CHANGELOG) },
onOpenInfo = { dependencies.navController.navigate(KairoRoutes.SETTINGS_INFO) },
onCheckForUpdates = dependencies.onCheckForUpdates,
onOpenStartingTutorial = dependencies.onOpenStartingTutorial,
Expand Down Expand Up @@ -99,6 +104,16 @@ internal fun NavGraphBuilder.settingsRoutes(dependencies: SettingsRouteDependenc
LanguageSettingsScreen(onBack = { dependencies.navController.popBackStack() })
}

settingsSearchDestination(KairoRoutes.SETTINGS_CHANGELOG) {
LaunchedEffect(Unit) {
dependencies.container.preferencesRepository.acknowledgeRelease(BuildConfig.VERSION_CODE)
}
ChangelogScreen(
onBack = { dependencies.navController.popBackStack() },
onStartTour = dependencies.onOpenReleaseTour,
)
}

settingsSearchDestination(KairoRoutes.SETTINGS_INFO) {
InfoSettingsScreen(onBack = { dependencies.navController.popBackStack() })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal fun SettingsSearchPage.route(): String? = when (this) {
SettingsSearchPage.BIONIC -> KairoRoutes.SETTINGS_BIONIC
SettingsSearchPage.FOCUS -> KairoRoutes.SETTINGS_FOCUS
SettingsSearchPage.LANGUAGE -> KairoRoutes.SETTINGS_LANGUAGE
SettingsSearchPage.CHANGELOG -> KairoRoutes.SETTINGS_CHANGELOG
SettingsSearchPage.INFO -> KairoRoutes.SETTINGS_INFO
else -> null
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ data class SettingsHomeActions(
val onClose: () -> Unit,
val onOpenSearchResult: ((SettingsSearchEntry) -> Unit)? = null,
val onOpenTheme: () -> Unit = {},
val onOpenChangelog: () -> Unit = {},
)

data class SettingsTutorialActions(val onNext: () -> Unit = {}, val onPrevious: () -> Unit = {}, val onSkip: () -> Unit = {},)
Expand All @@ -82,6 +83,8 @@ fun SettingsHomeScreen(
val tutorialTargetRequesters =
remember {
mapOf(
StartingTutorialTargetIds.SETTINGS_THEME to BringIntoViewRequester(),
StartingTutorialTargetIds.SETTINGS_CHANGELOG to BringIntoViewRequester(),
StartingTutorialTargetIds.SETTINGS_LANGUAGE to BringIntoViewRequester(),
StartingTutorialTargetIds.SETTINGS_RSVP to BringIntoViewRequester(),
StartingTutorialTargetIds.SETTINGS_READER to BringIntoViewRequester(),
Expand Down Expand Up @@ -164,6 +167,7 @@ private fun SettingsHomeRows(
title = stringResource(R.string.theme_settings_title),
subtitle = stringResource(R.string.theme_settings_subtitle),
icon = Icons.Default.Palette,
targetId = StartingTutorialTargetIds.SETTINGS_THEME,
onClick = actions.onOpenTheme,
),
SettingsHomeRow(
Expand Down Expand Up @@ -204,6 +208,13 @@ private fun SettingsHomeRows(
targetId = StartingTutorialTargetIds.SETTINGS_FOCUS,
onClick = actions.onOpenFocus,
),
SettingsHomeRow(
title = stringResource(R.string.whats_new_title),
subtitle = stringResource(R.string.whats_new_subtitle),
icon = Icons.Default.AutoStories,
targetId = StartingTutorialTargetIds.SETTINGS_CHANGELOG,
onClick = actions.onOpenChangelog,
),
SettingsHomeRow(
title = stringResource(R.string.update_check_title),
subtitle = stringResource(R.string.update_check_subtitle),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ internal fun SettingsHomeActions.openSearchResult(entry: SettingsSearchEntry) {
SettingsSearchPage.FOCUS -> onOpenFocus()
SettingsSearchPage.LANGUAGE -> onOpenLanguage()
SettingsSearchPage.INFO -> onOpenInfo()
SettingsSearchPage.CHANGELOG -> onOpenChangelog()
SettingsSearchPage.UPDATES -> onCheckForUpdates()
SettingsSearchPage.TUTORIAL -> onOpenStartingTutorial()
SettingsSearchPage.RESET -> Unit // The home screen opens the confirmation dialog.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ internal val languageSearchEntries = listOf(
)

internal val updatesSearchEntries = listOf(
SettingsSearchEntry(
id = "changelog.whats_new",
page = SettingsSearchPage.CHANGELOG,
titleRes = R.string.whats_new_title,
descriptionRes = R.string.whats_new_subtitle,
),
SettingsSearchEntry(
id = "updates.update_check_title",
page = SettingsSearchPage.UPDATES,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ enum class SettingsSearchPage(@StringRes val titleRes: Int) {
LANGUAGE(R.string.settings_language_title),
INFO(R.string.info_settings_title),
UPDATES(R.string.update_check_title),
CHANGELOG(R.string.whats_new_title),
TUTORIAL(R.string.settings_starting_tutorial_title),
RESET(R.string.settings_title),
}
Expand Down
Loading
Loading