Skip to content

Commit bfcd8ca

Browse files
authored
Merge branch 'feat/nav3' into fix/error-decoding-qr-msg
2 parents a8b6ebd + 4df76af commit bfcd8ca

30 files changed

+1537
-290
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package to.bitkit.di
2+
3+
import dagger.Module
4+
import dagger.Provides
5+
import dagger.hilt.InstallIn
6+
import dagger.hilt.components.SingletonComponent
7+
import kotlinx.coroutines.CoroutineScope
8+
import to.bitkit.utils.timedsheets.TimedSheetManager
9+
10+
@Module
11+
@InstallIn(SingletonComponent::class)
12+
object TimedSheetModule {
13+
14+
@Provides
15+
fun provideTimedSheetManagerProvider(): (CoroutineScope) -> TimedSheetManager {
16+
return ::TimedSheetManager
17+
}
18+
}

app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt

Lines changed: 87 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ package to.bitkit.repositories
22

33
import kotlinx.coroutines.CoroutineDispatcher
44
import kotlinx.coroutines.CoroutineScope
5+
import kotlinx.coroutines.Job
56
import kotlinx.coroutines.SupervisorJob
67
import kotlinx.coroutines.delay
78
import kotlinx.coroutines.flow.MutableStateFlow
89
import kotlinx.coroutines.flow.StateFlow
910
import kotlinx.coroutines.flow.asStateFlow
11+
import kotlinx.coroutines.flow.distinctUntilChanged
1012
import kotlinx.coroutines.flow.first
1113
import kotlinx.coroutines.flow.map
1214
import kotlinx.coroutines.flow.update
15+
import kotlinx.coroutines.isActive
1316
import kotlinx.coroutines.launch
1417
import kotlinx.coroutines.withContext
1518
import to.bitkit.data.SettingsStore
@@ -30,6 +33,7 @@ import to.bitkit.models.widget.HeadlinePreferences
3033
import to.bitkit.models.widget.PricePreferences
3134
import to.bitkit.models.widget.WeatherPreferences
3235
import to.bitkit.utils.Logger
36+
import java.util.concurrent.ConcurrentHashMap
3337
import javax.inject.Inject
3438
import javax.inject.Singleton
3539

@@ -44,9 +48,8 @@ class WidgetsRepo @Inject constructor(
4448
private val widgetsStore: WidgetsStore,
4549
private val settingsStore: SettingsStore,
4650
) {
47-
// TODO Only refresh in loop widgets displayed in the Home
48-
// TODO Perform a refresh when the preview screen is displayed
4951
private val repoScope = CoroutineScope(bgDispatcher + SupervisorJob())
52+
private val widgetJobs = ConcurrentHashMap<WidgetType, Job>()
5053

5154
val widgetsDataFlow = widgetsStore.data
5255
val showWidgetTitles = settingsStore.data.map { it.showWidgetTitles }
@@ -63,7 +66,86 @@ class WidgetsRepo @Inject constructor(
6366
val refreshStates: StateFlow<Map<WidgetType, Boolean>> = _refreshStates.asStateFlow()
6467

6568
init {
66-
startPeriodicUpdates()
69+
observeWidgetStateChanges()
70+
}
71+
72+
private fun observeWidgetStateChanges() {
73+
repoScope.launch {
74+
widgetsDataFlow
75+
.map { it.widgets.map { widget -> widget.type }.toSet() }
76+
.distinctUntilChanged()
77+
.collect { enabledWidgetTypes ->
78+
updateWidgetJobs(enabledWidgetTypes)
79+
}
80+
}
81+
}
82+
83+
private fun updateWidgetJobs(enabledWidgetTypes: Set<WidgetType>) {
84+
val widgetTypesWithServices = WidgetType.entries.filter {
85+
it != WidgetType.CALCULATOR
86+
}
87+
88+
widgetTypesWithServices.forEach { widgetType ->
89+
val isEnabled = widgetType in enabledWidgetTypes
90+
val hasRunningJob = widgetJobs.containsKey(widgetType) &&
91+
widgetJobs[widgetType]?.isActive == true
92+
93+
when {
94+
isEnabled && !hasRunningJob -> startWidgetRefresh(widgetType)
95+
!isEnabled && hasRunningJob -> stopWidgetRefresh(widgetType)
96+
}
97+
}
98+
}
99+
100+
private fun startWidgetRefresh(widgetType: WidgetType) {
101+
stopWidgetRefresh(widgetType)
102+
103+
val job = when (widgetType) {
104+
WidgetType.NEWS -> repoScope.launch {
105+
while (isActive) {
106+
updateWidget(newsService) { widgetsStore.updateArticles(it) }
107+
delay(newsService.refreshInterval)
108+
}
109+
}
110+
111+
WidgetType.FACTS -> repoScope.launch {
112+
while (isActive) {
113+
updateWidget(factsService) { widgetsStore.updateFacts(it) }
114+
delay(factsService.refreshInterval)
115+
}
116+
}
117+
118+
WidgetType.BLOCK -> repoScope.launch {
119+
while (isActive) {
120+
updateWidget(blocksService) { widgetsStore.updateBlock(it) }
121+
delay(blocksService.refreshInterval)
122+
}
123+
}
124+
125+
WidgetType.WEATHER -> repoScope.launch {
126+
while (isActive) {
127+
updateWidget(weatherService) { widgetsStore.updateWeather(it) }
128+
delay(weatherService.refreshInterval)
129+
}
130+
}
131+
132+
WidgetType.PRICE -> repoScope.launch {
133+
while (isActive) {
134+
updateWidget(priceService) { widgetsStore.updatePrice(it) }
135+
delay(priceService.refreshInterval)
136+
}
137+
}
138+
139+
WidgetType.CALCULATOR -> throw NotImplementedError("Calculator widget doesn't need a service")
140+
}
141+
142+
widgetJobs[widgetType] = job
143+
}
144+
145+
private fun stopWidgetRefresh(widgetType: WidgetType) {
146+
widgetJobs[widgetType]?.cancel()
147+
widgetJobs.remove(widgetType)
148+
Logger.verbose("Stopped refresh coroutine for $widgetType", context = TAG)
67149
}
68150

69151
suspend fun addWidget(type: WidgetType) = withContext(bgDispatcher) { widgetsStore.addWidget(type) }
@@ -96,56 +178,18 @@ class WidgetsRepo @Inject constructor(
96178

97179
suspend fun fetchAllPeriods() = withContext(bgDispatcher) { priceService.fetchAllPeriods() }
98180

99-
/**
100-
* Start periodic updates for all widgets
101-
*/
102-
private fun startPeriodicUpdates() {
103-
startPeriodicUpdate(newsService) { articles ->
104-
widgetsStore.updateArticles(articles)
105-
}
106-
startPeriodicUpdate(factsService) { facts ->
107-
widgetsStore.updateFacts(facts)
108-
}
109-
startPeriodicUpdate(blocksService) { block ->
110-
widgetsStore.updateBlock(block)
111-
}
112-
startPeriodicUpdate(weatherService) { weather ->
113-
widgetsStore.updateWeather(weather)
114-
}
115-
startPeriodicUpdate(priceService) { price ->
116-
widgetsStore.updatePrice(price)
117-
}
118-
}
119-
120-
/**
121-
* Generic method to start periodic updates for any widget service
122-
*/
123-
private fun <T> startPeriodicUpdate(
124-
service: WidgetService<T>,
125-
updateStore: suspend (T) -> Unit
126-
) {
127-
repoScope.launch {
128-
while (true) {
129-
updateWidget(service, updateStore)
130-
delay(service.refreshInterval)
131-
}
132-
}
133-
}
134181

135-
/**
136-
* Update a specific widget type
137-
*/
138182
private suspend fun <T> updateWidget(
139183
service: WidgetService<T>,
140-
updateStore: suspend (T) -> Unit
184+
updateStore: suspend (T) -> Unit,
141185
) {
142186
val widgetType = service.widgetType
143187
_refreshStates.update { it + (widgetType to true) }
144188

145189
service.fetchData()
146190
.onSuccess { data ->
147191
updateStore(data)
148-
Logger.verbose("Updated $widgetType widget successfully")
192+
Logger.verbose("Updated $widgetType widget successfully", context = TAG)
149193
}
150194
.onFailure { e ->
151195
Logger.verbose("Failed to update $widgetType widget", e = e, context = TAG)
@@ -154,27 +198,6 @@ class WidgetsRepo @Inject constructor(
154198
_refreshStates.update { it + (widgetType to false) }
155199
}
156200

157-
/**
158-
* Manually refresh all widgets
159-
*/
160-
suspend fun refreshAllWidgets(): Result<Unit> = runCatching {
161-
updateWidget(newsService) { articles ->
162-
widgetsStore.updateArticles(articles)
163-
}
164-
updateWidget(factsService) { facts ->
165-
widgetsStore.updateFacts(facts)
166-
}
167-
updateWidget(blocksService) { block ->
168-
widgetsStore.updateBlock(block)
169-
}
170-
updateWidget(weatherService) { weather ->
171-
widgetsStore.updateWeather(weather)
172-
}
173-
updateWidget(priceService) { price ->
174-
widgetsStore.updatePrice(price)
175-
}
176-
}
177-
178201
suspend fun refreshEnabledWidgets() = withContext(bgDispatcher) {
179202
widgetsDataFlow.first().widgets.forEach {
180203
refreshWidget(it.type)

app/src/main/java/to/bitkit/ui/ContentView.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ fun ContentView(
127127
appViewModel.mainScreenEffect.collect {
128128
when (it) {
129129
is MainScreenEffect.Navigate -> navigator.navigate(it.route)
130+
is MainScreenEffect.NavigateAndClearBackstack -> navigator.navigateAndClearBackstack(it.route)
130131
is MainScreenEffect.ProcessClipboardAutoRead -> {
131132
if (!navigator.isAtHome()) {
132133
navigator.navigateToHome()

app/src/main/java/to/bitkit/ui/nav/Navigator.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ class Navigator(@PublishedApi internal val backStack: NavBackStack<NavKey>) {
5050

5151
fun isAtHome(): Boolean = backStack.lastOrNull() is Routes.Home
5252

53+
fun navigateAndClearBackstack(route: Routes) {
54+
backStack.clear()
55+
backStack.add(route)
56+
}
57+
5358
fun shouldShowTabBar(): Boolean = when (backStack.lastOrNull()) {
5459
is Routes.Home, is Routes.Savings, is Routes.Spending, is Routes.Activity.All -> true
5560
else -> false

app/src/main/java/to/bitkit/ui/nav/entries/SheetEntries.kt

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package to.bitkit.ui.nav.entries
33
import android.content.Intent
44
import androidx.compose.foundation.layout.fillMaxSize
55
import androidx.compose.foundation.layout.navigationBarsPadding
6+
import androidx.compose.runtime.DisposableEffect
67
import androidx.compose.runtime.LaunchedEffect
78
import androidx.compose.runtime.getValue
89
import androidx.compose.runtime.remember
@@ -799,30 +800,33 @@ private fun EntryProviderScope<NavKey>.sheetFlowEntries(
799800
entry<Routes.Sheet.Update>(
800801
metadata = SheetSceneStrategy.sheet()
801802
) {
803+
DisposableEffect(Unit) {
804+
onDispose { appViewModel.dismissTimedSheet() }
805+
}
802806
UpdateSheet(
803-
onCancel = {
804-
appViewModel.dismissTimedSheet()
805-
navigator.goBack()
806-
},
807+
onCancel = { navigator.goBack() },
807808
)
808809
}
809810

810811
entry<Routes.Sheet.Backup>(
811812
metadata = SheetSceneStrategy.sheet()
812813
) {
814+
DisposableEffect(Unit) {
815+
onDispose { appViewModel.dismissTimedSheet() }
816+
}
813817
BackupIntroScreen(
814818
hasFunds = LocalBalances.current.totalSats > 0u,
815-
onClose = {
816-
appViewModel.dismissTimedSheet()
817-
navigator.goBack()
818-
},
819+
onClose = { navigator.goBack() },
819820
onConfirm = { navigator.navigate(Routes.Backup.ShowMnemonic) },
820821
)
821822
}
822823

823824
entry<Routes.Sheet.Notifications>(
824825
metadata = SheetSceneStrategy.sheet()
825826
) {
827+
DisposableEffect(Unit) {
828+
onDispose { appViewModel.dismissTimedSheet() }
829+
}
826830
BackgroundPaymentsIntroSheet(
827831
onContinue = {
828832
appViewModel.dismissTimedSheet(skipQueue = true)
@@ -834,6 +838,9 @@ private fun EntryProviderScope<NavKey>.sheetFlowEntries(
834838
entry<Routes.Sheet.QuickPay>(
835839
metadata = SheetSceneStrategy.sheet()
836840
) {
841+
DisposableEffect(Unit) {
842+
onDispose { appViewModel.dismissTimedSheet() }
843+
}
837844
QuickPayIntroSheet(
838845
onContinue = {
839846
appViewModel.dismissTimedSheet(skipQueue = true)
@@ -845,12 +852,12 @@ private fun EntryProviderScope<NavKey>.sheetFlowEntries(
845852
entry<Routes.Sheet.HighBalance>(
846853
metadata = SheetSceneStrategy.sheet()
847854
) {
855+
DisposableEffect(Unit) {
856+
onDispose { appViewModel.dismissTimedSheet() }
857+
}
848858
val context = LocalContext.current
849859
HighBalanceWarningSheet(
850-
understoodClick = {
851-
appViewModel.dismissTimedSheet()
852-
navigator.goBack()
853-
},
860+
understoodClick = { navigator.goBack() },
854861
learnMoreClick = {
855862
val intent = Intent(Intent.ACTION_VIEW, Env.STORING_BITCOINS_URL.toUri())
856863
context.startActivity(intent)

app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreen.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.width
1212
import androidx.compose.material3.HorizontalDivider
1313
import androidx.compose.material3.Icon
1414
import androidx.compose.runtime.Composable
15+
import androidx.compose.runtime.LaunchedEffect
1516
import androidx.compose.runtime.getValue
1617
import androidx.compose.ui.Alignment
1718
import androidx.compose.ui.Modifier
@@ -51,6 +52,10 @@ fun BlocksPreviewScreen(
5152
val currentBlock by blocksViewModel.currentBlock.collectAsStateWithLifecycle()
5253
val isBlocksWidgetEnabled by blocksViewModel.isBlocksWidgetEnabled.collectAsStateWithLifecycle()
5354

55+
LaunchedEffect(Unit) {
56+
blocksViewModel.refreshOnDisplay()
57+
}
58+
5459
BlocksPreviewContent(
5560
onBack = onBack,
5661
isBlocksWidgetEnabled = isBlocksWidgetEnabled,

app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksViewModel.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ class BlocksViewModel @Inject constructor(
122122
}
123123
}
124124

125+
fun refreshOnDisplay() {
126+
viewModelScope.launch {
127+
widgetsRepo.refreshWidget(WidgetType.BLOCK)
128+
}
129+
}
130+
125131
// MARK: - Private Methods
126132

127133
private fun initializeCustomPreferences() {

app/src/main/java/to/bitkit/ui/screens/widgets/facts/FactsPreviewScreen.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.width
1212
import androidx.compose.material3.HorizontalDivider
1313
import androidx.compose.material3.Icon
1414
import androidx.compose.runtime.Composable
15+
import androidx.compose.runtime.LaunchedEffect
1516
import androidx.compose.runtime.getValue
1617
import androidx.compose.ui.Alignment
1718
import androidx.compose.ui.Modifier
@@ -50,6 +51,10 @@ fun FactsPreviewScreen(
5051
val fact by factsViewModel.currentFact.collectAsStateWithLifecycle()
5152
val isFactsWidgetEnabled by factsViewModel.isFactsWidgetEnabled.collectAsStateWithLifecycle()
5253

54+
LaunchedEffect(Unit) {
55+
factsViewModel.refreshOnDisplay()
56+
}
57+
5358
FactsPreviewContent(
5459
onBack = onBack,
5560
isFactsWidgetEnabled = isFactsWidgetEnabled,

app/src/main/java/to/bitkit/ui/screens/widgets/facts/FactsViewModel.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ class FactsViewModel @Inject constructor(
8989
}
9090
}
9191

92+
fun refreshOnDisplay() {
93+
viewModelScope.launch {
94+
widgetsRepo.refreshWidget(WidgetType.FACTS)
95+
}
96+
}
97+
9298
// MARK: - Private Methods
9399

94100
private fun initializeCustomPreferences() {

0 commit comments

Comments
 (0)