From c45c917567eb8fd341134892a3b15bb965b7fb3b Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Wed, 11 May 2022 22:33:21 +0300 Subject: [PATCH 01/40] [#603] Disable disable Ads slide (#686) --- ios/CCC/UI/Slider/SliderView.swift | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/ios/CCC/UI/Slider/SliderView.swift b/ios/CCC/UI/Slider/SliderView.swift index 33f01bc845..a9f2737150 100644 --- a/ios/CCC/UI/Slider/SliderView.swift +++ b/ios/CCC/UI/Slider/SliderView.swift @@ -25,20 +25,20 @@ struct SliderView: View { buttonAction: { navigationStack.push( - SlideView( - title: MR.strings().slide_bug_report_title.get(), - image: Image(systemName: "ant.fill"), - subTitle1: MR.strings().slide_bug_report_text_1.get(), - subTitle2: MR.strings().slide_bug_report_text_2.get(), - buttonText: MR.strings().next.get(), - buttonAction: { - navigationStack.push( +// SlideView( +// title: MR.strings().slide_disable_ads_title.get(), +// image: Image(systemName: "eye.slash.fill"), +// subTitle1: MR.strings().slide_disable_ads_text_1.get(), +// subTitle2: MR.strings().slide_disable_ads_text_2.get(), +// buttonText: MR.strings().next.get(), +// buttonAction: { +// navigationStack.push( SlideView( - title: MR.strings().slide_disable_ads_title.get(), - image: Image(systemName: "eye.slash.fill"), - subTitle1: MR.strings().slide_disable_ads_text_1.get(), - subTitle2: MR.strings().slide_disable_ads_text_2.get(), + title: MR.strings().slide_bug_report_title.get(), + image: Image(systemName: "ant.fill"), + subTitle1: MR.strings().slide_bug_report_text_1.get(), + subTitle2: MR.strings().slide_bug_report_text_2.get(), buttonText: MR.strings().next.get(), buttonAction: { navigationStack.push( @@ -47,9 +47,9 @@ struct SliderView: View { } ) - ) - } - ) +// ) +// } +// ) ) } From 164cdc3e58f488783ed87eb0b1f8e699be45633a Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Wed, 11 May 2022 23:01:22 +0300 Subject: [PATCH 02/40] [#677] Remove bottom safe areas for Currencies and Settings (#685) --- ios/CCC/UI/Currencies/SelectionView.swift | 1 + ios/CCC/UI/Settings/SettingsView.swift | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ios/CCC/UI/Currencies/SelectionView.swift b/ios/CCC/UI/Currencies/SelectionView.swift index e69d044ca9..f210a693b1 100644 --- a/ios/CCC/UI/Currencies/SelectionView.swift +++ b/ios/CCC/UI/Currencies/SelectionView.swift @@ -31,5 +31,6 @@ struct SelectionView: View { } .padding(EdgeInsets(top: 15, leading: 10, bottom: 15, trailing: 20)) .background(MR.colors().background_weak.get()) + .frame(maxHeight: 50) } } diff --git a/ios/CCC/UI/Settings/SettingsView.swift b/ios/CCC/UI/Settings/SettingsView.swift index cb28ec2512..c12e8f5aaf 100644 --- a/ios/CCC/UI/Settings/SettingsView.swift +++ b/ios/CCC/UI/Settings/SettingsView.swift @@ -83,7 +83,9 @@ struct SettingsView: View { value: "", onClick: observable.event.onOnGitHubClick ) - }.background(MR.colors().background.get()) + } + .background(MR.colors().background.get()) + .edgesIgnoringSafeArea(.bottom) // if observable.viewModel.shouldShowBannerAd() { // BannerAdView( From ab5c7e89dd02b81b724649193ed1e0406146a647 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Wed, 11 May 2022 23:23:44 +0300 Subject: [PATCH 03/40] [#716] Add back Notification Screen (#718) --- .../android/ui/settings/SettingsFragment.kt | 1 + .../ccc/client/di/module/ClientModule.kt | 2 + .../notifications/NotificationsSEED.kt | 41 ++++++ .../notifications/NotificationsViewModel.kt | 71 ++++++++++ .../client/viewmodel/settings/SettingsSEED.kt | 2 + .../viewmodel/settings/SettingsViewModel.kt | 6 + .../client/viewmodel/SettingsViewModelTest.kt | 8 ++ ios/CCC.xcodeproj/project.pbxproj | 24 ++++ ios/CCC/DI/Koin.swift | 8 ++ ios/CCC/UI/Calculator/CalculatorView.swift | 2 +- .../NotificationCurrencyItem.swift | 28 ++++ .../Notifications/NotificationStateView.swift | 37 ++++++ .../NotificationsToolbarView.swift | 27 ++++ .../UI/Notifications/NotificationsView.swift | 124 ++++++++++++++++++ .../SelectCurrency/SelectCurrencyView.swift | 6 +- ios/CCC/UI/Settings/SettingsView.swift | 11 ++ .../commonMain/resources/MR/base/strings.xml | 15 ++- 17 files changed, 407 insertions(+), 6 deletions(-) create mode 100644 client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsSEED.kt create mode 100644 client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsViewModel.kt create mode 100644 ios/CCC/UI/Notifications/NotificationCurrencyItem.swift create mode 100644 ios/CCC/UI/Notifications/NotificationStateView.swift create mode 100644 ios/CCC/UI/Notifications/NotificationsToolbarView.swift create mode 100644 ios/CCC/UI/Notifications/NotificationsView.swift diff --git a/android/src/main/kotlin/com/oztechan/ccc/android/ui/settings/SettingsFragment.kt b/android/src/main/kotlin/com/oztechan/ccc/android/ui/settings/SettingsFragment.kt index 4ebacd9b06..554f6a9f29 100644 --- a/android/src/main/kotlin/com/oztechan/ccc/android/ui/settings/SettingsFragment.kt +++ b/android/src/main/kotlin/com/oztechan/ccc/android/ui/settings/SettingsFragment.kt @@ -184,6 +184,7 @@ class SettingsFragment : BaseVBFragment() { requireView(), R.string.txt_ads_already_disabled ) + SettingsEffect.OpenNotifications -> TODO("No Android implementation yet") } }.launchIn(viewLifecycleOwner.lifecycleScope) diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt index 12fd97a1eb..bcde53945d 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt @@ -7,6 +7,7 @@ import com.oztechan.ccc.client.viewmodel.adremove.AdRemoveViewModel import com.oztechan.ccc.client.viewmodel.calculator.CalculatorViewModel import com.oztechan.ccc.client.viewmodel.currencies.CurrenciesViewModel import com.oztechan.ccc.client.viewmodel.main.MainViewModel +import com.oztechan.ccc.client.viewmodel.notifications.NotificationsViewModel import com.oztechan.ccc.client.viewmodel.selectcurrency.SelectCurrencyViewModel import com.oztechan.ccc.client.viewmodel.settings.SettingsViewModel import com.oztechan.ccc.config.ConfigManager @@ -20,6 +21,7 @@ var clientModule = module { viewModelDefinition { CalculatorViewModel(get(), get(), get(), get(), get()) } viewModelDefinition { SelectCurrencyViewModel(get()) } viewModelDefinition { AdRemoveViewModel(get()) } + viewModelDefinition { NotificationsViewModel(get()) } single { ConfigManagerImpl() } single { SessionManagerImpl(get(), get()) } diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsSEED.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsSEED.kt new file mode 100644 index 0000000000..7d33e1f5c9 --- /dev/null +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsSEED.kt @@ -0,0 +1,41 @@ +package com.oztechan.ccc.client.viewmodel.notifications + + +import com.oztechan.ccc.client.base.BaseEffect +import com.oztechan.ccc.client.base.BaseEvent +import com.oztechan.ccc.client.base.BaseState +import kotlinx.coroutines.flow.MutableStateFlow + +data class NotificationsState( + val isEnabled: Boolean = false, + val base: String = "", + val target: String = "" +) : BaseState() + +sealed class NotificationsEffect : BaseEffect() { + object Back : NotificationsEffect() + object SelectBase : NotificationsEffect() + object SelectTarget : NotificationsEffect() +} + +interface NotificationsEvent : BaseEvent { + fun onBackClick() + fun onBaseChange(base: String) + fun onTargetChange(target: String) + fun onBaseClick() + fun onTargetClick() + fun onStateClick() +} + +// Extension +fun MutableStateFlow.update( + isEnabled: Boolean = value.isEnabled, + base: String = value.base, + target: String = value.target +) { + value = value.copy( + isEnabled = isEnabled, + base = base, + target = target + ) +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsViewModel.kt new file mode 100644 index 0000000000..21992faa07 --- /dev/null +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsViewModel.kt @@ -0,0 +1,71 @@ +package com.oztechan.ccc.client.viewmodel.notifications + +import co.touchlab.kermit.Logger +import com.oztechan.ccc.client.base.BaseData +import com.oztechan.ccc.client.base.BaseSEEDViewModel +import com.oztechan.ccc.client.util.launchIgnored +import com.oztechan.ccc.common.db.currency.CurrencyRepository +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class NotificationsViewModel( + currencyRepository: CurrencyRepository +) : BaseSEEDViewModel(), NotificationsEvent { + // region SEED + private val _state = MutableStateFlow(NotificationsState()) + override val state = _state.asStateFlow() + + override val event = this as NotificationsEvent + + private val _effect = MutableSharedFlow() + override val effect = _effect.asSharedFlow() + + override val data: BaseData? = null + + init { + currencyRepository.collectActiveCurrencies() + .onEach { + Logger.d { "NotificationsViewModel currencyList changed\n${it.joinToString("\n")}" } + _state.update( + base = it.firstOrNull()?.name ?: "", + target = it.lastOrNull()?.name ?: "" + ) + } + .launchIn(clientScope) + } + + override fun onBackClick() = clientScope.launchIgnored { + Logger.d { "NotificationsViewModel onBackClick" } + _effect.emit(NotificationsEffect.Back) + } + + override fun onBaseChange(base: String) { + Logger.d { "NotificationsViewModel onBaseClick $base" } + _state.update(base = base) + } + + override fun onTargetChange(target: String) { + Logger.d { "NotificationsViewModel onTargetChange $target" } + _state.update(target = target) + } + + override fun onBaseClick() = clientScope.launchIgnored { + Logger.d { "NotificationsViewModel onBaseClick" } + _effect.emit(NotificationsEffect.SelectBase) + } + + override fun onTargetClick() = clientScope.launchIgnored { + Logger.d { "NotificationsViewModel onTargetClick" } + _effect.emit(NotificationsEffect.SelectTarget) + } + + override fun onStateClick() { + Logger.d { "NotificationsViewModel onStateClick ${!state.value.isEnabled}" } + _state.update(isEnabled = !state.value.isEnabled) + } + // endregion +} diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt index 2629ce3a26..3df0f58554 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt @@ -19,6 +19,7 @@ data class SettingsState( interface SettingsEvent : BaseEvent { fun onBackClick() fun onCurrenciesClick() + fun onNotificationsClicked() fun onFeedBackClick() fun onShareClick() fun onSupportUsClick() @@ -32,6 +33,7 @@ interface SettingsEvent : BaseEvent { sealed class SettingsEffect : BaseEffect() { object Back : SettingsEffect() object OpenCurrencies : SettingsEffect() + object OpenNotifications : SettingsEffect() object FeedBack : SettingsEffect() object Share : SettingsEffect() object SupportUs : SettingsEffect() diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt index e8cb5a7c84..f21c696462 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt @@ -111,6 +111,12 @@ class SettingsViewModel( _effect.emit(SettingsEffect.OpenCurrencies) } + + override fun onNotificationsClicked() = clientScope.launchIgnored { + Logger.d { "SettingsViewModel onNotificationsClicked" } + _effect.emit(SettingsEffect.OpenNotifications) + } + override fun onFeedBackClick() = clientScope.launchIgnored { Logger.d { "SettingsViewModel onFeedBackClick" } _effect.emit(SettingsEffect.FeedBack) diff --git a/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt b/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt index 8869b00af4..b33220eb73 100644 --- a/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt +++ b/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt @@ -248,6 +248,14 @@ class SettingsViewModelTest { assertTrue { it is SettingsEffect.OpenCurrencies } } + + @Test + fun onNotificationsClicked() = viewModel.effect.before { + viewModel.event.onNotificationsClicked() + }.after { + assertEquals(SettingsEffect.OpenNotifications, it) + } + @Test fun onFeedBackClick() = viewModel.effect.before { viewModel.event.onFeedBackClick() diff --git a/ios/CCC.xcodeproj/project.pbxproj b/ios/CCC.xcodeproj/project.pbxproj index 018399a9df..63bf906848 100644 --- a/ios/CCC.xcodeproj/project.pbxproj +++ b/ios/CCC.xcodeproj/project.pbxproj @@ -24,10 +24,14 @@ 5C31E4362814308B008C42B9 /* SettingsItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C31E4352814308B008C42B9 /* SettingsItemView.swift */; }; 5C31E439281431A3008C42B9 /* SelectCurrencyItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C31E438281431A3008C42B9 /* SelectCurrencyItemView.swift */; }; 5C31E43F28145D32008C42B9 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5C31E43E28145D32008C42B9 /* Launch Screen.storyboard */; }; + 5C4B53692818057F00D10185 /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B53682818057F00D10185 /* NotificationsView.swift */; }; + 5C4B536B2818066000D10185 /* NotificationsToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B536A2818066000D10185 /* NotificationsToolbarView.swift */; }; + 5C4B536D28183B4900D10185 /* NotificationCurrencyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B536C28183B4900D10185 /* NotificationCurrencyItem.swift */; }; 5C5D09332562EB9E00DA9C4A /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5D09322562EB9E00DA9C4A /* Application.swift */; }; 5C5D09362562EBDE00DA9C4A /* Koin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5D09352562EBDE00DA9C4A /* Koin.swift */; }; 5C5D09392562EC0100DA9C4A /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5D09382562EC0100DA9C4A /* Extensions.swift */; }; 5C5D093C2562EC2D00DA9C4A /* CalculatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5D093B2562EC2D00DA9C4A /* CalculatorView.swift */; }; + 5C662D38281D644F00B65C2F /* NotificationStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C662D37281D644F00B65C2F /* NotificationStateView.swift */; }; 5C693EBA25C4AFF800C9373E /* SelectCurrenciesBottomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C693EB925C4AFF800C9373E /* SelectCurrenciesBottomView.swift */; }; 5C6E674025C5A711001CC0D6 /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E673F25C5A711001CC0D6 /* SliderView.swift */; }; 5C6E674D25C602BE001CC0D6 /* SnackBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E674C25C602BE001CC0D6 /* SnackBar.swift */; }; @@ -76,10 +80,14 @@ 5C31E4352814308B008C42B9 /* SettingsItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItemView.swift; sourceTree = ""; }; 5C31E438281431A3008C42B9 /* SelectCurrencyItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCurrencyItemView.swift; sourceTree = ""; }; 5C31E43E28145D32008C42B9 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; + 5C4B53682818057F00D10185 /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; + 5C4B536A2818066000D10185 /* NotificationsToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsToolbarView.swift; sourceTree = ""; }; + 5C4B536C28183B4900D10185 /* NotificationCurrencyItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCurrencyItem.swift; sourceTree = ""; }; 5C5D09322562EB9E00DA9C4A /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 5C5D09352562EBDE00DA9C4A /* Koin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Koin.swift; sourceTree = ""; }; 5C5D09382562EC0100DA9C4A /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 5C5D093B2562EC2D00DA9C4A /* CalculatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorView.swift; sourceTree = ""; }; + 5C662D37281D644F00B65C2F /* NotificationStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStateView.swift; sourceTree = ""; }; 5C693EB925C4AFF800C9373E /* SelectCurrenciesBottomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCurrenciesBottomView.swift; sourceTree = ""; }; 5C6E673F25C5A711001CC0D6 /* SliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderView.swift; sourceTree = ""; }; 5C6E674C25C602BE001CC0D6 /* SnackBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnackBar.swift; sourceTree = ""; }; @@ -179,6 +187,17 @@ path = Settings; sourceTree = ""; }; + 5C4B53652818053E00D10185 /* Notifications */ = { + isa = PBXGroup; + children = ( + 5C4B53682818057F00D10185 /* NotificationsView.swift */, + 5C4B536A2818066000D10185 /* NotificationsToolbarView.swift */, + 5C4B536C28183B4900D10185 /* NotificationCurrencyItem.swift */, + 5C662D37281D644F00B65C2F /* NotificationStateView.swift */, + ); + path = Notifications; + sourceTree = ""; + }; 5C4B536E28184AEA00D10185 /* SelectCurrency */ = { isa = PBXGroup; children = ( @@ -204,6 +223,7 @@ 5C31E41C28141C61008C42B9 /* Calculator */, 5C31E42B28142033008C42B9 /* Currencies */, 5C31E4322814304F008C42B9 /* Settings */, + 5C4B53652818053E00D10185 /* Notifications */, 5C4B536E28184AEA00D10185 /* SelectCurrency */, 5C039FD425C1B6A2008350A3 /* SubView */, ); @@ -448,12 +468,15 @@ buildActionMask = 2147483647; files = ( 5C314CBE25BA0AC0007B22D8 /* CurrenciesView.swift in Sources */, + 5C4B536B2818066000D10185 /* NotificationsToolbarView.swift in Sources */, 5CB954BF26932408007632DC /* BannerAdView.swift in Sources */, 5C31E42428141D1B008C42B9 /* RateStateView.swift in Sources */, + 5C4B536D28183B4900D10185 /* NotificationCurrencyItem.swift in Sources */, 5CF57E3A269588060081E4BB /* RewardedAd.swift in Sources */, 5C9C75C82603A36A00D66FDD /* ToolbarButton.swift in Sources */, 5CF8BE4227DE205B00E441F5 /* MailView.swift in Sources */, 5C31E4362814308B008C42B9 /* SettingsItemView.swift in Sources */, + 5C4B53692818057F00D10185 /* NotificationsView.swift in Sources */, 5C6E674025C5A711001CC0D6 /* SliderView.swift in Sources */, 5C31E42628141D3E008C42B9 /* CalculatorItemView.swift in Sources */, 5C039FD625C1B705008350A3 /* FormProgressView.swift in Sources */, @@ -471,6 +494,7 @@ 5C31E4342814306D008C42B9 /* SettingsToolbarView.swift in Sources */, 5CF57E3C2695A3B20081E4BB /* InterstitialAd.swift in Sources */, 5C5D09392562EC0100DA9C4A /* Extensions.swift in Sources */, + 5C662D38281D644F00B65C2F /* NotificationStateView.swift in Sources */, 5C17581A25BC74BD00D16BD9 /* SettingsView.swift in Sources */, 5C5D09332562EB9E00DA9C4A /* Application.swift in Sources */, 5C693EBA25C4AFF800C9373E /* SelectCurrenciesBottomView.swift in Sources */, diff --git a/ios/CCC/DI/Koin.swift b/ios/CCC/DI/Koin.swift index 9f60fc32a9..d90d935ff9 100644 --- a/ios/CCC/DI/Koin.swift +++ b/ios/CCC/DI/Koin.swift @@ -47,6 +47,10 @@ extension Koin_coreKoin { return koin.getDependency(objCClass: SettingsViewModel.self) as! SettingsViewModel } + func get() -> NotificationsViewModel { + return koin.getDependency(objCClass: NotificationsViewModel.self) as! NotificationsViewModel + } + // Observable func get() -> MainObservable { return MainObservable(viewModel: get()) @@ -64,6 +68,10 @@ extension Koin_coreKoin { return SettingsObservable(viewModel: get()) } + func get() -> NotificationsObservable { + return NotificationsObservable(viewModel: get()) + } + func get() -> CurrenciesObservable { return CurrenciesObservable(viewModel: get()) } diff --git a/ios/CCC/UI/Calculator/CalculatorView.swift b/ios/CCC/UI/Calculator/CalculatorView.swift index 554d4b8b41..85e8d2ce7f 100644 --- a/ios/CCC/UI/Calculator/CalculatorView.swift +++ b/ios/CCC/UI/Calculator/CalculatorView.swift @@ -90,7 +90,7 @@ struct CalculatorView: View { content: { SelectCurrencyView( isBarShown: $isBarShown, - onSelectCurrency: { observable.event.onBaseChange(base: $0)} + onCurrencySelected: { observable.event.onBaseChange(base: $0)} ).environmentObject(navigationStack) } ) diff --git a/ios/CCC/UI/Notifications/NotificationCurrencyItem.swift b/ios/CCC/UI/Notifications/NotificationCurrencyItem.swift new file mode 100644 index 0000000000..b4f8d1ede4 --- /dev/null +++ b/ios/CCC/UI/Notifications/NotificationCurrencyItem.swift @@ -0,0 +1,28 @@ +// +// NotificationCurrencyItem.swift +// CCC +// +// Created by Mustafa Ozhan on 26.04.22. +// Copyright © 2022 orgName. All rights reserved. +// + +import SwiftUI + +struct NotificationCurrencyItem: View { + let currencyName: String + let clickAction: () -> Void + + var body: some View { + HStack { + Image(uiImage: currencyName.getImage()) + .resizable() + .frame(width: 36, height: 36, alignment: .center) + .shadow(radius: 3) + + Text(currencyName) + + }.onTapGesture { + clickAction() + } + } +} diff --git a/ios/CCC/UI/Notifications/NotificationStateView.swift b/ios/CCC/UI/Notifications/NotificationStateView.swift new file mode 100644 index 0000000000..4eb9d25471 --- /dev/null +++ b/ios/CCC/UI/Notifications/NotificationStateView.swift @@ -0,0 +1,37 @@ +// +// NotificationStateView.swift +// CCC +// +// Created by Mustafa Ozhan on 30.04.22. +// Copyright © 2022 orgName. All rights reserved. +// + +import SwiftUI +import Resources + +struct NotificationStateView: View { + var isEnabled: Bool + var onClick: () -> Void + + var body: some View { + HStack { + + Text( + isEnabled + ? MR.strings().txt_notifications_enabled.get() + : MR.strings().txt_notifications_disabled.get() + ) + .font(.body) + .foregroundColor(MR.colors().text.get()) + + Spacer() + Image(systemName: isEnabled ? "checkmark.circle.fill" : "circle") + .foregroundColor(MR.colors().accent.get()) + + } + .padding(25) + .contentShape(Rectangle()) + .onTapGesture { onClick() } + .lineLimit(1) + } +} diff --git a/ios/CCC/UI/Notifications/NotificationsToolbarView.swift b/ios/CCC/UI/Notifications/NotificationsToolbarView.swift new file mode 100644 index 0000000000..5d6d302838 --- /dev/null +++ b/ios/CCC/UI/Notifications/NotificationsToolbarView.swift @@ -0,0 +1,27 @@ +// +// NotificationsToolbarView.swift +// CCC +// +// Created by Mustafa Ozhan on 26.04.22. +// Copyright © 2022 orgName. All rights reserved. +// + +import SwiftUI +import Resources + +struct NotificationsToolbarView: View { + var backEvent: () -> Void + + var body: some View { + HStack { + ToolbarButton(clickEvent: backEvent, imgName: "chevron.left") + + Text(MR.strings().txt_notifications.get()) + .font(.title3) + + Spacer() + } + .frame(width: .infinity, height: 36) + .padding(EdgeInsets(top: 15, leading: 10, bottom: 6, trailing: 20)) + } +} diff --git a/ios/CCC/UI/Notifications/NotificationsView.swift b/ios/CCC/UI/Notifications/NotificationsView.swift new file mode 100644 index 0000000000..6bf3b1b45d --- /dev/null +++ b/ios/CCC/UI/Notifications/NotificationsView.swift @@ -0,0 +1,124 @@ +// +// NotificationsView.swift +// CCC +// +// Created by Mustafa Ozhan on 26.04.22. +// Copyright © 2022 orgName. All rights reserved. +// + +import SwiftUI +import Client +import Resources +import NavigationStack + +typealias NotificationsObservable = ObservableSEED + + +struct NotificationsView: View { + + @State var tootleStatus = false + @State var isBaseBarShown = false + @State var isTargetBarShown = false + @State var amount = "" + @State private var favoriteColor = 0 + + @EnvironmentObject private var navigationStack: NavigationStack + @StateObject var observable: NotificationsObservable = koin.get() + + var body: some View { + ZStack { + MR.colors().background_strong.get().edgesIgnoringSafeArea(.all) + + VStack { + + NotificationsToolbarView(backEvent: observable.event.onBackClick) + .background(MR.colors().background_strong.get()) + + HStack { + Text(MR.strings().one.get()) + .font(.title2) + + NotificationCurrencyItem( + currencyName: observable.state.base, + clickAction: observable.event.onBaseClick + ) + } + + Picker("", selection: $favoriteColor) { + Text(MR.strings().txt_smaller.get()) + .font(.title2) + .tag(0) + Text(MR.strings().txt_grater.get()) + .font(.title2) + .tag(1) + } + .pickerStyle(.segmented) + .frame(maxWidth: 120) + + HStack { + TextField(MR.strings().txt_rate.get(), text: $amount) + .keyboardType(.decimalPad) + .multilineTextAlignment(TextAlignment.center) + .fixedSize() + .lineLimit(1) + + NotificationCurrencyItem( + currencyName: observable.state.target, + clickAction: observable.event.onTargetClick + ) + } + + NotificationStateView( + isEnabled: observable.state.isEnabled, + onClick: observable.event.onStateClick + ) + + Spacer() + + } + .background(MR.colors().background.get()) + .edgesIgnoringSafeArea(.bottom) + } + .sheet( + isPresented: $isBaseBarShown, + content: { + SelectCurrencyView( + isBarShown: $isBaseBarShown, + onCurrencySelected: { observable.event.onBaseChange(base: $0)} + ).environmentObject(navigationStack) + } + ) + .sheet( + isPresented: $isTargetBarShown, + content: { + SelectCurrencyView( + isBarShown: $isTargetBarShown, + onCurrencySelected: { observable.event.onTargetChange(target: $0)} + ).environmentObject(navigationStack) + } + ) + .onAppear { observable.startObserving() } + .onDisappear { observable.stopObserving() } + .onReceive(observable.effect) { onEffect(effect: $0) } + } + + private func onEffect(effect: NotificationsEffect) { + logger.i(message: {"NotificationsView onEffect \(effect.description)"}) + switch effect { + case is NotificationsEffect.Back: + navigationStack.pop() + case is NotificationsEffect.SelectBase: + isBaseBarShown.toggle() + case is NotificationsEffect.SelectTarget: + isTargetBarShown.toggle() + default: + logger.i(message: {"NotificationsView unknown effect"}) + } + } +} + +struct NotificationsView_Previews: PreviewProvider { + static var previews: some View { + NotificationsView() + } +} diff --git a/ios/CCC/UI/SelectCurrency/SelectCurrencyView.swift b/ios/CCC/UI/SelectCurrency/SelectCurrencyView.swift index 114474bb48..001cc0c1a0 100644 --- a/ios/CCC/UI/SelectCurrency/SelectCurrencyView.swift +++ b/ios/CCC/UI/SelectCurrency/SelectCurrencyView.swift @@ -21,7 +21,7 @@ struct SelectCurrencyView: View { @StateObject var observable: SelectCurrencyObservable = koin.get() @Binding var isBarShown: Bool - var onSelectCurrency: (String) -> Void + var onCurrencySelected: (String) -> Void var body: some View { @@ -71,10 +71,10 @@ struct SelectCurrencyView: View { switch effect { // swiftlint:disable force_cast case is SelectCurrencyEffect.CurrencyChange: - onSelectCurrency((effect as! SelectCurrencyEffect.CurrencyChange).newBase) + onCurrencySelected((effect as! SelectCurrencyEffect.CurrencyChange).newBase) isBarShown = false case is SelectCurrencyEffect.OpenCurrencies: - navigationStack.push(CurrenciesView(onBaseChange: onSelectCurrency)) + navigationStack.push(CurrenciesView(onBaseChange: onCurrencySelected)) default: logger.i(message: {"BarView unknown effect"}) } diff --git a/ios/CCC/UI/Settings/SettingsView.swift b/ios/CCC/UI/Settings/SettingsView.swift index c12e8f5aaf..62e31f7ce8 100644 --- a/ios/CCC/UI/Settings/SettingsView.swift +++ b/ios/CCC/UI/Settings/SettingsView.swift @@ -50,6 +50,14 @@ struct SettingsView: View { onClick: observable.event.onCurrenciesClick ) + SettingsItemView( + imgName: "bell", + title: MR.strings().settings_item_notifications_title.get(), + subTitle: MR.strings().settings_item_notifications_sub_title.get(), + value: MR.strings().settings_item_notifications_value_disabled.get(), + onClick: observable.event.onNotificationsClicked + ) + // SettingsItemView( // imgName: "eye.slash.fill", // title: MR.strings().settings_item_remove_ads_title.get(), @@ -133,6 +141,7 @@ struct SettingsView: View { .onReceive(observable.effect) { onEffect(effect: $0) } } + // swiftlint:disable cyclomatic_complexity private func onEffect(effect: SettingsEffect) { logger.i(message: {"SettingsView onEffect \(effect.description)"}) switch effect { @@ -140,6 +149,8 @@ struct SettingsView: View { navigationStack.pop() case is SettingsEffect.OpenCurrencies: navigationStack.push(CurrenciesView(onBaseChange: onBaseChange)) + case is SettingsEffect.OpenNotifications: + navigationStack.push(NotificationsView()) case is SettingsEffect.FeedBack: emailViewVisibility.toggle() case is SettingsEffect.OnGitHub: diff --git a/resources/src/commonMain/resources/MR/base/strings.xml b/resources/src/commonMain/resources/MR/base/strings.xml index fc3eab78ff..3d9966914a 100644 --- a/resources/src/commonMain/resources/MR/base/strings.xml +++ b/resources/src/commonMain/resources/MR/base/strings.xml @@ -91,6 +91,11 @@ Set your currencies %d active + Notifications + Set a rate watcher + Enabled + Disabled + Theme Change app theme Choose app theme @@ -99,8 +104,6 @@ Disable ads Expired\n Will expire\n%s - Active - Not active Ads are already disabled Synchronise @@ -124,6 +127,14 @@ Currencies + + Notifications + Rate + > + < + Notifications enabled + Notifications disabled + Remove Ads Would you like to watch a video to stop ads for 2 days ? From 7e967459934e5602f4495ab27220c145cfb98d51 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Fri, 13 May 2022 12:42:51 +0300 Subject: [PATCH 04/40] [#707] Create Notification Table (#708) --- .../ccc/client/di/module/ClientModule.kt | 2 +- .../notifications/NotificationsSEED.kt | 3 --- .../notifications/NotificationsViewModel.kt | 23 +++++++++-------- .../db/currency/CurrencyRepositoryImpl.kt | 6 ++--- .../com/oztechan/ccc/common/db/migrate/4.sqm | 7 ++++++ .../db/notification/NotificationRepository.kt | 9 +++++++ .../NotificationRepositoryImpl.kt | 25 +++++++++++++++++++ .../ccc/common/db/sql/Notification.sq | 13 ++++++++++ .../ccc/common/di/modules/DatabaseModule.kt | 4 +++ .../com/oztechan/ccc/common/mapper/Boolean.kt | 3 +++ .../com/oztechan/ccc/common/mapper/Long.kt | 7 ++++++ .../ccc/common/mapper/Notification.kt | 22 ++++++++++++++++ .../oztechan/ccc/common/model/Notification.kt | 9 +++++++ .../oztechan/ccc/common/util/DatabaseUtil.kt | 3 --- .../oztechan/ccc/common/mapper/BooleanTest.kt | 12 +++++++++ .../oztechan/ccc/common/mapper/LongTest.kt | 16 ++++++++++++ .../ccc/common/repo/CurrencyRepositoryTest.kt | 6 ++--- .../ccc/common/util/DatabaseUtilTest.kt | 12 --------- .../UI/Notifications/NotificationsView.swift | 2 +- 19 files changed, 147 insertions(+), 37 deletions(-) create mode 100644 common/src/commonMain/kotlin/com/oztechan/ccc/common/db/migrate/4.sqm create mode 100644 common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt create mode 100644 common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt create mode 100644 common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq create mode 100644 common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Boolean.kt create mode 100644 common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Long.kt create mode 100644 common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Notification.kt create mode 100644 common/src/commonMain/kotlin/com/oztechan/ccc/common/model/Notification.kt delete mode 100644 common/src/commonMain/kotlin/com/oztechan/ccc/common/util/DatabaseUtil.kt create mode 100644 common/src/commonTest/kotlin/com/oztechan/ccc/common/mapper/BooleanTest.kt create mode 100644 common/src/commonTest/kotlin/com/oztechan/ccc/common/mapper/LongTest.kt delete mode 100644 common/src/commonTest/kotlin/com/oztechan/ccc/common/util/DatabaseUtilTest.kt diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt index bcde53945d..dfe9a47cc1 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt @@ -21,7 +21,7 @@ var clientModule = module { viewModelDefinition { CalculatorViewModel(get(), get(), get(), get(), get()) } viewModelDefinition { SelectCurrencyViewModel(get()) } viewModelDefinition { AdRemoveViewModel(get()) } - viewModelDefinition { NotificationsViewModel(get()) } + viewModelDefinition { NotificationsViewModel(get(), get()) } single { ConfigManagerImpl() } single { SessionManagerImpl(get(), get()) } diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsSEED.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsSEED.kt index 7d33e1f5c9..2b3e8b2a72 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsSEED.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsSEED.kt @@ -7,7 +7,6 @@ import com.oztechan.ccc.client.base.BaseState import kotlinx.coroutines.flow.MutableStateFlow data class NotificationsState( - val isEnabled: Boolean = false, val base: String = "", val target: String = "" ) : BaseState() @@ -29,12 +28,10 @@ interface NotificationsEvent : BaseEvent { // Extension fun MutableStateFlow.update( - isEnabled: Boolean = value.isEnabled, base: String = value.base, target: String = value.target ) { value = value.copy( - isEnabled = isEnabled, base = base, target = target ) diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsViewModel.kt index 21992faa07..43513b648f 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsViewModel.kt @@ -5,6 +5,7 @@ import com.oztechan.ccc.client.base.BaseData import com.oztechan.ccc.client.base.BaseSEEDViewModel import com.oztechan.ccc.client.util.launchIgnored import com.oztechan.ccc.common.db.currency.CurrencyRepository +import com.oztechan.ccc.common.db.notification.NotificationRepository import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow @@ -13,7 +14,8 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach class NotificationsViewModel( - currencyRepository: CurrencyRepository + private val currencyRepository: CurrencyRepository, + private val notificationRepository: NotificationRepository ) : BaseSEEDViewModel(), NotificationsEvent { // region SEED private val _state = MutableStateFlow(NotificationsState()) @@ -27,15 +29,10 @@ class NotificationsViewModel( override val data: BaseData? = null init { - currencyRepository.collectActiveCurrencies() + notificationRepository.collectNotifications() .onEach { - Logger.d { "NotificationsViewModel currencyList changed\n${it.joinToString("\n")}" } - _state.update( - base = it.firstOrNull()?.name ?: "", - target = it.lastOrNull()?.name ?: "" - ) - } - .launchIn(clientScope) + + }.launchIn(clientScope) } override fun onBackClick() = clientScope.launchIgnored { @@ -64,8 +61,12 @@ class NotificationsViewModel( } override fun onStateClick() { - Logger.d { "NotificationsViewModel onStateClick ${!state.value.isEnabled}" } - _state.update(isEnabled = !state.value.isEnabled) + currencyRepository.getActiveCurrencies().let { + notificationRepository.addNotification( + base = it.firstOrNull()?.name ?: "", + target = it.lastOrNull()?.name ?: "" + ) + } } // endregion } diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/currency/CurrencyRepositoryImpl.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/currency/CurrencyRepositoryImpl.kt index af0d64b3fc..441b81904e 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/currency/CurrencyRepositoryImpl.kt +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/currency/CurrencyRepositoryImpl.kt @@ -3,9 +3,9 @@ package com.oztechan.ccc.common.db.currency import co.touchlab.kermit.Logger import com.oztechan.ccc.common.db.sql.CurrencyQueries import com.oztechan.ccc.common.mapper.mapToModel +import com.oztechan.ccc.common.mapper.toLong import com.oztechan.ccc.common.mapper.toModel import com.oztechan.ccc.common.mapper.toModelList -import com.oztechan.ccc.common.util.toDatabaseBoolean import com.squareup.sqldelight.runtime.coroutines.asFlow import com.squareup.sqldelight.runtime.coroutines.mapToList import kotlinx.coroutines.flow.map @@ -37,11 +37,11 @@ internal class CurrencyRepositoryImpl( .also { Logger.v { "CurrencyRepositoryImpl getActiveCurrencies" } } override fun updateCurrencyStateByName(name: String, isActive: Boolean) = currencyQueries - .updateCurrencyStateByName(isActive.toDatabaseBoolean(), name) + .updateCurrencyStateByName(isActive.toLong(), name) .also { Logger.v { "CurrencyRepositoryImpl updateCurrencyStateByName $name $isActive" } } override fun updateAllCurrencyState(value: Boolean) = currencyQueries - .updateAllCurrencyState(value.toDatabaseBoolean()) + .updateAllCurrencyState(value.toLong()) .also { Logger.v { "CurrencyRepositoryImpl updateAllCurrencyState $value" } } override fun getCurrencyByName(name: String) = currencyQueries diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/migrate/4.sqm b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/migrate/4.sqm new file mode 100644 index 0000000000..2afad8c0df --- /dev/null +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/migrate/4.sqm @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS notification( +id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, +base TEXT NOT NULL, +target TEXT NOT NULL, +isGreater INTEGER NOT NULL DEFAULT 1, +rate REAL NOT NULL DEFAULT 0.0 +); \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt new file mode 100644 index 0000000000..04cc784615 --- /dev/null +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt @@ -0,0 +1,9 @@ +package com.oztechan.ccc.common.db.notification + +import com.oztechan.ccc.common.model.Notification +import kotlinx.coroutines.flow.Flow + +interface NotificationRepository { + fun addNotification(base: String, target: String) + fun collectNotifications(): Flow> +} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt new file mode 100644 index 0000000000..44723ac98c --- /dev/null +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt @@ -0,0 +1,25 @@ +package com.oztechan.ccc.common.db.notification + +import co.touchlab.kermit.Logger +import com.oztechan.ccc.common.db.sql.NotificationQueries +import com.oztechan.ccc.common.mapper.mapToModel +import com.squareup.sqldelight.runtime.coroutines.asFlow +import com.squareup.sqldelight.runtime.coroutines.mapToList + +class NotificationRepositoryImpl( + private val notificationQueries: NotificationQueries +) : NotificationRepository { + + override fun addNotification( + base: String, + target: String + ) = notificationQueries.addNotification(base, target) + .also { Logger.v { "NotificationRepositoryImpl addNotification $base $target" } } + + override fun collectNotifications() = notificationQueries + .getNotifications() + .asFlow() + .mapToList() + .mapToModel() + .also { Logger.v { "NotificationRepositoryImpl collectNotifications" } } +} diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq new file mode 100644 index 0000000000..90392507a2 --- /dev/null +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS notification( +id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, +base TEXT NOT NULL, +target TEXT NOT NULL, +isGreater INTEGER NOT NULL DEFAULT 1, +rate REAL NOT NULL DEFAULT 1.0 +); + +addNotification: +INSERT OR REPLACE INTO notification(base, target) VALUES (?,?); + +getNotifications: +SELECT * FROM notification; \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/di/modules/DatabaseModule.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/di/modules/DatabaseModule.kt index d6ebbfca16..4ebd7412c6 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/di/modules/DatabaseModule.kt +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/di/modules/DatabaseModule.kt @@ -2,6 +2,8 @@ package com.oztechan.ccc.common.di.modules import com.oztechan.ccc.common.db.currency.CurrencyRepository import com.oztechan.ccc.common.db.currency.CurrencyRepositoryImpl +import com.oztechan.ccc.common.db.notification.NotificationRepository +import com.oztechan.ccc.common.db.notification.NotificationRepositoryImpl import com.oztechan.ccc.common.db.offlinerates.OfflineRatesRepository import com.oztechan.ccc.common.db.offlinerates.OfflineRatesRepositoryImpl import com.oztechan.ccc.common.db.sql.CurrencyConverterCalculatorDatabase @@ -13,9 +15,11 @@ private const val DATABASE_NAME = "application_database.sqlite" fun getDatabaseModule() = module { single { get().currencyQueries } single { get().offlineRatesQueries } + single { get().notificationQueries } single { CurrencyRepositoryImpl(get()) } single { OfflineRatesRepositoryImpl(get()) } + single { NotificationRepositoryImpl(get()) } single { provideDatabase(DATABASE_NAME) } } diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Boolean.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Boolean.kt new file mode 100644 index 0000000000..ca771fa4c7 --- /dev/null +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Boolean.kt @@ -0,0 +1,3 @@ +package com.oztechan.ccc.common.mapper + +fun Boolean.toLong() = if (this) 1L else 0L diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Long.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Long.kt new file mode 100644 index 0000000000..d7ca828c0b --- /dev/null +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Long.kt @@ -0,0 +1,7 @@ +package com.oztechan.ccc.common.mapper + +fun Long.toBoolean() = when (this) { + 1L -> true + 0L -> false + else -> throw IllegalStateException("Value can not be boolean") +} diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Notification.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Notification.kt new file mode 100644 index 0000000000..f683627f93 --- /dev/null +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Notification.kt @@ -0,0 +1,22 @@ +package com.oztechan.ccc.common.mapper + +import com.oztechan.ccc.common.model.Notification +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import com.oztechan.ccc.common.db.sql.Notification as NotificationEntity + +fun NotificationEntity.toModel() = Notification( + id = id, + base = base, + target = target, + isGreater = isGreater.toBoolean(), + rate = rate, +) + +internal fun List.toModelList(): List { + return map { it.toModel() } +} + +internal fun Flow>.mapToModel(): Flow> { + return this.map { it.toModelList() } +} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/model/Notification.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/model/Notification.kt new file mode 100644 index 0000000000..58a5ed7870 --- /dev/null +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/model/Notification.kt @@ -0,0 +1,9 @@ +package com.oztechan.ccc.common.model + +data class Notification( + val id: Long, + val base: String, + val target: String, + val isGreater: Boolean, + val rate: Double, +) \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/util/DatabaseUtil.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/util/DatabaseUtil.kt deleted file mode 100644 index 082758ad12..0000000000 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/util/DatabaseUtil.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.oztechan.ccc.common.util - -fun Boolean.toDatabaseBoolean() = if (this) 1L else 0L diff --git a/common/src/commonTest/kotlin/com/oztechan/ccc/common/mapper/BooleanTest.kt b/common/src/commonTest/kotlin/com/oztechan/ccc/common/mapper/BooleanTest.kt new file mode 100644 index 0000000000..a98eec7f2e --- /dev/null +++ b/common/src/commonTest/kotlin/com/oztechan/ccc/common/mapper/BooleanTest.kt @@ -0,0 +1,12 @@ +package com.oztechan.ccc.common.mapper + +import kotlin.test.Test +import kotlin.test.assertEquals + +class BooleanTest { + @Test + fun toLong() { + assertEquals(0L, false.toLong()) + assertEquals(1L, true.toLong()) + } +} diff --git a/common/src/commonTest/kotlin/com/oztechan/ccc/common/mapper/LongTest.kt b/common/src/commonTest/kotlin/com/oztechan/ccc/common/mapper/LongTest.kt new file mode 100644 index 0000000000..a05c5cf9b2 --- /dev/null +++ b/common/src/commonTest/kotlin/com/oztechan/ccc/common/mapper/LongTest.kt @@ -0,0 +1,16 @@ +package com.oztechan.ccc.common.mapper + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class LongTest { + @Test + fun toBoolean() { + assertEquals(true, 1L.toBoolean()) + assertEquals(false, 0L.toBoolean()) + assertFailsWith(IllegalStateException::class) { + 2L.toBoolean() + } + } +} diff --git a/common/src/commonTest/kotlin/com/oztechan/ccc/common/repo/CurrencyRepositoryTest.kt b/common/src/commonTest/kotlin/com/oztechan/ccc/common/repo/CurrencyRepositoryTest.kt index 36fa1fa7bc..6bbefcc385 100644 --- a/common/src/commonTest/kotlin/com/oztechan/ccc/common/repo/CurrencyRepositoryTest.kt +++ b/common/src/commonTest/kotlin/com/oztechan/ccc/common/repo/CurrencyRepositoryTest.kt @@ -4,7 +4,7 @@ import com.github.submob.logmob.initLogger import com.oztechan.ccc.common.db.currency.CurrencyRepository import com.oztechan.ccc.common.db.currency.CurrencyRepositoryImpl import com.oztechan.ccc.common.db.sql.CurrencyQueries -import com.oztechan.ccc.common.util.toDatabaseBoolean +import com.oztechan.ccc.common.mapper.toLong import io.mockative.Mock import io.mockative.classOf import io.mockative.mock @@ -35,7 +35,7 @@ class CurrencyRepositoryTest { repository.updateCurrencyStateByName(mockName, mockState) verify(currencyQueries) - .invocation { updateCurrencyStateByName(mockState.toDatabaseBoolean(), mockName) } + .invocation { updateCurrencyStateByName(mockState.toLong(), mockName) } .wasInvoked() } @@ -46,7 +46,7 @@ class CurrencyRepositoryTest { repository.updateAllCurrencyState(mockState) verify(currencyQueries) - .invocation { updateAllCurrencyState(mockState.toDatabaseBoolean()) } + .invocation { updateAllCurrencyState(mockState.toLong()) } .wasInvoked() } } diff --git a/common/src/commonTest/kotlin/com/oztechan/ccc/common/util/DatabaseUtilTest.kt b/common/src/commonTest/kotlin/com/oztechan/ccc/common/util/DatabaseUtilTest.kt deleted file mode 100644 index 8a1d443616..0000000000 --- a/common/src/commonTest/kotlin/com/oztechan/ccc/common/util/DatabaseUtilTest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.oztechan.ccc.common.util - -import kotlin.test.Test -import kotlin.test.assertEquals - -class DatabaseUtilTest { - @Test - fun toDatabaseBoolean() { - assertEquals(0L, false.toDatabaseBoolean()) - assertEquals(1L, true.toDatabaseBoolean()) - } -} diff --git a/ios/CCC/UI/Notifications/NotificationsView.swift b/ios/CCC/UI/Notifications/NotificationsView.swift index 6bf3b1b45d..11a8b41470 100644 --- a/ios/CCC/UI/Notifications/NotificationsView.swift +++ b/ios/CCC/UI/Notifications/NotificationsView.swift @@ -69,7 +69,7 @@ struct NotificationsView: View { } NotificationStateView( - isEnabled: observable.state.isEnabled, + isEnabled: false, onClick: observable.event.onStateClick ) From a80ee652ccb0e27ea57ccbe4acf915f07e3f240c Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sat, 14 May 2022 12:34:50 +0300 Subject: [PATCH 05/40] [#709] Setup NotificationList (#710) --- .../ccc/client/di/module/ClientModule.kt | 4 +- .../ccc/client/mapper/Notification.kt | 16 +++ .../oztechan/ccc/client/model/Notification.kt | 9 ++ .../notification/NotificationSEED.kt | 39 ++++++ .../notification/NotificationViewModel.kt | 84 ++++++++++++ .../notifications/NotificationsSEED.kt | 38 ------ .../notifications/NotificationsViewModel.kt | 72 ---------- .../db/notification/NotificationRepository.kt | 3 + .../NotificationRepositoryImpl.kt | 12 ++ .../ccc/common/db/sql/Notification.sq | 9 ++ ios/CCC.xcodeproj/project.pbxproj | 20 ++- ios/CCC/DI/Koin.swift | 8 +- .../NotificationCurrencyItem.swift | 28 ---- .../UI/Notifications/NotificationItem.swift | 72 ++++++++++ .../Notifications/NotificationStateView.swift | 37 ------ .../UI/Notifications/NotificationView.swift | 118 +++++++++++++++++ .../UI/Notifications/NotificationsView.swift | 124 ------------------ ios/CCC/UI/Settings/SettingsView.swift | 4 +- .../commonMain/resources/MR/base/strings.xml | 7 +- 19 files changed, 380 insertions(+), 324 deletions(-) create mode 100644 client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt create mode 100644 client/src/commonMain/kotlin/com/oztechan/ccc/client/model/Notification.kt create mode 100644 client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt create mode 100644 client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt delete mode 100644 client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsSEED.kt delete mode 100644 client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsViewModel.kt delete mode 100644 ios/CCC/UI/Notifications/NotificationCurrencyItem.swift create mode 100644 ios/CCC/UI/Notifications/NotificationItem.swift delete mode 100644 ios/CCC/UI/Notifications/NotificationStateView.swift create mode 100644 ios/CCC/UI/Notifications/NotificationView.swift delete mode 100644 ios/CCC/UI/Notifications/NotificationsView.swift diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt index dfe9a47cc1..b2db1b503c 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt @@ -7,7 +7,7 @@ import com.oztechan.ccc.client.viewmodel.adremove.AdRemoveViewModel import com.oztechan.ccc.client.viewmodel.calculator.CalculatorViewModel import com.oztechan.ccc.client.viewmodel.currencies.CurrenciesViewModel import com.oztechan.ccc.client.viewmodel.main.MainViewModel -import com.oztechan.ccc.client.viewmodel.notifications.NotificationsViewModel +import com.oztechan.ccc.client.viewmodel.notification.NotificationViewModel import com.oztechan.ccc.client.viewmodel.selectcurrency.SelectCurrencyViewModel import com.oztechan.ccc.client.viewmodel.settings.SettingsViewModel import com.oztechan.ccc.config.ConfigManager @@ -21,7 +21,7 @@ var clientModule = module { viewModelDefinition { CalculatorViewModel(get(), get(), get(), get(), get()) } viewModelDefinition { SelectCurrencyViewModel(get()) } viewModelDefinition { AdRemoveViewModel(get()) } - viewModelDefinition { NotificationsViewModel(get(), get()) } + viewModelDefinition { NotificationViewModel(get(), get()) } single { ConfigManagerImpl() } single { SessionManagerImpl(get(), get()) } diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt new file mode 100644 index 0000000000..d0d5090a8e --- /dev/null +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt @@ -0,0 +1,16 @@ +package com.oztechan.ccc.client.mapper + +import com.oztechan.ccc.common.model.Notification +import com.oztechan.ccc.client.model.Notification as NotificationUIModel + +fun Notification.toUIModel() = NotificationUIModel( + id = id, + base = base, + target = target, + isGreater = isGreater, + rate = rate +) + +fun List.toUIModelList() = map { + it.toUIModel() +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/model/Notification.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/model/Notification.kt new file mode 100644 index 0000000000..5c77379bc0 --- /dev/null +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/model/Notification.kt @@ -0,0 +1,9 @@ +package com.oztechan.ccc.client.model + +data class Notification( + val id: Long, + val base: String, + val target: String, + val isGreater: Boolean, + val rate: Double +) diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt new file mode 100644 index 0000000000..145e1c1971 --- /dev/null +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt @@ -0,0 +1,39 @@ +package com.oztechan.ccc.client.viewmodel.notification + + +import com.oztechan.ccc.client.base.BaseEffect +import com.oztechan.ccc.client.base.BaseEvent +import com.oztechan.ccc.client.base.BaseState +import com.oztechan.ccc.client.model.Notification +import kotlinx.coroutines.flow.MutableStateFlow + +data class NotificationState( + val notificationList: List = emptyList(), + val base: String = "", + val target: String = "" +) : BaseState() + +sealed class NotificationEffect : BaseEffect() { + object Back : NotificationEffect() + data class SelectBase(val notification: Notification) : NotificationEffect() + data class SelectTarget(val notification: Notification) : NotificationEffect() +} + +interface NotificationEvent : BaseEvent { + fun onBackClick() + fun onBaseClick(notification: Notification) + fun onTargetClick(notification: Notification) + fun onBaseChanged(notification: Notification?, newBase: String) + fun onTargetChanged(notification: Notification?, newTarget: String) + fun onAddClick() + fun onDeleteClick(notification: Notification) +} + +// Extension +fun MutableStateFlow.update( + notificationList: List = value.notificationList, +) { + value = value.copy( + notificationList = notificationList + ) +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt new file mode 100644 index 0000000000..1c2c203154 --- /dev/null +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt @@ -0,0 +1,84 @@ +package com.oztechan.ccc.client.viewmodel.notification + +import co.touchlab.kermit.Logger +import com.oztechan.ccc.client.base.BaseData +import com.oztechan.ccc.client.base.BaseSEEDViewModel +import com.oztechan.ccc.client.mapper.toUIModelList +import com.oztechan.ccc.client.model.Notification +import com.oztechan.ccc.client.util.launchIgnored +import com.oztechan.ccc.common.db.currency.CurrencyRepository +import com.oztechan.ccc.common.db.notification.NotificationRepository +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class NotificationViewModel( + private val currencyRepository: CurrencyRepository, + private val notificationRepository: NotificationRepository +) : BaseSEEDViewModel(), NotificationEvent { + // region SEED + private val _state = MutableStateFlow(NotificationState()) + override val state = _state.asStateFlow() + + override val event = this as NotificationEvent + + private val _effect = MutableSharedFlow() + override val effect = _effect.asSharedFlow() + + override val data: BaseData? = null + + init { + notificationRepository.collectNotifications() + .onEach { + _state.update(notificationList = it.toUIModelList()) + }.launchIn(clientScope) + } + + override fun onBackClick() = clientScope.launchIgnored { + Logger.d { "NotificationViewModel onBackClick" } + _effect.emit(NotificationEffect.Back) + } + + override fun onBaseClick(notification: Notification) = clientScope.launchIgnored { + Logger.d { "NotificationViewModel onBaseClick $notification" } + _effect.emit(NotificationEffect.SelectBase(notification)) + } + + override fun onTargetClick(notification: Notification) = clientScope.launchIgnored { + Logger.d { "NotificationViewModel onTargetClick $notification" } + _effect.emit(NotificationEffect.SelectTarget(notification)) + } + + override fun onBaseChanged(notification: Notification?, newBase: String) { + Logger.d { "NotificationViewModel onBaseChanged $notification $newBase" } + notification?.id?.let { + notificationRepository.updateBaseById(newBase, it) + } + } + + override fun onTargetChanged(notification: Notification?, newTarget: String) { + Logger.d { "NotificationViewModel onTargetChanged $notification $newTarget" } + notification?.id?.let { + notificationRepository.updateTargetById(newTarget, it) + } + } + + override fun onAddClick() { + Logger.d { "NotificationViewModel onAddClick" } + currencyRepository.getActiveCurrencies().let { list -> + notificationRepository.addNotification( + base = list.firstOrNull()?.name ?: "", + target = list.lastOrNull()?.name ?: "" + ) + } + } + + override fun onDeleteClick(notification: Notification) { + Logger.d { "NotificationViewModel onDeleteClick $notification" } + notificationRepository.deleteNotification(notification.id) + } + // endregion +} diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsSEED.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsSEED.kt deleted file mode 100644 index 2b3e8b2a72..0000000000 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsSEED.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.oztechan.ccc.client.viewmodel.notifications - - -import com.oztechan.ccc.client.base.BaseEffect -import com.oztechan.ccc.client.base.BaseEvent -import com.oztechan.ccc.client.base.BaseState -import kotlinx.coroutines.flow.MutableStateFlow - -data class NotificationsState( - val base: String = "", - val target: String = "" -) : BaseState() - -sealed class NotificationsEffect : BaseEffect() { - object Back : NotificationsEffect() - object SelectBase : NotificationsEffect() - object SelectTarget : NotificationsEffect() -} - -interface NotificationsEvent : BaseEvent { - fun onBackClick() - fun onBaseChange(base: String) - fun onTargetChange(target: String) - fun onBaseClick() - fun onTargetClick() - fun onStateClick() -} - -// Extension -fun MutableStateFlow.update( - base: String = value.base, - target: String = value.target -) { - value = value.copy( - base = base, - target = target - ) -} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsViewModel.kt deleted file mode 100644 index 43513b648f..0000000000 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notifications/NotificationsViewModel.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.oztechan.ccc.client.viewmodel.notifications - -import co.touchlab.kermit.Logger -import com.oztechan.ccc.client.base.BaseData -import com.oztechan.ccc.client.base.BaseSEEDViewModel -import com.oztechan.ccc.client.util.launchIgnored -import com.oztechan.ccc.common.db.currency.CurrencyRepository -import com.oztechan.ccc.common.db.notification.NotificationRepository -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach - -class NotificationsViewModel( - private val currencyRepository: CurrencyRepository, - private val notificationRepository: NotificationRepository -) : BaseSEEDViewModel(), NotificationsEvent { - // region SEED - private val _state = MutableStateFlow(NotificationsState()) - override val state = _state.asStateFlow() - - override val event = this as NotificationsEvent - - private val _effect = MutableSharedFlow() - override val effect = _effect.asSharedFlow() - - override val data: BaseData? = null - - init { - notificationRepository.collectNotifications() - .onEach { - - }.launchIn(clientScope) - } - - override fun onBackClick() = clientScope.launchIgnored { - Logger.d { "NotificationsViewModel onBackClick" } - _effect.emit(NotificationsEffect.Back) - } - - override fun onBaseChange(base: String) { - Logger.d { "NotificationsViewModel onBaseClick $base" } - _state.update(base = base) - } - - override fun onTargetChange(target: String) { - Logger.d { "NotificationsViewModel onTargetChange $target" } - _state.update(target = target) - } - - override fun onBaseClick() = clientScope.launchIgnored { - Logger.d { "NotificationsViewModel onBaseClick" } - _effect.emit(NotificationsEffect.SelectBase) - } - - override fun onTargetClick() = clientScope.launchIgnored { - Logger.d { "NotificationsViewModel onTargetClick" } - _effect.emit(NotificationsEffect.SelectTarget) - } - - override fun onStateClick() { - currencyRepository.getActiveCurrencies().let { - notificationRepository.addNotification( - base = it.firstOrNull()?.name ?: "", - target = it.lastOrNull()?.name ?: "" - ) - } - } - // endregion -} diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt index 04cc784615..2829118c71 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt @@ -6,4 +6,7 @@ import kotlinx.coroutines.flow.Flow interface NotificationRepository { fun addNotification(base: String, target: String) fun collectNotifications(): Flow> + fun deleteNotification(id: Long) + fun updateBaseById(base: String, id: Long) + fun updateTargetById(target: String, id: Long) } \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt index 44723ac98c..22246c1eef 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt @@ -22,4 +22,16 @@ class NotificationRepositoryImpl( .mapToList() .mapToModel() .also { Logger.v { "NotificationRepositoryImpl collectNotifications" } } + + override fun deleteNotification(id: Long) = notificationQueries + .deleteNotification(id) + .also { Logger.v { "NotificationRepositoryImpl addNotification $id" } } + + override fun updateBaseById(base: String, id: Long) = notificationQueries + .updateBaseById(base, id) + .also { Logger.v { "NotificationRepositoryImpl updateBaseById $base $id" } } + + override fun updateTargetById(target: String, id: Long) = notificationQueries + .updateTargetById(target, id) + .also { Logger.v { "NotificationRepositoryImpl updateTargetById $target $id" } } } diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq index 90392507a2..941a1dccb8 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq @@ -9,5 +9,14 @@ rate REAL NOT NULL DEFAULT 1.0 addNotification: INSERT OR REPLACE INTO notification(base, target) VALUES (?,?); +deleteNotification: +DELETE FROM notification WHERE id = ?; + +updateBaseById: +UPDATE notification SET base=? WHERE id=?; + +updateTargetById: +UPDATE notification SET target=? WHERE id=?; + getNotifications: SELECT * FROM notification; \ No newline at end of file diff --git a/ios/CCC.xcodeproj/project.pbxproj b/ios/CCC.xcodeproj/project.pbxproj index 63bf906848..378252b85c 100644 --- a/ios/CCC.xcodeproj/project.pbxproj +++ b/ios/CCC.xcodeproj/project.pbxproj @@ -24,14 +24,12 @@ 5C31E4362814308B008C42B9 /* SettingsItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C31E4352814308B008C42B9 /* SettingsItemView.swift */; }; 5C31E439281431A3008C42B9 /* SelectCurrencyItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C31E438281431A3008C42B9 /* SelectCurrencyItemView.swift */; }; 5C31E43F28145D32008C42B9 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5C31E43E28145D32008C42B9 /* Launch Screen.storyboard */; }; - 5C4B53692818057F00D10185 /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B53682818057F00D10185 /* NotificationsView.swift */; }; + 5C4B53692818057F00D10185 /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B53682818057F00D10185 /* NotificationView.swift */; }; 5C4B536B2818066000D10185 /* NotificationsToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B536A2818066000D10185 /* NotificationsToolbarView.swift */; }; - 5C4B536D28183B4900D10185 /* NotificationCurrencyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B536C28183B4900D10185 /* NotificationCurrencyItem.swift */; }; 5C5D09332562EB9E00DA9C4A /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5D09322562EB9E00DA9C4A /* Application.swift */; }; 5C5D09362562EBDE00DA9C4A /* Koin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5D09352562EBDE00DA9C4A /* Koin.swift */; }; 5C5D09392562EC0100DA9C4A /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5D09382562EC0100DA9C4A /* Extensions.swift */; }; 5C5D093C2562EC2D00DA9C4A /* CalculatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5D093B2562EC2D00DA9C4A /* CalculatorView.swift */; }; - 5C662D38281D644F00B65C2F /* NotificationStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C662D37281D644F00B65C2F /* NotificationStateView.swift */; }; 5C693EBA25C4AFF800C9373E /* SelectCurrenciesBottomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C693EB925C4AFF800C9373E /* SelectCurrenciesBottomView.swift */; }; 5C6E674025C5A711001CC0D6 /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E673F25C5A711001CC0D6 /* SliderView.swift */; }; 5C6E674D25C602BE001CC0D6 /* SnackBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E674C25C602BE001CC0D6 /* SnackBar.swift */; }; @@ -43,6 +41,7 @@ 5CDE468425BC3B2000CA0FB1 /* SelectCurrencyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDE468325BC3B2000CA0FB1 /* SelectCurrencyView.swift */; }; 5CF57E3A269588060081E4BB /* RewardedAd.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF57E39269588060081E4BB /* RewardedAd.swift */; }; 5CF57E3C2695A3B20081E4BB /* InterstitialAd.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF57E3B2695A3B20081E4BB /* InterstitialAd.swift */; }; + 5CF898D42823C1F900712580 /* NotificationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF898D32823C1F900712580 /* NotificationItem.swift */; }; 5CF8BE4227DE205B00E441F5 /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF8BE4127DE205B00E441F5 /* MailView.swift */; }; 5CF8BE4627DE334100E441F5 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF8BE4527DE334100E441F5 /* WebView.swift */; }; 7555FF85242A565B00829871 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7555FF84242A565B00829871 /* Assets.xcassets */; }; @@ -80,14 +79,12 @@ 5C31E4352814308B008C42B9 /* SettingsItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItemView.swift; sourceTree = ""; }; 5C31E438281431A3008C42B9 /* SelectCurrencyItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCurrencyItemView.swift; sourceTree = ""; }; 5C31E43E28145D32008C42B9 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; - 5C4B53682818057F00D10185 /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; + 5C4B53682818057F00D10185 /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = ""; }; 5C4B536A2818066000D10185 /* NotificationsToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsToolbarView.swift; sourceTree = ""; }; - 5C4B536C28183B4900D10185 /* NotificationCurrencyItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCurrencyItem.swift; sourceTree = ""; }; 5C5D09322562EB9E00DA9C4A /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 5C5D09352562EBDE00DA9C4A /* Koin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Koin.swift; sourceTree = ""; }; 5C5D09382562EC0100DA9C4A /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 5C5D093B2562EC2D00DA9C4A /* CalculatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorView.swift; sourceTree = ""; }; - 5C662D37281D644F00B65C2F /* NotificationStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStateView.swift; sourceTree = ""; }; 5C693EB925C4AFF800C9373E /* SelectCurrenciesBottomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCurrenciesBottomView.swift; sourceTree = ""; }; 5C6E673F25C5A711001CC0D6 /* SliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderView.swift; sourceTree = ""; }; 5C6E674C25C602BE001CC0D6 /* SnackBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnackBar.swift; sourceTree = ""; }; @@ -101,6 +98,7 @@ 5CDE468325BC3B2000CA0FB1 /* SelectCurrencyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCurrencyView.swift; sourceTree = ""; }; 5CF57E39269588060081E4BB /* RewardedAd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RewardedAd.swift; sourceTree = ""; }; 5CF57E3B2695A3B20081E4BB /* InterstitialAd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterstitialAd.swift; sourceTree = ""; }; + 5CF898D32823C1F900712580 /* NotificationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItem.swift; sourceTree = ""; }; 5CF8BE4127DE205B00E441F5 /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = ""; }; 5CF8BE4527DE334100E441F5 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; 7555FF7B242A565900829871 /* CCC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CCC.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -190,10 +188,9 @@ 5C4B53652818053E00D10185 /* Notifications */ = { isa = PBXGroup; children = ( - 5C4B53682818057F00D10185 /* NotificationsView.swift */, + 5C4B53682818057F00D10185 /* NotificationView.swift */, 5C4B536A2818066000D10185 /* NotificationsToolbarView.swift */, - 5C4B536C28183B4900D10185 /* NotificationCurrencyItem.swift */, - 5C662D37281D644F00B65C2F /* NotificationStateView.swift */, + 5CF898D32823C1F900712580 /* NotificationItem.swift */, ); path = Notifications; sourceTree = ""; @@ -469,14 +466,14 @@ files = ( 5C314CBE25BA0AC0007B22D8 /* CurrenciesView.swift in Sources */, 5C4B536B2818066000D10185 /* NotificationsToolbarView.swift in Sources */, + 5CF898D42823C1F900712580 /* NotificationItem.swift in Sources */, 5CB954BF26932408007632DC /* BannerAdView.swift in Sources */, 5C31E42428141D1B008C42B9 /* RateStateView.swift in Sources */, - 5C4B536D28183B4900D10185 /* NotificationCurrencyItem.swift in Sources */, 5CF57E3A269588060081E4BB /* RewardedAd.swift in Sources */, 5C9C75C82603A36A00D66FDD /* ToolbarButton.swift in Sources */, 5CF8BE4227DE205B00E441F5 /* MailView.swift in Sources */, 5C31E4362814308B008C42B9 /* SettingsItemView.swift in Sources */, - 5C4B53692818057F00D10185 /* NotificationsView.swift in Sources */, + 5C4B53692818057F00D10185 /* NotificationView.swift in Sources */, 5C6E674025C5A711001CC0D6 /* SliderView.swift in Sources */, 5C31E42628141D3E008C42B9 /* CalculatorItemView.swift in Sources */, 5C039FD625C1B705008350A3 /* FormProgressView.swift in Sources */, @@ -494,7 +491,6 @@ 5C31E4342814306D008C42B9 /* SettingsToolbarView.swift in Sources */, 5CF57E3C2695A3B20081E4BB /* InterstitialAd.swift in Sources */, 5C5D09392562EC0100DA9C4A /* Extensions.swift in Sources */, - 5C662D38281D644F00B65C2F /* NotificationStateView.swift in Sources */, 5C17581A25BC74BD00D16BD9 /* SettingsView.swift in Sources */, 5C5D09332562EB9E00DA9C4A /* Application.swift in Sources */, 5C693EBA25C4AFF800C9373E /* SelectCurrenciesBottomView.swift in Sources */, diff --git a/ios/CCC/DI/Koin.swift b/ios/CCC/DI/Koin.swift index d90d935ff9..12acb60cfe 100644 --- a/ios/CCC/DI/Koin.swift +++ b/ios/CCC/DI/Koin.swift @@ -47,8 +47,8 @@ extension Koin_coreKoin { return koin.getDependency(objCClass: SettingsViewModel.self) as! SettingsViewModel } - func get() -> NotificationsViewModel { - return koin.getDependency(objCClass: NotificationsViewModel.self) as! NotificationsViewModel + func get() -> NotificationViewModel { + return koin.getDependency(objCClass: NotificationViewModel.self) as! NotificationViewModel } // Observable @@ -68,8 +68,8 @@ extension Koin_coreKoin { return SettingsObservable(viewModel: get()) } - func get() -> NotificationsObservable { - return NotificationsObservable(viewModel: get()) + func get() -> NotificationObservable { + return NotificationObservable(viewModel: get()) } func get() -> CurrenciesObservable { diff --git a/ios/CCC/UI/Notifications/NotificationCurrencyItem.swift b/ios/CCC/UI/Notifications/NotificationCurrencyItem.swift deleted file mode 100644 index b4f8d1ede4..0000000000 --- a/ios/CCC/UI/Notifications/NotificationCurrencyItem.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// NotificationCurrencyItem.swift -// CCC -// -// Created by Mustafa Ozhan on 26.04.22. -// Copyright © 2022 orgName. All rights reserved. -// - -import SwiftUI - -struct NotificationCurrencyItem: View { - let currencyName: String - let clickAction: () -> Void - - var body: some View { - HStack { - Image(uiImage: currencyName.getImage()) - .resizable() - .frame(width: 36, height: 36, alignment: .center) - .shadow(radius: 3) - - Text(currencyName) - - }.onTapGesture { - clickAction() - } - } -} diff --git a/ios/CCC/UI/Notifications/NotificationItem.swift b/ios/CCC/UI/Notifications/NotificationItem.swift new file mode 100644 index 0000000000..cb77b1f145 --- /dev/null +++ b/ios/CCC/UI/Notifications/NotificationItem.swift @@ -0,0 +1,72 @@ +// +// NotificationItem.swift +// CCC +// +// Created by Mustafa Ozhan on 05.05.22. +// Copyright © 2022 orgName. All rights reserved. +// + +import SwiftUI +import Client +import Resources +import NavigationStack +import Combine + +struct NotificationItem: View { + @Binding var isBaseBarShown: Bool + @Binding var isTargetBarShown: Bool + + let event: NotificationEvent + let notification: Client.Notification + + @State private var relationSelection = 0 + @State var amount = "" + + var body: some View { + HStack { + Text(MR.strings().one.get()) + .font(.body) + + Image(uiImage: notification.base.getImage()) + .resizable() + .frame(width: 36, height: 36, alignment: .center) + .shadow(radius: 3) + .onTapGesture { event.onBaseClick(notification: notification) } + + Picker("", selection: $relationSelection) { + Text(MR.strings().txt_smaller.get()) + .font(.title) + .tag(0) + Text(MR.strings().txt_grater.get()) + .font(.title) + .tag(1) + } + .pickerStyle(.segmented) + .frame(maxWidth: 80) + + TextField(MR.strings().txt_rate.get(), text: $amount) + .keyboardType(.decimalPad) + .font(.body) + .multilineTextAlignment(TextAlignment.center) + .fixedSize() + .lineLimit(1) + + Image(uiImage: notification.target.getImage()) + .resizable() + .frame(width: 36, height: 36, alignment: .center) + .shadow(radius: 3) + .onTapGesture { event.onTargetClick(notification: notification) } + + Spacer() + + Button { + event.onDeleteClick(notification: notification) + } label: { + Label("", systemImage: "trash") + }.foregroundColor(MR.colors().text.get()) + + }.onAppear { + amount = "\(notification.rate)" + } + } +} diff --git a/ios/CCC/UI/Notifications/NotificationStateView.swift b/ios/CCC/UI/Notifications/NotificationStateView.swift deleted file mode 100644 index 4eb9d25471..0000000000 --- a/ios/CCC/UI/Notifications/NotificationStateView.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// NotificationStateView.swift -// CCC -// -// Created by Mustafa Ozhan on 30.04.22. -// Copyright © 2022 orgName. All rights reserved. -// - -import SwiftUI -import Resources - -struct NotificationStateView: View { - var isEnabled: Bool - var onClick: () -> Void - - var body: some View { - HStack { - - Text( - isEnabled - ? MR.strings().txt_notifications_enabled.get() - : MR.strings().txt_notifications_disabled.get() - ) - .font(.body) - .foregroundColor(MR.colors().text.get()) - - Spacer() - Image(systemName: isEnabled ? "checkmark.circle.fill" : "circle") - .foregroundColor(MR.colors().accent.get()) - - } - .padding(25) - .contentShape(Rectangle()) - .onTapGesture { onClick() } - .lineLimit(1) - } -} diff --git a/ios/CCC/UI/Notifications/NotificationView.swift b/ios/CCC/UI/Notifications/NotificationView.swift new file mode 100644 index 0000000000..a0a20d2044 --- /dev/null +++ b/ios/CCC/UI/Notifications/NotificationView.swift @@ -0,0 +1,118 @@ +// +// NotificationsView.swift +// CCC +// +// Created by Mustafa Ozhan on 26.04.22. +// Copyright © 2022 orgName. All rights reserved. +// + +import SwiftUI +import Client +import Resources +import NavigationStack + +typealias NotificationObservable = ObservableSEED + + +struct NotificationView: View { + @EnvironmentObject private var navigationStack: NavigationStack + @StateObject var observable: NotificationObservable = koin.get() + @State var baseBarInfo = BarInfo(isShown: false, notification: nil) + @State var targetBarInfo = BarInfo(isShown: false, notification: nil) + + var notification: Client.Notification? + + var body: some View { + ZStack { + MR.colors().background_strong.get().edgesIgnoringSafeArea(.all) + + VStack { + NotificationsToolbarView(backEvent: observable.event.onBackClick) + .background(MR.colors().background_strong.get()) + + Form { + List(observable.state.notificationList, id: \.id) { notification in + NotificationItem( + isBaseBarShown: $baseBarInfo.isShown, + isTargetBarShown: $targetBarInfo.isShown, + event: observable.event, + notification: notification + ) + } + .listRowInsets(.init()) + .listRowBackground(MR.colors().background.get()) + } + + VStack { + Button { + observable.event.onAddClick() + } label: { + Label(MR.strings().txt_add.get(), systemImage: "plus") + } + .padding(.top, 10) + .padding(.bottom, 20) + } + .padding(12) + .frame(maxWidth: .infinity, alignment: .center) + .background(MR.colors().background_strong.get()) + } + .background(MR.colors().background.get()) + .edgesIgnoringSafeArea(.bottom) + } + .sheet( + isPresented: $baseBarInfo.isShown, + content: { + SelectCurrencyView( + isBarShown: $baseBarInfo.isShown, + onCurrencySelected: { + observable.event.onBaseChanged( + notification: baseBarInfo.notification, + newBase: $0 + ) + } + ).environmentObject(navigationStack) + } + ) + .sheet( + isPresented: $targetBarInfo.isShown, + content: { + SelectCurrencyView( + isBarShown: $targetBarInfo.isShown, + onCurrencySelected: { + observable.event.onTargetChanged( + notification: targetBarInfo.notification, + newTarget: $0 + ) + } + ).environmentObject(navigationStack) + } + ) + .onAppear { observable.startObserving() } + .onDisappear { observable.stopObserving() } + .onReceive(observable.effect) { onEffect(effect: $0) } + .animation(.default) + } + + private func onEffect(effect: NotificationEffect) { + logger.i(message: {"NotificationView onEffect \(effect.description)"}) + switch effect { + case is NotificationEffect.Back: + navigationStack.pop() + // swiftlint:disable force_cast + case is NotificationEffect.SelectBase: + baseBarInfo.notification = (effect as! NotificationEffect.SelectBase).notification + baseBarInfo.isShown.toggle() + // swiftlint:disable force_cast + case is NotificationEffect.SelectTarget: + targetBarInfo.notification = (effect as! NotificationEffect.SelectTarget).notification + targetBarInfo.isShown.toggle() + default: + logger.i(message: {"NotificationView unknown effect"}) + } + } + + struct BarInfo { + var isShown: Bool + var notification: Client.Notification? + } +} diff --git a/ios/CCC/UI/Notifications/NotificationsView.swift b/ios/CCC/UI/Notifications/NotificationsView.swift deleted file mode 100644 index 11a8b41470..0000000000 --- a/ios/CCC/UI/Notifications/NotificationsView.swift +++ /dev/null @@ -1,124 +0,0 @@ -// -// NotificationsView.swift -// CCC -// -// Created by Mustafa Ozhan on 26.04.22. -// Copyright © 2022 orgName. All rights reserved. -// - -import SwiftUI -import Client -import Resources -import NavigationStack - -typealias NotificationsObservable = ObservableSEED - - -struct NotificationsView: View { - - @State var tootleStatus = false - @State var isBaseBarShown = false - @State var isTargetBarShown = false - @State var amount = "" - @State private var favoriteColor = 0 - - @EnvironmentObject private var navigationStack: NavigationStack - @StateObject var observable: NotificationsObservable = koin.get() - - var body: some View { - ZStack { - MR.colors().background_strong.get().edgesIgnoringSafeArea(.all) - - VStack { - - NotificationsToolbarView(backEvent: observable.event.onBackClick) - .background(MR.colors().background_strong.get()) - - HStack { - Text(MR.strings().one.get()) - .font(.title2) - - NotificationCurrencyItem( - currencyName: observable.state.base, - clickAction: observable.event.onBaseClick - ) - } - - Picker("", selection: $favoriteColor) { - Text(MR.strings().txt_smaller.get()) - .font(.title2) - .tag(0) - Text(MR.strings().txt_grater.get()) - .font(.title2) - .tag(1) - } - .pickerStyle(.segmented) - .frame(maxWidth: 120) - - HStack { - TextField(MR.strings().txt_rate.get(), text: $amount) - .keyboardType(.decimalPad) - .multilineTextAlignment(TextAlignment.center) - .fixedSize() - .lineLimit(1) - - NotificationCurrencyItem( - currencyName: observable.state.target, - clickAction: observable.event.onTargetClick - ) - } - - NotificationStateView( - isEnabled: false, - onClick: observable.event.onStateClick - ) - - Spacer() - - } - .background(MR.colors().background.get()) - .edgesIgnoringSafeArea(.bottom) - } - .sheet( - isPresented: $isBaseBarShown, - content: { - SelectCurrencyView( - isBarShown: $isBaseBarShown, - onCurrencySelected: { observable.event.onBaseChange(base: $0)} - ).environmentObject(navigationStack) - } - ) - .sheet( - isPresented: $isTargetBarShown, - content: { - SelectCurrencyView( - isBarShown: $isTargetBarShown, - onCurrencySelected: { observable.event.onTargetChange(target: $0)} - ).environmentObject(navigationStack) - } - ) - .onAppear { observable.startObserving() } - .onDisappear { observable.stopObserving() } - .onReceive(observable.effect) { onEffect(effect: $0) } - } - - private func onEffect(effect: NotificationsEffect) { - logger.i(message: {"NotificationsView onEffect \(effect.description)"}) - switch effect { - case is NotificationsEffect.Back: - navigationStack.pop() - case is NotificationsEffect.SelectBase: - isBaseBarShown.toggle() - case is NotificationsEffect.SelectTarget: - isTargetBarShown.toggle() - default: - logger.i(message: {"NotificationsView unknown effect"}) - } - } -} - -struct NotificationsView_Previews: PreviewProvider { - static var previews: some View { - NotificationsView() - } -} diff --git a/ios/CCC/UI/Settings/SettingsView.swift b/ios/CCC/UI/Settings/SettingsView.swift index 62e31f7ce8..7038f53afd 100644 --- a/ios/CCC/UI/Settings/SettingsView.swift +++ b/ios/CCC/UI/Settings/SettingsView.swift @@ -54,7 +54,7 @@ struct SettingsView: View { imgName: "bell", title: MR.strings().settings_item_notifications_title.get(), subTitle: MR.strings().settings_item_notifications_sub_title.get(), - value: MR.strings().settings_item_notifications_value_disabled.get(), + value: "", onClick: observable.event.onNotificationsClicked ) @@ -150,7 +150,7 @@ struct SettingsView: View { case is SettingsEffect.OpenCurrencies: navigationStack.push(CurrenciesView(onBaseChange: onBaseChange)) case is SettingsEffect.OpenNotifications: - navigationStack.push(NotificationsView()) + navigationStack.push(NotificationView()) case is SettingsEffect.FeedBack: emailViewVisibility.toggle() case is SettingsEffect.OnGitHub: diff --git a/resources/src/commonMain/resources/MR/base/strings.xml b/resources/src/commonMain/resources/MR/base/strings.xml index 3d9966914a..49862965b3 100644 --- a/resources/src/commonMain/resources/MR/base/strings.xml +++ b/resources/src/commonMain/resources/MR/base/strings.xml @@ -92,9 +92,7 @@ %d active Notifications - Set a rate watcher - Enabled - Disabled + Check rates every 1 hour Theme Change app theme @@ -132,8 +130,7 @@ Rate > < - Notifications enabled - Notifications disabled + Add Remove Ads From bc211454959e6511d94382eb0e65a96382289967 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sat, 14 May 2022 13:21:42 +0300 Subject: [PATCH 06/40] [#723] Add Relation and Rate change capability to notifications (#724) --- .../viewmodel/notification/NotificationSEED.kt | 2 ++ .../notification/NotificationViewModel.kt | 15 +++++++++++++++ .../db/notification/NotificationRepository.kt | 2 ++ .../notification/NotificationRepositoryImpl.kt | 9 +++++++++ .../oztechan/ccc/common/db/sql/Notification.sq | 6 ++++++ ios/CCC/UI/Notifications/NotificationItem.swift | 17 +++++++++++++++++ ios/CCC/UI/Notifications/NotificationView.swift | 1 + 7 files changed, 52 insertions(+) diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt index 145e1c1971..5e73eed5ff 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt @@ -27,6 +27,8 @@ interface NotificationEvent : BaseEvent { fun onTargetChanged(notification: Notification?, newTarget: String) fun onAddClick() fun onDeleteClick(notification: Notification) + fun onRelationChange(notification: Notification, isGreater: Boolean) + fun onRateChange(notification: Notification, rate: String) } // Extension diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt index 1c2c203154..c286867d81 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt @@ -6,6 +6,8 @@ import com.oztechan.ccc.client.base.BaseSEEDViewModel import com.oztechan.ccc.client.mapper.toUIModelList import com.oztechan.ccc.client.model.Notification import com.oztechan.ccc.client.util.launchIgnored +import com.oztechan.ccc.client.util.toStandardDigits +import com.oztechan.ccc.client.util.toSupportedCharacters import com.oztechan.ccc.common.db.currency.CurrencyRepository import com.oztechan.ccc.common.db.notification.NotificationRepository import kotlinx.coroutines.flow.MutableSharedFlow @@ -80,5 +82,18 @@ class NotificationViewModel( Logger.d { "NotificationViewModel onDeleteClick $notification" } notificationRepository.deleteNotification(notification.id) } + + override fun onRelationChange(notification: Notification, isGreater: Boolean) { + Logger.d { "NotificationViewModel onRelationChange $notification $isGreater" } + notificationRepository.updateRelationById(isGreater, notification.id) + } + + override fun onRateChange(notification: Notification, rate: String) { + Logger.d { "NotificationViewModel onRateChange $notification $rate" } + notificationRepository.updateRateById( + rate.toSupportedCharacters().toStandardDigits().toDoubleOrNull() ?: 0.0, + notification.id + ) + } // endregion } diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt index 2829118c71..b08ea03065 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt @@ -9,4 +9,6 @@ interface NotificationRepository { fun deleteNotification(id: Long) fun updateBaseById(base: String, id: Long) fun updateTargetById(target: String, id: Long) + fun updateRelationById(isGreater: Boolean, id: Long) + fun updateRateById(rate: Double, id: Long) } \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt index 22246c1eef..e3b8cf92d6 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt @@ -3,6 +3,7 @@ package com.oztechan.ccc.common.db.notification import co.touchlab.kermit.Logger import com.oztechan.ccc.common.db.sql.NotificationQueries import com.oztechan.ccc.common.mapper.mapToModel +import com.oztechan.ccc.common.mapper.toLong import com.squareup.sqldelight.runtime.coroutines.asFlow import com.squareup.sqldelight.runtime.coroutines.mapToList @@ -34,4 +35,12 @@ class NotificationRepositoryImpl( override fun updateTargetById(target: String, id: Long) = notificationQueries .updateTargetById(target, id) .also { Logger.v { "NotificationRepositoryImpl updateTargetById $target $id" } } + + override fun updateRelationById(isGreater: Boolean, id: Long) = notificationQueries + .updateRelationById(isGreater.toLong(), id) + .also { Logger.v { "NotificationRepositoryImpl updateRelationById $isGreater $id" } } + + override fun updateRateById(rate: Double, id: Long) = notificationQueries + .updateRateById(rate, id) + .also { Logger.v { "NotificationRepositoryImpl updateRateById $rate $id" } } } diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq index 941a1dccb8..52f1923189 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq @@ -18,5 +18,11 @@ UPDATE notification SET base=? WHERE id=?; updateTargetById: UPDATE notification SET target=? WHERE id=?; +updateRelationById: +UPDATE notification SET isGreater=? WHERE id=?; + +updateRateById: +UPDATE notification SET rate=? WHERE id=?; + getNotifications: SELECT * FROM notification; \ No newline at end of file diff --git a/ios/CCC/UI/Notifications/NotificationItem.swift b/ios/CCC/UI/Notifications/NotificationItem.swift index cb77b1f145..5ff48d1d2b 100644 --- a/ios/CCC/UI/Notifications/NotificationItem.swift +++ b/ios/CCC/UI/Notifications/NotificationItem.swift @@ -33,6 +33,8 @@ struct NotificationItem: View { .shadow(radius: 3) .onTapGesture { event.onBaseClick(notification: notification) } + Spacer() + Picker("", selection: $relationSelection) { Text(MR.strings().txt_smaller.get()) .font(.title) @@ -43,6 +45,11 @@ struct NotificationItem: View { } .pickerStyle(.segmented) .frame(maxWidth: 80) + .onChange(of: relationSelection) { relation in + event.onRelationChange(notification: notification, isGreater: relation == 1) + } + + Spacer() TextField(MR.strings().txt_rate.get(), text: $amount) .keyboardType(.decimalPad) @@ -50,6 +57,11 @@ struct NotificationItem: View { .multilineTextAlignment(TextAlignment.center) .fixedSize() .lineLimit(1) + .onChange(of: amount) { rate in + event.onRateChange(notification: notification, rate: rate) + } + + Spacer() Image(uiImage: notification.target.getImage()) .resizable() @@ -67,6 +79,11 @@ struct NotificationItem: View { }.onAppear { amount = "\(notification.rate)" + if notification.isGreater { + relationSelection = 1 + } else { + relationSelection = 0 + } } } } diff --git a/ios/CCC/UI/Notifications/NotificationView.swift b/ios/CCC/UI/Notifications/NotificationView.swift index a0a20d2044..983dfb1efa 100644 --- a/ios/CCC/UI/Notifications/NotificationView.swift +++ b/ios/CCC/UI/Notifications/NotificationView.swift @@ -49,6 +49,7 @@ struct NotificationView: View { } label: { Label(MR.strings().txt_add.get(), systemImage: "plus") } + .foregroundColor(MR.colors().text.get()) .padding(.top, 10) .padding(.bottom, 20) } From 683c02b7a33dc239e6f6e7ee7c9e0393f5b8ed92 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 15 May 2022 15:36:50 +0300 Subject: [PATCH 07/40] [#725] Show active notifications (#726) --- .../android/ui/settings/SettingsFragment.kt | 2 +- .../ccc/client/di/module/ClientModule.kt | 2 +- .../client/viewmodel/settings/SettingsSEED.kt | 3 +++ .../viewmodel/settings/SettingsViewModel.kt | 7 +++++++ .../client/viewmodel/SettingsViewModelTest.kt | 19 +++++++++++++++++++ .../UI/Notifications/NotificationItem.swift | 7 ++----- ios/CCC/UI/Settings/SettingsView.swift | 6 ++++-- .../commonMain/resources/MR/base/strings.xml | 2 +- 8 files changed, 38 insertions(+), 10 deletions(-) diff --git a/android/src/main/kotlin/com/oztechan/ccc/android/ui/settings/SettingsFragment.kt b/android/src/main/kotlin/com/oztechan/ccc/android/ui/settings/SettingsFragment.kt index 554f6a9f29..21ec1ed413 100644 --- a/android/src/main/kotlin/com/oztechan/ccc/android/ui/settings/SettingsFragment.kt +++ b/android/src/main/kotlin/com/oztechan/ccc/android/ui/settings/SettingsFragment.kt @@ -112,7 +112,7 @@ class SettingsFragment : BaseVBFragment() { with(it) { binding.loadingView.visibleIf(loading) binding.itemCurrencies.settingsItemValue.text = requireContext().getString( - R.string.settings_item_currencies_value, + R.string.settings_active_item_value, activeCurrencyCount ) binding.itemTheme.settingsItemValue.text = appThemeType.typeName diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt index b2db1b503c..93526fdd9a 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt @@ -15,7 +15,7 @@ import com.oztechan.ccc.config.ConfigManagerImpl import org.koin.dsl.module var clientModule = module { - viewModelDefinition { SettingsViewModel(get(), get(), get(), get(), get()) } + viewModelDefinition { SettingsViewModel(get(), get(), get(), get(), get(), get()) } viewModelDefinition { MainViewModel(get(), get(), get()) } viewModelDefinition { CurrenciesViewModel(get(), get(), get()) } viewModelDefinition { CalculatorViewModel(get(), get(), get(), get(), get()) } diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt index 3df0f58554..033c40117a 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.MutableStateFlow // State data class SettingsState( val activeCurrencyCount: Int = 0, + val activeNotificationCount: Int = 0, val appThemeType: AppTheme = AppTheme.SYSTEM_DEFAULT, val addFreeEndDate: String = "", val loading: Boolean = false @@ -57,12 +58,14 @@ data class SettingsData(var synced: Boolean = false) : BaseData() { // Extension fun MutableStateFlow.update( activeCurrencyCount: Int = value.activeCurrencyCount, + activeNotificationCount: Int = value.activeNotificationCount, appThemeType: AppTheme = value.appThemeType, addFreeEndDate: String = value.addFreeEndDate, loading: Boolean = value.loading ) { value = value.copy( activeCurrencyCount = activeCurrencyCount, + activeNotificationCount = activeNotificationCount, appThemeType = appThemeType, addFreeEndDate = addFreeEndDate, loading = loading diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt index f21c696462..fd94c4e5fa 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt @@ -16,6 +16,7 @@ import com.oztechan.ccc.client.util.toDateString import com.oztechan.ccc.client.viewmodel.settings.SettingsData.Companion.SYNC_DELAY import com.oztechan.ccc.common.api.repo.ApiRepository import com.oztechan.ccc.common.db.currency.CurrencyRepository +import com.oztechan.ccc.common.db.notification.NotificationRepository import com.oztechan.ccc.common.db.offlinerates.OfflineRatesRepository import com.oztechan.ccc.common.settings.SettingsRepository import com.oztechan.ccc.common.util.nowAsLong @@ -33,6 +34,7 @@ class SettingsViewModel( private val apiRepository: ApiRepository, private val currencyRepository: CurrencyRepository, private val offlineRatesRepository: OfflineRatesRepository, + notificationRepository: NotificationRepository, private val sessionManager: SessionManager ) : BaseSEEDViewModel(), SettingsEvent { // region SEED @@ -57,6 +59,11 @@ class SettingsViewModel( .onEach { _state.update(activeCurrencyCount = it.size) }.launchIn(clientScope) + + notificationRepository.collectNotifications() + .onEach { + _state.update(activeNotificationCount = it.size) + }.launchIn(clientScope) } private suspend fun synchroniseRates() { diff --git a/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt b/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt index b33220eb73..e7ad0adeee 100644 --- a/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt +++ b/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt @@ -17,8 +17,10 @@ import com.oztechan.ccc.client.viewmodel.settings.SettingsViewModel import com.oztechan.ccc.client.viewmodel.settings.update import com.oztechan.ccc.common.api.repo.ApiRepository import com.oztechan.ccc.common.db.currency.CurrencyRepository +import com.oztechan.ccc.common.db.notification.NotificationRepository import com.oztechan.ccc.common.db.offlinerates.OfflineRatesRepository import com.oztechan.ccc.common.model.Currency +import com.oztechan.ccc.common.model.Notification import com.oztechan.ccc.common.runTest import com.oztechan.ccc.common.settings.SettingsRepository import com.oztechan.ccc.common.util.DAY @@ -53,6 +55,9 @@ class SettingsViewModelTest { @Mock private val offlineRatesRepository = mock(classOf()) + @Mock + private val notificationRepository = mock(classOf()) + @Mock private val sessionManager = mock(classOf()) @@ -62,6 +67,7 @@ class SettingsViewModelTest { apiRepository, currencyRepository, offlineRatesRepository, + notificationRepository, sessionManager ) } @@ -71,6 +77,11 @@ class SettingsViewModelTest { Currency("", "", "") ) + private val notificationList = listOf( + Notification(1, "EUR", "USD", true, 1.1), + Notification(2, "USD", "EUR", false, 2.3) + ) + @BeforeTest fun setup() { initLogger(true) @@ -86,6 +97,10 @@ class SettingsViewModelTest { given(currencyRepository) .invocation { collectActiveCurrencies() } .thenReturn(flowOf(currencyList)) + + given(notificationRepository) + .invocation { collectNotifications() } + .then { flowOf(notificationList) } } // SEED @@ -95,6 +110,7 @@ class SettingsViewModelTest { val state = MutableStateFlow(SettingsState()) val activeCurrencyCount = Random.nextInt() + val activeNotificationCount = Random.nextInt() val appThemeType = AppTheme.getThemeByOrderOrDefault(Random.nextInt() % 3) val addFreeEndDate = "23.12.2121" val loading = Random.nextBoolean() @@ -102,12 +118,14 @@ class SettingsViewModelTest { state.before { state.update( activeCurrencyCount = activeCurrencyCount, + activeNotificationCount = activeNotificationCount, appThemeType = appThemeType, addFreeEndDate = addFreeEndDate, loading = loading ) }.after { assertEquals(activeCurrencyCount, it?.activeCurrencyCount) + assertEquals(activeNotificationCount, it?.activeNotificationCount) assertEquals(appThemeType, it?.appThemeType) assertEquals(addFreeEndDate, it?.addFreeEndDate) assertEquals(loading, it?.loading) @@ -120,6 +138,7 @@ class SettingsViewModelTest { viewModel.state.firstOrNull().let { assertEquals(AppTheme.SYSTEM_DEFAULT, it?.appThemeType) // mocked -1 assertEquals(currencyList.size, it?.activeCurrencyCount) + assertEquals(notificationList.size, it?.activeNotificationCount) } } diff --git a/ios/CCC/UI/Notifications/NotificationItem.swift b/ios/CCC/UI/Notifications/NotificationItem.swift index 5ff48d1d2b..af4923650a 100644 --- a/ios/CCC/UI/Notifications/NotificationItem.swift +++ b/ios/CCC/UI/Notifications/NotificationItem.swift @@ -71,11 +71,8 @@ struct NotificationItem: View { Spacer() - Button { - event.onDeleteClick(notification: notification) - } label: { - Label("", systemImage: "trash") - }.foregroundColor(MR.colors().text.get()) + Image(systemName: "trash") + .onTapGesture { event.onDeleteClick(notification: notification) } }.onAppear { amount = "\(notification.rate)" diff --git a/ios/CCC/UI/Settings/SettingsView.swift b/ios/CCC/UI/Settings/SettingsView.swift index 7038f53afd..ecdc3522a6 100644 --- a/ios/CCC/UI/Settings/SettingsView.swift +++ b/ios/CCC/UI/Settings/SettingsView.swift @@ -44,7 +44,7 @@ struct SettingsView: View { imgName: "dollarsign.circle.fill", title: MR.strings().settings_item_currencies_title.get(), subTitle: MR.strings().settings_item_currencies_sub_title.get(), - value: MR.strings().settings_item_currencies_value.get( + value: MR.strings().settings_active_item_value.get( parameter: observable.state.activeCurrencyCount ), onClick: observable.event.onCurrenciesClick @@ -54,7 +54,9 @@ struct SettingsView: View { imgName: "bell", title: MR.strings().settings_item_notifications_title.get(), subTitle: MR.strings().settings_item_notifications_sub_title.get(), - value: "", + value: MR.strings().settings_active_item_value.get( + parameter: observable.state.activeNotificationCount + ), onClick: observable.event.onNotificationsClicked ) diff --git a/resources/src/commonMain/resources/MR/base/strings.xml b/resources/src/commonMain/resources/MR/base/strings.xml index 49862965b3..260a3cfb48 100644 --- a/resources/src/commonMain/resources/MR/base/strings.xml +++ b/resources/src/commonMain/resources/MR/base/strings.xml @@ -86,10 +86,10 @@ Settings + %d active Currencies Set your currencies - %d active Notifications Check rates every 1 hour From 8a08fab6e3c157d3eb972286e590c8f8fcf35a76 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 15 May 2022 20:36:09 +0300 Subject: [PATCH 08/40] [#727] Create CurrencyImageView (#728) --- ios/CCC.xcodeproj/project.pbxproj | 4 ++++ .../UI/Calculator/CalculatorItemView.swift | 5 +---- ios/CCC/UI/Calculator/OutputView.swift | 5 +---- .../UI/Currencies/CurrenciesItemView.swift | 6 ++---- .../UI/Notifications/NotificationItem.swift | 10 ++-------- .../SelectCurrencyItemView.swift | 6 ++---- ios/CCC/UI/SubView/CurrencyImageView.swift | 20 +++++++++++++++++++ 7 files changed, 32 insertions(+), 24 deletions(-) create mode 100644 ios/CCC/UI/SubView/CurrencyImageView.swift diff --git a/ios/CCC.xcodeproj/project.pbxproj b/ios/CCC.xcodeproj/project.pbxproj index 378252b85c..177c2c8b07 100644 --- a/ios/CCC.xcodeproj/project.pbxproj +++ b/ios/CCC.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ 5C6E674D25C602BE001CC0D6 /* SnackBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E674C25C602BE001CC0D6 /* SnackBar.swift */; }; 5C8EB4A9260CB5E200DC4A90 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5C8EB4A8260CB5E200DC4A90 /* GoogleService-Info.plist */; }; 5C8FDBDD25BF3FBE00F280FF /* ObservableSEED.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8FDBDC25BF3FBE00F280FF /* ObservableSEED.swift */; }; + 5C94AC32282FA4B2004C9B3D /* CurrencyImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C94AC31282FA4B2004C9B3D /* CurrencyImageView.swift */; }; 5C9A59BB25C350DE006745B0 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A59BA25C350DE006745B0 /* MainView.swift */; }; 5C9C75C82603A36A00D66FDD /* ToolbarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9C75C72603A36A00D66FDD /* ToolbarButton.swift */; }; 5CB954BF26932408007632DC /* BannerAdView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB954BE26932408007632DC /* BannerAdView.swift */; }; @@ -90,6 +91,7 @@ 5C6E674C25C602BE001CC0D6 /* SnackBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnackBar.swift; sourceTree = ""; }; 5C8EB4A8260CB5E200DC4A90 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 5C8FDBDC25BF3FBE00F280FF /* ObservableSEED.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableSEED.swift; sourceTree = ""; }; + 5C94AC31282FA4B2004C9B3D /* CurrencyImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyImageView.swift; sourceTree = ""; }; 5C9A59BA25C350DE006745B0 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 5C9C75C72603A36A00D66FDD /* ToolbarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButton.swift; sourceTree = ""; }; 5CB954BE26932408007632DC /* BannerAdView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerAdView.swift; sourceTree = ""; }; @@ -130,6 +132,7 @@ 5C9C75C72603A36A00D66FDD /* ToolbarButton.swift */, 5CF8BE4127DE205B00E441F5 /* MailView.swift */, 5CF8BE4527DE334100E441F5 /* WebView.swift */, + 5C94AC31282FA4B2004C9B3D /* CurrencyImageView.swift */, ); path = SubView; sourceTree = ""; @@ -465,6 +468,7 @@ buildActionMask = 2147483647; files = ( 5C314CBE25BA0AC0007B22D8 /* CurrenciesView.swift in Sources */, + 5C94AC32282FA4B2004C9B3D /* CurrencyImageView.swift in Sources */, 5C4B536B2818066000D10185 /* NotificationsToolbarView.swift in Sources */, 5CF898D42823C1F900712580 /* NotificationItem.swift in Sources */, 5CB954BF26932408007632DC /* BannerAdView.swift in Sources */, diff --git a/ios/CCC/UI/Calculator/CalculatorItemView.swift b/ios/CCC/UI/Calculator/CalculatorItemView.swift index 8c1defd83b..f4edb9070f 100644 --- a/ios/CCC/UI/Calculator/CalculatorItemView.swift +++ b/ios/CCC/UI/Calculator/CalculatorItemView.swift @@ -37,10 +37,7 @@ struct CalculatorItemView: View { .onTapGesture { onItemClick(item) } .onLongPressGesture { onItemImageLongClick(item) } - Image(uiImage: item.name.getImage()) - .resizable() - .frame(width: 36, height: 36, alignment: .center) - .shadow(radius: 3) + CurrencyImageView(imageName: item.name) .onTapGesture { onItemClick(item) } .onLongPressGesture { onItemImageLongClick(item) } diff --git a/ios/CCC/UI/Calculator/OutputView.swift b/ios/CCC/UI/Calculator/OutputView.swift index 9a3cb2717f..59b1eb75cd 100644 --- a/ios/CCC/UI/Calculator/OutputView.swift +++ b/ios/CCC/UI/Calculator/OutputView.swift @@ -20,10 +20,7 @@ struct OutputView: View { VStack(alignment: .leading) { HStack { - Image(uiImage: baseCurrency.getImage()) - .resizable() - .frame(width: 36, height: 36, alignment: .center) - .shadow(radius: 3) + CurrencyImageView(imageName: baseCurrency) Text(baseCurrency).foregroundColor(MR.colors().text.get()) diff --git a/ios/CCC/UI/Currencies/CurrenciesItemView.swift b/ios/CCC/UI/Currencies/CurrenciesItemView.swift index b39186f90b..33bafa70cb 100644 --- a/ios/CCC/UI/Currencies/CurrenciesItemView.swift +++ b/ios/CCC/UI/Currencies/CurrenciesItemView.swift @@ -20,10 +20,8 @@ struct CurrenciesItemView: View { var body: some View { HStack { - Image(uiImage: item.name.getImage()) - .resizable() - .frame(width: 36, height: 36, alignment: .center) - .shadow(radius: 3) + CurrencyImageView(imageName: item.name) + Text(item.name) .frame(width: 45) .foregroundColor(MR.colors().text.get()) diff --git a/ios/CCC/UI/Notifications/NotificationItem.swift b/ios/CCC/UI/Notifications/NotificationItem.swift index af4923650a..54ae1a09cf 100644 --- a/ios/CCC/UI/Notifications/NotificationItem.swift +++ b/ios/CCC/UI/Notifications/NotificationItem.swift @@ -27,10 +27,7 @@ struct NotificationItem: View { Text(MR.strings().one.get()) .font(.body) - Image(uiImage: notification.base.getImage()) - .resizable() - .frame(width: 36, height: 36, alignment: .center) - .shadow(radius: 3) + CurrencyImageView(imageName: notification.base) .onTapGesture { event.onBaseClick(notification: notification) } Spacer() @@ -63,10 +60,7 @@ struct NotificationItem: View { Spacer() - Image(uiImage: notification.target.getImage()) - .resizable() - .frame(width: 36, height: 36, alignment: .center) - .shadow(radius: 3) + CurrencyImageView(imageName: notification.target) .onTapGesture { event.onTargetClick(notification: notification) } Spacer() diff --git a/ios/CCC/UI/SelectCurrency/SelectCurrencyItemView.swift b/ios/CCC/UI/SelectCurrency/SelectCurrencyItemView.swift index 94a7432654..271173687a 100644 --- a/ios/CCC/UI/SelectCurrency/SelectCurrencyItemView.swift +++ b/ios/CCC/UI/SelectCurrency/SelectCurrencyItemView.swift @@ -17,10 +17,8 @@ struct SelectCurrencyItemView: View { var body: some View { HStack { - Image(uiImage: item.name.getImage()) - .resizable() - .frame(width: 36, height: 36, alignment: .center) - .shadow(radius: 3) + CurrencyImageView(imageName: item.name) + Text(item.name) .frame(width: 45) .foregroundColor(MR.colors().text.get()) diff --git a/ios/CCC/UI/SubView/CurrencyImageView.swift b/ios/CCC/UI/SubView/CurrencyImageView.swift new file mode 100644 index 0000000000..c9c475159d --- /dev/null +++ b/ios/CCC/UI/SubView/CurrencyImageView.swift @@ -0,0 +1,20 @@ +// +// CurrencyImageView.swift +// CCC +// +// Created by Mustafa Ozhan on 14.05.22. +// Copyright © 2022 orgName. All rights reserved. +// + +import SwiftUI + +struct CurrencyImageView: View { + let imageName: String + + var body: some View { + Image(uiImage: imageName.getImage()) + .resizable() + .frame(width: 36, height: 36, alignment: .center) + .shadow(radius: 3) + } +} From b21625557861b5cbd34c1e3f3a68f7479a0273d8 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 15 May 2022 23:17:56 +0300 Subject: [PATCH 09/40] [#734] Handle maximum input error in Notifications (#735) --- .../ui/calculator/CalculatorFragment.kt | 2 +- .../notification/NotificationSEED.kt | 10 ++++++- .../notification/NotificationViewModel.kt | 26 +++++++++++----- ios/CCC/UI/Calculator/CalculatorView.swift | 2 +- .../UI/Notifications/NotificationItem.swift | 30 +++++++------------ .../UI/Notifications/NotificationView.swift | 8 +++-- ios/CCC/Util/SnackBar.swift | 5 ++-- .../commonMain/resources/MR/base/strings.xml | 6 ++-- 8 files changed, 51 insertions(+), 38 deletions(-) diff --git a/android/src/main/kotlin/com/oztechan/ccc/android/ui/calculator/CalculatorFragment.kt b/android/src/main/kotlin/com/oztechan/ccc/android/ui/calculator/CalculatorFragment.kt index 64947a15da..49161645ce 100755 --- a/android/src/main/kotlin/com/oztechan/ccc/android/ui/calculator/CalculatorFragment.kt +++ b/android/src/main/kotlin/com/oztechan/ccc/android/ui/calculator/CalculatorFragment.kt @@ -148,7 +148,7 @@ class CalculatorFragment : BaseVBFragment() { } CalculatorEffect.MaximumInput -> showSnack( requireView(), - R.string.max_input + R.string.text_max_input ) CalculatorEffect.OpenBar -> navigate( R.id.calculatorFragment, diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt index 5e73eed5ff..84915567de 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt @@ -1,6 +1,7 @@ package com.oztechan.ccc.client.viewmodel.notification +import com.oztechan.ccc.client.base.BaseData import com.oztechan.ccc.client.base.BaseEffect import com.oztechan.ccc.client.base.BaseEvent import com.oztechan.ccc.client.base.BaseState @@ -17,6 +18,7 @@ sealed class NotificationEffect : BaseEffect() { object Back : NotificationEffect() data class SelectBase(val notification: Notification) : NotificationEffect() data class SelectTarget(val notification: Notification) : NotificationEffect() + object MaximumInput : NotificationEffect() } interface NotificationEvent : BaseEvent { @@ -28,7 +30,13 @@ interface NotificationEvent : BaseEvent { fun onAddClick() fun onDeleteClick(notification: Notification) fun onRelationChange(notification: Notification, isGreater: Boolean) - fun onRateChange(notification: Notification, rate: String) + fun onRateChange(notification: Notification, rate: String): String +} + +class NotificationData : BaseData() { + companion object { + const val MAXIMUM_INPUT = 9 + } } // Extension diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt index c286867d81..d706fc7048 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt @@ -1,13 +1,13 @@ package com.oztechan.ccc.client.viewmodel.notification import co.touchlab.kermit.Logger -import com.oztechan.ccc.client.base.BaseData import com.oztechan.ccc.client.base.BaseSEEDViewModel import com.oztechan.ccc.client.mapper.toUIModelList import com.oztechan.ccc.client.model.Notification import com.oztechan.ccc.client.util.launchIgnored import com.oztechan.ccc.client.util.toStandardDigits import com.oztechan.ccc.client.util.toSupportedCharacters +import com.oztechan.ccc.client.viewmodel.notification.NotificationData.Companion.MAXIMUM_INPUT import com.oztechan.ccc.common.db.currency.CurrencyRepository import com.oztechan.ccc.common.db.notification.NotificationRepository import kotlinx.coroutines.flow.MutableSharedFlow @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch class NotificationViewModel( private val currencyRepository: CurrencyRepository, @@ -30,7 +31,7 @@ class NotificationViewModel( private val _effect = MutableSharedFlow() override val effect = _effect.asSharedFlow() - override val data: BaseData? = null + override val data = NotificationData() init { notificationRepository.collectNotifications() @@ -88,12 +89,23 @@ class NotificationViewModel( notificationRepository.updateRelationById(isGreater, notification.id) } - override fun onRateChange(notification: Notification, rate: String) { + override fun onRateChange(notification: Notification, rate: String): String { Logger.d { "NotificationViewModel onRateChange $notification $rate" } - notificationRepository.updateRateById( - rate.toSupportedCharacters().toStandardDigits().toDoubleOrNull() ?: 0.0, - notification.id - ) + return when { + rate.length > MAXIMUM_INPUT -> { + clientScope.launch { + _effect.emit(NotificationEffect.MaximumInput) + } + rate.dropLast(1) + } + else -> { + notificationRepository.updateRateById( + rate.toSupportedCharacters().toStandardDigits().toDoubleOrNull() ?: 0.0, + notification.id + ) + rate + } + } } // endregion } diff --git a/ios/CCC/UI/Calculator/CalculatorView.swift b/ios/CCC/UI/Calculator/CalculatorView.swift index 85e8d2ce7f..95c3581295 100644 --- a/ios/CCC/UI/Calculator/CalculatorView.swift +++ b/ios/CCC/UI/Calculator/CalculatorView.swift @@ -113,7 +113,7 @@ struct CalculatorView: View { } ) case is CalculatorEffect.MaximumInput: - showSnack(text: MR.strings().max_input.get()) + showSnack(text: MR.strings().text_max_input.get()) case is CalculatorEffect.OpenBar: isBarShown = true case is CalculatorEffect.OpenSettings: diff --git a/ios/CCC/UI/Notifications/NotificationItem.swift b/ios/CCC/UI/Notifications/NotificationItem.swift index 54ae1a09cf..4fe6e139cb 100644 --- a/ios/CCC/UI/Notifications/NotificationItem.swift +++ b/ios/CCC/UI/Notifications/NotificationItem.swift @@ -13,25 +13,22 @@ import NavigationStack import Combine struct NotificationItem: View { + @State private var relationSelection = 0 + @State private var amount = "" + @Binding var isBaseBarShown: Bool @Binding var isTargetBarShown: Bool + @State var notification: Client.Notification let event: NotificationEvent - let notification: Client.Notification - - @State private var relationSelection = 0 - @State var amount = "" var body: some View { HStack { - Text(MR.strings().one.get()) - .font(.body) + Text(MR.strings().one.get()).font(.body) CurrencyImageView(imageName: notification.base) .onTapGesture { event.onBaseClick(notification: notification) } - Spacer() - Picker("", selection: $relationSelection) { Text(MR.strings().txt_smaller.get()) .font(.title) @@ -42,8 +39,8 @@ struct NotificationItem: View { } .pickerStyle(.segmented) .frame(maxWidth: 80) - .onChange(of: relationSelection) { relation in - event.onRelationChange(notification: notification, isGreater: relation == 1) + .onChange(of: relationSelection) { + event.onRelationChange(notification: notification, isGreater: $0 == 1) } Spacer() @@ -54,8 +51,8 @@ struct NotificationItem: View { .multilineTextAlignment(TextAlignment.center) .fixedSize() .lineLimit(1) - .onChange(of: amount) { rate in - event.onRateChange(notification: notification, rate: rate) + .onChange(of: amount) { + amount = event.onRateChange(notification: notification, rate: $0) } Spacer() @@ -63,18 +60,13 @@ struct NotificationItem: View { CurrencyImageView(imageName: notification.target) .onTapGesture { event.onTargetClick(notification: notification) } - Spacer() - Image(systemName: "trash") + .padding(.leading, 10) .onTapGesture { event.onDeleteClick(notification: notification) } }.onAppear { + relationSelection = notification.isGreater ? 1 : 0 amount = "\(notification.rate)" - if notification.isGreater { - relationSelection = 1 - } else { - relationSelection = 0 - } } } } diff --git a/ios/CCC/UI/Notifications/NotificationView.swift b/ios/CCC/UI/Notifications/NotificationView.swift index 983dfb1efa..fb6fef058a 100644 --- a/ios/CCC/UI/Notifications/NotificationView.swift +++ b/ios/CCC/UI/Notifications/NotificationView.swift @@ -12,7 +12,7 @@ import Resources import NavigationStack typealias NotificationObservable = ObservableSEED - + struct NotificationView: View { @EnvironmentObject private var navigationStack: NavigationStack @@ -35,8 +35,8 @@ struct NotificationView: View { NotificationItem( isBaseBarShown: $baseBarInfo.isShown, isTargetBarShown: $targetBarInfo.isShown, - event: observable.event, - notification: notification + notification: notification, + event: observable.event ) } .listRowInsets(.init()) @@ -107,6 +107,8 @@ struct NotificationView: View { case is NotificationEffect.SelectTarget: targetBarInfo.notification = (effect as! NotificationEffect.SelectTarget).notification targetBarInfo.isShown.toggle() + case is NotificationEffect.MaximumInput: + showSnack(text: MR.strings().text_max_input.get(), isCentered: true) default: logger.i(message: {"NotificationView unknown effect"}) } diff --git a/ios/CCC/Util/SnackBar.swift b/ios/CCC/Util/SnackBar.swift index 9c5206fdb1..8bad01b6bf 100644 --- a/ios/CCC/Util/SnackBar.swift +++ b/ios/CCC/Util/SnackBar.swift @@ -14,7 +14,8 @@ func showSnack( text: String, buttonText: String? = nil, action: (() -> Void)? = nil, - iconImage: UIImage = MR.images().ic_app_logo.get() + iconImage: UIImage = MR.images().ic_app_logo.get(), + isCentered: Bool = false ) { let view = MessageView.viewFromNib(layout: .cardView) @@ -54,7 +55,7 @@ func showSnack( } var config = SwiftMessages.defaultConfig - config.presentationStyle = .bottom + config.presentationStyle = isCentered ? .center : .bottom config.presentationContext = .window(windowLevel: UIWindow.Level.normal) SwiftMessages.hide(animated: false) diff --git a/resources/src/commonMain/resources/MR/base/strings.xml b/resources/src/commonMain/resources/MR/base/strings.xml index 260a3cfb48..440760afe0 100644 --- a/resources/src/commonMain/resources/MR/base/strings.xml +++ b/resources/src/commonMain/resources/MR/base/strings.xml @@ -35,14 +35,12 @@ Done Select your currencies - + Please Select at least 2 currencies. OK Select Cancel - - - Maximum number of input exceeded. + Maximum number of input exceeded. Online! Last update: %s Cached! Last update: %s Offline! Last update: %s From 62b44e4ec38147eb41d1b04ed16e34cd2b6a7f89 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Tue, 17 May 2022 20:15:54 +0300 Subject: [PATCH 10/40] [#736] Handle input validation in Notifications (#737) * [#736] Handle input validation in Notifications * [#736] Suppress StringLiteralDuplication --- .../com/oztechan/ccc/client/mapper/Notification.kt | 3 ++- .../com/oztechan/ccc/client/model/Notification.kt | 2 +- .../com/oztechan/ccc/client/util/CalculatorUtil.kt | 2 ++ .../viewmodel/notification/NotificationSEED.kt | 1 + .../viewmodel/notification/NotificationViewModel.kt | 9 ++++++--- .../oztechan/ccc/client/util/CalculatorUtilTest.kt | 12 ++++++++++++ ios/CCC/UI/Notifications/NotificationItem.swift | 2 +- ios/CCC/UI/Notifications/NotificationView.swift | 4 +++- ios/CCC/Util/SnackBar.swift | 6 +++--- .../src/commonMain/resources/MR/base/strings.xml | 1 + 10 files changed, 32 insertions(+), 10 deletions(-) diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt index d0d5090a8e..b1c8207417 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt @@ -1,5 +1,6 @@ package com.oztechan.ccc.client.mapper +import com.oztechan.ccc.client.util.trimTrailingZero import com.oztechan.ccc.common.model.Notification import com.oztechan.ccc.client.model.Notification as NotificationUIModel @@ -8,7 +9,7 @@ fun Notification.toUIModel() = NotificationUIModel( base = base, target = target, isGreater = isGreater, - rate = rate + rate = rate.toString().trimTrailingZero() ) fun List.toUIModelList() = map { diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/model/Notification.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/model/Notification.kt index 5c77379bc0..67ada9f2af 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/model/Notification.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/model/Notification.kt @@ -5,5 +5,5 @@ data class Notification( val base: String, val target: String, val isGreater: Boolean, - val rate: Double + val rate: String ) diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/util/CalculatorUtil.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/util/CalculatorUtil.kt index c116294588..0f455b09a1 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/util/CalculatorUtil.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/util/CalculatorUtil.kt @@ -35,6 +35,8 @@ fun String.toStandardDigits(): String { return builder.toString() } +fun String.trimTrailingZero() = replace("0*$".toRegex(), "").replace("\\.$".toRegex(), "") + fun Currency.getCurrencyConversionByRate( base: String, rate: Rates? diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt index 84915567de..139d79869f 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt @@ -19,6 +19,7 @@ sealed class NotificationEffect : BaseEffect() { data class SelectBase(val notification: Notification) : NotificationEffect() data class SelectTarget(val notification: Notification) : NotificationEffect() object MaximumInput : NotificationEffect() + object InvalidInput : NotificationEffect() } interface NotificationEvent : BaseEvent { diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt index d706fc7048..62272175f2 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt @@ -91,13 +91,16 @@ class NotificationViewModel( override fun onRateChange(notification: Notification, rate: String): String { Logger.d { "NotificationViewModel onRateChange $notification $rate" } + return when { rate.length > MAXIMUM_INPUT -> { - clientScope.launch { - _effect.emit(NotificationEffect.MaximumInput) - } + clientScope.launch { _effect.emit(NotificationEffect.MaximumInput) } rate.dropLast(1) } + rate.toDoubleOrNull()?.isNaN() != false -> { + clientScope.launch { _effect.emit(NotificationEffect.InvalidInput) } + rate + } else -> { notificationRepository.updateRateById( rate.toSupportedCharacters().toStandardDigits().toDoubleOrNull() ?: 0.0, diff --git a/client/src/commonTest/kotlin/com/oztechan/ccc/client/util/CalculatorUtilTest.kt b/client/src/commonTest/kotlin/com/oztechan/ccc/client/util/CalculatorUtilTest.kt index b83a21c6de..b74307a3e4 100644 --- a/client/src/commonTest/kotlin/com/oztechan/ccc/client/util/CalculatorUtilTest.kt +++ b/client/src/commonTest/kotlin/com/oztechan/ccc/client/util/CalculatorUtilTest.kt @@ -75,6 +75,18 @@ class CalculatorUtilTest { assertEquals("1 234 567.789", actualDouble.getFormatted()) } + @Test + @Suppress("StringLiteralDuplication") + fun trimTrailingZero() { + assertEquals("12", "12".trimTrailingZero()) + assertEquals("12", "12.".trimTrailingZero()) + assertEquals("12", "12.0".trimTrailingZero()) + assertEquals("12", "12.00".trimTrailingZero()) + assertEquals("12.001", "12.001".trimTrailingZero()) + assertEquals("12.001", "12.0010".trimTrailingZero()) + } + + @Test fun toStandardDigits() { // https://en.wikipedia.org/w/index.php?title=Hindu%E2%80%93Arabic_numeral_system diff --git a/ios/CCC/UI/Notifications/NotificationItem.swift b/ios/CCC/UI/Notifications/NotificationItem.swift index 4fe6e139cb..fbdb14b2e3 100644 --- a/ios/CCC/UI/Notifications/NotificationItem.swift +++ b/ios/CCC/UI/Notifications/NotificationItem.swift @@ -19,7 +19,7 @@ struct NotificationItem: View { @Binding var isBaseBarShown: Bool @Binding var isTargetBarShown: Bool - @State var notification: Client.Notification + let notification: Client.Notification let event: NotificationEvent var body: some View { diff --git a/ios/CCC/UI/Notifications/NotificationView.swift b/ios/CCC/UI/Notifications/NotificationView.swift index fb6fef058a..9f26df1dc9 100644 --- a/ios/CCC/UI/Notifications/NotificationView.swift +++ b/ios/CCC/UI/Notifications/NotificationView.swift @@ -108,7 +108,9 @@ struct NotificationView: View { targetBarInfo.notification = (effect as! NotificationEffect.SelectTarget).notification targetBarInfo.isShown.toggle() case is NotificationEffect.MaximumInput: - showSnack(text: MR.strings().text_max_input.get(), isCentered: true) + showSnack(text: MR.strings().text_max_input.get(), isTop: true) + case is NotificationEffect.InvalidInput: + showSnack(text: MR.strings().text_invalid_input.get(), isTop: true) default: logger.i(message: {"NotificationView unknown effect"}) } diff --git a/ios/CCC/Util/SnackBar.swift b/ios/CCC/Util/SnackBar.swift index 8bad01b6bf..92859954d1 100644 --- a/ios/CCC/Util/SnackBar.swift +++ b/ios/CCC/Util/SnackBar.swift @@ -15,8 +15,9 @@ func showSnack( buttonText: String? = nil, action: (() -> Void)? = nil, iconImage: UIImage = MR.images().ic_app_logo.get(), - isCentered: Bool = false + isTop: Bool = false ) { + SwiftMessages.hide(animated: false) let view = MessageView.viewFromNib(layout: .cardView) view.configureTheme( @@ -55,10 +56,9 @@ func showSnack( } var config = SwiftMessages.defaultConfig - config.presentationStyle = isCentered ? .center : .bottom + config.presentationStyle = isTop ? .top : .bottom config.presentationContext = .window(windowLevel: UIWindow.Level.normal) - SwiftMessages.hide(animated: false) SwiftMessages.show(config: config, view: view) } diff --git a/resources/src/commonMain/resources/MR/base/strings.xml b/resources/src/commonMain/resources/MR/base/strings.xml index 440760afe0..836fdd98f0 100644 --- a/resources/src/commonMain/resources/MR/base/strings.xml +++ b/resources/src/commonMain/resources/MR/base/strings.xml @@ -48,6 +48,7 @@ No data! Check your connection or try again later. Copied to clipboard! + Invalid input! You can rate us and review our app in the market :) From 9a8f45126bb40613e7cfb853cb6c9df2ecde4d41 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Tue, 17 May 2022 23:12:41 +0300 Subject: [PATCH 11/40] [#738] Set maximum limit for Notifications (#739) * [#738] Set maximum limit for Notifications * [#738] Set maximum limit for Notifications * [#738] Set maximum limit for Notifications * [#738] Set maximum limit for Notifications --- .../oztechan/ccc/client/mapper/Notification.kt | 3 +-- .../oztechan/ccc/client/util/CalculatorUtil.kt | 2 -- .../viewmodel/notification/NotificationSEED.kt | 2 ++ .../notification/NotificationViewModel.kt | 15 ++++++++++----- .../ccc/client/util/CalculatorUtilTest.kt | 12 ------------ .../db/notification/NotificationRepository.kt | 1 + .../db/notification/NotificationRepositoryImpl.kt | 7 +++++++ .../oztechan/ccc/common/db/sql/Notification.sq | 2 +- ios/CCC/UI/Notifications/NotificationView.swift | 2 ++ .../src/commonMain/resources/MR/base/strings.xml | 1 + 10 files changed, 25 insertions(+), 22 deletions(-) diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt index b1c8207417..61eef1cd63 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt @@ -1,6 +1,5 @@ package com.oztechan.ccc.client.mapper -import com.oztechan.ccc.client.util.trimTrailingZero import com.oztechan.ccc.common.model.Notification import com.oztechan.ccc.client.model.Notification as NotificationUIModel @@ -9,7 +8,7 @@ fun Notification.toUIModel() = NotificationUIModel( base = base, target = target, isGreater = isGreater, - rate = rate.toString().trimTrailingZero() + rate = rate.toString() ) fun List.toUIModelList() = map { diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/util/CalculatorUtil.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/util/CalculatorUtil.kt index 0f455b09a1..c116294588 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/util/CalculatorUtil.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/util/CalculatorUtil.kt @@ -35,8 +35,6 @@ fun String.toStandardDigits(): String { return builder.toString() } -fun String.trimTrailingZero() = replace("0*$".toRegex(), "").replace("\\.$".toRegex(), "") - fun Currency.getCurrencyConversionByRate( base: String, rate: Rates? diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt index 139d79869f..dd018be700 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt @@ -20,6 +20,7 @@ sealed class NotificationEffect : BaseEffect() { data class SelectTarget(val notification: Notification) : NotificationEffect() object MaximumInput : NotificationEffect() object InvalidInput : NotificationEffect() + object MaximumNumberOfNotification : NotificationEffect() } interface NotificationEvent : BaseEvent { @@ -37,6 +38,7 @@ interface NotificationEvent : BaseEvent { class NotificationData : BaseData() { companion object { const val MAXIMUM_INPUT = 9 + const val MAXIMUM_NUMBER_OF_NOTIFICATIONS = 5 } } diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt index 62272175f2..a8e9cc8f1c 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt @@ -8,6 +8,7 @@ import com.oztechan.ccc.client.util.launchIgnored import com.oztechan.ccc.client.util.toStandardDigits import com.oztechan.ccc.client.util.toSupportedCharacters import com.oztechan.ccc.client.viewmodel.notification.NotificationData.Companion.MAXIMUM_INPUT +import com.oztechan.ccc.client.viewmodel.notification.NotificationData.Companion.MAXIMUM_NUMBER_OF_NOTIFICATIONS import com.oztechan.ccc.common.db.currency.CurrencyRepository import com.oztechan.ccc.common.db.notification.NotificationRepository import kotlinx.coroutines.flow.MutableSharedFlow @@ -71,11 +72,15 @@ class NotificationViewModel( override fun onAddClick() { Logger.d { "NotificationViewModel onAddClick" } - currencyRepository.getActiveCurrencies().let { list -> - notificationRepository.addNotification( - base = list.firstOrNull()?.name ?: "", - target = list.lastOrNull()?.name ?: "" - ) + if (notificationRepository.getNotifications().size >= MAXIMUM_NUMBER_OF_NOTIFICATIONS) { + clientScope.launch { _effect.emit(NotificationEffect.MaximumNumberOfNotification) } + } else { + currencyRepository.getActiveCurrencies().let { list -> + notificationRepository.addNotification( + base = list.firstOrNull()?.name ?: "", + target = list.lastOrNull()?.name ?: "" + ) + } } } diff --git a/client/src/commonTest/kotlin/com/oztechan/ccc/client/util/CalculatorUtilTest.kt b/client/src/commonTest/kotlin/com/oztechan/ccc/client/util/CalculatorUtilTest.kt index b74307a3e4..b83a21c6de 100644 --- a/client/src/commonTest/kotlin/com/oztechan/ccc/client/util/CalculatorUtilTest.kt +++ b/client/src/commonTest/kotlin/com/oztechan/ccc/client/util/CalculatorUtilTest.kt @@ -75,18 +75,6 @@ class CalculatorUtilTest { assertEquals("1 234 567.789", actualDouble.getFormatted()) } - @Test - @Suppress("StringLiteralDuplication") - fun trimTrailingZero() { - assertEquals("12", "12".trimTrailingZero()) - assertEquals("12", "12.".trimTrailingZero()) - assertEquals("12", "12.0".trimTrailingZero()) - assertEquals("12", "12.00".trimTrailingZero()) - assertEquals("12.001", "12.001".trimTrailingZero()) - assertEquals("12.001", "12.0010".trimTrailingZero()) - } - - @Test fun toStandardDigits() { // https://en.wikipedia.org/w/index.php?title=Hindu%E2%80%93Arabic_numeral_system diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt index b08ea03065..54adb787f7 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.Flow interface NotificationRepository { fun addNotification(base: String, target: String) fun collectNotifications(): Flow> + fun getNotifications(): List fun deleteNotification(id: Long) fun updateBaseById(base: String, id: Long) fun updateTargetById(target: String, id: Long) diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt index e3b8cf92d6..0658130262 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt @@ -4,6 +4,7 @@ import co.touchlab.kermit.Logger import com.oztechan.ccc.common.db.sql.NotificationQueries import com.oztechan.ccc.common.mapper.mapToModel import com.oztechan.ccc.common.mapper.toLong +import com.oztechan.ccc.common.mapper.toModelList import com.squareup.sqldelight.runtime.coroutines.asFlow import com.squareup.sqldelight.runtime.coroutines.mapToList @@ -24,6 +25,12 @@ class NotificationRepositoryImpl( .mapToModel() .also { Logger.v { "NotificationRepositoryImpl collectNotifications" } } + override fun getNotifications() = notificationQueries + .getNotifications() + .executeAsList() + .toModelList() + .also { Logger.v { "NotificationRepositoryImpl collectNotifications" } } + override fun deleteNotification(id: Long) = notificationQueries .deleteNotification(id) .also { Logger.v { "NotificationRepositoryImpl addNotification $id" } } diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq index 52f1923189..0d08d45852 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq @@ -3,7 +3,7 @@ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, base TEXT NOT NULL, target TEXT NOT NULL, isGreater INTEGER NOT NULL DEFAULT 1, -rate REAL NOT NULL DEFAULT 1.0 +rate REAL NOT NULL DEFAULT 0.0 ); addNotification: diff --git a/ios/CCC/UI/Notifications/NotificationView.swift b/ios/CCC/UI/Notifications/NotificationView.swift index 9f26df1dc9..fc38a0614b 100644 --- a/ios/CCC/UI/Notifications/NotificationView.swift +++ b/ios/CCC/UI/Notifications/NotificationView.swift @@ -111,6 +111,8 @@ struct NotificationView: View { showSnack(text: MR.strings().text_max_input.get(), isTop: true) case is NotificationEffect.InvalidInput: showSnack(text: MR.strings().text_invalid_input.get(), isTop: true) + case is NotificationEffect.MaximumNumberOfNotification: + showSnack(text: MR.strings().text_maximum_number_of_notifications.get(), isTop: true) default: logger.i(message: {"NotificationView unknown effect"}) } diff --git a/resources/src/commonMain/resources/MR/base/strings.xml b/resources/src/commonMain/resources/MR/base/strings.xml index 836fdd98f0..6da1bd51e7 100644 --- a/resources/src/commonMain/resources/MR/base/strings.xml +++ b/resources/src/commonMain/resources/MR/base/strings.xml @@ -49,6 +49,7 @@ Check your connection or try again later. Copied to clipboard! Invalid input! + Maximum number of notifications exceeded You can rate us and review our app in the market :) From c45b31f653d07c533c4ed6657686f1f4057e26e8 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Thu, 19 May 2022 16:01:49 +0300 Subject: [PATCH 12/40] [#733] Auto assign PRs to creator (#742) * [#733] Auto assign PRs to creator * [#733] Auto assign PRs to creator --- .github/auto_assign.yml | 1 + .github/workflows/board.yml | 12 +----------- 2 files changed, 2 insertions(+), 11 deletions(-) create mode 100644 .github/auto_assign.yml diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 0000000000..89a8617cbb --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1 @@ +addAssignees: true diff --git a/.github/workflows/board.yml b/.github/workflows/board.yml index 07d05e8425..ad29196a90 100644 --- a/.github/workflows/board.yml +++ b/.github/workflows/board.yml @@ -48,17 +48,7 @@ jobs: status_value: "🚧 In Progress" - name: 'Move PR to "🏗 PR Review"' - if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'review_requested') - uses: leonsteinhaeuser/project-beta-automations@v1.2.1 - with: - gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - organization: Oztechan - project_id: 2 - resource_node_id: ${{ github.event.pull_request.node_id }} - status_value: "🏗 PR Review" - - - name: 'Move PR to "🏗 PR Review"' - if: github.event_name == 'pull_request' && github.event.action == 'ready_for_review' + if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'ready_for_review') uses: leonsteinhaeuser/project-beta-automations@v1.2.1 with: gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} From 4888b92bf4064e4a9d3c2313c727dfdb72fd986c Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Thu, 19 May 2022 16:04:37 +0300 Subject: [PATCH 13/40] [#733] Auto assign PRs to creator (#743) --- .github/auto_assign.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml index 89a8617cbb..717af2a0c8 100644 --- a/.github/auto_assign.yml +++ b/.github/auto_assign.yml @@ -1 +1 @@ -addAssignees: true +addAssignees: author From 381af086d6acfb389c9ab32b6a30eb8f114b877d Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Fri, 20 May 2022 00:16:13 +0300 Subject: [PATCH 14/40] [#740] Style Notification Rate Input field (#741) --- .../oztechan/ccc/client/util/AndroidCalculatorUtil.kt | 6 ++++++ .../com/oztechan/ccc/client/mapper/Notification.kt | 3 ++- .../com/oztechan/ccc/client/util/CalculatorUtil.kt | 2 ++ .../com/oztechan/ccc/client/util/CalculatorUtilTest.kt | 9 +++++++++ .../com/oztechan/ccc/client/util/IOSCalculatorUtil.kt | 8 ++++++++ .../common/db/notification/NotificationRepositoryImpl.kt | 2 +- ios/CCC/UI/Notifications/NotificationItem.swift | 3 +++ 7 files changed, 31 insertions(+), 2 deletions(-) diff --git a/client/src/androidMain/kotlin/com/oztechan/ccc/client/util/AndroidCalculatorUtil.kt b/client/src/androidMain/kotlin/com/oztechan/ccc/client/util/AndroidCalculatorUtil.kt index 3b6b5a2f29..c7a92c4bbc 100644 --- a/client/src/androidMain/kotlin/com/oztechan/ccc/client/util/AndroidCalculatorUtil.kt +++ b/client/src/androidMain/kotlin/com/oztechan/ccc/client/util/AndroidCalculatorUtil.kt @@ -4,6 +4,7 @@ package com.oztechan.ccc.client.util +import com.oztechan.ccc.client.viewmodel.notification.NotificationData import java.text.DecimalFormat import java.text.DecimalFormatSymbols import java.util.Locale @@ -25,3 +26,8 @@ actual fun Double.getFormatted(): String { decimalFormat = "$decimalFormat#" return DecimalFormat(decimalFormat, symbols).format(this) } + +actual fun Double.removeScientificNotation() = DecimalFormat("#.#").apply { + maximumFractionDigits = NotificationData.MAXIMUM_INPUT + decimalFormatSymbols = DecimalFormatSymbols.getInstance(Locale.ENGLISH) +}.format(this) diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt index 61eef1cd63..99e9d9ef69 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt @@ -1,5 +1,6 @@ package com.oztechan.ccc.client.mapper +import com.oztechan.ccc.client.util.removeScientificNotation import com.oztechan.ccc.common.model.Notification import com.oztechan.ccc.client.model.Notification as NotificationUIModel @@ -8,7 +9,7 @@ fun Notification.toUIModel() = NotificationUIModel( base = base, target = target, isGreater = isGreater, - rate = rate.toString() + rate = rate.removeScientificNotation() ) fun List.toUIModelList() = map { diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/util/CalculatorUtil.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/util/CalculatorUtil.kt index c116294588..fbc81ca2d6 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/util/CalculatorUtil.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/util/CalculatorUtil.kt @@ -13,6 +13,8 @@ import com.oztechan.ccc.common.model.Rates expect fun Double.getFormatted(): String +expect fun Double.removeScientificNotation(): String + fun Rates?.calculateResult(name: String, input: String?) = this ?.whetherNot { input.isNullOrEmpty() } ?.getConversionByName(name) diff --git a/client/src/commonTest/kotlin/com/oztechan/ccc/client/util/CalculatorUtilTest.kt b/client/src/commonTest/kotlin/com/oztechan/ccc/client/util/CalculatorUtilTest.kt index b83a21c6de..4795f90219 100644 --- a/client/src/commonTest/kotlin/com/oztechan/ccc/client/util/CalculatorUtilTest.kt +++ b/client/src/commonTest/kotlin/com/oztechan/ccc/client/util/CalculatorUtilTest.kt @@ -75,6 +75,15 @@ class CalculatorUtilTest { assertEquals("1 234 567.789", actualDouble.getFormatted()) } + @Test + fun removeScientificNotation() { + assertEquals("1234567.789", 1234567.7890.removeScientificNotation()) + assertEquals("1234567.789123", 1234567.7891230.removeScientificNotation()) + assertEquals("1234567.7", 1234567.7.removeScientificNotation()) + assertEquals("0.7", .7.removeScientificNotation()) + assertEquals("7.7", 7.7.removeScientificNotation()) + } + @Test fun toStandardDigits() { // https://en.wikipedia.org/w/index.php?title=Hindu%E2%80%93Arabic_numeral_system diff --git a/client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCalculatorUtil.kt b/client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCalculatorUtil.kt index 3eb35e7499..6471dbf853 100644 --- a/client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCalculatorUtil.kt +++ b/client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCalculatorUtil.kt @@ -4,6 +4,7 @@ package com.oztechan.ccc.client.util +import com.oztechan.ccc.client.viewmodel.notification.NotificationData import platform.Foundation.NSNumber import platform.Foundation.NSNumberFormatter import platform.Foundation.NSNumberFormatterDecimalStyle @@ -13,3 +14,10 @@ actual fun Double.getFormatted() = NSNumberFormatter().apply { setGroupingSeparator(" ") setDecimalSeparator(".") }.stringFromNumber(NSNumber(this)) ?: "" + +actual fun Double.removeScientificNotation() = NSNumberFormatter().apply { + setNumberStyle(NSNumberFormatterDecimalStyle) + setGroupingSeparator("") + setDecimalSeparator(".") + setMaximumFractionDigits(NotificationData.MAXIMUM_INPUT.toULong()) +}.stringFromNumber(NSNumber(this)) ?: "" diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt index 0658130262..b46df10c6b 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt @@ -29,7 +29,7 @@ class NotificationRepositoryImpl( .getNotifications() .executeAsList() .toModelList() - .also { Logger.v { "NotificationRepositoryImpl collectNotifications" } } + .also { Logger.v { "NotificationRepositoryImpl getNotifications" } } override fun deleteNotification(id: Long) = notificationQueries .deleteNotification(id) diff --git a/ios/CCC/UI/Notifications/NotificationItem.swift b/ios/CCC/UI/Notifications/NotificationItem.swift index fbdb14b2e3..e823d2007e 100644 --- a/ios/CCC/UI/Notifications/NotificationItem.swift +++ b/ios/CCC/UI/Notifications/NotificationItem.swift @@ -51,6 +51,9 @@ struct NotificationItem: View { .multilineTextAlignment(TextAlignment.center) .fixedSize() .lineLimit(1) + .padding(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15)) + .background(MR.colors().background_weak.get()) + .cornerRadius(7) .onChange(of: amount) { amount = event.onRateChange(notification: notification, rate: $0) } From 8513ce3d09d3fbae987971177a5d0f76c7c5f101 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Fri, 20 May 2022 10:12:45 +0300 Subject: [PATCH 15/40] [#744] Add notification screen description (#745) * [#740] Style Notification Rate Input field * [#744] Add notification screen description --- ios/CCC/UI/Notifications/NotificationView.swift | 8 ++++++++ ios/CCC/UI/Notifications/NotificationsToolbarView.swift | 1 + resources/src/commonMain/resources/MR/base/strings.xml | 1 + 3 files changed, 10 insertions(+) diff --git a/ios/CCC/UI/Notifications/NotificationView.swift b/ios/CCC/UI/Notifications/NotificationView.swift index fc38a0614b..e114193bfe 100644 --- a/ios/CCC/UI/Notifications/NotificationView.swift +++ b/ios/CCC/UI/Notifications/NotificationView.swift @@ -28,7 +28,15 @@ struct NotificationView: View { VStack { NotificationsToolbarView(backEvent: observable.event.onBackClick) + + Text(MR.strings().txt_notifications_description.get()) + .font(.footnote) + .padding(18) + .multilineTextAlignment(.center) .background(MR.colors().background_strong.get()) + .foregroundColor(MR.colors().text_weak.get()) + .contentShape(Rectangle()) + .padding(-8) Form { List(observable.state.notificationList, id: \.id) { notification in diff --git a/ios/CCC/UI/Notifications/NotificationsToolbarView.swift b/ios/CCC/UI/Notifications/NotificationsToolbarView.swift index 5d6d302838..9222950d17 100644 --- a/ios/CCC/UI/Notifications/NotificationsToolbarView.swift +++ b/ios/CCC/UI/Notifications/NotificationsToolbarView.swift @@ -23,5 +23,6 @@ struct NotificationsToolbarView: View { } .frame(width: .infinity, height: 36) .padding(EdgeInsets(top: 15, leading: 10, bottom: 6, trailing: 20)) + .background(MR.colors().background_strong.get()) } } diff --git a/resources/src/commonMain/resources/MR/base/strings.xml b/resources/src/commonMain/resources/MR/base/strings.xml index 6da1bd51e7..cb8d1d0270 100644 --- a/resources/src/commonMain/resources/MR/base/strings.xml +++ b/resources/src/commonMain/resources/MR/base/strings.xml @@ -127,6 +127,7 @@ Notifications + Watchers will be checked every hour and if the condition is verified then you will get a notification! Rate > < From 18c7db257477ae94bea17ccb13eb4db6b40f8eed Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 22 May 2022 12:08:08 +0300 Subject: [PATCH 16/40] [#746] Rename notifications to watchers (#750) --- .../android/ui/settings/SettingsFragment.kt | 2 +- .../ccc/client/util/AndroidCalculatorUtil.kt | 4 +- .../ccc/client/di/module/ClientModule.kt | 4 +- .../ccc/client/mapper/Notification.kt | 8 +- .../model/{Notification.kt => Watcher.kt} | 2 +- .../notification/NotificationSEED.kt | 52 -------- .../notification/NotificationViewModel.kt | 119 ------------------ .../client/viewmodel/settings/SettingsSEED.kt | 10 +- .../viewmodel/settings/SettingsViewModel.kt | 14 +-- .../client/viewmodel/watchers/WatchersSEED.kt | 52 ++++++++ .../viewmodel/watchers/WatchersViewModel.kt | 119 ++++++++++++++++++ .../client/viewmodel/SettingsViewModelTest.kt | 34 ++--- .../ccc/client/util/IOSCalculatorUtil.kt | 4 +- .../com/oztechan/ccc/common/db/migrate/4.sqm | 2 +- .../db/notification/NotificationRepository.kt | 15 --- .../NotificationRepositoryImpl.kt | 53 -------- .../ccc/common/db/sql/Notification.sq | 28 ----- .../com/oztechan/ccc/common/db/sql/Watcher.sq | 28 +++++ .../common/db/watcher/WatcherRepository.kt | 15 +++ .../db/watcher/WatcherRepositoryImpl.kt | 53 ++++++++ .../ccc/common/di/modules/DatabaseModule.kt | 8 +- .../ccc/common/mapper/Notification.kt | 10 +- .../model/{Notification.kt => Watcher.kt} | 2 +- ios/CCC.xcodeproj/project.pbxproj | 30 ++--- ios/CCC/DI/Koin.swift | 8 +- ios/CCC/UI/Settings/SettingsItemView.swift | 3 +- ios/CCC/UI/Settings/SettingsView.swift | 14 +-- .../WatcherItem.swift} | 26 ++-- .../WatchersToolbarView.swift} | 6 +- .../WatchersView.swift} | 56 ++++----- .../commonMain/resources/MR/base/strings.xml | 12 +- 31 files changed, 396 insertions(+), 397 deletions(-) rename client/src/commonMain/kotlin/com/oztechan/ccc/client/model/{Notification.kt => Watcher.kt} (86%) delete mode 100644 client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt delete mode 100644 client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt create mode 100644 client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt create mode 100644 client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt delete mode 100644 common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt delete mode 100644 common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt delete mode 100644 common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq create mode 100644 common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Watcher.sq create mode 100644 common/src/commonMain/kotlin/com/oztechan/ccc/common/db/watcher/WatcherRepository.kt create mode 100644 common/src/commonMain/kotlin/com/oztechan/ccc/common/db/watcher/WatcherRepositoryImpl.kt rename common/src/commonMain/kotlin/com/oztechan/ccc/common/model/{Notification.kt => Watcher.kt} (86%) rename ios/CCC/UI/{Notifications/NotificationItem.swift => Watchers/WatcherItem.swift} (66%) rename ios/CCC/UI/{Notifications/NotificationsToolbarView.swift => Watchers/WatchersToolbarView.swift} (80%) rename ios/CCC/UI/{Notifications/NotificationView.swift => Watchers/WatchersView.swift} (66%) diff --git a/android/src/main/kotlin/com/oztechan/ccc/android/ui/settings/SettingsFragment.kt b/android/src/main/kotlin/com/oztechan/ccc/android/ui/settings/SettingsFragment.kt index 21ec1ed413..86005d9be1 100644 --- a/android/src/main/kotlin/com/oztechan/ccc/android/ui/settings/SettingsFragment.kt +++ b/android/src/main/kotlin/com/oztechan/ccc/android/ui/settings/SettingsFragment.kt @@ -184,7 +184,7 @@ class SettingsFragment : BaseVBFragment() { requireView(), R.string.txt_ads_already_disabled ) - SettingsEffect.OpenNotifications -> TODO("No Android implementation yet") + SettingsEffect.OpenWatchers -> TODO("No Android implementation yet") } }.launchIn(viewLifecycleOwner.lifecycleScope) diff --git a/client/src/androidMain/kotlin/com/oztechan/ccc/client/util/AndroidCalculatorUtil.kt b/client/src/androidMain/kotlin/com/oztechan/ccc/client/util/AndroidCalculatorUtil.kt index c7a92c4bbc..8c30387ca8 100644 --- a/client/src/androidMain/kotlin/com/oztechan/ccc/client/util/AndroidCalculatorUtil.kt +++ b/client/src/androidMain/kotlin/com/oztechan/ccc/client/util/AndroidCalculatorUtil.kt @@ -4,7 +4,7 @@ package com.oztechan.ccc.client.util -import com.oztechan.ccc.client.viewmodel.notification.NotificationData +import com.oztechan.ccc.client.viewmodel.watchers.WatchersData import java.text.DecimalFormat import java.text.DecimalFormatSymbols import java.util.Locale @@ -28,6 +28,6 @@ actual fun Double.getFormatted(): String { } actual fun Double.removeScientificNotation() = DecimalFormat("#.#").apply { - maximumFractionDigits = NotificationData.MAXIMUM_INPUT + maximumFractionDigits = WatchersData.MAXIMUM_INPUT decimalFormatSymbols = DecimalFormatSymbols.getInstance(Locale.ENGLISH) }.format(this) diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt index 93526fdd9a..e1d9b14d46 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt @@ -7,9 +7,9 @@ import com.oztechan.ccc.client.viewmodel.adremove.AdRemoveViewModel import com.oztechan.ccc.client.viewmodel.calculator.CalculatorViewModel import com.oztechan.ccc.client.viewmodel.currencies.CurrenciesViewModel import com.oztechan.ccc.client.viewmodel.main.MainViewModel -import com.oztechan.ccc.client.viewmodel.notification.NotificationViewModel import com.oztechan.ccc.client.viewmodel.selectcurrency.SelectCurrencyViewModel import com.oztechan.ccc.client.viewmodel.settings.SettingsViewModel +import com.oztechan.ccc.client.viewmodel.watchers.WatchersViewModel import com.oztechan.ccc.config.ConfigManager import com.oztechan.ccc.config.ConfigManagerImpl import org.koin.dsl.module @@ -21,7 +21,7 @@ var clientModule = module { viewModelDefinition { CalculatorViewModel(get(), get(), get(), get(), get()) } viewModelDefinition { SelectCurrencyViewModel(get()) } viewModelDefinition { AdRemoveViewModel(get()) } - viewModelDefinition { NotificationViewModel(get(), get()) } + viewModelDefinition { WatchersViewModel(get(), get()) } single { ConfigManagerImpl() } single { SessionManagerImpl(get(), get()) } diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt index 99e9d9ef69..eb39dbca55 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/mapper/Notification.kt @@ -1,10 +1,10 @@ package com.oztechan.ccc.client.mapper import com.oztechan.ccc.client.util.removeScientificNotation -import com.oztechan.ccc.common.model.Notification -import com.oztechan.ccc.client.model.Notification as NotificationUIModel +import com.oztechan.ccc.common.model.Watcher +import com.oztechan.ccc.client.model.Watcher as WatcherUIModel -fun Notification.toUIModel() = NotificationUIModel( +fun Watcher.toUIModel() = WatcherUIModel( id = id, base = base, target = target, @@ -12,6 +12,6 @@ fun Notification.toUIModel() = NotificationUIModel( rate = rate.removeScientificNotation() ) -fun List.toUIModelList() = map { +fun List.toUIModelList() = map { it.toUIModel() } \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/model/Notification.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/model/Watcher.kt similarity index 86% rename from client/src/commonMain/kotlin/com/oztechan/ccc/client/model/Notification.kt rename to client/src/commonMain/kotlin/com/oztechan/ccc/client/model/Watcher.kt index 67ada9f2af..3483b21e4f 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/model/Notification.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/model/Watcher.kt @@ -1,6 +1,6 @@ package com.oztechan.ccc.client.model -data class Notification( +data class Watcher( val id: Long, val base: String, val target: String, diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt deleted file mode 100644 index dd018be700..0000000000 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationSEED.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.oztechan.ccc.client.viewmodel.notification - - -import com.oztechan.ccc.client.base.BaseData -import com.oztechan.ccc.client.base.BaseEffect -import com.oztechan.ccc.client.base.BaseEvent -import com.oztechan.ccc.client.base.BaseState -import com.oztechan.ccc.client.model.Notification -import kotlinx.coroutines.flow.MutableStateFlow - -data class NotificationState( - val notificationList: List = emptyList(), - val base: String = "", - val target: String = "" -) : BaseState() - -sealed class NotificationEffect : BaseEffect() { - object Back : NotificationEffect() - data class SelectBase(val notification: Notification) : NotificationEffect() - data class SelectTarget(val notification: Notification) : NotificationEffect() - object MaximumInput : NotificationEffect() - object InvalidInput : NotificationEffect() - object MaximumNumberOfNotification : NotificationEffect() -} - -interface NotificationEvent : BaseEvent { - fun onBackClick() - fun onBaseClick(notification: Notification) - fun onTargetClick(notification: Notification) - fun onBaseChanged(notification: Notification?, newBase: String) - fun onTargetChanged(notification: Notification?, newTarget: String) - fun onAddClick() - fun onDeleteClick(notification: Notification) - fun onRelationChange(notification: Notification, isGreater: Boolean) - fun onRateChange(notification: Notification, rate: String): String -} - -class NotificationData : BaseData() { - companion object { - const val MAXIMUM_INPUT = 9 - const val MAXIMUM_NUMBER_OF_NOTIFICATIONS = 5 - } -} - -// Extension -fun MutableStateFlow.update( - notificationList: List = value.notificationList, -) { - value = value.copy( - notificationList = notificationList - ) -} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt deleted file mode 100644 index a8e9cc8f1c..0000000000 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/notification/NotificationViewModel.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.oztechan.ccc.client.viewmodel.notification - -import co.touchlab.kermit.Logger -import com.oztechan.ccc.client.base.BaseSEEDViewModel -import com.oztechan.ccc.client.mapper.toUIModelList -import com.oztechan.ccc.client.model.Notification -import com.oztechan.ccc.client.util.launchIgnored -import com.oztechan.ccc.client.util.toStandardDigits -import com.oztechan.ccc.client.util.toSupportedCharacters -import com.oztechan.ccc.client.viewmodel.notification.NotificationData.Companion.MAXIMUM_INPUT -import com.oztechan.ccc.client.viewmodel.notification.NotificationData.Companion.MAXIMUM_NUMBER_OF_NOTIFICATIONS -import com.oztechan.ccc.common.db.currency.CurrencyRepository -import com.oztechan.ccc.common.db.notification.NotificationRepository -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch - -class NotificationViewModel( - private val currencyRepository: CurrencyRepository, - private val notificationRepository: NotificationRepository -) : BaseSEEDViewModel(), NotificationEvent { - // region SEED - private val _state = MutableStateFlow(NotificationState()) - override val state = _state.asStateFlow() - - override val event = this as NotificationEvent - - private val _effect = MutableSharedFlow() - override val effect = _effect.asSharedFlow() - - override val data = NotificationData() - - init { - notificationRepository.collectNotifications() - .onEach { - _state.update(notificationList = it.toUIModelList()) - }.launchIn(clientScope) - } - - override fun onBackClick() = clientScope.launchIgnored { - Logger.d { "NotificationViewModel onBackClick" } - _effect.emit(NotificationEffect.Back) - } - - override fun onBaseClick(notification: Notification) = clientScope.launchIgnored { - Logger.d { "NotificationViewModel onBaseClick $notification" } - _effect.emit(NotificationEffect.SelectBase(notification)) - } - - override fun onTargetClick(notification: Notification) = clientScope.launchIgnored { - Logger.d { "NotificationViewModel onTargetClick $notification" } - _effect.emit(NotificationEffect.SelectTarget(notification)) - } - - override fun onBaseChanged(notification: Notification?, newBase: String) { - Logger.d { "NotificationViewModel onBaseChanged $notification $newBase" } - notification?.id?.let { - notificationRepository.updateBaseById(newBase, it) - } - } - - override fun onTargetChanged(notification: Notification?, newTarget: String) { - Logger.d { "NotificationViewModel onTargetChanged $notification $newTarget" } - notification?.id?.let { - notificationRepository.updateTargetById(newTarget, it) - } - } - - override fun onAddClick() { - Logger.d { "NotificationViewModel onAddClick" } - if (notificationRepository.getNotifications().size >= MAXIMUM_NUMBER_OF_NOTIFICATIONS) { - clientScope.launch { _effect.emit(NotificationEffect.MaximumNumberOfNotification) } - } else { - currencyRepository.getActiveCurrencies().let { list -> - notificationRepository.addNotification( - base = list.firstOrNull()?.name ?: "", - target = list.lastOrNull()?.name ?: "" - ) - } - } - } - - override fun onDeleteClick(notification: Notification) { - Logger.d { "NotificationViewModel onDeleteClick $notification" } - notificationRepository.deleteNotification(notification.id) - } - - override fun onRelationChange(notification: Notification, isGreater: Boolean) { - Logger.d { "NotificationViewModel onRelationChange $notification $isGreater" } - notificationRepository.updateRelationById(isGreater, notification.id) - } - - override fun onRateChange(notification: Notification, rate: String): String { - Logger.d { "NotificationViewModel onRateChange $notification $rate" } - - return when { - rate.length > MAXIMUM_INPUT -> { - clientScope.launch { _effect.emit(NotificationEffect.MaximumInput) } - rate.dropLast(1) - } - rate.toDoubleOrNull()?.isNaN() != false -> { - clientScope.launch { _effect.emit(NotificationEffect.InvalidInput) } - rate - } - else -> { - notificationRepository.updateRateById( - rate.toSupportedCharacters().toStandardDigits().toDoubleOrNull() ?: 0.0, - notification.id - ) - rate - } - } - } - // endregion -} diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt index 033c40117a..27c0e3750c 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.MutableStateFlow // State data class SettingsState( val activeCurrencyCount: Int = 0, - val activeNotificationCount: Int = 0, + val activeWatcherCount: Int = 0, val appThemeType: AppTheme = AppTheme.SYSTEM_DEFAULT, val addFreeEndDate: String = "", val loading: Boolean = false @@ -20,7 +20,7 @@ data class SettingsState( interface SettingsEvent : BaseEvent { fun onBackClick() fun onCurrenciesClick() - fun onNotificationsClicked() + fun onWatchersClicked() fun onFeedBackClick() fun onShareClick() fun onSupportUsClick() @@ -34,7 +34,7 @@ interface SettingsEvent : BaseEvent { sealed class SettingsEffect : BaseEffect() { object Back : SettingsEffect() object OpenCurrencies : SettingsEffect() - object OpenNotifications : SettingsEffect() + object OpenWatchers : SettingsEffect() object FeedBack : SettingsEffect() object Share : SettingsEffect() object SupportUs : SettingsEffect() @@ -58,14 +58,14 @@ data class SettingsData(var synced: Boolean = false) : BaseData() { // Extension fun MutableStateFlow.update( activeCurrencyCount: Int = value.activeCurrencyCount, - activeNotificationCount: Int = value.activeNotificationCount, + activeWatcherCount: Int = value.activeWatcherCount, appThemeType: AppTheme = value.appThemeType, addFreeEndDate: String = value.addFreeEndDate, loading: Boolean = value.loading ) { value = value.copy( activeCurrencyCount = activeCurrencyCount, - activeNotificationCount = activeNotificationCount, + activeWatcherCount = activeWatcherCount, appThemeType = appThemeType, addFreeEndDate = addFreeEndDate, loading = loading diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt index fd94c4e5fa..a1d91618fb 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt @@ -16,8 +16,8 @@ import com.oztechan.ccc.client.util.toDateString import com.oztechan.ccc.client.viewmodel.settings.SettingsData.Companion.SYNC_DELAY import com.oztechan.ccc.common.api.repo.ApiRepository import com.oztechan.ccc.common.db.currency.CurrencyRepository -import com.oztechan.ccc.common.db.notification.NotificationRepository import com.oztechan.ccc.common.db.offlinerates.OfflineRatesRepository +import com.oztechan.ccc.common.db.watcher.WatcherRepository import com.oztechan.ccc.common.settings.SettingsRepository import com.oztechan.ccc.common.util.nowAsLong import kotlinx.coroutines.delay @@ -34,7 +34,7 @@ class SettingsViewModel( private val apiRepository: ApiRepository, private val currencyRepository: CurrencyRepository, private val offlineRatesRepository: OfflineRatesRepository, - notificationRepository: NotificationRepository, + watcherRepository: WatcherRepository, private val sessionManager: SessionManager ) : BaseSEEDViewModel(), SettingsEvent { // region SEED @@ -60,9 +60,9 @@ class SettingsViewModel( _state.update(activeCurrencyCount = it.size) }.launchIn(clientScope) - notificationRepository.collectNotifications() + watcherRepository.collectWatchers() .onEach { - _state.update(activeNotificationCount = it.size) + _state.update(activeWatcherCount = it.size) }.launchIn(clientScope) } @@ -119,9 +119,9 @@ class SettingsViewModel( } - override fun onNotificationsClicked() = clientScope.launchIgnored { - Logger.d { "SettingsViewModel onNotificationsClicked" } - _effect.emit(SettingsEffect.OpenNotifications) + override fun onWatchersClicked() = clientScope.launchIgnored { + Logger.d { "SettingsViewModel onWatchersClicked" } + _effect.emit(SettingsEffect.OpenWatchers) } override fun onFeedBackClick() = clientScope.launchIgnored { diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt new file mode 100644 index 0000000000..d396977a9b --- /dev/null +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt @@ -0,0 +1,52 @@ +package com.oztechan.ccc.client.viewmodel.watchers + + +import com.oztechan.ccc.client.base.BaseData +import com.oztechan.ccc.client.base.BaseEffect +import com.oztechan.ccc.client.base.BaseEvent +import com.oztechan.ccc.client.base.BaseState +import com.oztechan.ccc.client.model.Watcher +import kotlinx.coroutines.flow.MutableStateFlow + +data class WatchersState( + val watcherList: List = emptyList(), + val base: String = "", + val target: String = "" +) : BaseState() + +sealed class WatchersEffect : BaseEffect() { + object Back : WatchersEffect() + data class SelectBase(val watcher: Watcher) : WatchersEffect() + data class SelectTarget(val watcher: Watcher) : WatchersEffect() + object MaximumInput : WatchersEffect() + object InvalidInput : WatchersEffect() + object MaximumNumberOfWatchers : WatchersEffect() +} + +interface WatchersEvent : BaseEvent { + fun onBackClick() + fun onBaseClick(watcher: Watcher) + fun onTargetClick(watcher: Watcher) + fun onBaseChanged(watcher: Watcher?, newBase: String) + fun onTargetChanged(watcher: Watcher?, newTarget: String) + fun onAddClick() + fun onDeleteClick(watcher: Watcher) + fun onRelationChange(watcher: Watcher, isGreater: Boolean) + fun onRateChange(watcher: Watcher, rate: String): String +} + +class WatchersData : BaseData() { + companion object { + const val MAXIMUM_INPUT = 9 + const val MAXIMUM_NUMBER_OF_WATCHER = 5 + } +} + +// Extension +fun MutableStateFlow.update( + watcherList: List = value.watcherList, +) { + value = value.copy( + watcherList = watcherList + ) +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt new file mode 100644 index 0000000000..a0f6fa2e93 --- /dev/null +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt @@ -0,0 +1,119 @@ +package com.oztechan.ccc.client.viewmodel.watchers + +import co.touchlab.kermit.Logger +import com.oztechan.ccc.client.base.BaseSEEDViewModel +import com.oztechan.ccc.client.mapper.toUIModelList +import com.oztechan.ccc.client.model.Watcher +import com.oztechan.ccc.client.util.launchIgnored +import com.oztechan.ccc.client.util.toStandardDigits +import com.oztechan.ccc.client.util.toSupportedCharacters +import com.oztechan.ccc.client.viewmodel.watchers.WatchersData.Companion.MAXIMUM_INPUT +import com.oztechan.ccc.client.viewmodel.watchers.WatchersData.Companion.MAXIMUM_NUMBER_OF_WATCHER +import com.oztechan.ccc.common.db.currency.CurrencyRepository +import com.oztechan.ccc.common.db.watcher.WatcherRepository +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch + +class WatchersViewModel( + private val currencyRepository: CurrencyRepository, + private val watcherRepository: WatcherRepository +) : BaseSEEDViewModel(), WatchersEvent { + // region SEED + private val _state = MutableStateFlow(WatchersState()) + override val state = _state.asStateFlow() + + override val event = this as WatchersEvent + + private val _effect = MutableSharedFlow() + override val effect = _effect.asSharedFlow() + + override val data = WatchersData() + + init { + watcherRepository.collectWatchers() + .onEach { + _state.update(watcherList = it.toUIModelList()) + }.launchIn(clientScope) + } + + override fun onBackClick() = clientScope.launchIgnored { + Logger.d { "WatcherViewModel onBackClick" } + _effect.emit(WatchersEffect.Back) + } + + override fun onBaseClick(watcher: Watcher) = clientScope.launchIgnored { + Logger.d { "WatcherViewModel onBaseClick $watcher" } + _effect.emit(WatchersEffect.SelectBase(watcher)) + } + + override fun onTargetClick(watcher: Watcher) = clientScope.launchIgnored { + Logger.d { "WatcherViewModel onTargetClick $watcher" } + _effect.emit(WatchersEffect.SelectTarget(watcher)) + } + + override fun onBaseChanged(watcher: Watcher?, newBase: String) { + Logger.d { "WatcherViewModel onBaseChanged $watcher $newBase" } + watcher?.id?.let { + watcherRepository.updateBaseById(newBase, it) + } + } + + override fun onTargetChanged(watcher: Watcher?, newTarget: String) { + Logger.d { "WatcherViewModel onTargetChanged $watcher $newTarget" } + watcher?.id?.let { + watcherRepository.updateTargetById(newTarget, it) + } + } + + override fun onAddClick() { + Logger.d { "WatcherViewModel onAddClick" } + if (watcherRepository.getWatchers().size >= MAXIMUM_NUMBER_OF_WATCHER) { + clientScope.launch { _effect.emit(WatchersEffect.MaximumNumberOfWatchers) } + } else { + currencyRepository.getActiveCurrencies().let { list -> + watcherRepository.addWatcher( + base = list.firstOrNull()?.name ?: "", + target = list.lastOrNull()?.name ?: "" + ) + } + } + } + + override fun onDeleteClick(watcher: Watcher) { + Logger.d { "WatcherViewModel onDeleteClick $watcher" } + watcherRepository.deleteWatcher(watcher.id) + } + + override fun onRelationChange(watcher: Watcher, isGreater: Boolean) { + Logger.d { "WatcherViewModel onRelationChange $watcher $isGreater" } + watcherRepository.updateRelationById(isGreater, watcher.id) + } + + override fun onRateChange(watcher: Watcher, rate: String): String { + Logger.d { "WatcherViewModel onRateChange $watcher $rate" } + + return when { + rate.length > MAXIMUM_INPUT -> { + clientScope.launch { _effect.emit(WatchersEffect.MaximumInput) } + rate.dropLast(1) + } + rate.toDoubleOrNull()?.isNaN() != false -> { + clientScope.launch { _effect.emit(WatchersEffect.InvalidInput) } + rate + } + else -> { + watcherRepository.updateRateById( + rate.toSupportedCharacters().toStandardDigits().toDoubleOrNull() ?: 0.0, + watcher.id + ) + rate + } + } + } + // endregion +} diff --git a/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt b/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt index e7ad0adeee..eadffc16dd 100644 --- a/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt +++ b/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt @@ -17,10 +17,10 @@ import com.oztechan.ccc.client.viewmodel.settings.SettingsViewModel import com.oztechan.ccc.client.viewmodel.settings.update import com.oztechan.ccc.common.api.repo.ApiRepository import com.oztechan.ccc.common.db.currency.CurrencyRepository -import com.oztechan.ccc.common.db.notification.NotificationRepository import com.oztechan.ccc.common.db.offlinerates.OfflineRatesRepository +import com.oztechan.ccc.common.db.watcher.WatcherRepository import com.oztechan.ccc.common.model.Currency -import com.oztechan.ccc.common.model.Notification +import com.oztechan.ccc.common.model.Watcher import com.oztechan.ccc.common.runTest import com.oztechan.ccc.common.settings.SettingsRepository import com.oztechan.ccc.common.util.DAY @@ -56,7 +56,7 @@ class SettingsViewModelTest { private val offlineRatesRepository = mock(classOf()) @Mock - private val notificationRepository = mock(classOf()) + private val watcherRepository = mock(classOf()) @Mock private val sessionManager = mock(classOf()) @@ -67,7 +67,7 @@ class SettingsViewModelTest { apiRepository, currencyRepository, offlineRatesRepository, - notificationRepository, + watcherRepository, sessionManager ) } @@ -77,9 +77,9 @@ class SettingsViewModelTest { Currency("", "", "") ) - private val notificationList = listOf( - Notification(1, "EUR", "USD", true, 1.1), - Notification(2, "USD", "EUR", false, 2.3) + private val watcherLists = listOf( + Watcher(1, "EUR", "USD", true, 1.1), + Watcher(2, "USD", "EUR", false, 2.3) ) @BeforeTest @@ -98,9 +98,9 @@ class SettingsViewModelTest { .invocation { collectActiveCurrencies() } .thenReturn(flowOf(currencyList)) - given(notificationRepository) - .invocation { collectNotifications() } - .then { flowOf(notificationList) } + given(watcherRepository) + .invocation { collectWatchers() } + .then { flowOf(watcherLists) } } // SEED @@ -110,7 +110,7 @@ class SettingsViewModelTest { val state = MutableStateFlow(SettingsState()) val activeCurrencyCount = Random.nextInt() - val activeNotificationCount = Random.nextInt() + val activeWatcherCount = Random.nextInt() val appThemeType = AppTheme.getThemeByOrderOrDefault(Random.nextInt() % 3) val addFreeEndDate = "23.12.2121" val loading = Random.nextBoolean() @@ -118,14 +118,14 @@ class SettingsViewModelTest { state.before { state.update( activeCurrencyCount = activeCurrencyCount, - activeNotificationCount = activeNotificationCount, + activeWatcherCount = activeWatcherCount, appThemeType = appThemeType, addFreeEndDate = addFreeEndDate, loading = loading ) }.after { assertEquals(activeCurrencyCount, it?.activeCurrencyCount) - assertEquals(activeNotificationCount, it?.activeNotificationCount) + assertEquals(activeWatcherCount, it?.activeWatcherCount) assertEquals(appThemeType, it?.appThemeType) assertEquals(addFreeEndDate, it?.addFreeEndDate) assertEquals(loading, it?.loading) @@ -138,7 +138,7 @@ class SettingsViewModelTest { viewModel.state.firstOrNull().let { assertEquals(AppTheme.SYSTEM_DEFAULT, it?.appThemeType) // mocked -1 assertEquals(currencyList.size, it?.activeCurrencyCount) - assertEquals(notificationList.size, it?.activeNotificationCount) + assertEquals(watcherLists.size, it?.activeWatcherCount) } } @@ -269,10 +269,10 @@ class SettingsViewModelTest { @Test - fun onNotificationsClicked() = viewModel.effect.before { - viewModel.event.onNotificationsClicked() + fun onWatchersClicked() = viewModel.effect.before { + viewModel.event.onWatchersClicked() }.after { - assertEquals(SettingsEffect.OpenNotifications, it) + assertEquals(SettingsEffect.OpenWatchers, it) } @Test diff --git a/client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCalculatorUtil.kt b/client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCalculatorUtil.kt index 6471dbf853..5b4a64fea9 100644 --- a/client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCalculatorUtil.kt +++ b/client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCalculatorUtil.kt @@ -4,7 +4,7 @@ package com.oztechan.ccc.client.util -import com.oztechan.ccc.client.viewmodel.notification.NotificationData +import com.oztechan.ccc.client.viewmodel.watchers.WatchersData import platform.Foundation.NSNumber import platform.Foundation.NSNumberFormatter import platform.Foundation.NSNumberFormatterDecimalStyle @@ -19,5 +19,5 @@ actual fun Double.removeScientificNotation() = NSNumberFormatter().apply { setNumberStyle(NSNumberFormatterDecimalStyle) setGroupingSeparator("") setDecimalSeparator(".") - setMaximumFractionDigits(NotificationData.MAXIMUM_INPUT.toULong()) + setMaximumFractionDigits(WatchersData.MAXIMUM_INPUT.toULong()) }.stringFromNumber(NSNumber(this)) ?: "" diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/migrate/4.sqm b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/migrate/4.sqm index 2afad8c0df..65001132e7 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/migrate/4.sqm +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/migrate/4.sqm @@ -1,4 +1,4 @@ -CREATE TABLE IF NOT EXISTS notification( +CREATE TABLE IF NOT EXISTS watcher( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, base TEXT NOT NULL, target TEXT NOT NULL, diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt deleted file mode 100644 index 54adb787f7..0000000000 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepository.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.oztechan.ccc.common.db.notification - -import com.oztechan.ccc.common.model.Notification -import kotlinx.coroutines.flow.Flow - -interface NotificationRepository { - fun addNotification(base: String, target: String) - fun collectNotifications(): Flow> - fun getNotifications(): List - fun deleteNotification(id: Long) - fun updateBaseById(base: String, id: Long) - fun updateTargetById(target: String, id: Long) - fun updateRelationById(isGreater: Boolean, id: Long) - fun updateRateById(rate: Double, id: Long) -} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt deleted file mode 100644 index b46df10c6b..0000000000 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/notification/NotificationRepositoryImpl.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.oztechan.ccc.common.db.notification - -import co.touchlab.kermit.Logger -import com.oztechan.ccc.common.db.sql.NotificationQueries -import com.oztechan.ccc.common.mapper.mapToModel -import com.oztechan.ccc.common.mapper.toLong -import com.oztechan.ccc.common.mapper.toModelList -import com.squareup.sqldelight.runtime.coroutines.asFlow -import com.squareup.sqldelight.runtime.coroutines.mapToList - -class NotificationRepositoryImpl( - private val notificationQueries: NotificationQueries -) : NotificationRepository { - - override fun addNotification( - base: String, - target: String - ) = notificationQueries.addNotification(base, target) - .also { Logger.v { "NotificationRepositoryImpl addNotification $base $target" } } - - override fun collectNotifications() = notificationQueries - .getNotifications() - .asFlow() - .mapToList() - .mapToModel() - .also { Logger.v { "NotificationRepositoryImpl collectNotifications" } } - - override fun getNotifications() = notificationQueries - .getNotifications() - .executeAsList() - .toModelList() - .also { Logger.v { "NotificationRepositoryImpl getNotifications" } } - - override fun deleteNotification(id: Long) = notificationQueries - .deleteNotification(id) - .also { Logger.v { "NotificationRepositoryImpl addNotification $id" } } - - override fun updateBaseById(base: String, id: Long) = notificationQueries - .updateBaseById(base, id) - .also { Logger.v { "NotificationRepositoryImpl updateBaseById $base $id" } } - - override fun updateTargetById(target: String, id: Long) = notificationQueries - .updateTargetById(target, id) - .also { Logger.v { "NotificationRepositoryImpl updateTargetById $target $id" } } - - override fun updateRelationById(isGreater: Boolean, id: Long) = notificationQueries - .updateRelationById(isGreater.toLong(), id) - .also { Logger.v { "NotificationRepositoryImpl updateRelationById $isGreater $id" } } - - override fun updateRateById(rate: Double, id: Long) = notificationQueries - .updateRateById(rate, id) - .also { Logger.v { "NotificationRepositoryImpl updateRateById $rate $id" } } -} diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq deleted file mode 100644 index 0d08d45852..0000000000 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Notification.sq +++ /dev/null @@ -1,28 +0,0 @@ -CREATE TABLE IF NOT EXISTS notification( -id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, -base TEXT NOT NULL, -target TEXT NOT NULL, -isGreater INTEGER NOT NULL DEFAULT 1, -rate REAL NOT NULL DEFAULT 0.0 -); - -addNotification: -INSERT OR REPLACE INTO notification(base, target) VALUES (?,?); - -deleteNotification: -DELETE FROM notification WHERE id = ?; - -updateBaseById: -UPDATE notification SET base=? WHERE id=?; - -updateTargetById: -UPDATE notification SET target=? WHERE id=?; - -updateRelationById: -UPDATE notification SET isGreater=? WHERE id=?; - -updateRateById: -UPDATE notification SET rate=? WHERE id=?; - -getNotifications: -SELECT * FROM notification; \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Watcher.sq b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Watcher.sq new file mode 100644 index 0000000000..8c8e5294e1 --- /dev/null +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/sql/Watcher.sq @@ -0,0 +1,28 @@ +CREATE TABLE IF NOT EXISTS watcher( +id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, +base TEXT NOT NULL, +target TEXT NOT NULL, +isGreater INTEGER NOT NULL DEFAULT 1, +rate REAL NOT NULL DEFAULT 0.0 +); + +addWatcher: +INSERT OR REPLACE INTO watcher(base, target) VALUES (?,?); + +deleteWatcher: +DELETE FROM watcher WHERE id = ?; + +updateBaseById: +UPDATE watcher SET base=? WHERE id=?; + +updateTargetById: +UPDATE watcher SET target=? WHERE id=?; + +updateRelationById: +UPDATE watcher SET isGreater=? WHERE id=?; + +updateRateById: +UPDATE watcher SET rate=? WHERE id=?; + +getWatchers: +SELECT * FROM watcher; \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/watcher/WatcherRepository.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/watcher/WatcherRepository.kt new file mode 100644 index 0000000000..e40a883097 --- /dev/null +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/watcher/WatcherRepository.kt @@ -0,0 +1,15 @@ +package com.oztechan.ccc.common.db.watcher + +import com.oztechan.ccc.common.model.Watcher +import kotlinx.coroutines.flow.Flow + +interface WatcherRepository { + fun addWatcher(base: String, target: String) + fun collectWatchers(): Flow> + fun getWatchers(): List + fun deleteWatcher(id: Long) + fun updateBaseById(base: String, id: Long) + fun updateTargetById(target: String, id: Long) + fun updateRelationById(isGreater: Boolean, id: Long) + fun updateRateById(rate: Double, id: Long) +} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/watcher/WatcherRepositoryImpl.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/watcher/WatcherRepositoryImpl.kt new file mode 100644 index 0000000000..36e726781c --- /dev/null +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/db/watcher/WatcherRepositoryImpl.kt @@ -0,0 +1,53 @@ +package com.oztechan.ccc.common.db.watcher + +import co.touchlab.kermit.Logger +import com.oztechan.ccc.common.db.sql.WatcherQueries +import com.oztechan.ccc.common.mapper.mapToModel +import com.oztechan.ccc.common.mapper.toLong +import com.oztechan.ccc.common.mapper.toModelList +import com.squareup.sqldelight.runtime.coroutines.asFlow +import com.squareup.sqldelight.runtime.coroutines.mapToList + +class WatcherRepositoryImpl( + private val watcherQueries: WatcherQueries +) : WatcherRepository { + + override fun addWatcher( + base: String, + target: String + ) = watcherQueries.addWatcher(base, target) + .also { Logger.v { "WatcherRepositoryImpl addWatcher $base $target" } } + + override fun collectWatchers() = watcherQueries + .getWatchers() + .asFlow() + .mapToList() + .mapToModel() + .also { Logger.v { "WatcherRepositoryImpl collectWatchers" } } + + override fun getWatchers() = watcherQueries + .getWatchers() + .executeAsList() + .toModelList() + .also { Logger.v { "WatcherRepositoryImpl getWatchers" } } + + override fun deleteWatcher(id: Long) = watcherQueries + .deleteWatcher(id) + .also { Logger.v { "WatcherRepositoryImpl addWatcher $id" } } + + override fun updateBaseById(base: String, id: Long) = watcherQueries + .updateBaseById(base, id) + .also { Logger.v { "WatcherRepositoryImpl updateBaseById $base $id" } } + + override fun updateTargetById(target: String, id: Long) = watcherQueries + .updateTargetById(target, id) + .also { Logger.v { "WatcherRepositoryImpl updateTargetById $target $id" } } + + override fun updateRelationById(isGreater: Boolean, id: Long) = watcherQueries + .updateRelationById(isGreater.toLong(), id) + .also { Logger.v { "WatcherRepositoryImpl updateRelationById $isGreater $id" } } + + override fun updateRateById(rate: Double, id: Long) = watcherQueries + .updateRateById(rate, id) + .also { Logger.v { "WatcherRepositoryImpl updateRateById $rate $id" } } +} diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/di/modules/DatabaseModule.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/di/modules/DatabaseModule.kt index 4ebd7412c6..3e7c8991e2 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/di/modules/DatabaseModule.kt +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/di/modules/DatabaseModule.kt @@ -2,11 +2,11 @@ package com.oztechan.ccc.common.di.modules import com.oztechan.ccc.common.db.currency.CurrencyRepository import com.oztechan.ccc.common.db.currency.CurrencyRepositoryImpl -import com.oztechan.ccc.common.db.notification.NotificationRepository -import com.oztechan.ccc.common.db.notification.NotificationRepositoryImpl import com.oztechan.ccc.common.db.offlinerates.OfflineRatesRepository import com.oztechan.ccc.common.db.offlinerates.OfflineRatesRepositoryImpl import com.oztechan.ccc.common.db.sql.CurrencyConverterCalculatorDatabase +import com.oztechan.ccc.common.db.watcher.WatcherRepository +import com.oztechan.ccc.common.db.watcher.WatcherRepositoryImpl import org.koin.core.scope.Scope import org.koin.dsl.module @@ -15,11 +15,11 @@ private const val DATABASE_NAME = "application_database.sqlite" fun getDatabaseModule() = module { single { get().currencyQueries } single { get().offlineRatesQueries } - single { get().notificationQueries } + single { get().watcherQueries } single { CurrencyRepositoryImpl(get()) } single { OfflineRatesRepositoryImpl(get()) } - single { NotificationRepositoryImpl(get()) } + single { WatcherRepositoryImpl(get()) } single { provideDatabase(DATABASE_NAME) } } diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Notification.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Notification.kt index f683627f93..04f3b5a1ba 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Notification.kt +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/mapper/Notification.kt @@ -1,11 +1,11 @@ package com.oztechan.ccc.common.mapper -import com.oztechan.ccc.common.model.Notification +import com.oztechan.ccc.common.model.Watcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import com.oztechan.ccc.common.db.sql.Notification as NotificationEntity +import com.oztechan.ccc.common.db.sql.Watcher as WatcherEntity -fun NotificationEntity.toModel() = Notification( +fun WatcherEntity.toModel() = Watcher( id = id, base = base, target = target, @@ -13,10 +13,10 @@ fun NotificationEntity.toModel() = Notification( rate = rate, ) -internal fun List.toModelList(): List { +internal fun List.toModelList(): List { return map { it.toModel() } } -internal fun Flow>.mapToModel(): Flow> { +internal fun Flow>.mapToModel(): Flow> { return this.map { it.toModelList() } } \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/oztechan/ccc/common/model/Notification.kt b/common/src/commonMain/kotlin/com/oztechan/ccc/common/model/Watcher.kt similarity index 86% rename from common/src/commonMain/kotlin/com/oztechan/ccc/common/model/Notification.kt rename to common/src/commonMain/kotlin/com/oztechan/ccc/common/model/Watcher.kt index 58a5ed7870..a3f95bf8c0 100644 --- a/common/src/commonMain/kotlin/com/oztechan/ccc/common/model/Notification.kt +++ b/common/src/commonMain/kotlin/com/oztechan/ccc/common/model/Watcher.kt @@ -1,6 +1,6 @@ package com.oztechan.ccc.common.model -data class Notification( +data class Watcher( val id: Long, val base: String, val target: String, diff --git a/ios/CCC.xcodeproj/project.pbxproj b/ios/CCC.xcodeproj/project.pbxproj index 177c2c8b07..bcf327f8bf 100644 --- a/ios/CCC.xcodeproj/project.pbxproj +++ b/ios/CCC.xcodeproj/project.pbxproj @@ -24,8 +24,8 @@ 5C31E4362814308B008C42B9 /* SettingsItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C31E4352814308B008C42B9 /* SettingsItemView.swift */; }; 5C31E439281431A3008C42B9 /* SelectCurrencyItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C31E438281431A3008C42B9 /* SelectCurrencyItemView.swift */; }; 5C31E43F28145D32008C42B9 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5C31E43E28145D32008C42B9 /* Launch Screen.storyboard */; }; - 5C4B53692818057F00D10185 /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B53682818057F00D10185 /* NotificationView.swift */; }; - 5C4B536B2818066000D10185 /* NotificationsToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B536A2818066000D10185 /* NotificationsToolbarView.swift */; }; + 5C4B53692818057F00D10185 /* WatchersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B53682818057F00D10185 /* WatchersView.swift */; }; + 5C4B536B2818066000D10185 /* WatchersToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B536A2818066000D10185 /* WatchersToolbarView.swift */; }; 5C5D09332562EB9E00DA9C4A /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5D09322562EB9E00DA9C4A /* Application.swift */; }; 5C5D09362562EBDE00DA9C4A /* Koin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5D09352562EBDE00DA9C4A /* Koin.swift */; }; 5C5D09392562EC0100DA9C4A /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5D09382562EC0100DA9C4A /* Extensions.swift */; }; @@ -42,7 +42,7 @@ 5CDE468425BC3B2000CA0FB1 /* SelectCurrencyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDE468325BC3B2000CA0FB1 /* SelectCurrencyView.swift */; }; 5CF57E3A269588060081E4BB /* RewardedAd.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF57E39269588060081E4BB /* RewardedAd.swift */; }; 5CF57E3C2695A3B20081E4BB /* InterstitialAd.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF57E3B2695A3B20081E4BB /* InterstitialAd.swift */; }; - 5CF898D42823C1F900712580 /* NotificationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF898D32823C1F900712580 /* NotificationItem.swift */; }; + 5CF898D42823C1F900712580 /* WatcherItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF898D32823C1F900712580 /* WatcherItem.swift */; }; 5CF8BE4227DE205B00E441F5 /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF8BE4127DE205B00E441F5 /* MailView.swift */; }; 5CF8BE4627DE334100E441F5 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF8BE4527DE334100E441F5 /* WebView.swift */; }; 7555FF85242A565B00829871 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7555FF84242A565B00829871 /* Assets.xcassets */; }; @@ -80,8 +80,8 @@ 5C31E4352814308B008C42B9 /* SettingsItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItemView.swift; sourceTree = ""; }; 5C31E438281431A3008C42B9 /* SelectCurrencyItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCurrencyItemView.swift; sourceTree = ""; }; 5C31E43E28145D32008C42B9 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; - 5C4B53682818057F00D10185 /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = ""; }; - 5C4B536A2818066000D10185 /* NotificationsToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsToolbarView.swift; sourceTree = ""; }; + 5C4B53682818057F00D10185 /* WatchersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchersView.swift; sourceTree = ""; }; + 5C4B536A2818066000D10185 /* WatchersToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchersToolbarView.swift; sourceTree = ""; }; 5C5D09322562EB9E00DA9C4A /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 5C5D09352562EBDE00DA9C4A /* Koin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Koin.swift; sourceTree = ""; }; 5C5D09382562EC0100DA9C4A /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; @@ -100,7 +100,7 @@ 5CDE468325BC3B2000CA0FB1 /* SelectCurrencyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCurrencyView.swift; sourceTree = ""; }; 5CF57E39269588060081E4BB /* RewardedAd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RewardedAd.swift; sourceTree = ""; }; 5CF57E3B2695A3B20081E4BB /* InterstitialAd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterstitialAd.swift; sourceTree = ""; }; - 5CF898D32823C1F900712580 /* NotificationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItem.swift; sourceTree = ""; }; + 5CF898D32823C1F900712580 /* WatcherItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatcherItem.swift; sourceTree = ""; }; 5CF8BE4127DE205B00E441F5 /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = ""; }; 5CF8BE4527DE334100E441F5 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; 7555FF7B242A565900829871 /* CCC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CCC.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -188,14 +188,14 @@ path = Settings; sourceTree = ""; }; - 5C4B53652818053E00D10185 /* Notifications */ = { + 5C4B53652818053E00D10185 /* Watchers */ = { isa = PBXGroup; children = ( - 5C4B53682818057F00D10185 /* NotificationView.swift */, - 5C4B536A2818066000D10185 /* NotificationsToolbarView.swift */, - 5CF898D32823C1F900712580 /* NotificationItem.swift */, + 5C4B53682818057F00D10185 /* WatchersView.swift */, + 5C4B536A2818066000D10185 /* WatchersToolbarView.swift */, + 5CF898D32823C1F900712580 /* WatcherItem.swift */, ); - path = Notifications; + path = Watchers; sourceTree = ""; }; 5C4B536E28184AEA00D10185 /* SelectCurrency */ = { @@ -223,7 +223,7 @@ 5C31E41C28141C61008C42B9 /* Calculator */, 5C31E42B28142033008C42B9 /* Currencies */, 5C31E4322814304F008C42B9 /* Settings */, - 5C4B53652818053E00D10185 /* Notifications */, + 5C4B53652818053E00D10185 /* Watchers */, 5C4B536E28184AEA00D10185 /* SelectCurrency */, 5C039FD425C1B6A2008350A3 /* SubView */, ); @@ -469,15 +469,15 @@ files = ( 5C314CBE25BA0AC0007B22D8 /* CurrenciesView.swift in Sources */, 5C94AC32282FA4B2004C9B3D /* CurrencyImageView.swift in Sources */, - 5C4B536B2818066000D10185 /* NotificationsToolbarView.swift in Sources */, - 5CF898D42823C1F900712580 /* NotificationItem.swift in Sources */, + 5C4B536B2818066000D10185 /* WatchersToolbarView.swift in Sources */, + 5CF898D42823C1F900712580 /* WatcherItem.swift in Sources */, 5CB954BF26932408007632DC /* BannerAdView.swift in Sources */, 5C31E42428141D1B008C42B9 /* RateStateView.swift in Sources */, 5CF57E3A269588060081E4BB /* RewardedAd.swift in Sources */, 5C9C75C82603A36A00D66FDD /* ToolbarButton.swift in Sources */, 5CF8BE4227DE205B00E441F5 /* MailView.swift in Sources */, 5C31E4362814308B008C42B9 /* SettingsItemView.swift in Sources */, - 5C4B53692818057F00D10185 /* NotificationView.swift in Sources */, + 5C4B53692818057F00D10185 /* WatchersView.swift in Sources */, 5C6E674025C5A711001CC0D6 /* SliderView.swift in Sources */, 5C31E42628141D3E008C42B9 /* CalculatorItemView.swift in Sources */, 5C039FD625C1B705008350A3 /* FormProgressView.swift in Sources */, diff --git a/ios/CCC/DI/Koin.swift b/ios/CCC/DI/Koin.swift index 12acb60cfe..6cdde84cb2 100644 --- a/ios/CCC/DI/Koin.swift +++ b/ios/CCC/DI/Koin.swift @@ -47,8 +47,8 @@ extension Koin_coreKoin { return koin.getDependency(objCClass: SettingsViewModel.self) as! SettingsViewModel } - func get() -> NotificationViewModel { - return koin.getDependency(objCClass: NotificationViewModel.self) as! NotificationViewModel + func get() -> WatchersViewModel { + return koin.getDependency(objCClass: WatchersViewModel.self) as! WatchersViewModel } // Observable @@ -68,8 +68,8 @@ extension Koin_coreKoin { return SettingsObservable(viewModel: get()) } - func get() -> NotificationObservable { - return NotificationObservable(viewModel: get()) + func get() -> WatchersObservable { + return WatchersObservable(viewModel: get()) } func get() -> CurrenciesObservable { diff --git a/ios/CCC/UI/Settings/SettingsItemView.swift b/ios/CCC/UI/Settings/SettingsItemView.swift index 66b0259ce9..4b1199a02a 100644 --- a/ios/CCC/UI/Settings/SettingsItemView.swift +++ b/ios/CCC/UI/Settings/SettingsItemView.swift @@ -24,8 +24,7 @@ struct SettingsItemView: View { .font(.system(size: 24)) .imageScale(.large) .accentColor(MR.colors().text.get()) - .padding(.bottom, 8) - .padding(.top, 8) + .padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 8)) VStack { HStack { diff --git a/ios/CCC/UI/Settings/SettingsView.swift b/ios/CCC/UI/Settings/SettingsView.swift index ecdc3522a6..c85337be2a 100644 --- a/ios/CCC/UI/Settings/SettingsView.swift +++ b/ios/CCC/UI/Settings/SettingsView.swift @@ -51,13 +51,13 @@ struct SettingsView: View { ) SettingsItemView( - imgName: "bell", - title: MR.strings().settings_item_notifications_title.get(), - subTitle: MR.strings().settings_item_notifications_sub_title.get(), + imgName: "eyeglasses", + title: MR.strings().settings_item_watchers_title.get(), + subTitle: MR.strings().settings_item_watchers_sub_title.get(), value: MR.strings().settings_active_item_value.get( - parameter: observable.state.activeNotificationCount + parameter: observable.state.activeWatcherCount ), - onClick: observable.event.onNotificationsClicked + onClick: observable.event.onWatchersClicked ) // SettingsItemView( @@ -151,8 +151,8 @@ struct SettingsView: View { navigationStack.pop() case is SettingsEffect.OpenCurrencies: navigationStack.push(CurrenciesView(onBaseChange: onBaseChange)) - case is SettingsEffect.OpenNotifications: - navigationStack.push(NotificationView()) + case is SettingsEffect.OpenWatchers: + navigationStack.push(WatchersView()) case is SettingsEffect.FeedBack: emailViewVisibility.toggle() case is SettingsEffect.OnGitHub: diff --git a/ios/CCC/UI/Notifications/NotificationItem.swift b/ios/CCC/UI/Watchers/WatcherItem.swift similarity index 66% rename from ios/CCC/UI/Notifications/NotificationItem.swift rename to ios/CCC/UI/Watchers/WatcherItem.swift index e823d2007e..a732e4b6b6 100644 --- a/ios/CCC/UI/Notifications/NotificationItem.swift +++ b/ios/CCC/UI/Watchers/WatcherItem.swift @@ -1,5 +1,5 @@ // -// NotificationItem.swift +// WatcherItem.swift // CCC // // Created by Mustafa Ozhan on 05.05.22. @@ -12,22 +12,22 @@ import Resources import NavigationStack import Combine -struct NotificationItem: View { +struct WatcherItem: View { @State private var relationSelection = 0 @State private var amount = "" @Binding var isBaseBarShown: Bool @Binding var isTargetBarShown: Bool - let notification: Client.Notification - let event: NotificationEvent + let watcher: Client.Watcher + let event: WatchersEvent var body: some View { HStack { Text(MR.strings().one.get()).font(.body) - CurrencyImageView(imageName: notification.base) - .onTapGesture { event.onBaseClick(notification: notification) } + CurrencyImageView(imageName: watcher.base) + .onTapGesture { event.onBaseClick(watcher: watcher) } Picker("", selection: $relationSelection) { Text(MR.strings().txt_smaller.get()) @@ -40,7 +40,7 @@ struct NotificationItem: View { .pickerStyle(.segmented) .frame(maxWidth: 80) .onChange(of: relationSelection) { - event.onRelationChange(notification: notification, isGreater: $0 == 1) + event.onRelationChange(watcher: watcher, isGreater: $0 == 1) } Spacer() @@ -55,21 +55,21 @@ struct NotificationItem: View { .background(MR.colors().background_weak.get()) .cornerRadius(7) .onChange(of: amount) { - amount = event.onRateChange(notification: notification, rate: $0) + amount = event.onRateChange(watcher: watcher, rate: $0) } Spacer() - CurrencyImageView(imageName: notification.target) - .onTapGesture { event.onTargetClick(notification: notification) } + CurrencyImageView(imageName: watcher.target) + .onTapGesture { event.onTargetClick(watcher: watcher) } Image(systemName: "trash") .padding(.leading, 10) - .onTapGesture { event.onDeleteClick(notification: notification) } + .onTapGesture { event.onDeleteClick(watcher: watcher) } }.onAppear { - relationSelection = notification.isGreater ? 1 : 0 - amount = "\(notification.rate)" + relationSelection = watcher.isGreater ? 1 : 0 + amount = "\(watcher.rate)" } } } diff --git a/ios/CCC/UI/Notifications/NotificationsToolbarView.swift b/ios/CCC/UI/Watchers/WatchersToolbarView.swift similarity index 80% rename from ios/CCC/UI/Notifications/NotificationsToolbarView.swift rename to ios/CCC/UI/Watchers/WatchersToolbarView.swift index 9222950d17..9b2d4b9e97 100644 --- a/ios/CCC/UI/Notifications/NotificationsToolbarView.swift +++ b/ios/CCC/UI/Watchers/WatchersToolbarView.swift @@ -1,5 +1,5 @@ // -// NotificationsToolbarView.swift +// WatchersToolbarView.swift // CCC // // Created by Mustafa Ozhan on 26.04.22. @@ -9,14 +9,14 @@ import SwiftUI import Resources -struct NotificationsToolbarView: View { +struct WatchersToolbarView: View { var backEvent: () -> Void var body: some View { HStack { ToolbarButton(clickEvent: backEvent, imgName: "chevron.left") - Text(MR.strings().txt_notifications.get()) + Text(MR.strings().txt_watchers.get()) .font(.title3) Spacer() diff --git a/ios/CCC/UI/Notifications/NotificationView.swift b/ios/CCC/UI/Watchers/WatchersView.swift similarity index 66% rename from ios/CCC/UI/Notifications/NotificationView.swift rename to ios/CCC/UI/Watchers/WatchersView.swift index e114193bfe..618c506e12 100644 --- a/ios/CCC/UI/Notifications/NotificationView.swift +++ b/ios/CCC/UI/Watchers/WatchersView.swift @@ -1,5 +1,5 @@ // -// NotificationsView.swift +// WatchersView.swift // CCC // // Created by Mustafa Ozhan on 26.04.22. @@ -11,25 +11,25 @@ import Client import Resources import NavigationStack -typealias NotificationObservable = ObservableSEED - +typealias WatchersObservable = ObservableSEED + -struct NotificationView: View { +struct WatchersView: View { @EnvironmentObject private var navigationStack: NavigationStack - @StateObject var observable: NotificationObservable = koin.get() - @State var baseBarInfo = BarInfo(isShown: false, notification: nil) - @State var targetBarInfo = BarInfo(isShown: false, notification: nil) + @StateObject var observable: WatchersObservable = koin.get() + @State var baseBarInfo = BarInfo(isShown: false, watcher: nil) + @State var targetBarInfo = BarInfo(isShown: false, watcher: nil) - var notification: Client.Notification? + var watcher: Client.Watcher? var body: some View { ZStack { MR.colors().background_strong.get().edgesIgnoringSafeArea(.all) VStack { - NotificationsToolbarView(backEvent: observable.event.onBackClick) + WatchersToolbarView(backEvent: observable.event.onBackClick) - Text(MR.strings().txt_notifications_description.get()) + Text(MR.strings().txt_txt_watchers_description.get()) .font(.footnote) .padding(18) .multilineTextAlignment(.center) @@ -39,11 +39,11 @@ struct NotificationView: View { .padding(-8) Form { - List(observable.state.notificationList, id: \.id) { notification in - NotificationItem( + List(observable.state.watcherList, id: \.id) { watcher in + WatcherItem( isBaseBarShown: $baseBarInfo.isShown, isTargetBarShown: $targetBarInfo.isShown, - notification: notification, + watcher: watcher, event: observable.event ) } @@ -75,7 +75,7 @@ struct NotificationView: View { isBarShown: $baseBarInfo.isShown, onCurrencySelected: { observable.event.onBaseChanged( - notification: baseBarInfo.notification, + watcher: baseBarInfo.watcher, newBase: $0 ) } @@ -89,7 +89,7 @@ struct NotificationView: View { isBarShown: $targetBarInfo.isShown, onCurrencySelected: { observable.event.onTargetChanged( - notification: targetBarInfo.notification, + watcher: targetBarInfo.watcher, newTarget: $0 ) } @@ -102,32 +102,32 @@ struct NotificationView: View { .animation(.default) } - private func onEffect(effect: NotificationEffect) { - logger.i(message: {"NotificationView onEffect \(effect.description)"}) + private func onEffect(effect: WatchersEffect) { + logger.i(message: {"WatchersView onEffect \(effect.description)"}) switch effect { - case is NotificationEffect.Back: + case is WatchersEffect.Back: navigationStack.pop() // swiftlint:disable force_cast - case is NotificationEffect.SelectBase: - baseBarInfo.notification = (effect as! NotificationEffect.SelectBase).notification + case is WatchersEffect.SelectBase: + baseBarInfo.watcher = (effect as! WatchersEffect.SelectBase).watcher baseBarInfo.isShown.toggle() // swiftlint:disable force_cast - case is NotificationEffect.SelectTarget: - targetBarInfo.notification = (effect as! NotificationEffect.SelectTarget).notification + case is WatchersEffect.SelectTarget: + targetBarInfo.watcher = (effect as! WatchersEffect.SelectTarget).watcher targetBarInfo.isShown.toggle() - case is NotificationEffect.MaximumInput: + case is WatchersEffect.MaximumInput: showSnack(text: MR.strings().text_max_input.get(), isTop: true) - case is NotificationEffect.InvalidInput: + case is WatchersEffect.InvalidInput: showSnack(text: MR.strings().text_invalid_input.get(), isTop: true) - case is NotificationEffect.MaximumNumberOfNotification: - showSnack(text: MR.strings().text_maximum_number_of_notifications.get(), isTop: true) + case is WatchersEffect.MaximumNumberOfWatchers: + showSnack(text: MR.strings().text_maximum_number_of_watchers.get(), isTop: true) default: - logger.i(message: {"NotificationView unknown effect"}) + logger.i(message: {"WatchersView unknown effect"}) } } struct BarInfo { var isShown: Bool - var notification: Client.Notification? + var watcher: Client.Watcher? } } diff --git a/resources/src/commonMain/resources/MR/base/strings.xml b/resources/src/commonMain/resources/MR/base/strings.xml index cb8d1d0270..6598b6bcbf 100644 --- a/resources/src/commonMain/resources/MR/base/strings.xml +++ b/resources/src/commonMain/resources/MR/base/strings.xml @@ -49,7 +49,7 @@ Check your connection or try again later. Copied to clipboard! Invalid input! - Maximum number of notifications exceeded + Maximum number of watchers exceeded You can rate us and review our app in the market :) @@ -91,8 +91,8 @@ Currencies Set your currencies - Notifications - Check rates every 1 hour + Watchers + Watch rates every 1 hour Theme Change app theme @@ -125,9 +125,9 @@ Currencies - - Notifications - Watchers will be checked every hour and if the condition is verified then you will get a notification! + + Watchers + Watchers will be checked every hour and if the condition is verified then you will get a notification! Rate > < From 32c158a363c2238a01861d65f916625d27d8e35a Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 22 May 2022 12:39:22 +0300 Subject: [PATCH 17/40] [#749] Add orEmpty() for empty strings (#751) --- .../kotlin/com/oztechan/ccc/backend/util/SourceUtil.kt | 2 +- .../client/viewmodel/calculator/CalculatorViewModel.kt | 2 +- .../client/viewmodel/currencies/CurrenciesViewModel.kt | 10 +++++----- .../ccc/client/viewmodel/watchers/WatchersViewModel.kt | 4 ++-- .../com/oztechan/ccc/client/util/IOSCalculatorUtil.kt | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/src/jvmMain/kotlin/com/oztechan/ccc/backend/util/SourceUtil.kt b/backend/src/jvmMain/kotlin/com/oztechan/ccc/backend/util/SourceUtil.kt index d30db68a74..349cf8f7c4 100644 --- a/backend/src/jvmMain/kotlin/com/oztechan/ccc/backend/util/SourceUtil.kt +++ b/backend/src/jvmMain/kotlin/com/oztechan/ccc/backend/util/SourceUtil.kt @@ -6,4 +6,4 @@ package com.oztechan.ccc.backend.util fun ClassLoader.getResourceByName( source: String -) = getResource(source)?.readText() ?: "" +) = getResource(source)?.readText().orEmpty() diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt index 4133b72d1a..5bfac6298d 100755 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt @@ -166,7 +166,7 @@ class CalculatorViewModel( _state.update( base = newBase, input = _state.value.input, - symbol = currencyRepository.getCurrencyByName(newBase)?.symbol ?: "" + symbol = currencyRepository.getCurrencyByName(newBase)?.symbol.orEmpty() ) } diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt index d85cf77ba0..31beb6d0fc 100755 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt @@ -77,11 +77,11 @@ class CurrenciesViewModel( .filter { it.name == base } .toList().firstOrNull()?.isActive == false } - )?.let { - (state.value.currencyList.firstOrNull { it.isActive }?.name ?: "").let { newBase -> - settingsRepository.currentBase = newBase - clientScope.launch { _effect.emit(CurrenciesEffect.ChangeBase(newBase)) } - } + )?.mapTo { + state.value.currencyList.firstOrNull { it.isActive } + }?.name.orEmpty().let { newBase -> + settingsRepository.currentBase = newBase + clientScope.launch { _effect.emit(CurrenciesEffect.ChangeBase(newBase)) } } private fun filterList(txt: String) = data.unFilteredList diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt index a0f6fa2e93..38c1e0d2e4 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt @@ -77,8 +77,8 @@ class WatchersViewModel( } else { currencyRepository.getActiveCurrencies().let { list -> watcherRepository.addWatcher( - base = list.firstOrNull()?.name ?: "", - target = list.lastOrNull()?.name ?: "" + base = list.firstOrNull()?.name.orEmpty(), + target = list.lastOrNull()?.name.orEmpty() ) } } diff --git a/client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCalculatorUtil.kt b/client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCalculatorUtil.kt index 5b4a64fea9..4259b7fe8e 100644 --- a/client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCalculatorUtil.kt +++ b/client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCalculatorUtil.kt @@ -13,11 +13,11 @@ actual fun Double.getFormatted() = NSNumberFormatter().apply { setNumberStyle(NSNumberFormatterDecimalStyle) setGroupingSeparator(" ") setDecimalSeparator(".") -}.stringFromNumber(NSNumber(this)) ?: "" +}.stringFromNumber(NSNumber(this)).orEmpty() actual fun Double.removeScientificNotation() = NSNumberFormatter().apply { setNumberStyle(NSNumberFormatterDecimalStyle) setGroupingSeparator("") setDecimalSeparator(".") setMaximumFractionDigits(WatchersData.MAXIMUM_INPUT.toULong()) -}.stringFromNumber(NSNumber(this)) ?: "" +}.stringFromNumber(NSNumber(this)).orEmpty() From fa0aa745d31a1d869d2f35701916d7d709594cf9 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 22 May 2022 13:54:02 +0300 Subject: [PATCH 18/40] [#752] Dependency Updates (#753) --- buildSrc/src/main/kotlin/Versions.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 9f87404771..8f164ebf57 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -6,9 +6,9 @@ object Versions { const val KOTLIN = "1.6.21" const val KSP = "$KOTLIN-1.0.5" - const val ANDROID_GRADLE_PLUGIN = "7.1.3" + const val ANDROID_GRADLE_PLUGIN = "7.2.0" const val ANDROID_MATERIAL = "1.6.0" - const val CONSTRAINT_LAYOUT = "2.1.3" + const val CONSTRAINT_LAYOUT = "2.1.4" const val KTOR = "1.6.8" const val LOG_BACK = "1.2.11" const val KOIN = "3.1.6" @@ -27,15 +27,15 @@ object Versions { const val LEAK_CANARY = "2.9.1" const val SQL_DELIGHT = "1.5.3" const val LIFECYCLE = "2.4.1" - const val MOKO_RESOURCES = "0.19.1" + const val MOKO_RESOURCES = "0.20.0" const val DEPENDENCY_UPDATES = "0.42.0" - const val BUILD_HEALTH = "1.1.0" - const val BUILD_KONFIG = "0.11.0" + const val BUILD_HEALTH = "1.2.1" + const val BUILD_KONFIG = "0.12.0" const val WORK_RUNTIME = "2.7.1" const val SPLASH_SCREEN = "1.0.0-alpha02" - const val KOVER = "0.5.0" + const val KOVER = "0.5.1" const val ROOT_BEER = "0.1.0" - const val MOCKATIVE = "1.1.4" + const val MOCKATIVE = "1.2.1" const val SCOPE_MOB = "2.1.5" const val PARSER_MOB = "1.1.6" const val BASE_MOB = "2.1.4" From 617280a1f8f370e0c295b07b7879ef4534597cf8 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 22 May 2022 16:22:04 +0300 Subject: [PATCH 19/40] [#747] Reorganise gitignore for iOS (#748) --- .gitignore | 7 -- ios/.gitignore | 1 + .../xcshareddata/xcschemes/CCC.xcscheme | 78 +++++++++++++++++++ ios/CCC.xcworkspace/contents.xcworkspacedata | 30 +++++++ 4 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 ios/CCC.xcodeproj/xcshareddata/xcschemes/CCC.xcscheme create mode 100644 ios/CCC.xcworkspace/contents.xcworkspacedata diff --git a/.gitignore b/.gitignore index f4d12a97c0..cde9d5941b 100755 --- a/.gitignore +++ b/.gitignore @@ -17,10 +17,3 @@ .cxx local.properties secret.properties - -# Xcode -*.xcworkspacedata -*.xcuserstate -*.xcscheme -xcschememanagement.plist -*.xcbkptlist \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore index 1ec657ac73..5b873e0a77 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -5,3 +5,4 @@ CCC/Resources/Release.xcconfig fastlane/.env fastlane/README.md fastlane/report.xml +xcuserdata diff --git a/ios/CCC.xcodeproj/xcshareddata/xcschemes/CCC.xcscheme b/ios/CCC.xcodeproj/xcshareddata/xcschemes/CCC.xcscheme new file mode 100644 index 0000000000..10aec321bc --- /dev/null +++ b/ios/CCC.xcodeproj/xcshareddata/xcschemes/CCC.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/CCC.xcworkspace/contents.xcworkspacedata b/ios/CCC.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..b0966d756f --- /dev/null +++ b/ios/CCC.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + From 9e98ea6542a326271a49adcf17f7d742df11cb5c Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Thu, 26 May 2022 15:34:32 +0300 Subject: [PATCH 20/40] [#732] Add ProjectHealth task (#754) --- .github/workflows/dependency.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dependency.yml b/.github/workflows/dependency.yml index b628cbc9b7..8eeb282471 100644 --- a/.github/workflows/dependency.yml +++ b/.github/workflows/dependency.yml @@ -22,15 +22,21 @@ jobs: with: java-version: 11 - - name: Run dependencyUpdates Task - run: ./gradlew dependencyUpdates --parallel + - name: Run dependencyUpdates and buildHealth tasks + run: ./gradlew dependencyUpdates buildHealth --parallel - - name: Upload dependencies report + - name: Upload Dependency Updates report uses: actions/upload-artifact@v2 with: - name: report.txt + name: dependency-updates.txt path: build/dependencyUpdates/report.txt + - name: Upload Build Healt Report + uses: actions/upload-artifact@v2 + with: + name: build-health.txt + path: build/reports/dependency-analysis/build-health-report.txt + - name: Set Job Status id: status run: echo "::set-output name=status::success" From 6d20a81fd071790fac4c0d4830ad1d42225fa9e7 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sat, 28 May 2022 10:43:22 +0300 Subject: [PATCH 21/40] [#756] Create NotificationManager (#757) --- ios/CCC.xcodeproj/project.pbxproj | 4 ++ ios/CCC/UI/Watchers/WatchersView.swift | 92 +++++++++++++++++--------- ios/CCC/Util/NotificationManager.swift | 39 +++++++++++ 3 files changed, 104 insertions(+), 31 deletions(-) create mode 100644 ios/CCC/Util/NotificationManager.swift diff --git a/ios/CCC.xcodeproj/project.pbxproj b/ios/CCC.xcodeproj/project.pbxproj index bcf327f8bf..8b21bbf413 100644 --- a/ios/CCC.xcodeproj/project.pbxproj +++ b/ios/CCC.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 5C9C75C82603A36A00D66FDD /* ToolbarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9C75C72603A36A00D66FDD /* ToolbarButton.swift */; }; 5CB954BF26932408007632DC /* BannerAdView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB954BE26932408007632DC /* BannerAdView.swift */; }; 5CDE468425BC3B2000CA0FB1 /* SelectCurrencyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDE468325BC3B2000CA0FB1 /* SelectCurrencyView.swift */; }; + 5CEA86F52840CF65001386FB /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEA86F42840CF65001386FB /* NotificationManager.swift */; }; 5CF57E3A269588060081E4BB /* RewardedAd.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF57E39269588060081E4BB /* RewardedAd.swift */; }; 5CF57E3C2695A3B20081E4BB /* InterstitialAd.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF57E3B2695A3B20081E4BB /* InterstitialAd.swift */; }; 5CF898D42823C1F900712580 /* WatcherItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF898D32823C1F900712580 /* WatcherItem.swift */; }; @@ -98,6 +99,7 @@ 5CB954C526934EFC007632DC /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 5CB954CD269362E2007632DC /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; 5CDE468325BC3B2000CA0FB1 /* SelectCurrencyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCurrencyView.swift; sourceTree = ""; }; + 5CEA86F42840CF65001386FB /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 5CF57E39269588060081E4BB /* RewardedAd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RewardedAd.swift; sourceTree = ""; }; 5CF57E3B2695A3B20081E4BB /* InterstitialAd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterstitialAd.swift; sourceTree = ""; }; 5CF898D32823C1F900712580 /* WatcherItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatcherItem.swift; sourceTree = ""; }; @@ -251,6 +253,7 @@ 5C6E674C25C602BE001CC0D6 /* SnackBar.swift */, 5CF57E39269588060081E4BB /* RewardedAd.swift */, 5CF57E3B2695A3B20081E4BB /* InterstitialAd.swift */, + 5CEA86F42840CF65001386FB /* NotificationManager.swift */, ); path = Util; sourceTree = ""; @@ -497,6 +500,7 @@ 5C5D09392562EC0100DA9C4A /* Extensions.swift in Sources */, 5C17581A25BC74BD00D16BD9 /* SettingsView.swift in Sources */, 5C5D09332562EB9E00DA9C4A /* Application.swift in Sources */, + 5CEA86F52840CF65001386FB /* NotificationManager.swift in Sources */, 5C693EBA25C4AFF800C9373E /* SelectCurrenciesBottomView.swift in Sources */, 5C31E41E28141C7B008C42B9 /* InputView.swift in Sources */, 5C31E42A28141F1B008C42B9 /* SlideView.swift in Sources */, diff --git a/ios/CCC/UI/Watchers/WatchersView.swift b/ios/CCC/UI/Watchers/WatchersView.swift index 618c506e12..0104bdea01 100644 --- a/ios/CCC/UI/Watchers/WatchersView.swift +++ b/ios/CCC/UI/Watchers/WatchersView.swift @@ -17,6 +17,7 @@ typealias WatchersObservable = ObservableSEED struct WatchersView: View { @EnvironmentObject private var navigationStack: NavigationStack @StateObject var observable: WatchersObservable = koin.get() + @StateObject var notificationManager = NotificationManager() @State var baseBarInfo = BarInfo(isShown: false, watcher: nil) @State var targetBarInfo = BarInfo(isShown: false, watcher: nil) @@ -29,41 +30,52 @@ struct WatchersView: View { VStack { WatchersToolbarView(backEvent: observable.event.onBackClick) - Text(MR.strings().txt_txt_watchers_description.get()) - .font(.footnote) - .padding(18) - .multilineTextAlignment(.center) - .background(MR.colors().background_strong.get()) - .foregroundColor(MR.colors().text_weak.get()) - .contentShape(Rectangle()) - .padding(-8) + if notificationManager.authorizationStatus == .authorized { + Text(MR.strings().txt_txt_watchers_description.get()) + .font(.footnote) + .padding(18) + .multilineTextAlignment(.center) + .background(MR.colors().background_strong.get()) + .foregroundColor(MR.colors().text_weak.get()) + .contentShape(Rectangle()) + .padding(-8) - Form { - List(observable.state.watcherList, id: \.id) { watcher in - WatcherItem( - isBaseBarShown: $baseBarInfo.isShown, - isTargetBarShown: $targetBarInfo.isShown, - watcher: watcher, - event: observable.event - ) + Form { + List(observable.state.watcherList, id: \.id) { watcher in + WatcherItem( + isBaseBarShown: $baseBarInfo.isShown, + isTargetBarShown: $targetBarInfo.isShown, + watcher: watcher, + event: observable.event + ) + } + .listRowInsets(.init()) + .listRowBackground(MR.colors().background.get()) } - .listRowInsets(.init()) - .listRowBackground(MR.colors().background.get()) - } - VStack { - Button { - observable.event.onAddClick() - } label: { - Label(MR.strings().txt_add.get(), systemImage: "plus") + VStack { + Button { + observable.event.onAddClick() + } label: { + Label(MR.strings().txt_add.get(), systemImage: "plus") + } + .foregroundColor(MR.colors().text.get()) + .padding(.top, 10) + .padding(.bottom, 20) } - .foregroundColor(MR.colors().text.get()) - .padding(.top, 10) - .padding(.bottom, 20) + .padding(12) + .frame(maxWidth: .infinity, alignment: .center) + .background(MR.colors().background_strong.get()) + + } else { + VStack { + Button { + notificationManager.requestAuthorisation() + } label: { + Text("Request Permission") + } + }.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) } - .padding(12) - .frame(maxWidth: .infinity, alignment: .center) - .background(MR.colors().background_strong.get()) } .background(MR.colors().background.get()) .edgesIgnoringSafeArea(.bottom) @@ -96,9 +108,15 @@ struct WatchersView: View { ).environmentObject(navigationStack) } ) - .onAppear { observable.startObserving() } + .onAppear { + observable.startObserving() + notificationManager.reloadAuthorisationStatus() + } .onDisappear { observable.stopObserving() } .onReceive(observable.effect) { onEffect(effect: $0) } + .onChange(of: notificationManager.authorizationStatus) { + onAuthorisationChange(authorizationStatus: $0) + } .animation(.default) } @@ -126,6 +144,18 @@ struct WatchersView: View { } } + private func onAuthorisationChange(authorizationStatus: UNAuthorizationStatus?) { + logger.i(message: {"WatchersView onAuthorisationChange \(String(describing: authorizationStatus?.rawValue))"}) + switch authorizationStatus { + case .notDetermined: + notificationManager.requestAuthorisation() + case .authorized: + notificationManager.reloadAuthorisationStatus() + default: + break + } + } + struct BarInfo { var isShown: Bool var watcher: Client.Watcher? diff --git a/ios/CCC/Util/NotificationManager.swift b/ios/CCC/Util/NotificationManager.swift new file mode 100644 index 0000000000..ad826a0670 --- /dev/null +++ b/ios/CCC/Util/NotificationManager.swift @@ -0,0 +1,39 @@ +// +// NotificationManager.swift +// CCC +// +// Created by Mustafa Ozhan on 27.05.22. +// Copyright © 2022 orgName. All rights reserved. +// + +import Client +import UserNotifications + +final class NotificationManager: ObservableObject { + @Published var authorizationStatus: UNAuthorizationStatus? + + func reloadAuthorisationStatus() { + logger.i(message: {"NotificationManager reloadAuthorisationStatus"}) + + UNUserNotificationCenter.current().getNotificationSettings { settings in + DispatchQueue.main.async { + self.authorizationStatus = settings.authorizationStatus + } + } + } + + func requestAuthorisation() { + logger.i(message: {"NotificationManager requestAuthorisation"}) + UNUserNotificationCenter.current().requestAuthorization( + options: [.badge, .alert, .sound] + ) { isGranted, error in + + logger.i(message: { + "NotificationManager requestAuthorisation error: \(String(describing: error)) isGradted: \(isGranted)" + }) + DispatchQueue.main.async { + self.authorizationStatus = isGranted ? .authorized : .denied + } + } + } +} From 7edf41442062cb92b1837eaa7393c9275cbfcb7c Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sat, 28 May 2022 22:29:16 +0300 Subject: [PATCH 22/40] [#758] Reflect Notification Permission change and link to Settings (#759) * [#756] Create NotificationManager * [#758] Reflect Notification Permission change and link to Settings --- ios/CCC/UI/Watchers/WatchersView.swift | 20 +++++++++++++++++-- ios/CCC/Util/NotificationManager.swift | 2 +- .../commonMain/resources/MR/base/strings.xml | 1 + 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/ios/CCC/UI/Watchers/WatchersView.swift b/ios/CCC/UI/Watchers/WatchersView.swift index 0104bdea01..c8db0263b8 100644 --- a/ios/CCC/UI/Watchers/WatchersView.swift +++ b/ios/CCC/UI/Watchers/WatchersView.swift @@ -69,11 +69,22 @@ struct WatchersView: View { } else { VStack { + Text(MR.strings().txt_enable_notification_permission.get()) + .multilineTextAlignment(.center) Button { - notificationManager.requestAuthorisation() + if let url = URL( + string: UIApplication.openSettingsURLString + ), UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } } label: { - Text("Request Permission") + Label(MR.strings().txt_settings.get(), systemImage: "gear") } + .padding() + .background(MR.colors().background_weak.get()) + .foregroundColor(MR.colors().text.get()) + .cornerRadius(5) + }.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) } } @@ -114,6 +125,11 @@ struct WatchersView: View { } .onDisappear { observable.stopObserving() } .onReceive(observable.effect) { onEffect(effect: $0) } + .onReceive(NotificationCenter.default.publisher( + for: UIApplication.willEnterForegroundNotification + )) { _ in + notificationManager.reloadAuthorisationStatus() + } .onChange(of: notificationManager.authorizationStatus) { onAuthorisationChange(authorizationStatus: $0) } diff --git a/ios/CCC/Util/NotificationManager.swift b/ios/CCC/Util/NotificationManager.swift index ad826a0670..a31b00584d 100644 --- a/ios/CCC/Util/NotificationManager.swift +++ b/ios/CCC/Util/NotificationManager.swift @@ -10,7 +10,7 @@ import Client import UserNotifications final class NotificationManager: ObservableObject { - @Published var authorizationStatus: UNAuthorizationStatus? + @Published var authorizationStatus: UNAuthorizationStatus = .notDetermined func reloadAuthorisationStatus() { logger.i(message: {"NotificationManager reloadAuthorisationStatus"}) diff --git a/resources/src/commonMain/resources/MR/base/strings.xml b/resources/src/commonMain/resources/MR/base/strings.xml index 6598b6bcbf..ef2121b8e6 100644 --- a/resources/src/commonMain/resources/MR/base/strings.xml +++ b/resources/src/commonMain/resources/MR/base/strings.xml @@ -128,6 +128,7 @@ Watchers Watchers will be checked every hour and if the condition is verified then you will get a notification! + Please enable notification permission in Settings Rate > < From 3e2bbc2dc8387baf7896552c8cce3813df8ffe0f Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 29 May 2022 10:46:39 +0300 Subject: [PATCH 23/40] [#760] Add send Notification capability (#761) --- ios/CCC/Util/NotificationManager.swift | 24 +++++++++++++++++++ .../commonMain/resources/MR/base/strings.xml | 4 +++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/ios/CCC/Util/NotificationManager.swift b/ios/CCC/Util/NotificationManager.swift index a31b00584d..26288facf8 100644 --- a/ios/CCC/Util/NotificationManager.swift +++ b/ios/CCC/Util/NotificationManager.swift @@ -7,6 +7,7 @@ // import Client +import Resources import UserNotifications final class NotificationManager: ObservableObject { @@ -36,4 +37,27 @@ final class NotificationManager: ObservableObject { } } } + + func sendNotification(title: String, body: String) { + logger.i(message: {"NotificationManager sendNotification"}) + + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3, repeats: false) + + let content = UNMutableNotificationContent() + content.sound = .default + content.title = MR.strings().txt_watcher_alert_title.get() + content.body = MR.strings().txt_watcher_alert_sub_title.get() + + let request = UNNotificationRequest( + identifier: UUID().uuidString, + content: content, + trigger: trigger + ) + + UNUserNotificationCenter.current().add(request) { error in + logger.i(message: { + "NotificationManager sendNotification error: \(String(describing: error))" + }) + } + } } diff --git a/resources/src/commonMain/resources/MR/base/strings.xml b/resources/src/commonMain/resources/MR/base/strings.xml index ef2121b8e6..39d239e15d 100644 --- a/resources/src/commonMain/resources/MR/base/strings.xml +++ b/resources/src/commonMain/resources/MR/base/strings.xml @@ -127,8 +127,10 @@ Watchers - Watchers will be checked every hour and if the condition is verified then you will get a notification! + Watchers will be checked every hour and if the threshold is reached then you will get a notification! Please enable notification permission in Settings + Watcher Alert! + The rate threshold is reached. Rate > < From 01e29b51101b42d0807f5ae1d5abe1ef60e24ee6 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Tue, 31 May 2022 22:25:36 +0300 Subject: [PATCH 24/40] [#762] Fix Project Health task (#763) --- .github/workflows/dependency.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependency.yml b/.github/workflows/dependency.yml index 8eeb282471..2570ec4808 100644 --- a/.github/workflows/dependency.yml +++ b/.github/workflows/dependency.yml @@ -17,6 +17,15 @@ jobs: with: fetch-depth: 0 + - name: Adding secret files + run: | + mkdir android/src/release + echo "${{ secrets.RELEASE_GOOGLE_SERVICES_JSON_ASC }}" > google-services.json.asc + gpg -d --passphrase "${{ secrets.SECRET_PASSWORD }}" --batch google-services.json.asc > android/src/release/google-services.json + mkdir android/src/debug + echo "${{ secrets.DEBUG_GOOGLE_SERVICES_JSON_ASC }}" > google-services.json.asc + gpg -d --passphrase "${{ secrets.SECRET_PASSWORD }}" --batch google-services.json.asc > android/src/debug/google-services.json + - name: Set up JDK 11 uses: actions/setup-java@v1 with: @@ -28,13 +37,13 @@ jobs: - name: Upload Dependency Updates report uses: actions/upload-artifact@v2 with: - name: dependency-updates.txt + name: dependency-updates path: build/dependencyUpdates/report.txt - name: Upload Build Healt Report uses: actions/upload-artifact@v2 with: - name: build-health.txt + name: build-health path: build/reports/dependency-analysis/build-health-report.txt - name: Set Job Status From ca352747a38c0ce6d0be72ada02574fc9decd14c Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Wed, 1 Jun 2022 06:43:39 +0300 Subject: [PATCH 25/40] [#764] Dependency Updates (#765) --- .../google/kotlin/com/oztechan/ccc/ad/AdManagerImpl.kt | 8 +++++--- buildSrc/src/main/kotlin/Versions.kt | 10 +++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ad/src/google/kotlin/com/oztechan/ccc/ad/AdManagerImpl.kt b/ad/src/google/kotlin/com/oztechan/ccc/ad/AdManagerImpl.kt index e1961ba92f..e67f183d13 100644 --- a/ad/src/google/kotlin/com/oztechan/ccc/ad/AdManagerImpl.kt +++ b/ad/src/google/kotlin/com/oztechan/ccc/ad/AdManagerImpl.kt @@ -36,9 +36,11 @@ class AdManagerImpl : AdManager { width.toFloat() } - adSize = AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize( - context, - (adWidthPixels / resources.displayMetrics.density).toInt() + setAdSize( + AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize( + context, + (adWidthPixels / resources.displayMetrics.density).toInt() + ) ) adUnitId = adId adListener = object : AdListener() { diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 8f164ebf57..6863204175 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -6,7 +6,7 @@ object Versions { const val KOTLIN = "1.6.21" const val KSP = "$KOTLIN-1.0.5" - const val ANDROID_GRADLE_PLUGIN = "7.2.0" + const val ANDROID_GRADLE_PLUGIN = "7.2.1" const val ANDROID_MATERIAL = "1.6.0" const val CONSTRAINT_LAYOUT = "2.1.4" const val KTOR = "1.6.8" @@ -17,8 +17,8 @@ object Versions { const val FIREBASE_REMOTE_CONFIG = "21.1.0" const val DESUGARING = "1.1.5" const val GSM = "4.3.10" - const val CRASHLYTICS = "2.8.1" - const val ADMOB = "20.6.0" + const val CRASHLYTICS = "2.9.0" + const val ADMOB = "21.0.0" const val NAVIGATION = "2.3.5" const val PLAY_CORE = "1.10.3" const val KOTLIN_X_DATE_TIME = "0.3.2" @@ -27,9 +27,9 @@ object Versions { const val LEAK_CANARY = "2.9.1" const val SQL_DELIGHT = "1.5.3" const val LIFECYCLE = "2.4.1" - const val MOKO_RESOURCES = "0.20.0" + const val MOKO_RESOURCES = "0.20.1" const val DEPENDENCY_UPDATES = "0.42.0" - const val BUILD_HEALTH = "1.2.1" + const val BUILD_HEALTH = "1.4.0" const val BUILD_KONFIG = "0.12.0" const val WORK_RUNTIME = "2.7.1" const val SPLASH_SCREEN = "1.0.0-alpha02" From 25611d53f1d8da6fc8fef7f310eb380b38a42d5a Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sat, 11 Jun 2022 23:23:51 +0200 Subject: [PATCH 26/40] [#766] Convert Helpers to Managers (#767) --- .../kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt | 4 ++-- .../ccc/client/{helper => manager/session}/SessionManager.kt | 2 +- .../client/{helper => manager/session}/SessionManagerImpl.kt | 2 +- .../ccc/client/viewmodel/calculator/CalculatorViewModel.kt | 2 +- .../ccc/client/viewmodel/currencies/CurrenciesViewModel.kt | 2 +- .../com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt | 2 +- .../ccc/client/viewmodel/settings/SettingsViewModel.kt | 3 +-- .../ccc/client/{helper => manager}/SessionManagerTest.kt | 3 ++- .../oztechan/ccc/client/viewmodel/CalculatorViewModelTest.kt | 2 +- .../oztechan/ccc/client/viewmodel/CurrenciesViewModelTest.kt | 2 +- .../com/oztechan/ccc/client/viewmodel/MainViewModelTest.kt | 2 +- .../oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt | 3 +-- 12 files changed, 14 insertions(+), 15 deletions(-) rename client/src/commonMain/kotlin/com/oztechan/ccc/client/{helper => manager/session}/SessionManager.kt (81%) rename client/src/commonMain/kotlin/com/oztechan/ccc/client/{helper => manager/session}/SessionManagerImpl.kt (96%) rename client/src/commonTest/kotlin/com/oztechan/ccc/client/{helper => manager}/SessionManagerTest.kt (99%) diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt index e1d9b14d46..ec56324fae 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt @@ -1,8 +1,8 @@ package com.oztechan.ccc.client.di.module import com.oztechan.ccc.client.di.viewModelDefinition -import com.oztechan.ccc.client.helper.SessionManager -import com.oztechan.ccc.client.helper.SessionManagerImpl +import com.oztechan.ccc.client.manager.session.SessionManager +import com.oztechan.ccc.client.manager.session.SessionManagerImpl import com.oztechan.ccc.client.viewmodel.adremove.AdRemoveViewModel import com.oztechan.ccc.client.viewmodel.calculator.CalculatorViewModel import com.oztechan.ccc.client.viewmodel.currencies.CurrenciesViewModel diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/helper/SessionManager.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/session/SessionManager.kt similarity index 81% rename from client/src/commonMain/kotlin/com/oztechan/ccc/client/helper/SessionManager.kt rename to client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/session/SessionManager.kt index 1df5ad3e04..3f1ade2bc7 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/helper/SessionManager.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/session/SessionManager.kt @@ -1,4 +1,4 @@ -package com.oztechan.ccc.client.helper +package com.oztechan.ccc.client.manager.session interface SessionManager { fun shouldShowBannerAd(): Boolean diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/helper/SessionManagerImpl.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/session/SessionManagerImpl.kt similarity index 96% rename from client/src/commonMain/kotlin/com/oztechan/ccc/client/helper/SessionManagerImpl.kt rename to client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/session/SessionManagerImpl.kt index 77fc059e3e..92689e6059 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/helper/SessionManagerImpl.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/session/SessionManagerImpl.kt @@ -1,4 +1,4 @@ -package com.oztechan.ccc.client.helper +package com.oztechan.ccc.client.manager.session import com.github.submob.scopemob.mapTo import com.github.submob.scopemob.whether diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt index 5bfac6298d..09a8dbae61 100755 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt @@ -8,7 +8,7 @@ import com.github.submob.scopemob.mapTo import com.github.submob.scopemob.whether import com.github.submob.scopemob.whetherNot import com.oztechan.ccc.client.base.BaseSEEDViewModel -import com.oztechan.ccc.client.helper.SessionManager +import com.oztechan.ccc.client.manager.session.SessionManager import com.oztechan.ccc.client.mapper.toRates import com.oztechan.ccc.client.mapper.toTodayResponse import com.oztechan.ccc.client.mapper.toUIModelList diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt index 31beb6d0fc..548a7d3a2d 100755 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt @@ -9,7 +9,7 @@ import com.github.submob.scopemob.mapTo import com.github.submob.scopemob.whether import com.github.submob.scopemob.whetherNot import com.oztechan.ccc.client.base.BaseSEEDViewModel -import com.oztechan.ccc.client.helper.SessionManager +import com.oztechan.ccc.client.manager.session.SessionManager import com.oztechan.ccc.client.mapper.toUIModelList import com.oztechan.ccc.client.model.Currency import com.oztechan.ccc.client.util.launchIgnored diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt index 4c58c7dad1..de02e80b84 100755 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt @@ -6,7 +6,7 @@ package com.oztechan.ccc.client.viewmodel.main import co.touchlab.kermit.Logger import com.oztechan.ccc.client.base.BaseSEEDViewModel import com.oztechan.ccc.client.base.BaseState -import com.oztechan.ccc.client.helper.SessionManager +import com.oztechan.ccc.client.manager.session.SessionManager import com.oztechan.ccc.client.util.isRewardExpired import com.oztechan.ccc.common.settings.SettingsRepository import com.oztechan.ccc.config.ConfigManager diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt index a1d91618fb..c90b1e6bd9 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt @@ -6,7 +6,7 @@ package com.oztechan.ccc.client.viewmodel.settings import co.touchlab.kermit.Logger import com.github.submob.logmob.e import com.oztechan.ccc.client.base.BaseSEEDViewModel -import com.oztechan.ccc.client.helper.SessionManager +import com.oztechan.ccc.client.manager.session.SessionManager import com.oztechan.ccc.client.model.AppTheme import com.oztechan.ccc.client.model.RemoveAdType import com.oztechan.ccc.client.util.calculateAdRewardEnd @@ -118,7 +118,6 @@ class SettingsViewModel( _effect.emit(SettingsEffect.OpenCurrencies) } - override fun onWatchersClicked() = clientScope.launchIgnored { Logger.d { "SettingsViewModel onWatchersClicked" } _effect.emit(SettingsEffect.OpenWatchers) diff --git a/client/src/commonTest/kotlin/com/oztechan/ccc/client/helper/SessionManagerTest.kt b/client/src/commonTest/kotlin/com/oztechan/ccc/client/manager/SessionManagerTest.kt similarity index 99% rename from client/src/commonTest/kotlin/com/oztechan/ccc/client/helper/SessionManagerTest.kt rename to client/src/commonTest/kotlin/com/oztechan/ccc/client/manager/SessionManagerTest.kt index 5ce4f2ea12..497012a956 100644 --- a/client/src/commonTest/kotlin/com/oztechan/ccc/client/helper/SessionManagerTest.kt +++ b/client/src/commonTest/kotlin/com/oztechan/ccc/client/manager/SessionManagerTest.kt @@ -1,7 +1,8 @@ -package com.oztechan.ccc.client.helper +package com.oztechan.ccc.client.manager import com.oztechan.ccc.client.BuildKonfig import com.oztechan.ccc.client.device +import com.oztechan.ccc.client.manager.session.SessionManagerImpl import com.oztechan.ccc.common.settings.SettingsRepository import com.oztechan.ccc.common.util.nowAsLong import com.oztechan.ccc.config.ConfigManager diff --git a/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/CalculatorViewModelTest.kt b/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/CalculatorViewModelTest.kt index 6a83c1107c..5b1ee4c526 100644 --- a/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/CalculatorViewModelTest.kt +++ b/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/CalculatorViewModelTest.kt @@ -4,7 +4,7 @@ package com.oztechan.ccc.client.viewmodel import com.github.submob.logmob.initLogger -import com.oztechan.ccc.client.helper.SessionManager +import com.oztechan.ccc.client.manager.session.SessionManager import com.oztechan.ccc.client.mapper.toUIModel import com.oztechan.ccc.client.util.after import com.oztechan.ccc.client.util.before diff --git a/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/CurrenciesViewModelTest.kt b/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/CurrenciesViewModelTest.kt index f2fa056bce..2aa9131b65 100644 --- a/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/CurrenciesViewModelTest.kt +++ b/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/CurrenciesViewModelTest.kt @@ -4,7 +4,7 @@ package com.oztechan.ccc.client.viewmodel import com.github.submob.logmob.initLogger -import com.oztechan.ccc.client.helper.SessionManager +import com.oztechan.ccc.client.manager.session.SessionManager import com.oztechan.ccc.client.mapper.toUIModel import com.oztechan.ccc.client.util.after import com.oztechan.ccc.client.util.before diff --git a/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/MainViewModelTest.kt b/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/MainViewModelTest.kt index 288b4da225..2db1e33c7c 100644 --- a/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/MainViewModelTest.kt +++ b/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/MainViewModelTest.kt @@ -8,7 +8,7 @@ import com.github.submob.logmob.initLogger import com.github.submob.scopemob.castTo import com.oztechan.ccc.client.BuildKonfig import com.oztechan.ccc.client.device -import com.oztechan.ccc.client.helper.SessionManager +import com.oztechan.ccc.client.manager.session.SessionManager import com.oztechan.ccc.client.util.after import com.oztechan.ccc.client.util.before import com.oztechan.ccc.client.viewmodel.main.MainEffect diff --git a/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt b/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt index eadffc16dd..f0d1ccd7b7 100644 --- a/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt +++ b/client/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/SettingsViewModelTest.kt @@ -4,7 +4,7 @@ package com.oztechan.ccc.client.viewmodel import com.github.submob.logmob.initLogger -import com.oztechan.ccc.client.helper.SessionManager +import com.oztechan.ccc.client.manager.session.SessionManager import com.oztechan.ccc.client.model.AppTheme import com.oztechan.ccc.client.model.RemoveAdType import com.oztechan.ccc.client.util.after @@ -267,7 +267,6 @@ class SettingsViewModelTest { assertTrue { it is SettingsEffect.OpenCurrencies } } - @Test fun onWatchersClicked() = viewModel.effect.before { viewModel.event.onWatchersClicked() From b9342abe073b20a3db805e0abf7ed00dade40a01 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 12 Jun 2022 13:08:13 +0200 Subject: [PATCH 27/40] [#768] Library updates (#770) --- buildSrc/src/main/kotlin/Versions.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 6863204175..0e047797b5 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -7,7 +7,7 @@ object Versions { const val KOTLIN = "1.6.21" const val KSP = "$KOTLIN-1.0.5" const val ANDROID_GRADLE_PLUGIN = "7.2.1" - const val ANDROID_MATERIAL = "1.6.0" + const val ANDROID_MATERIAL = "1.6.1" const val CONSTRAINT_LAYOUT = "2.1.4" const val KTOR = "1.6.8" const val LOG_BACK = "1.2.11" @@ -29,13 +29,13 @@ object Versions { const val LIFECYCLE = "2.4.1" const val MOKO_RESOURCES = "0.20.1" const val DEPENDENCY_UPDATES = "0.42.0" - const val BUILD_HEALTH = "1.4.0" + const val BUILD_HEALTH = "1.5.0" const val BUILD_KONFIG = "0.12.0" const val WORK_RUNTIME = "2.7.1" const val SPLASH_SCREEN = "1.0.0-alpha02" const val KOVER = "0.5.1" const val ROOT_BEER = "0.1.0" - const val MOCKATIVE = "1.2.1" + const val MOCKATIVE = "1.2.5" const val SCOPE_MOB = "2.1.5" const val PARSER_MOB = "1.1.6" const val BASE_MOB = "2.1.4" From d8ceae08e18fe78d7c29e28bb39a744ddf0e9904 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 12 Jun 2022 20:45:23 +0200 Subject: [PATCH 28/40] [#769] Kotlin update (#771) --- buildSrc/src/main/kotlin/Versions.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 0e047797b5..c83af445d8 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -4,8 +4,8 @@ @Suppress("SpellCheckingInspection") object Versions { - const val KOTLIN = "1.6.21" - const val KSP = "$KOTLIN-1.0.5" + const val KOTLIN = "1.7.0" + const val KSP = "$KOTLIN-1.0.6" const val ANDROID_GRADLE_PLUGIN = "7.2.1" const val ANDROID_MATERIAL = "1.6.1" const val CONSTRAINT_LAYOUT = "2.1.4" From 26284bde89f1ae1bad32965b03afc5af5473ebaa Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Mon, 20 Jun 2022 12:20:03 +0200 Subject: [PATCH 29/40] [#777] Change exact time definition in Watchers (#780) --- ios/CCC/UI/Watchers/WatchersView.swift | 17 +++++++++-------- .../commonMain/resources/MR/base/strings.xml | 6 +++--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/ios/CCC/UI/Watchers/WatchersView.swift b/ios/CCC/UI/Watchers/WatchersView.swift index c8db0263b8..08306fbcc6 100644 --- a/ios/CCC/UI/Watchers/WatchersView.swift +++ b/ios/CCC/UI/Watchers/WatchersView.swift @@ -30,15 +30,16 @@ struct WatchersView: View { VStack { WatchersToolbarView(backEvent: observable.event.onBackClick) + Text(MR.strings().txt_txt_watchers_description.get()) + .font(.footnote) + .padding(18) + .multilineTextAlignment(.center) + .background(MR.colors().background_strong.get()) + .foregroundColor(MR.colors().text_weak.get()) + .contentShape(Rectangle()) + .padding(-8) + if notificationManager.authorizationStatus == .authorized { - Text(MR.strings().txt_txt_watchers_description.get()) - .font(.footnote) - .padding(18) - .multilineTextAlignment(.center) - .background(MR.colors().background_strong.get()) - .foregroundColor(MR.colors().text_weak.get()) - .contentShape(Rectangle()) - .padding(-8) Form { List(observable.state.watcherList, id: \.id) { watcher in diff --git a/resources/src/commonMain/resources/MR/base/strings.xml b/resources/src/commonMain/resources/MR/base/strings.xml index 39d239e15d..f84e72e9df 100644 --- a/resources/src/commonMain/resources/MR/base/strings.xml +++ b/resources/src/commonMain/resources/MR/base/strings.xml @@ -92,7 +92,7 @@ Set your currencies Watchers - Watch rates every 1 hour + Check in the background Theme Change app theme @@ -127,7 +127,7 @@ Watchers - Watchers will be checked every hour and if the threshold is reached then you will get a notification! + Watchers will be checked on the background and if the threshold is reached then you will get a notification! Please enable notification permission in Settings Watcher Alert! The rate threshold is reached. @@ -145,4 +145,4 @@ New Version Available Looks like you have an older version of the app. Please update to get latest features and bug fixes to get best experience. - \ No newline at end of file + From d13bdffe6837b108f1a2f39c5a431a2fe7b9e9b5 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Tue, 21 Jun 2022 23:40:47 +0200 Subject: [PATCH 30/40] [#779] Selecting base currency on fly doesn't work at first start (#781) --- .../ccc/client/viewmodel/currencies/CurrenciesViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt index 548a7d3a2d..bb546874e3 100755 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt @@ -78,8 +78,8 @@ class CurrenciesViewModel( .toList().firstOrNull()?.isActive == false } )?.mapTo { - state.value.currencyList.firstOrNull { it.isActive } - }?.name.orEmpty().let { newBase -> + state.value.currencyList.firstOrNull { it.isActive }?.name.orEmpty() + }?.let { newBase -> settingsRepository.currentBase = newBase clientScope.launch { _effect.emit(CurrenciesEffect.ChangeBase(newBase)) } } From 7421cb19f53c88131df2cf49851c5ee666e86a1d Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Wed, 22 Jun 2022 14:43:06 +0200 Subject: [PATCH 31/40] [#782] Move observe utility into common and move sqldelight closable (#783) --- .../viewmodel/settings/SettingsViewModel.kt | 3 +-- .../oztechan/ccc/client/base/BaseViewModel.kt | 19 --------------- .../ccc/client/util/IOSCoroutineUtil.kt | 24 +++++++++++++++++++ ios/CCC/Util/ObservableSEED.swift | 6 ++--- 4 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCoroutineUtil.kt diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt index c90b1e6bd9..8513dee92b 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt @@ -100,8 +100,7 @@ class SettingsViewModel( fun getAppTheme() = settingsRepository.appTheme - // used in ios - @Suppress("unused") + @Suppress("unused") // used in iOS fun updateAddFreeDate() = RemoveAdType.VIDEO.calculateAdRewardEnd(nowAsLong()).let { settingsRepository.adFreeEndDate = it _state.update(addFreeEndDate = it.toDateString()) diff --git a/client/src/iosMain/kotlin/com/oztechan/ccc/client/base/BaseViewModel.kt b/client/src/iosMain/kotlin/com/oztechan/ccc/client/base/BaseViewModel.kt index 62e6c6e7ec..31cd849cc3 100644 --- a/client/src/iosMain/kotlin/com/oztechan/ccc/client/base/BaseViewModel.kt +++ b/client/src/iosMain/kotlin/com/oztechan/ccc/client/base/BaseViewModel.kt @@ -5,15 +5,10 @@ package com.oztechan.ccc.client.base import co.touchlab.kermit.Logger -import io.ktor.utils.io.core.Closeable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach @Suppress("EmptyDefaultConstructor", "unused") actual open class BaseViewModel actual constructor() { @@ -33,18 +28,4 @@ actual open class BaseViewModel actual constructor() { Logger.d { "${this::class.simpleName} onCleared" } viewModelJob.cancelChildren() } - - fun Flow.observe(onChange: ((T) -> Unit)): Closeable { - val job = Job() - onEach { - onChange(it) - }.launchIn( - CoroutineScope(Dispatchers.Main + job) - ) - return object : Closeable { - override fun close() { - job.cancel() - } - } - } } diff --git a/client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCoroutineUtil.kt b/client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCoroutineUtil.kt new file mode 100644 index 0000000000..1e20bf2bf4 --- /dev/null +++ b/client/src/iosMain/kotlin/com/oztechan/ccc/client/util/IOSCoroutineUtil.kt @@ -0,0 +1,24 @@ +package com.oztechan.ccc.client.util + +import com.squareup.sqldelight.db.Closeable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@Suppress("unused") // used in iOS +fun Flow.observeWithCloseable(onChange: ((T) -> Unit)): Closeable { + val job = Job() + onEach { + onChange(it) + }.launchIn( + CoroutineScope(Dispatchers.Main + job) + ) + return object : Closeable { + override fun close() { + job.cancel() + } + } +} diff --git a/ios/CCC/Util/ObservableSEED.swift b/ios/CCC/Util/ObservableSEED.swift index 79a3aa20a0..c9095e1a79 100644 --- a/ios/CCC/Util/ObservableSEED.swift +++ b/ios/CCC/Util/ObservableSEED.swift @@ -26,7 +26,7 @@ final class ObservableSEED< let data: Data? - private var closeable: Ktor_ioCloseable! + private var closeable: RuntimeCloseable! // swiftlint:disable force_cast init(viewModel: ViewModel) { @@ -46,12 +46,12 @@ final class ObservableSEED< logger.i(message: {"ObservableSEED \(ViewModel.description()) startObserving"}) if viewModel.state != nil { - closeable = viewModel.observe(viewModel.state!, onChange: { + closeable = IOSCoroutineUtilKt.observeWithCloseable(viewModel.state!, onChange: { self.state = $0 as! State }) } if viewModel.effect != nil { - closeable = viewModel.observe(viewModel.effect!, onChange: { + closeable = IOSCoroutineUtilKt.observeWithCloseable(viewModel.effect!, onChange: { self.effect.send($0 as! Effect) }) } From d6ef45b4992844f8d9ce2c5571c18f7ac1a10cc4 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Fri, 24 Jun 2022 14:24:05 +0200 Subject: [PATCH 32/40] [#778] Notification permission issue in iOS 15.5 (#786) --- ios/CCC/Util/NotificationManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/CCC/Util/NotificationManager.swift b/ios/CCC/Util/NotificationManager.swift index 26288facf8..e6ac459bfc 100644 --- a/ios/CCC/Util/NotificationManager.swift +++ b/ios/CCC/Util/NotificationManager.swift @@ -11,7 +11,7 @@ import Resources import UserNotifications final class NotificationManager: ObservableObject { - @Published var authorizationStatus: UNAuthorizationStatus = .notDetermined + @Published var authorizationStatus: UNAuthorizationStatus? func reloadAuthorisationStatus() { logger.i(message: {"NotificationManager reloadAuthorisationStatus"}) From 142bdc4bd2b83f74325aead5a2f411f53591be29 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sat, 25 Jun 2022 14:50:00 +0200 Subject: [PATCH 33/40] [#787] Unify conversion calculation (#788) --- .../viewmodel/calculator/CalculatorViewModel.kt | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt index 09a8dbae61..46287f712b 100755 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt @@ -90,9 +90,8 @@ class CalculatorViewModel( .launchIn(clientScope) } - private fun getRates() = data.rates?.let { rates -> - calculateConversions(rates) - _state.update(rateState = RateState.Cached(rates.date)) + private fun getRates() = data.rates?.let { + calculateConversions(it, RateState.Cached(it.date)) } ?: clientScope.launch { runCatching { apiRepository.getRatesByBackend(settingsRepository.currentBase) } .onFailure(::getRatesFailed) @@ -102,8 +101,7 @@ class CalculatorViewModel( private fun getRatesSuccess(currencyResponse: CurrencyResponse) = currencyResponse .toRates().let { data.rates = it - calculateConversions(it) - _state.update(rateState = RateState.Online(it.date)) + calculateConversions(it, RateState.Online(it.date)) }.also { offlineRatesRepository.insertOfflineRates(currencyResponse.toTodayResponse()) } @@ -112,9 +110,8 @@ class CalculatorViewModel( Logger.w(t) { "CalculatorViewModel getRatesFailed" } offlineRatesRepository.getOfflineRatesByBase( settingsRepository.currentBase - )?.let { offlineRates -> - calculateConversions(offlineRates) - _state.update(rateState = RateState.Offline(offlineRates.date)) + )?.let { + calculateConversions(it, RateState.Offline(it.date)) } ?: clientScope.launch { Logger.w(Exception("No offline rates")) { this@CalculatorViewModel::class.simpleName.toString() } @@ -153,10 +150,11 @@ class CalculatorViewModel( } } - private fun calculateConversions(rates: Rates?) = _state.update( + private fun calculateConversions(rates: Rates, rateState: RateState) = _state.update( currencyList = _state.value.currencyList.onEach { it.rate = rates.calculateResult(it.name, _state.value.output) }, + rateState = rateState, loading = false ) From 94a2d8b8db7a264b2aa50c530624d3f4a3b9fcc0 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sat, 25 Jun 2022 15:19:52 +0200 Subject: [PATCH 34/40] [#784] BackgroundManager Implementation (#785) * [#784] BackgroundManager Implementation * [#778] add todo * [#778] suppress LabeledExpression --- .../ccc/client/di/module/ClientModule.kt | 3 ++ .../manager/background/BackgroundManager.kt | 5 +++ .../background/BackgroundManagerImpl.kt | 37 +++++++++++++++++++ ios/CCC/Application.swift | 4 ++ ios/CCC/DI/Koin.swift | 4 ++ 5 files changed, 53 insertions(+) create mode 100644 client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/background/BackgroundManager.kt create mode 100644 client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/background/BackgroundManagerImpl.kt diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt index ec56324fae..c4f6a0c99d 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt @@ -1,6 +1,7 @@ package com.oztechan.ccc.client.di.module import com.oztechan.ccc.client.di.viewModelDefinition +import com.oztechan.ccc.client.manager.background.BackgroundManagerImpl import com.oztechan.ccc.client.manager.session.SessionManager import com.oztechan.ccc.client.manager.session.SessionManagerImpl import com.oztechan.ccc.client.viewmodel.adremove.AdRemoveViewModel @@ -25,4 +26,6 @@ var clientModule = module { single { ConfigManagerImpl() } single { SessionManagerImpl(get(), get()) } + // todo https://github.com/InsertKoinIO/koin/issues/1366 + single { BackgroundManagerImpl(get(), get()) } } diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/background/BackgroundManager.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/background/BackgroundManager.kt new file mode 100644 index 0000000000..da5eee9a77 --- /dev/null +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/background/BackgroundManager.kt @@ -0,0 +1,5 @@ +package com.oztechan.ccc.client.manager.background + +interface BackgroundManager { + fun shouldSendNotification(): Boolean +} diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/background/BackgroundManagerImpl.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/background/BackgroundManagerImpl.kt new file mode 100644 index 0000000000..f22013ae38 --- /dev/null +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/background/BackgroundManagerImpl.kt @@ -0,0 +1,37 @@ +package com.oztechan.ccc.client.manager.background + +import co.touchlab.kermit.Logger +import com.oztechan.ccc.client.util.getConversionByName +import com.oztechan.ccc.common.api.repo.ApiRepository +import com.oztechan.ccc.common.db.watcher.WatcherRepository +import kotlinx.coroutines.runBlocking + +class BackgroundManagerImpl( + private val watchersRepository: WatcherRepository, + private val apiRepository: ApiRepository +) : BackgroundManager { + + init { + Logger.d { "BackgroundManagerImpl init" } + } + + + @Suppress("LabeledExpression") + override fun shouldSendNotification() = runBlocking { + Logger.d { "BackgroundManagerImpl checkNotifications" } + + watchersRepository.getWatchers().forEach { watcher -> + apiRepository + .getRatesByBackend(watcher.base) + .rates + .getConversionByName(watcher.target) + ?.let { conversionRate -> + when { + watcher.isGreater && conversionRate > watcher.rate -> return@runBlocking true + !watcher.isGreater && conversionRate < watcher.rate -> return@runBlocking true + } + } + } + return@runBlocking false + } +} diff --git a/ios/CCC/Application.swift b/ios/CCC/Application.swift index 385f2a4085..e1889cda8b 100644 --- a/ios/CCC/Application.swift +++ b/ios/CCC/Application.swift @@ -17,6 +17,10 @@ let logger = LoggerKt.doInitLogger() @main struct Application: App { + lazy var backgroundManager: BackgroundManager = { + koin.get() + }() + init() { logger.i(message: {"Application init"}) diff --git a/ios/CCC/DI/Koin.swift b/ios/CCC/DI/Koin.swift index 6cdde84cb2..5939544ebf 100644 --- a/ios/CCC/DI/Koin.swift +++ b/ios/CCC/DI/Koin.swift @@ -51,6 +51,10 @@ extension Koin_coreKoin { return koin.getDependency(objCClass: WatchersViewModel.self) as! WatchersViewModel } + func get() -> BackgroundManager { + return koin.getDependency(objCClass: BackgroundManagerImpl.self) as! BackgroundManager + } + // Observable func get() -> MainObservable { return MainObservable(viewModel: get()) From 9c5d050f5a3148773ed196ef12991fca0c3f9f93 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sat, 25 Jun 2022 19:40:31 +0200 Subject: [PATCH 35/40] [#789] Update Watcher headline style (#790) --- ios/CCC/UI/Watchers/WatchersToolbarView.swift | 24 +++++++++++++------ ios/CCC/UI/Watchers/WatchersView.swift | 9 ------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/ios/CCC/UI/Watchers/WatchersToolbarView.swift b/ios/CCC/UI/Watchers/WatchersToolbarView.swift index 9b2d4b9e97..e48e9b0794 100644 --- a/ios/CCC/UI/Watchers/WatchersToolbarView.swift +++ b/ios/CCC/UI/Watchers/WatchersToolbarView.swift @@ -13,16 +13,26 @@ struct WatchersToolbarView: View { var backEvent: () -> Void var body: some View { - HStack { - ToolbarButton(clickEvent: backEvent, imgName: "chevron.left") + VStack { + HStack { + ToolbarButton(clickEvent: backEvent, imgName: "chevron.left") - Text(MR.strings().txt_watchers.get()) - .font(.title3) + Text(MR.strings().txt_watchers.get()) + .font(.title3) - Spacer() + Spacer() + } + + Text(MR.strings().txt_txt_watchers_description.get()) + .contentShape(Rectangle()) + .font(.footnote) + .multilineTextAlignment(.center) + .background(MR.colors().background_strong.get()) + .foregroundColor(MR.colors().text_weak.get()) + .padding(.top, 10) } - .frame(width: .infinity, height: 36) - .padding(EdgeInsets(top: 15, leading: 10, bottom: 6, trailing: 20)) + .frame(width: .infinity, height: .nan) + .padding(EdgeInsets(top: 15, leading: 10, bottom: 5, trailing: 20)) .background(MR.colors().background_strong.get()) } } diff --git a/ios/CCC/UI/Watchers/WatchersView.swift b/ios/CCC/UI/Watchers/WatchersView.swift index 08306fbcc6..4e429d9cc7 100644 --- a/ios/CCC/UI/Watchers/WatchersView.swift +++ b/ios/CCC/UI/Watchers/WatchersView.swift @@ -30,15 +30,6 @@ struct WatchersView: View { VStack { WatchersToolbarView(backEvent: observable.event.onBackClick) - Text(MR.strings().txt_txt_watchers_description.get()) - .font(.footnote) - .padding(18) - .multilineTextAlignment(.center) - .background(MR.colors().background_strong.get()) - .foregroundColor(MR.colors().text_weak.get()) - .contentShape(Rectangle()) - .padding(-8) - if notificationManager.authorizationStatus == .authorized { Form { From c083ab954bfe136971a07f86a98ecc6de11545ea Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 26 Jun 2022 15:49:03 +0200 Subject: [PATCH 36/40] [#791] Click Add button warning (#792) --- ios/CCC/UI/Watchers/WatchersToolbarView.swift | 4 ++-- ios/CCC/UI/Watchers/WatchersView.swift | 7 +++++++ resources/src/commonMain/resources/MR/base/strings.xml | 3 ++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ios/CCC/UI/Watchers/WatchersToolbarView.swift b/ios/CCC/UI/Watchers/WatchersToolbarView.swift index e48e9b0794..afec7ca264 100644 --- a/ios/CCC/UI/Watchers/WatchersToolbarView.swift +++ b/ios/CCC/UI/Watchers/WatchersToolbarView.swift @@ -23,13 +23,13 @@ struct WatchersToolbarView: View { Spacer() } - Text(MR.strings().txt_txt_watchers_description.get()) + Text(MR.strings().txt_watchers_description.get()) .contentShape(Rectangle()) .font(.footnote) .multilineTextAlignment(.center) .background(MR.colors().background_strong.get()) .foregroundColor(MR.colors().text_weak.get()) - .padding(.top, 10) + .padding(10) } .frame(width: .infinity, height: .nan) .padding(EdgeInsets(top: 15, leading: 10, bottom: 5, trailing: 20)) diff --git a/ios/CCC/UI/Watchers/WatchersView.swift b/ios/CCC/UI/Watchers/WatchersView.swift index 4e429d9cc7..d6ac2e200c 100644 --- a/ios/CCC/UI/Watchers/WatchersView.swift +++ b/ios/CCC/UI/Watchers/WatchersView.swift @@ -45,6 +45,13 @@ struct WatchersView: View { .listRowBackground(MR.colors().background.get()) } + if observable.state.watcherList.count == 0 { + Text(MR.strings().txt_click_to_add.get()) + .font(.footnote) + .frame(width: .infinity, height: .infinity, alignment: .center) + .padding() + } + VStack { Button { observable.event.onAddClick() diff --git a/resources/src/commonMain/resources/MR/base/strings.xml b/resources/src/commonMain/resources/MR/base/strings.xml index f84e72e9df..6ee81bb3c3 100644 --- a/resources/src/commonMain/resources/MR/base/strings.xml +++ b/resources/src/commonMain/resources/MR/base/strings.xml @@ -127,7 +127,7 @@ Watchers - Watchers will be checked on the background and if the threshold is reached then you will get a notification! + Watchers will be checked on the background and if the threshold is reached then you will get a notification! Please enable notification permission in Settings Watcher Alert! The rate threshold is reached. @@ -135,6 +135,7 @@ > < Add + Click Add button to add a new watcher Remove Ads From 0f782380d3fed92537758e9231bbcfbca7c541b8 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 26 Jun 2022 21:33:11 +0200 Subject: [PATCH 37/40] [#795] Create BackgroundTask for Watcher (#797) --- ios/CCC/Application.swift | 65 ++++++++++++++++++++++++++++++++++-- ios/CCC/Resources/Info.plist | 8 +++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/ios/CCC/Application.swift b/ios/CCC/Application.swift index e1889cda8b..834cd86722 100644 --- a/ios/CCC/Application.swift +++ b/ios/CCC/Application.swift @@ -11,15 +11,19 @@ import Resources import Client import Firebase import GoogleMobileAds +import BackgroundTasks let logger = LoggerKt.doInitLogger() @main struct Application: App { + @Environment(\.scenePhase) private var scenePhase - lazy var backgroundManager: BackgroundManager = { - koin.get() - }() + private let notificationManager = NotificationManager() + private let backgroundManager: BackgroundManager + + private let taskID = "com.oztechan.ccc.CCC.fetch" + private let earliestTaskPeriod: Double = 1 * 60 * 60 // 1 hour init() { logger.i(message: {"Application init"}) @@ -39,11 +43,66 @@ struct Application: App { height: Double.leastNonzeroMagnitude )) UITableView.appearance().backgroundColor = MR.colors().transparent.get() + + self.backgroundManager = koin.get() } var body: some Scene { WindowGroup { MainView() + }.onChange(of: scenePhase) { phase in + logger.i(message: {"Application \(phase)"}) + + switch phase { + case .active: + registerAppRefresh() + case .background: + scheduleAppRefresh() + default: break + } + } + } + + private func scheduleAppRefresh() { + logger.i(message: {"Application scheduleAppRefresh"}) + + let request = BGAppRefreshTaskRequest(identifier: taskID) + request.earliestBeginDate = Date(timeIntervalSinceNow: earliestTaskPeriod) + + do { + try BGTaskScheduler.shared.submit(request) + } catch { + logger.i(message: {"Application scheduleAppRefresh Could not schedule app refresh: \(error)"}) + } + } + + private func registerAppRefresh() { + logger.i(message: {"Application registerAppRefresh"}) + + BGTaskScheduler.shared.cancelAllTaskRequests() + + // swiftlint:disable force_cast + BGTaskScheduler.shared.register(forTaskWithIdentifier: taskID, using: nil) { task in + handleAppRefresh(task: task as! BGAppRefreshTask) + + task.expirationHandler = { + logger.i(message: {"Application handleAppRefresh BackgroundTask Expired"}) + + task.setTaskCompleted(success: false) + } + } + } + + private func handleAppRefresh(task: BGAppRefreshTask) { + logger.i(message: {"Application handleAppRefresh"}) + + scheduleAppRefresh() + + if backgroundManager.shouldSendNotification() { + self.notificationManager.sendNotification(title: "", body: "") + task.setTaskCompleted(success: true) + } else { + task.setTaskCompleted(success: true) } } } diff --git a/ios/CCC/Resources/Info.plist b/ios/CCC/Resources/Info.plist index fca327e95b..ff045547f0 100644 --- a/ios/CCC/Resources/Info.plist +++ b/ios/CCC/Resources/Info.plist @@ -2,6 +2,10 @@ + BGTaskSchedulerPermittedIdentifiers + + com.oztechan.ccc.CCC.fetch + BANNER_AD_UNIT_ID_CALCULATOR $(BANNER_AD_UNIT_ID_CALCULATOR) BANNER_AD_UNIT_ID_CURRENCIES @@ -188,6 +192,10 @@ UIApplicationSupportsMultipleScenes + UIBackgroundModes + + fetch + UILaunchStoryboardName Launch Screen UIRequiredDeviceCapabilities From 0c918dc5a4d049ca375fe2c0e5e76ffb9c30a86e Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Mon, 27 Jun 2022 12:30:54 +0200 Subject: [PATCH 38/40] [#796] Create alert to handle Watchers when the app is open (#798) --- ios/CCC/Application.swift | 27 ++++++++++++++++++++------ ios/CCC/Util/NotificationManager.swift | 8 ++++++-- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/ios/CCC/Application.swift b/ios/CCC/Application.swift index 834cd86722..b0ffedb0c6 100644 --- a/ios/CCC/Application.swift +++ b/ios/CCC/Application.swift @@ -18,6 +18,7 @@ let logger = LoggerKt.doInitLogger() @main struct Application: App { @Environment(\.scenePhase) private var scenePhase + @State var alertVisibility: Bool = false private let notificationManager = NotificationManager() private let backgroundManager: BackgroundManager @@ -45,20 +46,25 @@ struct Application: App { UITableView.appearance().backgroundColor = MR.colors().transparent.get() self.backgroundManager = koin.get() + + registerAppRefresh() } var body: some Scene { WindowGroup { MainView() + .alert(isPresented: $alertVisibility) { + Alert( + title: Text(MR.strings().txt_watcher_alert_title.get()), + message: Text(MR.strings().txt_watcher_alert_sub_title.get()), + dismissButton: .destructive(Text(MR.strings().txt_ok.get())) + ) + } }.onChange(of: scenePhase) { phase in logger.i(message: {"Application \(phase)"}) - switch phase { - case .active: - registerAppRefresh() - case .background: + if phase == .background { scheduleAppRefresh() - default: break } } } @@ -99,7 +105,16 @@ struct Application: App { scheduleAppRefresh() if backgroundManager.shouldSendNotification() { - self.notificationManager.sendNotification(title: "", body: "") + + if scenePhase == .background { + self.notificationManager.sendNotification( + title: MR.strings().txt_watcher_alert_title.get(), + body: MR.strings().txt_watcher_alert_sub_title.get() + ) + } else { + self.alertVisibility = true + } + task.setTaskCompleted(success: true) } else { task.setTaskCompleted(success: true) diff --git a/ios/CCC/Util/NotificationManager.swift b/ios/CCC/Util/NotificationManager.swift index e6ac459bfc..a2bf9dfc48 100644 --- a/ios/CCC/Util/NotificationManager.swift +++ b/ios/CCC/Util/NotificationManager.swift @@ -13,6 +13,10 @@ import UserNotifications final class NotificationManager: ObservableObject { @Published var authorizationStatus: UNAuthorizationStatus? + init() { + logger.i(message: {"NotificationManager init"}) + } + func reloadAuthorisationStatus() { logger.i(message: {"NotificationManager reloadAuthorisationStatus"}) @@ -45,8 +49,8 @@ final class NotificationManager: ObservableObject { let content = UNMutableNotificationContent() content.sound = .default - content.title = MR.strings().txt_watcher_alert_title.get() - content.body = MR.strings().txt_watcher_alert_sub_title.get() + content.title = title + content.body = body let request = UNNotificationRequest( identifier: UUID().uuidString, From a4a5454e924873ad73b28d2354ce61e83a44e453 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Mon, 27 Jun 2022 12:56:47 +0200 Subject: [PATCH 39/40] [#799] Inject Swift protocols by koin (#800) --- .../kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt | 4 ++-- .../src/iosMain/kotlin/com/oztechan/ccc/client/di/KoinIOS.kt | 5 +++++ ios/CCC/DI/Koin.swift | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt index c4f6a0c99d..1a31f503c4 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/di/module/ClientModule.kt @@ -1,6 +1,7 @@ package com.oztechan.ccc.client.di.module import com.oztechan.ccc.client.di.viewModelDefinition +import com.oztechan.ccc.client.manager.background.BackgroundManager import com.oztechan.ccc.client.manager.background.BackgroundManagerImpl import com.oztechan.ccc.client.manager.session.SessionManager import com.oztechan.ccc.client.manager.session.SessionManagerImpl @@ -26,6 +27,5 @@ var clientModule = module { single { ConfigManagerImpl() } single { SessionManagerImpl(get(), get()) } - // todo https://github.com/InsertKoinIO/koin/issues/1366 - single { BackgroundManagerImpl(get(), get()) } + single { BackgroundManagerImpl(get(), get()) } } diff --git a/client/src/iosMain/kotlin/com/oztechan/ccc/client/di/KoinIOS.kt b/client/src/iosMain/kotlin/com/oztechan/ccc/client/di/KoinIOS.kt index 38b5ac7ec3..7e9a8f0346 100644 --- a/client/src/iosMain/kotlin/com/oztechan/ccc/client/di/KoinIOS.kt +++ b/client/src/iosMain/kotlin/com/oztechan/ccc/client/di/KoinIOS.kt @@ -15,6 +15,7 @@ import com.oztechan.ccc.common.di.modules.apiModule import com.oztechan.ccc.common.di.modules.getDatabaseModule import com.oztechan.ccc.common.di.modules.getSettingsModule import kotlinx.cinterop.ObjCClass +import kotlinx.cinterop.ObjCProtocol import kotlinx.cinterop.getOriginalKotlinClass import org.koin.core.Koin import org.koin.core.context.startKoin @@ -49,3 +50,7 @@ actual inline fun Module.viewModelDefinition( fun Koin.getDependency(objCClass: ObjCClass): T? = getOriginalKotlinClass(objCClass)?.let { getDependency(it) } + +fun Koin.getDependency(objCProtocol: ObjCProtocol): T? = getOriginalKotlinClass(objCProtocol)?.let { + getDependency(it) +} diff --git a/ios/CCC/DI/Koin.swift b/ios/CCC/DI/Koin.swift index 5939544ebf..cb01dcd436 100644 --- a/ios/CCC/DI/Koin.swift +++ b/ios/CCC/DI/Koin.swift @@ -52,7 +52,7 @@ extension Koin_coreKoin { } func get() -> BackgroundManager { - return koin.getDependency(objCClass: BackgroundManagerImpl.self) as! BackgroundManager + return koin.getDependency(objCProtocol: BackgroundManager.self) as! BackgroundManager } // Observable From dcd54b25a77fc26683850ae6c86ffc788c3e285d Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Mon, 27 Jun 2022 14:04:20 +0200 Subject: [PATCH 40/40] [#801] Increase version and release adjustments (#802) --- buildSrc/src/main/kotlin/ProjectSettings.kt | 4 +-- .../background/BackgroundManagerImpl.kt | 34 +++++++++++-------- ios/CCC/Application.swift | 4 +-- ios/CCC/Util/NotificationManager.swift | 2 +- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/buildSrc/src/main/kotlin/ProjectSettings.kt b/buildSrc/src/main/kotlin/ProjectSettings.kt index d1dc6db933..6bf6b78232 100644 --- a/buildSrc/src/main/kotlin/ProjectSettings.kt +++ b/buildSrc/src/main/kotlin/ProjectSettings.kt @@ -8,10 +8,10 @@ import java.io.File object ProjectSettings { private const val MAYOR_VERSION = 2 - private const val MINOR_VERSION = 6 + private const val MINOR_VERSION = 7 // git rev-list --first-parent --count master +1 - private const val VERSION_DIF = 713 + private const val VERSION_DIF = 728 private const val BASE_VERSION_CODE = 316 const val PROJECT_ID = "com.oztechan.ccc" diff --git a/client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/background/BackgroundManagerImpl.kt b/client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/background/BackgroundManagerImpl.kt index f22013ae38..4b0f98195a 100644 --- a/client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/background/BackgroundManagerImpl.kt +++ b/client/src/commonMain/kotlin/com/oztechan/ccc/client/manager/background/BackgroundManagerImpl.kt @@ -15,23 +15,27 @@ class BackgroundManagerImpl( Logger.d { "BackgroundManagerImpl init" } } + @Suppress("LabeledExpression", "TooGenericExceptionCaught") + override fun shouldSendNotification() = try { + Logger.d { "BackgroundManagerImpl shouldSendNotification" } - @Suppress("LabeledExpression") - override fun shouldSendNotification() = runBlocking { - Logger.d { "BackgroundManagerImpl checkNotifications" } - - watchersRepository.getWatchers().forEach { watcher -> - apiRepository - .getRatesByBackend(watcher.base) - .rates - .getConversionByName(watcher.target) - ?.let { conversionRate -> - when { - watcher.isGreater && conversionRate > watcher.rate -> return@runBlocking true - !watcher.isGreater && conversionRate < watcher.rate -> return@runBlocking true + runBlocking { + watchersRepository.getWatchers().forEach { watcher -> + apiRepository + .getRatesByBackend(watcher.base) + .rates + .getConversionByName(watcher.target) + ?.let { conversionRate -> + when { + watcher.isGreater && conversionRate > watcher.rate -> return@runBlocking true + !watcher.isGreater && conversionRate < watcher.rate -> return@runBlocking true + } } - } + } + return@runBlocking false } - return@runBlocking false + } catch (e: Exception) { + Logger.w { "BackgroundManagerImpl shouldSendNotification error: $e" } + false } } diff --git a/ios/CCC/Application.swift b/ios/CCC/Application.swift index b0ffedb0c6..9edef8b34b 100644 --- a/ios/CCC/Application.swift +++ b/ios/CCC/Application.swift @@ -61,7 +61,7 @@ struct Application: App { ) } }.onChange(of: scenePhase) { phase in - logger.i(message: {"Application \(phase)"}) + logger.i(message: {"Application onChange scenePhase \(phase)"}) if phase == .background { scheduleAppRefresh() @@ -92,7 +92,7 @@ struct Application: App { handleAppRefresh(task: task as! BGAppRefreshTask) task.expirationHandler = { - logger.i(message: {"Application handleAppRefresh BackgroundTask Expired"}) + logger.i(message: {"Application registerAppRefresh BackgroundTask Expired"}) task.setTaskCompleted(success: false) } diff --git a/ios/CCC/Util/NotificationManager.swift b/ios/CCC/Util/NotificationManager.swift index a2bf9dfc48..595f9e4eb0 100644 --- a/ios/CCC/Util/NotificationManager.swift +++ b/ios/CCC/Util/NotificationManager.swift @@ -43,7 +43,7 @@ final class NotificationManager: ObservableObject { } func sendNotification(title: String, body: String) { - logger.i(message: {"NotificationManager sendNotification"}) + logger.i(message: {"NotificationManager sendNotification title:\(title) body:\(body)"}) let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3, repeats: false)