-
Notifications
You must be signed in to change notification settings - Fork 1
fix: unify timed sheet behavior for master #565
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c0e045f
ec46cfb
2964646
601f0b6
ad7d313
d58d46a
cc384ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package to.bitkit.di | ||
|
|
||
| import dagger.Module | ||
| import dagger.Provides | ||
| import dagger.hilt.InstallIn | ||
| import dagger.hilt.components.SingletonComponent | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import to.bitkit.utils.timedsheets.TimedSheetManager | ||
|
|
||
| @Module | ||
| @InstallIn(SingletonComponent::class) | ||
| object TimedSheetModule { | ||
|
|
||
| @Provides | ||
| fun provideTimedSheetManagerProvider(): (CoroutineScope) -> TimedSheetManager { | ||
| return ::TimedSheetManager | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -135,7 +135,6 @@ private fun TermsText( | |
| } | ||
| } | ||
|
|
||
|
|
||
| @Preview(showSystemUi = true) | ||
| @Composable | ||
| private fun TermsPreview() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package to.bitkit.utils.timedsheets | ||
|
|
||
| import to.bitkit.ui.components.TimedSheetType | ||
|
|
||
| interface TimedSheetItem { | ||
| val type: TimedSheetType | ||
| val priority: Int | ||
|
|
||
| suspend fun shouldShow(): Boolean | ||
| suspend fun onShown() | ||
| suspend fun onDismissed() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Name should not be past tense as per callback naming best practices. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package to.bitkit.utils.timedsheets | ||
|
|
||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.Job | ||
| import kotlinx.coroutines.delay | ||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import kotlinx.coroutines.flow.asStateFlow | ||
| import kotlinx.coroutines.launch | ||
| import to.bitkit.ui.components.TimedSheetType | ||
| import to.bitkit.utils.Logger | ||
|
|
||
| class TimedSheetManager(private val scope: CoroutineScope) { | ||
| private val _currentSheet = MutableStateFlow<TimedSheetType?>(null) | ||
| val currentSheet: StateFlow<TimedSheetType?> = _currentSheet.asStateFlow() | ||
|
|
||
| private val registeredSheets = mutableListOf<TimedSheetItem>() | ||
| private var currentTimedSheet: TimedSheetItem? = null | ||
| private var checkJob: Job? = null | ||
|
|
||
| fun registerSheet(sheet: TimedSheetItem) { | ||
| registeredSheets.add(sheet) | ||
| registeredSheets.sortByDescending { it.priority } | ||
| Logger.debug( | ||
| "Registered timed sheet: ${sheet.type.name} with priority: ${sheet.priority}", | ||
| context = TAG | ||
| ) | ||
| } | ||
|
|
||
| fun onHomeScreenEntered() { | ||
| Logger.debug("User entered home screen, starting timer", context = TAG) | ||
| checkJob?.cancel() | ||
| checkJob = scope.launch { | ||
| delay(CHECK_DELAY_MILLIS) | ||
| checkAndShowNextSheet() | ||
| } | ||
| } | ||
|
|
||
| fun onHomeScreenExited() { | ||
| Logger.debug("User exited home screen, cancelling timer", context = TAG) | ||
| checkJob?.cancel() | ||
| checkJob = null | ||
| } | ||
|
|
||
| fun dismissCurrentSheet() { | ||
| if (currentTimedSheet == null) return | ||
|
|
||
| scope.launch { | ||
| currentTimedSheet?.onDismissed() | ||
| _currentSheet.value = null | ||
| currentTimedSheet = null | ||
|
|
||
| Logger.debug("Clearing timed sheet queue", context = TAG) | ||
| } | ||
| } | ||
|
|
||
| private suspend fun checkAndShowNextSheet() { | ||
| Logger.debug("Registered sheets: ${registeredSheets.map { it.type.name }}") | ||
| for (sheet in registeredSheets.toList()) { | ||
| if (sheet.shouldShow()) { | ||
| Logger.debug( | ||
| "Showing timed sheet: ${sheet.type.name} with priority: ${sheet.priority}", | ||
| context = TAG | ||
| ) | ||
| currentTimedSheet = sheet | ||
| _currentSheet.value = sheet.type | ||
| sheet.onShown() | ||
| registeredSheets.remove(sheet) | ||
| return | ||
| } | ||
| } | ||
|
|
||
| Logger.debug("No timed sheets need to be shown", context = TAG) | ||
| _currentSheet.value = null | ||
| currentTimedSheet = null | ||
| } | ||
|
|
||
| companion object { | ||
| private const val TAG = "TimedSheetManager" | ||
| private const val CHECK_DELAY_MILLIS = 2000L | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package to.bitkit.utils.timedsheets | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: we don't need another package only for this. nit: Also, we should collectively put shared code in "shared", not utils. nit: and lastly, I things that belong together should stay together, this global scope function is better placed in
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: package of all these should be |
||
|
|
||
| import kotlin.time.Clock | ||
| import kotlin.time.ExperimentalTime | ||
|
|
||
| @OptIn(ExperimentalTime::class) | ||
| fun checkTimeout( | ||
| lastIgnoredMillis: Long, | ||
| intervalMillis: Long, | ||
| additionalCondition: Boolean = true, | ||
| ): Boolean { | ||
| if (!additionalCondition) return false | ||
|
|
||
| val currentTime = Clock.System.now().toEpochMilliseconds() | ||
| val isTimeOutOver = lastIgnoredMillis == 0L || | ||
| (currentTime - lastIgnoredMillis > intervalMillis) | ||
| return isTimeOutOver | ||
| } | ||
|
|
||
| const val ONE_DAY_ASK_INTERVAL_MILLIS = 1000 * 60 * 60 * 24L | ||
| const val ONE_WEEK_ASK_INTERVAL_MILLIS = ONE_DAY_ASK_INTERVAL_MILLIS * 7L | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package to.bitkit.utils.timedsheets.sheets | ||
|
|
||
| import kotlinx.coroutines.CoroutineDispatcher | ||
| import kotlinx.coroutines.withContext | ||
| import to.bitkit.BuildConfig | ||
| import to.bitkit.di.BgDispatcher | ||
| import to.bitkit.services.AppUpdaterService | ||
| import to.bitkit.ui.components.TimedSheetType | ||
| import to.bitkit.utils.Logger | ||
| import to.bitkit.utils.timedsheets.TimedSheetItem | ||
| import javax.inject.Inject | ||
|
|
||
| class AppUpdateTimedSheet @Inject constructor( | ||
| private val appUpdaterService: AppUpdaterService, | ||
| @BgDispatcher private val bgDispatcher: CoroutineDispatcher, | ||
| ) : TimedSheetItem { | ||
| override val type = TimedSheetType.APP_UPDATE | ||
| override val priority = 5 | ||
|
|
||
| override suspend fun shouldShow(): Boolean = withContext(bgDispatcher) { | ||
| try { | ||
| val androidReleaseInfo = appUpdaterService.getReleaseInfo().platforms.android | ||
| val currentBuildNumber = BuildConfig.VERSION_CODE | ||
|
|
||
| if (androidReleaseInfo.buildNumber <= currentBuildNumber) return@withContext false | ||
|
|
||
| if (androidReleaseInfo.isCritical) { | ||
| return@withContext false | ||
| } | ||
|
|
||
| return@withContext true | ||
| } catch (e: Exception) { | ||
jvsena42 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Logger.warn("Failure fetching new releases", e = e, context = TAG) | ||
| return@withContext false | ||
| } | ||
| } | ||
|
|
||
| override suspend fun onShown() { | ||
| Logger.debug("App update sheet shown", context = TAG) | ||
| } | ||
|
|
||
| override suspend fun onDismissed() { | ||
| Logger.debug("App update sheet dismissed", context = TAG) | ||
| } | ||
|
|
||
| companion object { | ||
| private const val TAG = "AppUpdateTimedSheet" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package to.bitkit.utils.timedsheets.sheets | ||
|
|
||
| import kotlinx.coroutines.flow.first | ||
| import to.bitkit.data.SettingsStore | ||
| import to.bitkit.ext.nowMillis | ||
| import to.bitkit.repositories.WalletRepo | ||
| import to.bitkit.ui.components.TimedSheetType | ||
| import to.bitkit.utils.Logger | ||
| import to.bitkit.utils.timedsheets.ONE_DAY_ASK_INTERVAL_MILLIS | ||
| import to.bitkit.utils.timedsheets.TimedSheetItem | ||
| import to.bitkit.utils.timedsheets.checkTimeout | ||
| import javax.inject.Inject | ||
| import kotlin.time.ExperimentalTime | ||
|
|
||
| class BackupTimedSheet @Inject constructor( | ||
| private val settingsStore: SettingsStore, | ||
| private val walletRepo: WalletRepo, | ||
| ) : TimedSheetItem { | ||
| override val type = TimedSheetType.BACKUP | ||
| override val priority = 4 | ||
|
|
||
| override suspend fun shouldShow(): Boolean { | ||
| val settings = settingsStore.data.first() | ||
| if (settings.backupVerified) return false | ||
|
|
||
| val hasBalance = walletRepo.balanceState.value.totalSats > 0U | ||
| if (!hasBalance) return false | ||
|
|
||
| return checkTimeout( | ||
| lastIgnoredMillis = settings.backupWarningIgnoredMillis, | ||
| intervalMillis = ONE_DAY_ASK_INTERVAL_MILLIS | ||
| ) | ||
| } | ||
|
|
||
| override suspend fun onShown() { | ||
| Logger.debug("Backup sheet shown", context = TAG) | ||
| } | ||
|
|
||
| @OptIn(ExperimentalTime::class) | ||
| override suspend fun onDismissed() { | ||
| val currentTime = nowMillis() | ||
| settingsStore.update { | ||
| it.copy(backupWarningIgnoredMillis = currentTime) | ||
| } | ||
| Logger.debug("Backup sheet dismissed", context = TAG) | ||
| } | ||
|
|
||
| companion object { | ||
| private const val TAG = "BackupTimedSheet" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package to.bitkit.utils.timedsheets.sheets | ||
|
|
||
| import kotlinx.coroutines.flow.first | ||
| import to.bitkit.data.SettingsStore | ||
| import to.bitkit.ext.nowMillis | ||
| import to.bitkit.repositories.CurrencyRepo | ||
| import to.bitkit.repositories.WalletRepo | ||
| import to.bitkit.ui.components.TimedSheetType | ||
| import to.bitkit.utils.Logger | ||
| import to.bitkit.utils.timedsheets.ONE_DAY_ASK_INTERVAL_MILLIS | ||
| import to.bitkit.utils.timedsheets.TimedSheetItem | ||
| import to.bitkit.utils.timedsheets.checkTimeout | ||
| import java.math.BigDecimal | ||
| import javax.inject.Inject | ||
| import kotlin.time.ExperimentalTime | ||
|
|
||
| class HighBalanceTimedSheet @Inject constructor( | ||
| private val settingsStore: SettingsStore, | ||
| private val walletRepo: WalletRepo, | ||
| private val currencyRepo: CurrencyRepo, | ||
| ) : TimedSheetItem { | ||
| override val type = TimedSheetType.HIGH_BALANCE | ||
| override val priority = 1 | ||
|
|
||
| override suspend fun shouldShow(): Boolean { | ||
| val settings = settingsStore.data.first() | ||
|
|
||
| val totalOnChainSats = walletRepo.balanceState.value.totalSats | ||
| val balanceUsd = satsToUsd(totalOnChainSats) ?: return false | ||
| val thresholdReached = balanceUsd > BigDecimal(BALANCE_THRESHOLD_USD) | ||
|
|
||
| if (!thresholdReached) { | ||
| settingsStore.update { it.copy(balanceWarningTimes = 0) } | ||
| return false | ||
| } | ||
|
|
||
| val belowMaxWarnings = settings.balanceWarningTimes < MAX_WARNINGS | ||
|
|
||
| return checkTimeout( | ||
| lastIgnoredMillis = settings.balanceWarningIgnoredMillis, | ||
| intervalMillis = ONE_DAY_ASK_INTERVAL_MILLIS, | ||
| additionalCondition = belowMaxWarnings | ||
| ) | ||
| } | ||
|
|
||
| override suspend fun onShown() { | ||
| Logger.debug("High balance sheet shown", context = TAG) | ||
| } | ||
|
|
||
| @OptIn(ExperimentalTime::class) | ||
| override suspend fun onDismissed() { | ||
| val currentTime = nowMillis() | ||
| settingsStore.update { | ||
| it.copy( | ||
| balanceWarningTimes = it.balanceWarningTimes + 1, | ||
| balanceWarningIgnoredMillis = currentTime, | ||
| ) | ||
| } | ||
| Logger.debug("High balance sheet dismissed", context = TAG) | ||
| } | ||
|
|
||
| private fun satsToUsd(sats: ULong): BigDecimal? { | ||
| val converted = currencyRepo.convertSatsToFiat(sats = sats.toLong(), currency = "USD").getOrNull() | ||
| return converted?.value | ||
| } | ||
|
|
||
| companion object { | ||
| private const val TAG = "HighBalanceTimedSheet" | ||
| private const val BALANCE_THRESHOLD_USD = 500L | ||
| private const val MAX_WARNINGS = 3 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package to.bitkit.utils.timedsheets.sheets | ||
|
|
||
| import kotlinx.coroutines.flow.first | ||
| import to.bitkit.data.SettingsStore | ||
| import to.bitkit.ext.nowMillis | ||
| import to.bitkit.repositories.WalletRepo | ||
| import to.bitkit.ui.components.TimedSheetType | ||
| import to.bitkit.utils.Logger | ||
| import to.bitkit.utils.timedsheets.ONE_WEEK_ASK_INTERVAL_MILLIS | ||
| import to.bitkit.utils.timedsheets.TimedSheetItem | ||
| import to.bitkit.utils.timedsheets.checkTimeout | ||
| import javax.inject.Inject | ||
| import kotlin.time.ExperimentalTime | ||
|
|
||
| class NotificationsTimedSheet @Inject constructor( | ||
| private val settingsStore: SettingsStore, | ||
| private val walletRepo: WalletRepo, | ||
| ) : TimedSheetItem { | ||
| override val type = TimedSheetType.NOTIFICATIONS | ||
| override val priority = 3 | ||
|
|
||
| override suspend fun shouldShow(): Boolean { | ||
| val settings = settingsStore.data.first() | ||
| if (settings.notificationsGranted) return false | ||
| if (walletRepo.balanceState.value.totalLightningSats == 0UL) return false | ||
|
|
||
| return checkTimeout( | ||
| lastIgnoredMillis = settings.notificationsIgnoredMillis, | ||
| intervalMillis = ONE_WEEK_ASK_INTERVAL_MILLIS | ||
| ) | ||
| } | ||
|
|
||
| override suspend fun onShown() { | ||
| Logger.debug("Notifications sheet shown", context = TAG) | ||
| } | ||
|
|
||
| @OptIn(ExperimentalTime::class) | ||
| override suspend fun onDismissed() { | ||
| val currentTime = nowMillis() | ||
| settingsStore.update { | ||
| it.copy(notificationsIgnoredMillis = currentTime) | ||
| } | ||
| Logger.debug("Notifications sheet dismissed", context = TAG) | ||
| } | ||
|
|
||
| companion object { | ||
| private const val TAG = "NotificationsTimedSheet" | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: do we really need a provider for this?!
Can't we maybe inherit
BaseCoroutineScope?!AFAIU delegation using
by CoroutineScope(coroutineContext)is the recommended best practice