diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 317de4523..72ff4f699 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -71,6 +71,7 @@ android { dependencies { implementation(projects.core.android) + implementation(projects.core.androidx.compose) implementation(projects.core.androidx.fragment) implementation(projects.core.androidx.lifecycle) implementation(projects.core.androidx.navigationCompose) @@ -78,6 +79,8 @@ dependencies { implementation(projects.core.androidx.transition) implementation(projects.core.annotation) implementation(projects.core.component.tag) + implementation(projects.core.component.usermessage) + implementation(projects.core.component.usermessageHandle) implementation(projects.core.dataDi) implementation(projects.core.data) implementation(projects.core.designsystem) @@ -90,7 +93,6 @@ dependencies { implementation(projects.core.statekit) kapt(projects.statekit.compiler) implementation(projects.core.translation) - implementation(projects.core.uiCompose) implementation(projects.core.uitext) implementation(libs.kotlinx.coroutines.android) diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index d16fd18e5..12d59b2a2 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -29,10 +29,10 @@ android:exported="false" tools:node="merge"> diff --git a/app/src/debug/java/com/nlab/reminder/internal/common/android/startup/init/FlipperInitializer.kt b/app/src/debug/kotlin/com/nlab/reminder/apps/startup/init/FlipperInitializer.kt similarity index 93% rename from app/src/debug/java/com/nlab/reminder/internal/common/android/startup/init/FlipperInitializer.kt rename to app/src/debug/kotlin/com/nlab/reminder/apps/startup/init/FlipperInitializer.kt index e76abc41e..c037402a6 100644 --- a/app/src/debug/java/com/nlab/reminder/internal/common/android/startup/init/FlipperInitializer.kt +++ b/app/src/debug/kotlin/com/nlab/reminder/apps/startup/init/FlipperInitializer.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.nlab.reminder.internal.common.android.startup.init +package com.nlab.reminder.apps.startup.init import android.content.Context import androidx.startup.Initializer @@ -27,7 +27,7 @@ import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin import com.facebook.flipper.plugins.leakcanary2.FlipperLeakEventListener import com.facebook.flipper.plugins.leakcanary2.LeakCanary2FlipperPlugin import com.facebook.soloader.SoLoader -import com.nlab.reminder.internal.common.android.startup.EmptyDependencies +import com.nlab.reminder.apps.startup.EmptyDependencies import leakcanary.LeakCanary /** diff --git a/app/src/debug/java/com/nlab/reminder/internal/common/android/startup/init/TimberInitializer.kt b/app/src/debug/kotlin/com/nlab/reminder/apps/startup/init/TimberInitializer.kt similarity index 87% rename from app/src/debug/java/com/nlab/reminder/internal/common/android/startup/init/TimberInitializer.kt rename to app/src/debug/kotlin/com/nlab/reminder/apps/startup/init/TimberInitializer.kt index 26259a9e2..9f1604ffa 100644 --- a/app/src/debug/java/com/nlab/reminder/internal/common/android/startup/init/TimberInitializer.kt +++ b/app/src/debug/kotlin/com/nlab/reminder/apps/startup/init/TimberInitializer.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package com.nlab.reminder.internal.common.android.startup.init +package com.nlab.reminder.apps.startup.init import android.content.Context import androidx.startup.Initializer -import com.nlab.reminder.internal.common.android.startup.EmptyDependencies +import com.nlab.reminder.apps.startup.EmptyDependencies import timber.log.Timber /** diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index af9212e9a..81df6f6b1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,7 +20,7 @@ diff --git a/app/src/main/java/com/nlab/reminder/ReminderApplication.kt b/app/src/main/java/com/nlab/reminder/PlaneatApplication.kt similarity index 95% rename from app/src/main/java/com/nlab/reminder/ReminderApplication.kt rename to app/src/main/java/com/nlab/reminder/PlaneatApplication.kt index e7a99e7ae..35dcc1fbb 100644 --- a/app/src/main/java/com/nlab/reminder/ReminderApplication.kt +++ b/app/src/main/java/com/nlab/reminder/PlaneatApplication.kt @@ -25,4 +25,4 @@ import dagger.hilt.android.HiltAndroidApp */ @ExcludeFromGeneratedTestReport @HiltAndroidApp -class ReminderApplication : Application() \ No newline at end of file +class PlaneatApplication : Application() \ No newline at end of file diff --git a/app/src/main/java/com/nlab/reminder/internal/common/android/startup/Initializers.kt b/app/src/main/java/com/nlab/reminder/apps/startup/Initializers.kt similarity index 93% rename from app/src/main/java/com/nlab/reminder/internal/common/android/startup/Initializers.kt rename to app/src/main/java/com/nlab/reminder/apps/startup/Initializers.kt index 3ccfba30e..6309e2be8 100644 --- a/app/src/main/java/com/nlab/reminder/internal/common/android/startup/Initializers.kt +++ b/app/src/main/java/com/nlab/reminder/apps/startup/Initializers.kt @@ -16,7 +16,7 @@ @file:Suppress("FunctionName") -package com.nlab.reminder.internal.common.android.startup +package com.nlab.reminder.apps.startup import androidx.startup.Initializer diff --git a/app/src/main/java/com/nlab/reminder/internal/common/android/startup/init/CoilInitializer.kt b/app/src/main/java/com/nlab/reminder/apps/startup/init/CoilInitializer.kt similarity index 92% rename from app/src/main/java/com/nlab/reminder/internal/common/android/startup/init/CoilInitializer.kt rename to app/src/main/java/com/nlab/reminder/apps/startup/init/CoilInitializer.kt index befb86250..94587d9cd 100644 --- a/app/src/main/java/com/nlab/reminder/internal/common/android/startup/init/CoilInitializer.kt +++ b/app/src/main/java/com/nlab/reminder/apps/startup/init/CoilInitializer.kt @@ -16,7 +16,7 @@ @file:Suppress("unused") -package com.nlab.reminder.internal.common.android.startup.init +package com.nlab.reminder.apps.startup.init import android.content.Context import androidx.startup.Initializer @@ -24,7 +24,7 @@ import coil.Coil import coil.ImageLoader import coil.disk.DiskCache import coil.memory.MemoryCache -import com.nlab.reminder.internal.common.android.startup.EmptyDependencies +import com.nlab.reminder.apps.startup.EmptyDependencies /** * @author Doohyun diff --git a/app/src/main/java/com/nlab/reminder/internal/common/android/startup/init/StateKitPluginInitializer.kt b/app/src/main/java/com/nlab/reminder/apps/startup/init/StateKitPluginInitializer.kt similarity index 50% rename from app/src/main/java/com/nlab/reminder/internal/common/android/startup/init/StateKitPluginInitializer.kt rename to app/src/main/java/com/nlab/reminder/apps/startup/init/StateKitPluginInitializer.kt index 901fcf9a4..6cea66b78 100644 --- a/app/src/main/java/com/nlab/reminder/internal/common/android/startup/init/StateKitPluginInitializer.kt +++ b/app/src/main/java/com/nlab/reminder/apps/startup/init/StateKitPluginInitializer.kt @@ -15,12 +15,16 @@ */ @file:Suppress("unused") -package com.nlab.reminder.internal.common.android.startup.init +package com.nlab.reminder.apps.startup.init import android.content.Context import androidx.startup.Initializer import com.nlab.reminder.core.statekit.plugins.StateKitPlugin -import com.nlab.reminder.internal.common.android.startup.EmptyDependencies +import com.nlab.reminder.apps.startup.EmptyDependencies +import com.nlab.reminder.core.component.usermessage.UserMessageException +import com.nlab.reminder.core.component.usermessage.handle.di.getUserMessageBroadcastMonitor +import com.nlab.reminder.core.component.usermessage.handle.impl.UserMessageBroadcastMonitor +import kotlinx.coroutines.CancellationException import timber.log.Timber /** @@ -28,7 +32,26 @@ import timber.log.Timber */ internal class StateKitPluginInitializer : Initializer { override fun create(context: Context) { - StateKitPlugin.addGlobalExceptionHandler { _, throwable -> Timber.tag("StateKitGlobalErr").e(throwable) } + val tag = "StateKitGlobalErr" + val userMessageBroadcastMonitor: UserMessageBroadcastMonitor = + context.getUserMessageBroadcastMonitor() + + StateKitPlugin.addGlobalExceptionHandler { _, throwable -> + when (throwable) { + is UserMessageException -> { + Timber.tag(tag).e(throwable.origin) + userMessageBroadcastMonitor.send(userMessage = throwable.userMessage) + } + + is CancellationException -> { + // do nothing + } + + else -> { + Timber.tag(tag).e(throwable) + } + } + } } override fun dependencies() = EmptyDependencies() diff --git a/app/src/main/java/com/nlab/reminder/apps/ui/MainActivity.kt b/app/src/main/java/com/nlab/reminder/apps/ui/MainActivity.kt index 3c9221f00..d22189475 100644 --- a/app/src/main/java/com/nlab/reminder/apps/ui/MainActivity.kt +++ b/app/src/main/java/com/nlab/reminder/apps/ui/MainActivity.kt @@ -21,21 +21,27 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import com.nlab.reminder.core.android.widget.Toast import com.nlab.reminder.core.designsystem.compose.theme.PlaneatTheme import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject /** * @author Doohyun */ @AndroidEntryPoint class MainActivity : AppCompatActivity() { + @Inject + lateinit var appToast: Toast + override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() super.onCreate(savedInstanceState) - enableEdgeToEdge() setContent { - val appState = rememberPlaneatAppState() + val appState = rememberPlaneatAppState( + appToast = appToast + ) PlaneatTheme { PlaneatApp(appState = appState) } diff --git a/app/src/main/java/com/nlab/reminder/apps/ui/PlaneatApp.kt b/app/src/main/java/com/nlab/reminder/apps/ui/PlaneatApp.kt index f700b9d7e..79e2cdb55 100644 --- a/app/src/main/java/com/nlab/reminder/apps/ui/PlaneatApp.kt +++ b/app/src/main/java/com/nlab/reminder/apps/ui/PlaneatApp.kt @@ -19,16 +19,18 @@ package com.nlab.reminder.apps.ui import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.nlab.reminder.core.component.usermessage.handle.ui.UserMessageHandler /** * @author Thalys */ @Composable -fun PlaneatApp( - appState: PlaneatAppState -) { +fun PlaneatApp(appState: PlaneatAppState) { PlaneatNavHost( modifier = Modifier.fillMaxSize(), appState = appState ) + UserMessageHandler( + showApplicationToast = { message -> appState.showApplicationToast(message) } + ) } \ No newline at end of file diff --git a/app/src/main/java/com/nlab/reminder/apps/ui/PlaneatAppState.kt b/app/src/main/java/com/nlab/reminder/apps/ui/PlaneatAppState.kt index 6e22133b9..9f7a6f25c 100644 --- a/app/src/main/java/com/nlab/reminder/apps/ui/PlaneatAppState.kt +++ b/app/src/main/java/com/nlab/reminder/apps/ui/PlaneatAppState.kt @@ -20,18 +20,26 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController +import com.nlab.reminder.core.android.widget.Toast /** * @author Thalys */ @Stable class PlaneatAppState( - val navController: NavHostController -) + val navController: NavHostController, + private val appToast: Toast, +) { + fun showApplicationToast(message: String) { + appToast.showToast(text = message) + } +} @Composable fun rememberPlaneatAppState( - navController: NavHostController = rememberNavController() + navController: NavHostController = rememberNavController(), + appToast: Toast, ): PlaneatAppState = PlaneatAppState( - navController = navController + navController = navController, + appToast = appToast ) \ No newline at end of file diff --git a/app/src/main/java/com/nlab/reminder/domain/common/android/widget/Toasts.kt b/app/src/main/java/com/nlab/reminder/domain/common/android/widget/Toasts.kt deleted file mode 100644 index b5f098f40..000000000 --- a/app/src/main/java/com/nlab/reminder/domain/common/android/widget/Toasts.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.nlab.reminder.domain.common.android.widget - -import android.content.Context -import androidx.annotation.StringRes - -/** - * @author Doohyun - */ -fun Context.showToast(@StringRes stringResource: Int) { - widgetEntryPoint() - .toastHandle() - .showToast(stringResource) -} - -fun Context.showToast(text: String) { - widgetEntryPoint() - .toastHandle() - .showToast(text) -} \ No newline at end of file diff --git a/app/src/main/java/com/nlab/reminder/domain/common/android/widget/WidgetEntryPoint.kt b/app/src/main/java/com/nlab/reminder/domain/common/android/widget/WidgetEntryPoint.kt deleted file mode 100644 index 96a729cdc..000000000 --- a/app/src/main/java/com/nlab/reminder/domain/common/android/widget/WidgetEntryPoint.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.nlab.reminder.domain.common.android.widget - -import android.content.Context -import com.nlab.reminder.core.android.widget.ToastHandle -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent - -/** - * @author Doohyun - */ -@EntryPoint -@InstallIn(SingletonComponent::class) -internal interface WidgetEntryPoint { - fun toastHandle(): ToastHandle -} - -internal fun Context.widgetEntryPoint(): WidgetEntryPoint = - EntryPointAccessors - .fromApplication(this, WidgetEntryPoint::class.java) \ No newline at end of file diff --git a/app/src/main/java/com/nlab/reminder/domain/feature/home/HomeAction.kt b/app/src/main/java/com/nlab/reminder/domain/feature/home/HomeAction.kt index 6af39e0b6..cd3b063dc 100644 --- a/app/src/main/java/com/nlab/reminder/domain/feature/home/HomeAction.kt +++ b/app/src/main/java/com/nlab/reminder/domain/feature/home/HomeAction.kt @@ -17,7 +17,6 @@ package com.nlab.reminder.domain.feature.home import com.nlab.reminder.core.component.tag.edit.TagEditState -import com.nlab.reminder.core.text.UiText import com.nlab.reminder.core.data.model.Tag import com.nlab.reminder.core.kotlin.NonNegativeLong import com.nlab.statekit.annotation.UiAction @@ -35,11 +34,6 @@ internal sealed class HomeAction private constructor() { data class TagEditStateSynced(val state: TagEditState?) : HomeAction() - data class UserMessagePosted(val message: UiText) : HomeAction() - - @UiAction - data class UserMessageShown(val message: UiText) : HomeAction() - @UiAction data object Interacted : HomeAction() diff --git a/app/src/main/java/com/nlab/reminder/domain/feature/home/HomeReduce.kt b/app/src/main/java/com/nlab/reminder/domain/feature/home/HomeReduce.kt index 4ee97cf63..68f21f18a 100644 --- a/app/src/main/java/com/nlab/reminder/domain/feature/home/HomeReduce.kt +++ b/app/src/main/java/com/nlab/reminder/domain/feature/home/HomeReduce.kt @@ -16,9 +16,7 @@ package com.nlab.reminder.domain.feature.home -import com.nlab.reminder.core.translation.StringIds -import com.nlab.reminder.core.text.UiText -import com.nlab.reminder.core.kotlin.onFailure +import com.nlab.reminder.core.component.usermessage.getOrThrowMessage import com.nlab.statekit.dsl.reduce.DslReduce import com.nlab.statekit.reduce.Reduce import com.nlab.reminder.domain.feature.home.HomeAction.* @@ -37,8 +35,7 @@ internal fun HomeReduce(environment: HomeEnvironment): HomeReduce = DslReduce { timetableScheduleCount = action.timetableSchedulesCount, allScheduleCount = action.allSchedulesCount, tags = action.sortedTags, - interaction = HomeInteraction.Empty, - userMessages = emptyList() + interaction = HomeInteraction.Empty ) } transition { @@ -79,7 +76,7 @@ internal fun HomeReduce(environment: HomeEnvironment): HomeReduce = DslReduce { suspendEffect { environment.tagEditDelegate .startEditing(tag = action.tag) - .onFailure { dispatch(UserMessagePosted(UiText(StringIds.tag_not_found))) } + .getOrThrowMessage() } } scope(isMatch = { current.interaction is HomeInteraction.TagEdit }) { @@ -89,23 +86,21 @@ internal fun HomeReduce(environment: HomeEnvironment): HomeReduce = DslReduce { suspendEffect { environment.tagEditDelegate .tryUpdateTagName(current.tags) - .onFailure { dispatch(UserMessagePosted(UiText(StringIds.unknown_error))) } + .getOrThrowMessage() } suspendEffect { environment.tagEditDelegate .mergeTag() - .onFailure { dispatch(UserMessagePosted(UiText(StringIds.unknown_error))) } + .getOrThrowMessage() } effect { environment.tagEditDelegate.cancelMergeTag() } effect { environment.tagEditDelegate.startDelete() } suspendEffect { environment.tagEditDelegate .deleteTag() - .onFailure { dispatch(UserMessagePosted(UiText(StringIds.unknown_error))) } + .getOrThrowMessage() } } - transition { current.copy(userMessages = current.userMessages + action.message) } - transition { current.copy(userMessages = current.userMessages - action.message) } transition { current.copy(interaction = HomeInteraction.Empty) } effect { if (current.interaction is HomeInteraction.TagEdit) { diff --git a/app/src/main/java/com/nlab/reminder/domain/feature/home/HomeUiState.kt b/app/src/main/java/com/nlab/reminder/domain/feature/home/HomeUiState.kt index 1ab447b38..50b508a8f 100644 --- a/app/src/main/java/com/nlab/reminder/domain/feature/home/HomeUiState.kt +++ b/app/src/main/java/com/nlab/reminder/domain/feature/home/HomeUiState.kt @@ -18,7 +18,6 @@ package com.nlab.reminder.domain.feature.home import com.nlab.reminder.core.data.model.Tag import com.nlab.reminder.core.kotlin.NonNegativeLong -import com.nlab.reminder.core.text.UiText /** * @author Doohyun @@ -32,6 +31,5 @@ internal sealed class HomeUiState private constructor() { val allScheduleCount: NonNegativeLong, val tags: List, val interaction: HomeInteraction, - val userMessages: List ) : HomeUiState() } \ No newline at end of file diff --git a/app/src/main/java/com/nlab/reminder/domain/feature/home/ui/HomeScreen.kt b/app/src/main/java/com/nlab/reminder/domain/feature/home/ui/HomeScreen.kt index 36b9464d4..ed608db0f 100644 --- a/app/src/main/java/com/nlab/reminder/domain/feature/home/ui/HomeScreen.kt +++ b/app/src/main/java/com/nlab/reminder/domain/feature/home/ui/HomeScreen.kt @@ -68,8 +68,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.nlab.reminder.core.ui.compose.DelayedContent -import com.nlab.reminder.core.ui.compose.tooling.preview.Previews +import com.nlab.reminder.core.androidx.compose.ui.DelayedContent +import com.nlab.reminder.core.androidx.compose.ui.tooling.preview.Previews import com.nlab.reminder.core.data.model.Tag import com.nlab.reminder.core.data.model.TagId import com.nlab.reminder.core.designsystem.compose.theme.PlaneatTheme @@ -85,8 +85,8 @@ import com.nlab.reminder.core.android.resources.font.CategoryCountFontFamily import com.nlab.reminder.core.android.resources.icon.IcHomeCategoryAll import com.nlab.reminder.core.android.resources.icon.IcHomeCategoryTimetable import com.nlab.reminder.core.android.resources.icon.IcHomeCategoryToday -import com.nlab.reminder.core.ui.compose.ColorPressButton -import com.nlab.reminder.core.ui.compose.throttleClick +import com.nlab.reminder.core.androidx.compose.ui.ColorPressButton +import com.nlab.reminder.core.androidx.compose.ui.throttleClick import com.nlab.reminder.core.component.tag.edit.ui.compose.TagEditStateHandler import com.nlab.reminder.core.component.tag.ui.compose.TagCard import com.nlab.reminder.core.designsystem.compose.component.PlaneatLoadingContent @@ -744,7 +744,6 @@ private fun HomeScreenPopulated() { ) }, interaction = HomeInteraction.Empty, - userMessages = emptyList() ), onTodayCategoryClicked = {}, onTimetableCategoryClicked = {}, diff --git a/app/src/test/java/com/nlab/reminder/domain/feature/home/HomeReduceKtTest.kt b/app/src/test/java/com/nlab/reminder/domain/feature/home/HomeReduceKtTest.kt index 6128c63d2..b2ddc2c36 100644 --- a/app/src/test/java/com/nlab/reminder/domain/feature/home/HomeReduceKtTest.kt +++ b/app/src/test/java/com/nlab/reminder/domain/feature/home/HomeReduceKtTest.kt @@ -18,15 +18,12 @@ package com.nlab.reminder.domain.feature.home import com.nlab.reminder.core.component.tag.edit.TagEditDelegate import com.nlab.reminder.core.component.tag.edit.genTagEditState -import com.nlab.reminder.core.text.UiText -import com.nlab.reminder.core.text.genUiTexts import com.nlab.reminder.core.data.model.genTag import com.nlab.reminder.core.kotlin.Result -import com.nlab.reminder.core.translation.StringIds import com.nlab.statekit.test.reduce.effectScenario +import com.nlab.statekit.test.reduce.launchAndJoin import com.nlab.statekit.test.reduce.transitionScenario import com.nlab.testkit.faker.genBothify -import com.nlab.testkit.faker.genInt import kotlinx.coroutines.test.runTest import org.junit.Test import org.mockito.kotlin.any @@ -54,8 +51,7 @@ class HomeReduceKtTest { timetableScheduleCount = action.timetableSchedulesCount, allScheduleCount = action.allSchedulesCount, tags = action.sortedTags, - interaction = HomeInteraction.Empty, - userMessages = emptyList() + interaction = HomeInteraction.Empty ) } .verify() @@ -74,7 +70,6 @@ class HomeReduceKtTest { allScheduleCount = action.allSchedulesCount, tags = action.sortedTags, interaction = initState.interaction, - userMessages = initState.userMessages ) } .verify() @@ -165,18 +160,17 @@ class HomeReduceKtTest { } @Test - fun `Given success with no interaction and user messages, When tag long clicked, Then user message filled if TagEditDelegate startEditing result fails`() = runTest { + fun `Given success with no interaction, When tag long clicked, Then TagEditDelegate invoke startEditing`() = runTest { val tagEditDelegate: TagEditDelegate = mock { - whenever(mock.startEditing(any())) doReturn Result.Failure(IllegalStateException()) + whenever(mock.startEditing(any())) doReturn Result.Success(Unit) } genHomeReduce(environment = genHomeEnvironment(tagEditDelegate)) - .transitionScenario() - .initState(genHomeUiStateSuccess(interaction = HomeInteraction.Empty, userMessages = emptyList())) + .effectScenario() + .initState(genHomeUiStateSuccess(interaction = HomeInteraction.Empty)) .action(HomeAction.OnTagLongClicked(genTag())) - .expectedStateFromInput { - initState.copy(userMessages = listOf(UiText(StringIds.tag_not_found))) + .launchAndJoin { + verify(tagEditDelegate, once()).startEditing(action.tag) } - .verify(shouldVerifyWithEffect = true) } @Test @@ -204,53 +198,41 @@ class HomeReduceKtTest { @Test fun `Given success with tagEdit interaction, When tag rename inputted, Then tagEditDelegate called changeRenameText`() = runTest { val tagEditDelegate: TagEditDelegate = mock() - val input = genBothify() genHomeReduce(environment = genHomeEnvironment(tagEditDelegate)) .effectScenario() .initState(genHomeUiStateSuccess(interaction = HomeInteraction.TagEdit(genTagEditState()))) - .action(HomeAction.OnTagRenameInputted(input)) - .launchAndJoin() - verify(tagEditDelegate, once()).changeRenameText(input) + .action(HomeAction.OnTagRenameInputted(genBothify())) + .launchAndJoin { + verify(tagEditDelegate, once()).changeRenameText(action.text) + } } @Test - fun `Given success with tagEdit interaction and no user messages, When tag rename confirmed, Then user message filled if TagEditDelegate tryUpdateTagRename result fails`() = runTest { + fun `Given success with tagEdit interaction, When tag rename confirmed, Then TagEditDelegate invoke tryUpdateTagRename`() = runTest { val tagEditDelegate: TagEditDelegate = mock { - whenever(mock.tryUpdateTagName(any())) doReturn Result.Failure(IllegalStateException()) + whenever(mock.tryUpdateTagName(any())) doReturn Result.Success(Unit) } genHomeReduce(environment = genHomeEnvironment(tagEditDelegate)) - .transitionScenario() - .initState( - genHomeUiStateSuccess( - interaction = HomeInteraction.TagEdit(genTagEditState()), - userMessages = emptyList() - ) - ) + .effectScenario() + .initState( genHomeUiStateSuccess(interaction = HomeInteraction.TagEdit(genTagEditState()))) .action(HomeAction.OnTagRenameConfirmClicked) - .expectedStateFromInput { - initState.copy(userMessages = listOf(UiText(StringIds.unknown_error))) + .launchAndJoin { + verify(tagEditDelegate, once()).tryUpdateTagName(initState.tags) } - .verify(shouldVerifyWithEffect = true) } @Test - fun `Given success with tagEdit interaction and no user messages, When tag replace confirm clicked, Then user message filled if TagEditDelegate mergeTag result fails`() = runTest { + fun `Given success with tagEdit interaction, When tag replace confirm clicked, Then TagEditDelegate invoke mergeTag`() = runTest { val tagEditDelegate: TagEditDelegate = mock { - whenever(mock.mergeTag()) doReturn Result.Failure(IllegalStateException()) + whenever(mock.mergeTag()) doReturn Result.Success(Unit) } genHomeReduce(environment = genHomeEnvironment(tagEditDelegate)) - .transitionScenario() - .initState( - genHomeUiStateSuccess( - interaction = HomeInteraction.TagEdit(genTagEditState()), - userMessages = emptyList() - ) - ) + .effectScenario() + .initState(genHomeUiStateSuccess(interaction = HomeInteraction.TagEdit(genTagEditState()))) .action(HomeAction.OnTagReplaceConfirmClicked) - .expectedStateFromInput { - initState.copy(userMessages = listOf(UiText(StringIds.unknown_error))) + .launchAndJoin { + verify(tagEditDelegate, once()).mergeTag() } - .verify(shouldVerifyWithEffect = true) } @Test @@ -276,53 +258,17 @@ class HomeReduceKtTest { } @Test - fun `Given success with tagEdit interaction and no user messages, When tag delete confirm clicked, Then user message filled if TagEditDelegate deleteTag result fails`() = runTest { + fun `Given success with tagEdit interaction, When tag delete confirm clicked, Then TagEditDelegate invoked deleteTag`() = runTest { val tagEditDelegate: TagEditDelegate = mock { - whenever(mock.deleteTag()) doReturn Result.Failure(IllegalStateException()) + whenever(mock.deleteTag()) doReturn Result.Success(Unit) } genHomeReduce(environment = genHomeEnvironment(tagEditDelegate)) - .transitionScenario() - .initState( - genHomeUiStateSuccess( - interaction = HomeInteraction.TagEdit(genTagEditState()), - userMessages = emptyList() - ) - ) + .effectScenario() + .initState(genHomeUiStateSuccess(interaction = HomeInteraction.TagEdit(genTagEditState()))) .action(HomeAction.OnTagDeleteConfirmClicked) - .expectedStateFromInput { - initState.copy(userMessages = listOf(UiText(StringIds.unknown_error))) - } - .verify(shouldVerifyWithEffect = true) - } - - @Test - fun `Given success with empty user messages, When user message posted, Then user message added to state`() = runTest { - genHomeReduce() - .transitionScenario() - .initState(genHomeUiStateSuccess(userMessages = emptyList())) - .action(HomeAction.UserMessagePosted(UiText(genBothify()))) - .expectedStateFromInput { - initState.copy( - userMessages = listOf(action.message) - ) + .launchAndJoin { + verify(tagEditDelegate, once()).deleteTag() } - .verify() - } - - @Test - fun `Given success with 2 or more user messages, When user message shown, Then user message removed`() = runTest { - val userMessages = genUiTexts(genInt(min = 2, max = 10)) - - genHomeReduce() - .transitionScenario() - .initState(genHomeUiStateSuccess(userMessages = userMessages)) - .action(HomeAction.UserMessageShown(userMessages[1])) - .expectedStateFromInput { - initState.copy( - userMessages = initState.userMessages - action.message - ) - } - .verify() } @Test diff --git a/app/src/test/java/com/nlab/reminder/domain/feature/home/HomeTestGenerator.kt b/app/src/test/java/com/nlab/reminder/domain/feature/home/HomeTestGenerator.kt index 74e5c8a33..78064d8c5 100644 --- a/app/src/test/java/com/nlab/reminder/domain/feature/home/HomeTestGenerator.kt +++ b/app/src/test/java/com/nlab/reminder/domain/feature/home/HomeTestGenerator.kt @@ -18,8 +18,6 @@ package com.nlab.reminder.domain.feature.home import com.nlab.reminder.core.component.tag.edit.TagEditDelegate import com.nlab.reminder.core.component.tag.edit.genTagEditState -import com.nlab.reminder.core.text.UiText -import com.nlab.reminder.core.text.genUiTexts import com.nlab.reminder.core.data.model.Tag import com.nlab.reminder.core.data.model.genTags import com.nlab.reminder.core.data.repository.ScheduleRepository @@ -62,14 +60,12 @@ internal fun genHomeUiStateSuccess( allScheduleCount: NonNegativeLong = genNonNegativeLong(), tags: List = genTags(), interaction: HomeInteraction = genHomeInteraction(), - userMessages: List = genUiTexts() ) = HomeUiState.Success( todayScheduleCount = todayScheduleCount, timetableScheduleCount = timetableScheduleCount, allScheduleCount = allScheduleCount, tags = tags, interaction = interaction, - userMessages = userMessages ) private val sampleHomeInteractions get() = listOf( diff --git a/build-logic/convention/src/main/kotlin/com/nlab/reminder/AndroidComposeDependencies.kt b/build-logic/convention/src/main/kotlin/com/nlab/reminder/AndroidComposeDependencies.kt index bf1131416..ccca9d6a9 100644 --- a/build-logic/convention/src/main/kotlin/com/nlab/reminder/AndroidComposeDependencies.kt +++ b/build-logic/convention/src/main/kotlin/com/nlab/reminder/AndroidComposeDependencies.kt @@ -24,8 +24,8 @@ import org.gradle.kotlin.dsl.dependencies */ fun Project.configureStdComposeDependencies() { dependencies { + "implementation"(project(":core:androidx:compose")) "implementation"(project(":core:designsystem")) - "implementation"(project(":core:ui-compose")) "implementation"(libs.findLibrary("androidx-compose-foundation").get()) "implementation"(libs.findLibrary("androidx-compose-material3").get()) diff --git a/build-logic/convention/src/main/kotlin/com/nlab/reminder/AndroidJacoco.kt b/build-logic/convention/src/main/kotlin/com/nlab/reminder/AndroidJacoco.kt index df51361f0..560805e83 100644 --- a/build-logic/convention/src/main/kotlin/com/nlab/reminder/AndroidJacoco.kt +++ b/build-logic/convention/src/main/kotlin/com/nlab/reminder/AndroidJacoco.kt @@ -79,11 +79,12 @@ private fun Project.androidJacocoClassDirectories(variant: Variant): Configurabl "**/BuildConfig.*", "**/Manifest*.*", "**/android/**", - "**/ui/**", - "**/navigation/**", - "**/test/**", "**/di/**", "**/fake/**", + "**/navigation/**", + "**/startup/**", + "**/test/**", + "**/ui/**", /* filtering unnecessary feature components */ "**/*Action$*.class", diff --git a/core/android/build.gradle.kts b/core/android/build.gradle.kts index 3fb2f5ffe..082786c1d 100644 --- a/core/android/build.gradle.kts +++ b/core/android/build.gradle.kts @@ -15,6 +15,7 @@ */ plugins { alias(libs.plugins.nlab.android.library) + alias(libs.plugins.nlab.android.library.di) } android { diff --git a/core/android/src/main/java/com/nlab/reminder/core/android/di/AppScopeAndroidModule.kt b/core/android/src/main/java/com/nlab/reminder/core/android/di/AppScopeAndroidModule.kt new file mode 100644 index 000000000..59d9a6b15 --- /dev/null +++ b/core/android/src/main/java/com/nlab/reminder/core/android/di/AppScopeAndroidModule.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The N's lab Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nlab.reminder.core.android.di + +import android.content.Context +import com.nlab.reminder.core.android.widget.Toast +import com.nlab.reminder.core.inject.qualifiers.coroutine.AppScope +import com.nlab.reminder.core.inject.qualifiers.coroutine.Dispatcher +import com.nlab.reminder.core.inject.qualifiers.coroutine.DispatcherOption.Main +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.plus +import javax.inject.Singleton + +/** + * @author Thalys + */ +@Module +@InstallIn(SingletonComponent::class) +internal class AppScopeAndroidModule { + @Singleton + @Provides + fun provideToast( + @ApplicationContext context: Context, + @AppScope coroutineScope: CoroutineScope, + @Dispatcher(Main) dispatcher: CoroutineDispatcher + ): Toast = Toast(context = context, coroutineScope = coroutineScope + dispatcher) +} \ No newline at end of file diff --git a/core/android/src/main/java/com/nlab/reminder/core/android/widget/ToastHandle.kt b/core/android/src/main/java/com/nlab/reminder/core/android/widget/Toast.kt similarity index 60% rename from core/android/src/main/java/com/nlab/reminder/core/android/widget/ToastHandle.kt rename to core/android/src/main/java/com/nlab/reminder/core/android/widget/Toast.kt index 93c9b6b55..83123cfcc 100644 --- a/core/android/src/main/java/com/nlab/reminder/core/android/widget/ToastHandle.kt +++ b/core/android/src/main/java/com/nlab/reminder/core/android/widget/Toast.kt @@ -17,29 +17,34 @@ package com.nlab.reminder.core.android.widget import android.content.Context -import android.os.Handler -import android.os.Looper import android.widget.Toast import androidx.annotation.StringRes +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import android.widget.Toast as ToastOrigin +import java.lang.ref.WeakReference /** * @author thalys */ -class ToastHandle(private val context: Context) { - private var curToast: Toast? = null +class Toast( + private val context: Context, + private val coroutineScope: CoroutineScope, +) { + private var curToastRef: WeakReference? = null fun showToast(@StringRes resId: Int) { - showToast { Toast.makeText(context, resId, Toast.LENGTH_SHORT) } + showToast { ToastOrigin.makeText(context, resId, ToastOrigin.LENGTH_SHORT) } } fun showToast(text: String) { - showToast { Toast.makeText(context, text, Toast.LENGTH_SHORT) } + showToast { ToastOrigin.makeText(context, text, ToastOrigin.LENGTH_SHORT) } } private inline fun showToast(crossinline getToast: () -> Toast) { - Handler(Looper.getMainLooper()).post { - curToast?.cancel() - curToast = getToast().also { it.show() } + coroutineScope.launch { + curToastRef?.get()?.cancel() + curToastRef = WeakReference(getToast().also { it.show() }) } } } \ No newline at end of file diff --git a/core/ui-compose/.gitignore b/core/androidx/compose/.gitignore similarity index 100% rename from core/ui-compose/.gitignore rename to core/androidx/compose/.gitignore diff --git a/core/ui-compose/build.gradle.kts b/core/androidx/compose/build.gradle.kts similarity index 87% rename from core/ui-compose/build.gradle.kts rename to core/androidx/compose/build.gradle.kts index 030b9f0e1..2c69e45eb 100644 --- a/core/ui-compose/build.gradle.kts +++ b/core/androidx/compose/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } android { - namespace = "com.nlab.reminder.core.ui.compose" + namespace = "com.nlab.reminder.core.androidx.compose" } dependencies { diff --git a/core/ui-compose/src/main/AndroidManifest.xml b/core/androidx/compose/src/main/AndroidManifest.xml similarity index 100% rename from core/ui-compose/src/main/AndroidManifest.xml rename to core/androidx/compose/src/main/AndroidManifest.xml diff --git a/core/ui-compose/src/main/kotlin/com/nlab/reminder/core/ui/compose/ColorPressButton.kt b/core/androidx/compose/src/main/kotlin/com/nlab/reminder/core/androidx/compose/ui/ColorPressButton.kt similarity index 98% rename from core/ui-compose/src/main/kotlin/com/nlab/reminder/core/ui/compose/ColorPressButton.kt rename to core/androidx/compose/src/main/kotlin/com/nlab/reminder/core/androidx/compose/ui/ColorPressButton.kt index 7dc5366eb..cad9ba859 100644 --- a/core/ui-compose/src/main/kotlin/com/nlab/reminder/core/ui/compose/ColorPressButton.kt +++ b/core/androidx/compose/src/main/kotlin/com/nlab/reminder/core/androidx/compose/ui/ColorPressButton.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.nlab.reminder.core.ui.compose +package com.nlab.reminder.core.androidx.compose.ui import android.content.res.Configuration.UI_MODE_NIGHT_NO import androidx.compose.foundation.clickable diff --git a/core/ui-compose/src/main/kotlin/com/nlab/reminder/core/ui/compose/DelayedContent.kt b/core/androidx/compose/src/main/kotlin/com/nlab/reminder/core/androidx/compose/ui/DelayedContent.kt similarity index 97% rename from core/ui-compose/src/main/kotlin/com/nlab/reminder/core/ui/compose/DelayedContent.kt rename to core/androidx/compose/src/main/kotlin/com/nlab/reminder/core/androidx/compose/ui/DelayedContent.kt index 44ee1f179..0ac2d6122 100644 --- a/core/ui-compose/src/main/kotlin/com/nlab/reminder/core/ui/compose/DelayedContent.kt +++ b/core/androidx/compose/src/main/kotlin/com/nlab/reminder/core/androidx/compose/ui/DelayedContent.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.nlab.reminder.core.ui.compose +package com.nlab.reminder.core.androidx.compose.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect diff --git a/core/ui-compose/src/main/kotlin/com/nlab/reminder/core/ui/compose/TextFieldValues.kt b/core/androidx/compose/src/main/kotlin/com/nlab/reminder/core/androidx/compose/ui/TextFieldValues.kt similarity index 96% rename from core/ui-compose/src/main/kotlin/com/nlab/reminder/core/ui/compose/TextFieldValues.kt rename to core/androidx/compose/src/main/kotlin/com/nlab/reminder/core/androidx/compose/ui/TextFieldValues.kt index ebd45a68c..1b9ebe4f7 100644 --- a/core/ui-compose/src/main/kotlin/com/nlab/reminder/core/ui/compose/TextFieldValues.kt +++ b/core/androidx/compose/src/main/kotlin/com/nlab/reminder/core/androidx/compose/ui/TextFieldValues.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.nlab.reminder.core.ui.compose +package com.nlab.reminder.core.androidx.compose.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf diff --git a/core/ui-compose/src/main/kotlin/com/nlab/reminder/core/ui/compose/ThrottleClick.kt b/core/androidx/compose/src/main/kotlin/com/nlab/reminder/core/androidx/compose/ui/ThrottleClick.kt similarity index 96% rename from core/ui-compose/src/main/kotlin/com/nlab/reminder/core/ui/compose/ThrottleClick.kt rename to core/androidx/compose/src/main/kotlin/com/nlab/reminder/core/androidx/compose/ui/ThrottleClick.kt index 6ea6f74a5..713650052 100644 --- a/core/ui-compose/src/main/kotlin/com/nlab/reminder/core/ui/compose/ThrottleClick.kt +++ b/core/androidx/compose/src/main/kotlin/com/nlab/reminder/core/androidx/compose/ui/ThrottleClick.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.nlab.reminder.core.ui.compose +package com.nlab.reminder.core.androidx.compose.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect diff --git a/core/ui-compose/src/main/kotlin/com/nlab/reminder/core/ui/compose/tooling/preview/Previews.kt b/core/androidx/compose/src/main/kotlin/com/nlab/reminder/core/androidx/compose/ui/tooling/preview/Previews.kt similarity index 94% rename from core/ui-compose/src/main/kotlin/com/nlab/reminder/core/ui/compose/tooling/preview/Previews.kt rename to core/androidx/compose/src/main/kotlin/com/nlab/reminder/core/androidx/compose/ui/tooling/preview/Previews.kt index 0a48ea32d..48d1abc06 100644 --- a/core/ui-compose/src/main/kotlin/com/nlab/reminder/core/ui/compose/tooling/preview/Previews.kt +++ b/core/androidx/compose/src/main/kotlin/com/nlab/reminder/core/androidx/compose/ui/tooling/preview/Previews.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.nlab.reminder.core.ui.compose.tooling.preview +package com.nlab.reminder.core.androidx.compose.ui.tooling.preview import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES diff --git a/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/edit/TagEditDelegate.kt b/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/edit/TagEditDelegate.kt index 1ea6b2e9d..a2642d0e7 100644 --- a/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/edit/TagEditDelegate.kt +++ b/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/edit/TagEditDelegate.kt @@ -42,10 +42,11 @@ class TagEditDelegate( private val _state = MutableStateFlow(initialState) val state: StateFlow = _state.asStateFlow() - suspend fun startEditing(tag: Tag): Result = + suspend fun startEditing(tag: Tag): Result = tagRepository.getUsageCount(id = tag.id) .map { usageCount -> TagEditState.Intro(tag, usageCount) } .onSuccess { intro -> _state.update { current -> current ?: intro } } + .map {} fun startRename() { _state.updateIfTypeOf { current -> diff --git a/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/edit/ui/compose/TagEditIntroDialog.kt b/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/edit/ui/compose/TagEditIntroDialog.kt index e400ed5d2..8dcb9981b 100644 --- a/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/edit/ui/compose/TagEditIntroDialog.kt +++ b/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/edit/ui/compose/TagEditIntroDialog.kt @@ -41,8 +41,8 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.nlab.reminder.core.ui.compose.throttleClick -import com.nlab.reminder.core.ui.compose.tooling.preview.Previews +import com.nlab.reminder.core.androidx.compose.ui.throttleClick +import com.nlab.reminder.core.androidx.compose.ui.tooling.preview.Previews import com.nlab.reminder.core.designsystem.compose.component.PlaneatDialog import com.nlab.reminder.core.designsystem.compose.theme.PlaneatTheme import com.nlab.reminder.core.kotlin.NonBlankString diff --git a/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagCard.kt b/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagCard.kt index 43d83d11d..0847f72af 100644 --- a/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagCard.kt +++ b/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagCard.kt @@ -36,8 +36,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.nlab.reminder.core.ui.compose.tooling.preview.Previews -import com.nlab.reminder.core.ui.compose.throttleClick +import com.nlab.reminder.core.androidx.compose.ui.tooling.preview.Previews +import com.nlab.reminder.core.androidx.compose.ui.throttleClick import com.nlab.reminder.core.designsystem.compose.theme.PlaneatTheme import com.nlab.reminder.core.translation.StringIds diff --git a/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagDeleteBottomSheet.kt b/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagDeleteBottomSheet.kt index 286d1a5a6..2b2da2b22 100644 --- a/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagDeleteBottomSheet.kt +++ b/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagDeleteBottomSheet.kt @@ -37,8 +37,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.nlab.reminder.core.ui.compose.throttleClick -import com.nlab.reminder.core.ui.compose.tooling.preview.Previews +import com.nlab.reminder.core.androidx.compose.ui.throttleClick +import com.nlab.reminder.core.androidx.compose.ui.tooling.preview.Previews import com.nlab.reminder.core.component.tag.ui.getUsageCountLabel import com.nlab.reminder.core.designsystem.compose.component.PlaneatBottomSheet import com.nlab.reminder.core.designsystem.compose.theme.PlaneatTheme diff --git a/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagDialogButtons.kt b/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagDialogButtons.kt index 8a7135d89..14a33f5ff 100644 --- a/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagDialogButtons.kt +++ b/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagDialogButtons.kt @@ -30,9 +30,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.nlab.reminder.core.ui.compose.ColorPressButton -import com.nlab.reminder.core.ui.compose.throttleClick -import com.nlab.reminder.core.ui.compose.tooling.preview.Previews +import com.nlab.reminder.core.androidx.compose.ui.ColorPressButton +import com.nlab.reminder.core.androidx.compose.ui.throttleClick +import com.nlab.reminder.core.androidx.compose.ui.tooling.preview.Previews import com.nlab.reminder.core.designsystem.compose.theme.PlaneatTheme import com.nlab.reminder.core.translation.StringIds diff --git a/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagMergeDialog.kt b/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagMergeDialog.kt index f56714eb2..68f4bc1ce 100644 --- a/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagMergeDialog.kt +++ b/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagMergeDialog.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.nlab.reminder.core.ui.compose.tooling.preview.Previews +import com.nlab.reminder.core.androidx.compose.ui.tooling.preview.Previews import com.nlab.reminder.core.designsystem.compose.component.PlaneatDialog import com.nlab.reminder.core.designsystem.compose.theme.PlaneatTheme import com.nlab.reminder.core.kotlin.NonBlankString diff --git a/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagRenameDialog.kt b/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagRenameDialog.kt index 66d238d23..d2d7b66f6 100644 --- a/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagRenameDialog.kt +++ b/core/component/tag/src/main/kotlin/com/nlab/reminder/core/component/tag/ui/compose/TagRenameDialog.kt @@ -57,9 +57,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.nlab.reminder.core.ui.compose.rememberDebouncedTextFieldValueState -import com.nlab.reminder.core.ui.compose.throttleClick -import com.nlab.reminder.core.ui.compose.tooling.preview.Previews +import com.nlab.reminder.core.androidx.compose.ui.rememberDebouncedTextFieldValueState +import com.nlab.reminder.core.androidx.compose.ui.throttleClick +import com.nlab.reminder.core.androidx.compose.ui.tooling.preview.Previews import com.nlab.reminder.core.component.tag.ui.getUsageCountLabel import com.nlab.reminder.core.designsystem.compose.component.PlaneatDialog import com.nlab.reminder.core.designsystem.compose.resource.DrawableIds diff --git a/statekit/runtime/.gitignore b/core/component/usermessage-handle/.gitignore similarity index 100% rename from statekit/runtime/.gitignore rename to core/component/usermessage-handle/.gitignore diff --git a/core/component/usermessage-handle/build.gradle.kts b/core/component/usermessage-handle/build.gradle.kts new file mode 100644 index 000000000..428764dcf --- /dev/null +++ b/core/component/usermessage-handle/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + alias(libs.plugins.nlab.android.library) + alias(libs.plugins.nlab.android.library.compose.component) + alias(libs.plugins.nlab.android.library.di) + alias(libs.plugins.nlab.android.library.jacoco) + kotlin("kapt") +} + +android { + namespace = "com.nlab.reminder.core.component.usermessage.handle" +} + +dependencies { + implementation(projects.core.annotation) + implementation(projects.core.component.usermessage) + implementation(projects.core.kotlinxCoroutine) + implementation(projects.core.statekit) + kapt(projects.statekit.compiler) + + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.androidx.lifecycle.runtimeCompose) + implementation(libs.androidx.navigation.compose) + + testImplementation(projects.core.uitextTest) + testImplementation(projects.statekit.test) + testImplementation(projects.testkit) +} \ No newline at end of file diff --git a/core/component/usermessage-handle/src/main/AndroidManifest.xml b/core/component/usermessage-handle/src/main/AndroidManifest.xml new file mode 100644 index 000000000..568741e54 --- /dev/null +++ b/core/component/usermessage-handle/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/UserMessageHandleFeatures.kt b/core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/UserMessageHandleFeatures.kt new file mode 100644 index 000000000..5f4abdb3a --- /dev/null +++ b/core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/UserMessageHandleFeatures.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 The N's lab Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nlab.reminder.core.component.usermessage.handle + +import com.nlab.reminder.core.annotation.ExcludeFromGeneratedTestReport +import com.nlab.reminder.core.component.usermessage.UserMessage +import com.nlab.reminder.core.component.usermessage.handle.UserMessageHandleAction.* +import com.nlab.reminder.core.kotlinx.coroutine.flow.map +import com.nlab.reminder.core.statekit.store.androidx.lifecycle.StoreViewModel +import com.nlab.reminder.core.statekit.store.androidx.lifecycle.createStore +import com.nlab.statekit.annotation.UiAction +import com.nlab.statekit.annotation.UiActionMapping +import com.nlab.statekit.bootstrap.DeliveryStarted +import com.nlab.statekit.bootstrap.collectAsBootstrap +import com.nlab.statekit.dsl.reduce.DslReduce +import com.nlab.statekit.reduce.Reduce +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +typealias UserMessageHandleReduce = Reduce + +/** + * @author Thalys + */ +fun UserMessageHandleReduce(): UserMessageHandleReduce = DslReduce { + stateScope { + transition { + current.copy(userMessages = current.userMessages + action.message) + } + transition { + current.copy(userMessages = current.userMessages - action.message) + } + } +} + +sealed class UserMessageHandleAction { + data class UserMessagePosted(val message: UserMessage) : UserMessageHandleAction() + + @UiAction + data class UserMessageShown(val message: UserMessage) : UserMessageHandleAction() +} + +data class UserMessageUiState( + val userMessages: List +) + +@ExcludeFromGeneratedTestReport +@UiActionMapping(UserMessageHandleAction::class) +@HiltViewModel +class UserMessageHandleViewModel @Inject constructor( + private val userMessageMonitor: UserMessageMonitor +) : StoreViewModel() { + override fun onCreateStore() = createStore( + initState = UserMessageUiState(userMessages = emptyList()), + reduce = UserMessageHandleReduce(), + bootstrap = userMessageMonitor.message + .map(::UserMessagePosted) + .collectAsBootstrap(DeliveryStarted.Lazily) + ) +} \ No newline at end of file diff --git a/statekit/runtime/src/main/kotlin/com/nlab/statekit/lifecycle/UiActionDispatchable.kt b/core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/UserMessageMonitor.kt similarity index 65% rename from statekit/runtime/src/main/kotlin/com/nlab/statekit/lifecycle/UiActionDispatchable.kt rename to core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/UserMessageMonitor.kt index 79d96c77a..7232f78e1 100644 --- a/statekit/runtime/src/main/kotlin/com/nlab/statekit/lifecycle/UiActionDispatchable.kt +++ b/core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/UserMessageMonitor.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The N's lab Open Source Project + * Copyright (C) 2024 The N's lab Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,14 @@ * limitations under the License. */ -package com.nlab.statekit.lifecycle +package com.nlab.reminder.core.component.usermessage.handle -import kotlinx.coroutines.Job +import com.nlab.reminder.core.component.usermessage.UserMessage +import kotlinx.coroutines.flow.Flow -interface UiActionDispatchable { - fun dispatch(action: T): Job +/** + * @author Thalys + */ +interface UserMessageMonitor { + val message: Flow } \ No newline at end of file diff --git a/app/src/main/java/com/nlab/reminder/internal/common/di/UtilityModule.kt b/core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/di/AppScopeUserMessageHandleModule.kt similarity index 53% rename from app/src/main/java/com/nlab/reminder/internal/common/di/UtilityModule.kt rename to core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/di/AppScopeUserMessageHandleModule.kt index dfbb117d7..2b923cd13 100644 --- a/app/src/main/java/com/nlab/reminder/internal/common/di/UtilityModule.kt +++ b/core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/di/AppScopeUserMessageHandleModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The N's lab Open Source Project + * Copyright (C) 2024 The N's lab Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,24 +14,29 @@ * limitations under the License. */ -package com.nlab.reminder.internal.common.di +package com.nlab.reminder.core.component.usermessage.handle.di -import android.content.Context -import com.nlab.reminder.core.android.widget.ToastHandle +import com.nlab.reminder.core.component.usermessage.handle.UserMessageMonitor +import com.nlab.reminder.core.component.usermessage.handle.impl.UserMessageBroadcastMonitor +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton /** - * @author Doohyun + * @author Thalys */ @Module @InstallIn(SingletonComponent::class) -class UtilityModule { - @Singleton - @Provides - fun provideToastHandle(@ApplicationContext context: Context): ToastHandle = ToastHandle(context) +internal abstract class AppScopeUserMessageHandleModule { + @Binds + abstract fun bindUserMesMessageMonitor(impl: UserMessageBroadcastMonitor): UserMessageMonitor + + companion object { + @Singleton + @Provides + fun provideUserMessageBroadcastMonitor(): UserMessageBroadcastMonitor = UserMessageBroadcastMonitor() + } } \ No newline at end of file diff --git a/core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/di/UserMessageHandleModuleEntryPoint.kt b/core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/di/UserMessageHandleModuleEntryPoint.kt new file mode 100644 index 000000000..326601146 --- /dev/null +++ b/core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/di/UserMessageHandleModuleEntryPoint.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 The N's lab Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nlab.reminder.core.component.usermessage.handle.di + +import android.content.Context +import com.nlab.reminder.core.component.usermessage.handle.impl.UserMessageBroadcastMonitor +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent + +/** + * @author Thalys + */ +@EntryPoint +@InstallIn(SingletonComponent::class) +internal interface UserMessageHandleModuleEntryPoint { + fun userMessageBroadcastMonitor(): UserMessageBroadcastMonitor +} + +fun Context.getUserMessageBroadcastMonitor(): UserMessageBroadcastMonitor = + EntryPointAccessors + .fromApplication(context = this, UserMessageHandleModuleEntryPoint::class.java) + .userMessageBroadcastMonitor() \ No newline at end of file diff --git a/statekit/runtime/src/main/kotlin/com/nlab/statekit/lifecycle/viewmodel/ContractUiAction.kt b/core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/impl/UserMessageBroadcastMonitor.kt similarity index 50% rename from statekit/runtime/src/main/kotlin/com/nlab/statekit/lifecycle/viewmodel/ContractUiAction.kt rename to core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/impl/UserMessageBroadcastMonitor.kt index 660df10b0..ba8cb948b 100644 --- a/statekit/runtime/src/main/kotlin/com/nlab/statekit/lifecycle/viewmodel/ContractUiAction.kt +++ b/core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/impl/UserMessageBroadcastMonitor.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The N's lab Open Source Project + * Copyright (C) 2024 The N's lab Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,20 +14,22 @@ * limitations under the License. */ -package com.nlab.statekit.lifecycle.viewmodel +package com.nlab.reminder.core.component.usermessage.handle.impl + +import com.nlab.reminder.core.component.usermessage.UserMessage +import com.nlab.reminder.core.component.usermessage.handle.UserMessageMonitor +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.receiveAsFlow /** - * Generate UiAction dispatch method without receiver type. - * The first simple names of annotated class should suffixed with "Action". - * **SampleUiAction.OnClick**, **SampleUiAction.DialogAction.OnClick** - * - * For example, if you have a action like this: **SimpleAction.OnClick** - * Annotation Processor will be generate method like this: - * - * fun SampleViewModel.onClick() { - * } - * @author Doohyun + * @author Thalys */ -@Retention(AnnotationRetention.SOURCE) -@Target(AnnotationTarget.CLASS) -annotation class ContractUiAction(val isPublic: Boolean = false) \ No newline at end of file +class UserMessageBroadcastMonitor : UserMessageMonitor { + private val _message = Channel(Channel.RENDEZVOUS) + override val message: Flow = _message.receiveAsFlow() + + fun send(userMessage: UserMessage) { + _message.trySend(userMessage) + } +} \ No newline at end of file diff --git a/core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/ui/UserMessageHandler.kt b/core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/ui/UserMessageHandler.kt new file mode 100644 index 000000000..9fa58a0b3 --- /dev/null +++ b/core/component/usermessage-handle/src/main/kotlin/com/nlab/reminder/core/component/usermessage/handle/ui/UserMessageHandler.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 The N's lab Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nlab.reminder.core.component.usermessage.handle.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.nlab.reminder.core.component.usermessage.UserMessage +import com.nlab.reminder.core.component.usermessage.handle.UserMessageHandleViewModel +import com.nlab.reminder.core.component.usermessage.handle.UserMessageUiState +import com.nlab.reminder.core.component.usermessage.handle.userMessageShown +import com.nlab.reminder.core.text.ui.compose.toText + + +/** + * @author Thalys + */ +@Composable +fun UserMessageHandler( + showApplicationToast: (String) -> Unit, + viewModel: UserMessageHandleViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + UserMessageHandler( + uiState = uiState, + userMessageShown = { viewModel.userMessageShown(it) }, + showApplicationToast = showApplicationToast + ) +} + +@Composable +private fun UserMessageHandler( + uiState: UserMessageUiState, + userMessageShown: (UserMessage) -> Unit, + showApplicationToast: (String) -> Unit, +) { + val userMessage = uiState.userMessages.firstOrNull() ?: return + val messageText = userMessage.message.toText() + LaunchedEffect(userMessage) { + showApplicationToast(messageText) // TODO implements user message with priority + userMessageShown(userMessage) + } +} \ No newline at end of file diff --git a/core/component/usermessage-handle/src/test/kotlin/com/nlab/reminder/core/component/usermessage/handle/UserMessageHandleReduceTest.kt b/core/component/usermessage-handle/src/test/kotlin/com/nlab/reminder/core/component/usermessage/handle/UserMessageHandleReduceTest.kt new file mode 100644 index 000000000..d9270651f --- /dev/null +++ b/core/component/usermessage-handle/src/test/kotlin/com/nlab/reminder/core/component/usermessage/handle/UserMessageHandleReduceTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The N's lab Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nlab.reminder.core.component.usermessage.handle + +import com.nlab.reminder.core.component.usermessage.FeedbackPriority +import com.nlab.reminder.core.component.usermessage.UserMessage +import com.nlab.reminder.core.text.genUiText +import com.nlab.statekit.test.reduce.transitionScenario +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** + * @author Thalys + */ +class UserMessageHandleReduceTest { + @Test + fun `Given success with empty userMessage, When user message posted, Then state changed with user message`() = runTest { + UserMessageHandleReduce() + .transitionScenario() + .initState(UserMessageUiState(userMessages = emptyList())) + .action( + UserMessageHandleAction.UserMessagePosted( + UserMessage( + message = genUiText(), + priority = FeedbackPriority.LOW + ) + ) + ) + .expectedStateFromInput { + initState.copy(userMessages = listOf(action.message)) + } + .verify() + } + + @Test + fun `Given success with single user message, When user message shown, Then state changed empty user message`() = runTest { + val userMessage = UserMessage( + message = genUiText(), + priority = FeedbackPriority.URGENT + ) + UserMessageHandleReduce() + .transitionScenario() + .initState(UserMessageUiState(listOf(userMessage))) + .action(UserMessageHandleAction.UserMessageShown(userMessage)) + .expectedStateFromInput { initState.copy(userMessages = emptyList()) } + .verify() + } +} \ No newline at end of file diff --git a/core/component/usermessage-handle/src/test/kotlin/com/nlab/reminder/core/component/usermessage/handle/impl/UserMessageBroadcastMonitorTest.kt b/core/component/usermessage-handle/src/test/kotlin/com/nlab/reminder/core/component/usermessage/handle/impl/UserMessageBroadcastMonitorTest.kt new file mode 100644 index 000000000..bbbb02af0 --- /dev/null +++ b/core/component/usermessage-handle/src/test/kotlin/com/nlab/reminder/core/component/usermessage/handle/impl/UserMessageBroadcastMonitorTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 The N's lab Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nlab.reminder.core.component.usermessage.handle.impl + +import com.nlab.reminder.core.component.usermessage.FeedbackPriority +import com.nlab.reminder.core.component.usermessage.UserMessage +import com.nlab.reminder.core.text.genUiText +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.assertFlowEmissionsLazy +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** + * @author Thalys + */ +class UserMessageBroadcastMonitorTest { + @Test + fun `Given user message, When send after subscribe, Then monitor send user message`() = runTest { + val userMessage = UserMessage( + message = genUiText(), + priority = FeedbackPriority.HIGH + ) + val userMessageProvider = UserMessageBroadcastMonitor() + val assertion = assertFlowEmissionsLazy(userMessageProvider.message, listOf(userMessage)) + userMessageProvider.send(userMessage) + assertion() + } + + @Test + fun `Given user message, When send before subscribe, Then monitor not send user message`() = runTest { + val userMessage = UserMessage( + message = genUiText(), + priority = FeedbackPriority.URGENT + ) + val userMessageProvider = UserMessageBroadcastMonitor() + userMessageProvider.send(userMessage) + + val assertion = assertFlowEmissionsLazy(userMessageProvider.message, emptyList()) + advanceUntilIdle() + assertion() + } +} \ No newline at end of file diff --git a/core/component/usermessage/.gitignore b/core/component/usermessage/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/component/usermessage/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/component/usermessage/build.gradle.kts b/core/component/usermessage/build.gradle.kts new file mode 100644 index 000000000..ba7f8c065 --- /dev/null +++ b/core/component/usermessage/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(libs.plugins.nlab.android.library) + alias(libs.plugins.nlab.android.library.compose) + alias(libs.plugins.nlab.android.library.di) + alias(libs.plugins.nlab.android.library.jacoco) +} + +android { + namespace = "com.nlab.reminder.core.component.usermessage" +} + +dependencies { + api(projects.core.uitext) + implementation(projects.core.annotation) + implementation(projects.core.kotlin) + implementation(projects.core.translation) + + testImplementation(projects.core.uitextTest) + testImplementation(projects.testkit) +} \ No newline at end of file diff --git a/core/component/usermessage/src/main/AndroidManifest.xml b/core/component/usermessage/src/main/AndroidManifest.xml new file mode 100644 index 000000000..568741e54 --- /dev/null +++ b/core/component/usermessage/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/statekit/runtime/src/main/kotlin/com/nlab/statekit/lifecycle/UiAction.kt b/core/component/usermessage/src/main/kotlin/com/nlab/reminder/core/component/usermessage/FeedbackPriority.kt similarity index 65% rename from statekit/runtime/src/main/kotlin/com/nlab/statekit/lifecycle/UiAction.kt rename to core/component/usermessage/src/main/kotlin/com/nlab/reminder/core/component/usermessage/FeedbackPriority.kt index adcdff8f9..70a3f0ae8 100644 --- a/statekit/runtime/src/main/kotlin/com/nlab/statekit/lifecycle/UiAction.kt +++ b/core/component/usermessage/src/main/kotlin/com/nlab/reminder/core/component/usermessage/FeedbackPriority.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The N's lab Open Source Project + * Copyright (C) 2024 The N's lab Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,17 @@ * limitations under the License. */ -package com.nlab.statekit.lifecycle +package com.nlab.reminder.core.component.usermessage -import kotlin.reflect.KClass +import com.nlab.reminder.core.annotation.ExcludeFromGeneratedTestReport /** * @author Doohyun */ -@Retention(AnnotationRetention.SOURCE) -@Target(AnnotationTarget.CLASS) -annotation class UiAction( - val receiverTypes: Array>>, - val isPublic: Boolean = false, -) \ No newline at end of file +@ExcludeFromGeneratedTestReport +enum class FeedbackPriority { + LOW, + MEDIUM, + HIGH, + URGENT +} \ No newline at end of file diff --git a/core/component/usermessage/src/main/kotlin/com/nlab/reminder/core/component/usermessage/UserMessage.kt b/core/component/usermessage/src/main/kotlin/com/nlab/reminder/core/component/usermessage/UserMessage.kt new file mode 100644 index 000000000..81f3acb9f --- /dev/null +++ b/core/component/usermessage/src/main/kotlin/com/nlab/reminder/core/component/usermessage/UserMessage.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The N's lab Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nlab.reminder.core.component.usermessage + +import com.nlab.reminder.core.annotation.ExcludeFromGeneratedTestReport +import com.nlab.reminder.core.text.UiText + +/** + * @author Doohyun + */ +@ExcludeFromGeneratedTestReport +data class UserMessage( + val message: UiText, + val priority: FeedbackPriority +) \ No newline at end of file diff --git a/statekit/runtime/build.gradle.kts b/core/component/usermessage/src/main/kotlin/com/nlab/reminder/core/component/usermessage/UserMessageException.kt similarity index 62% rename from statekit/runtime/build.gradle.kts rename to core/component/usermessage/src/main/kotlin/com/nlab/reminder/core/component/usermessage/UserMessageException.kt index 69555e4e7..31ababe83 100644 --- a/statekit/runtime/build.gradle.kts +++ b/core/component/usermessage/src/main/kotlin/com/nlab/reminder/core/component/usermessage/UserMessageException.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The N's lab Open Source Project + * Copyright (C) 2024 The N's lab Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +14,15 @@ * limitations under the License. */ -plugins { - alias(libs.plugins.nlab.jvm.library) -} +package com.nlab.reminder.core.component.usermessage -dependencies { - api(projects.statekit.core) - implementation(libs.kotlinx.coroutines.core) -} \ No newline at end of file +import com.nlab.reminder.core.annotation.ExcludeFromGeneratedTestReport + +/** + * @author Doohyun + */ +@ExcludeFromGeneratedTestReport +class UserMessageException( + val userMessage: UserMessage, + val origin: Throwable +) : RuntimeException() \ No newline at end of file diff --git a/core/component/usermessage/src/main/kotlin/com/nlab/reminder/core/component/usermessage/UserMessageExt.kt b/core/component/usermessage/src/main/kotlin/com/nlab/reminder/core/component/usermessage/UserMessageExt.kt new file mode 100644 index 000000000..4a2afd1ab --- /dev/null +++ b/core/component/usermessage/src/main/kotlin/com/nlab/reminder/core/component/usermessage/UserMessageExt.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The N's lab Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nlab.reminder.core.component.usermessage + +import com.nlab.reminder.core.kotlin.Result +import com.nlab.reminder.core.kotlin.getOrThrow +import com.nlab.reminder.core.text.UiText +import com.nlab.reminder.core.translation.StringIds + +/** + * @author Doohyun + */ +fun Result.getOrThrowMessage( + message: UiText? = null, + priority: FeedbackPriority? = null +): T { + try { + return getOrThrow() + } catch (e: UserMessageException) { + throw if (message == null && priority == null) e + else { + val originUserMessage = e.userMessage + UserMessageException( + userMessage = UserMessage( + message = message ?: originUserMessage.message, + priority = priority ?: originUserMessage.priority + ), + origin = e.origin + ) + } + } catch (e: Throwable) { + throw UserMessageException( + userMessage = UserMessage( + message = message ?: UiText(StringIds.unknown_error), + priority = priority ?: FeedbackPriority.LOW + ), + origin = e + ) + } +} \ No newline at end of file diff --git a/core/component/usermessage/src/test/kotlin/com/nlab/reminder/core/component/usermessage/UserMessageExtKtTest.kt b/core/component/usermessage/src/test/kotlin/com/nlab/reminder/core/component/usermessage/UserMessageExtKtTest.kt new file mode 100644 index 000000000..16a0656be --- /dev/null +++ b/core/component/usermessage/src/test/kotlin/com/nlab/reminder/core/component/usermessage/UserMessageExtKtTest.kt @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2024 The N's lab Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nlab.reminder.core.component.usermessage + +import com.nlab.reminder.core.kotlin.Result +import com.nlab.reminder.core.text.UiText +import com.nlab.reminder.core.text.genUiText +import com.nlab.reminder.core.translation.StringIds +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.sameInstance +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test + +/** + * @author Thalys + */ +class UserMessageExtKtTest { + @Test + fun `Given success result, When getOrThrowMessage, Then result return value`() { + val value = Any() + val result = Result.Success(value) + assertThat(result.getOrThrowMessage(), sameInstance(value)) + } + + @Test + fun `Given failed result with user message, When getOrThrowMessage, Then result throw origin`() { + val throwable = UserMessageException( + userMessage = UserMessage(message = genUiText(), FeedbackPriority.HIGH), + origin = Throwable() + ) + val result = Result.Failure(throwable) + try { + result.getOrThrowMessage() + } catch (e: UserMessageException) { + assertThat(e, sameInstance(throwable)) + } + } + + @Test + fun `Given message, priority and failed result with user message, When getOrThrowMessage with message and priority, Then result UserMessageException with message and priority `() { + val message = genUiText() + val priority = FeedbackPriority.URGENT + val throwable = Throwable() + val result = Result.Failure( + UserMessageException( + userMessage = UserMessage(message = genUiText(), FeedbackPriority.HIGH), + origin = throwable + ) + ) + try { + result.getOrThrowMessage( + message = message, + priority = priority + ) + } catch (e: UserMessageException) { + assertThat( + e.userMessage, + equalTo(UserMessage(message = message, priority = priority)) + ) + assertThat( + e.origin, + sameInstance(throwable) + ) + } + } + + @Test + fun `Given message and failed result with user message, When getOrThrowMessage with message, Then result throw UserMessageException with message`() { + val message = genUiText() + val priority = FeedbackPriority.MEDIUM + val throwable = Throwable() + val result = Result.Failure( + UserMessageException( + userMessage = UserMessage(message = genUiText(), priority), + origin = throwable + ) + ) + try { + result.getOrThrowMessage(message = message) + } catch (e: UserMessageException) { + assertThat( + e.userMessage, + equalTo(UserMessage(message = message, priority = priority)) + ) + assertThat( + e.origin, + sameInstance(throwable) + ) + } + } + + @Test + fun `Given priority and failed result with user message, When getOrThrowMessage with message, Then result throw UserMessageException with priority`() { + val message = genUiText() + val priority = FeedbackPriority.MEDIUM + val throwable = Throwable() + val result = Result.Failure( + UserMessageException( + userMessage = UserMessage(message = message, FeedbackPriority.HIGH), + origin = throwable + ) + ) + try { + result.getOrThrowMessage(priority = priority) + } catch (e: UserMessageException) { + assertThat( + e.userMessage, + equalTo(UserMessage(message = message, priority = priority)) + ) + assertThat( + e.origin, + sameInstance(throwable) + ) + } + } + + @Test + fun `Given message, priority and failed result, When getOrThrowMessage with message and priority, Then result throw UserMessageException`() { + val message = genUiText() + val priority = FeedbackPriority.MEDIUM + val result = Result.Failure(IllegalStateException()) + try { + result.getOrThrowMessage( + message = message, + priority = priority + ) + } catch (e: UserMessageException) { + assertThat( + e.userMessage, + equalTo(UserMessage(message = message, priority = priority)) + ) + } + } + + @Test + fun `Given failed result, When getOrThrowMessage, Then result throw UserMessageException with default options`() { + val result = Result.Failure(IllegalStateException()) + try { + result.getOrThrowMessage() + } catch (e: UserMessageException) { + assertThat( + e.userMessage, + equalTo(UserMessage(message = UiText(StringIds.unknown_error), priority = FeedbackPriority.LOW)) + ) + } + } +} \ No newline at end of file diff --git a/core/statekit/build.gradle.kts b/core/statekit/build.gradle.kts index caabd4283..3b44bbd59 100644 --- a/core/statekit/build.gradle.kts +++ b/core/statekit/build.gradle.kts @@ -10,7 +10,6 @@ android { dependencies { api(projects.statekit.core) api(projects.statekit.dsl) - api(projects.statekit.runtime) implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.kotlinx.coroutines.android) testImplementation(projects.testkit) diff --git a/core/statekit/src/main/kotlin/com/nlab/reminder/core/statekit/store/androidx/lifecycle/StoreViewModel.kt b/core/statekit/src/main/kotlin/com/nlab/reminder/core/statekit/store/androidx/lifecycle/StoreViewModel.kt index f75e55176..38eb03875 100644 --- a/core/statekit/src/main/kotlin/com/nlab/reminder/core/statekit/store/androidx/lifecycle/StoreViewModel.kt +++ b/core/statekit/src/main/kotlin/com/nlab/reminder/core/statekit/store/androidx/lifecycle/StoreViewModel.kt @@ -17,7 +17,6 @@ package com.nlab.reminder.core.statekit.store.androidx.lifecycle import androidx.lifecycle.ViewModel -import com.nlab.statekit.lifecycle.UiActionDispatchable import com.nlab.statekit.store.Store import kotlinx.coroutines.Job import kotlinx.coroutines.flow.StateFlow @@ -25,10 +24,10 @@ import kotlinx.coroutines.flow.StateFlow /** * @author Doohyun */ -abstract class StoreViewModel : ViewModel(), UiActionDispatchable { +abstract class StoreViewModel : ViewModel() { private val store: Store by lazy(LazyThreadSafetyMode.NONE) { onCreateStore() } val uiState: StateFlow get() = store.state - final override fun dispatch(action: A): Job = store.dispatch(action) + fun dispatch(action: A): Job = store.dispatch(action) protected abstract fun onCreateStore(): Store } \ No newline at end of file diff --git a/core/translation/src/main/res/values-en/strings.xml b/core/translation/src/main/res/values-en/strings.xml index 127300198..a2782a190 100644 --- a/core/translation/src/main/res/values-en/strings.xml +++ b/core/translation/src/main/res/values-en/strings.xml @@ -75,7 +75,6 @@ In addition to #%1$s, #%2$d tags are used in %3$d(+) notifications. %1$s is being used in %2$d(+) notifications. - Tag not found. Tag Already Exists Do you want to replace #%1$s with #%2$s in all of your schedules? diff --git a/core/translation/src/main/res/values/strings.xml b/core/translation/src/main/res/values/strings.xml index 3d24367f2..d459b3878 100644 --- a/core/translation/src/main/res/values/strings.xml +++ b/core/translation/src/main/res/values/strings.xml @@ -74,7 +74,6 @@ #%1$s 외 #%2$d개의 태그가 %3$d(+)개의 알림에서 사용되고 있습니다. #%1$s이(가) %2$d(+)개의 알림에서 사용되고 있습니다. - 태그를 찾을 수 없습니다. 태그가 이미 존재함 사용자의 모든 스케줄에서 #%1$s을(를) #%2$s(으)로 대치하겠습니까? diff --git a/settings.gradle.kts b/settings.gradle.kts index 0a02843b6..f959702fb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -40,7 +40,6 @@ include( ":statekit:compiler", ":statekit:core", ":statekit:dsl", - ":statekit:runtime", ":statekit:test", ":testkit" ) @@ -50,6 +49,7 @@ include( ) include( ":core:android", + ":core:androidx:compose", ":core:androidx:fragment", ":core:androidx:lifecycle", ":core:androidx:navigation-compose", @@ -58,6 +58,8 @@ include( ":core:annotation", ":core:component:tag", ":core:component:tag-test", + ":core:component:usermessage", + ":core:component:usermessage-handle", ":core:data", ":core:data-di", ":core:data-impl", @@ -78,7 +80,6 @@ include( ":core:schedule-test", ":core:statekit", ":core:translation", - ":core:ui-compose", ":core:uitext", ":core:uitext-test", ) \ No newline at end of file diff --git a/statekit/test/src/main/kotlin/com/nlab/statekit/test/reduce/EffectScenarios.kt b/statekit/test/src/main/kotlin/com/nlab/statekit/test/reduce/EffectScenarios.kt index 868dcfaec..bafbaa64a 100644 --- a/statekit/test/src/main/kotlin/com/nlab/statekit/test/reduce/EffectScenarios.kt +++ b/statekit/test/src/main/kotlin/com/nlab/statekit/test/reduce/EffectScenarios.kt @@ -21,6 +21,7 @@ import com.nlab.statekit.reduce.Reduce import com.nlab.statekit.store.createStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.currentCoroutineContext /** @@ -55,7 +56,7 @@ class EffectScenario internal constructor( fun launchIn( coroutineScope: CoroutineScope, shouldLaunchWithTransition: Boolean = false - ): Job { + ): EffectScenarioTestJob { val reduce = if (shouldLaunchWithTransition) reduce else Reduce(effect = reduce.effect) val store = createStore( coroutineScope = coroutineScope, @@ -64,11 +65,10 @@ class EffectScenario internal constructor( additionalEffects.fold(baseEffect) { acc, effect -> Effect.Composite(effect, acc) } }) ) - return store.dispatch(input.action) - } - - suspend fun launchAndJoin(shouldLaunchWithTransition: Boolean = false) { - launchIn(CoroutineScope(currentCoroutineContext()), shouldLaunchWithTransition).join() + return EffectScenarioTestJob( + input = input, + job = store.dispatch(input.action) + ) } } @@ -79,4 +79,26 @@ class TestEffectScope internal constructor( ) { val inputIAction: IA get() = input.action val inputState: IS get() = input.initState +} + +class EffectScenarioTestJob internal constructor( + val input: ScenarioInput, + val job: Job +) { + suspend fun join() { + job.join() + } + + suspend fun cancelAndJoin() { + job.cancelAndJoin() + } +} + +suspend inline fun EffectScenario.launchAndJoin( + shouldLaunchWithTransition: Boolean = false, + verifyBlock: ScenarioInput.() -> Unit = {} +) { + val job = launchIn(CoroutineScope(currentCoroutineContext()), shouldLaunchWithTransition) + job.join() + verifyBlock(job.input) } \ No newline at end of file