diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6eabb63b..e7abd75a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ + = dataStore.data .catch { emit(emptyPreferences()) } @@ -61,9 +63,9 @@ class UserPreferences @Inject constructor( .map { it[Keys.FORTUNE_ID] } .distinctUntilChanged() - val fortuneDateFlow: Flow = dataStore.data + val fortuneDateEpochFlow: Flow = dataStore.data .catch { emit(emptyPreferences()) } - .map { it[Keys.FORTUNE_DATE] } + .map { it[Keys.FORTUNE_DATE_EPOCH] } .distinctUntilChanged() val fortuneImageIdFlow: Flow = dataStore.data @@ -79,18 +81,17 @@ class UserPreferences @Inject constructor( val hasUnseenFortuneFlow: Flow = dataStore.data .catch { emit(emptyPreferences()) } .map { pref -> - pref[Keys.FORTUNE_DATE] == today() && - pref[Keys.FORTUNE_ID] != null && - (pref[Keys.FORTUNE_SEEN] != true) + val isToday = pref[Keys.FORTUNE_DATE_EPOCH] == todayEpoch() + isToday && (pref[Keys.FORTUNE_ID] != null) && (pref[Keys.FORTUNE_SEEN] != true) } .distinctUntilChanged() val shouldShowFortuneToolTipFlow: Flow = dataStore.data .catch { emit(emptyPreferences()) } .map { pref -> - val hasTodayFortune = pref[Keys.FORTUNE_DATE] == today() && pref[Keys.FORTUNE_ID] != null - val tooltipNotShown = pref[Keys.FORTUNE_TOOLTIP_SHOWN] ?: false - hasTodayFortune && !tooltipNotShown + val hasTodayFortune = (pref[Keys.FORTUNE_DATE_EPOCH] == todayEpoch()) && (pref[Keys.FORTUNE_ID] != null) + val tooltipShown = pref[Keys.FORTUNE_TOOLTIP_SHOWN] ?: false + hasTodayFortune && !tooltipShown } .distinctUntilChanged() @@ -108,27 +109,31 @@ class UserPreferences @Inject constructor( .catch { emit(emptyPreferences()) } .map { pref -> val flag = pref[Keys.FIRST_ALARM_DISMISSED_TODAY] ?: false - val date = pref[Keys.FIRST_ALARM_DISMISSED_DATE] - flag && date == today() + val isToday = pref[Keys.FIRST_ALARM_DISMISSED_DATE_EPOCH] == todayEpoch() + flag && isToday } .distinctUntilChanged() + val updateNoticeDontShowVersionFlow: Flow = dataStore.data + .catch { emit(emptyPreferences()) } + .map { it[Keys.UPDATE_NOTICE_DONT_SHOW_VERSION] } + .distinctUntilChanged() + + val updateNoticeLastShownDateEpochFlow: Flow = dataStore.data + .catch { emit(emptyPreferences()) } + .map { it[Keys.UPDATE_NOTICE_LAST_SHOWN_DATE_EPOCH] } + .distinctUntilChanged() + suspend fun saveUserId(userId: Long) { - dataStore.edit { pref -> - pref[Keys.USER_ID] = userId - } + dataStore.edit { it[Keys.USER_ID] = userId } } suspend fun saveUserName(userName: String) { - dataStore.edit { pref -> - pref[Keys.USER_NAME] = userName - } + dataStore.edit { it[Keys.USER_NAME] = userName } } suspend fun setOnboardingCompleted() { - dataStore.edit { pref -> - pref[Keys.ONBOARDING_COMPLETED] = true - } + dataStore.edit { it[Keys.ONBOARDING_COMPLETED] = true } } suspend fun markFortuneCreating() { @@ -140,10 +145,12 @@ class UserPreferences @Inject constructor( suspend fun markFortuneCreated(fortuneId: Long) { dataStore.edit { pref -> - val isNewForToday = pref[Keys.FORTUNE_ID] != fortuneId || pref[Keys.FORTUNE_DATE] != today() + val today = todayEpoch() + val prevDate = pref[Keys.FORTUNE_DATE_EPOCH] + val isNewForToday = (pref[Keys.FORTUNE_ID] != fortuneId) || (prevDate != today) pref[Keys.FORTUNE_ID] = fortuneId - pref[Keys.FORTUNE_DATE] = today() + pref[Keys.FORTUNE_DATE_EPOCH] = today pref[Keys.FORTUNE_CREATING] = false pref[Keys.FORTUNE_FAILED] = false @@ -162,44 +169,46 @@ class UserPreferences @Inject constructor( } suspend fun markFortuneSeen() { - dataStore.edit { pref -> - pref[Keys.FORTUNE_SEEN] = true - } + dataStore.edit { it[Keys.FORTUNE_SEEN] = true } } suspend fun markFortuneTooltipShown() { - dataStore.edit { pref -> - pref[Keys.FORTUNE_TOOLTIP_SHOWN] = true - } + dataStore.edit { it[Keys.FORTUNE_TOOLTIP_SHOWN] = true } } suspend fun saveFortuneImageId(imageResId: Int) { - dataStore.edit { pref -> - pref[Keys.FORTUNE_IMAGE_ID] = imageResId - } + dataStore.edit { it[Keys.FORTUNE_IMAGE_ID] = imageResId } } suspend fun saveFortuneScore(score: Int) { - dataStore.edit { pref -> - pref[Keys.FORTUNE_SCORE] = score - } + dataStore.edit { it[Keys.FORTUNE_SCORE] = score } } suspend fun markFirstAlarmDismissedToday() { dataStore.edit { pref -> pref[Keys.FIRST_ALARM_DISMISSED_TODAY] = true - pref[Keys.FIRST_ALARM_DISMISSED_DATE] = today() + pref[Keys.FIRST_ALARM_DISMISSED_DATE_EPOCH] = todayEpoch() + } + } + + suspend fun markUpdateNoticeDontShow(version: String) { + dataStore.edit { it[Keys.UPDATE_NOTICE_DONT_SHOW_VERSION] = version } + } + + suspend fun markUpdateNoticeShownToday() { + dataStore.edit { pref -> + pref[Keys.UPDATE_NOTICE_LAST_SHOWN_DATE_EPOCH] = todayEpoch() } } suspend fun clearUserData() { - dataStore.edit { pref -> pref.clear() } + dataStore.edit { it.clear() } } suspend fun clearFortuneData() { dataStore.edit { pref -> pref.remove(Keys.FORTUNE_ID) - pref.remove(Keys.FORTUNE_DATE) + pref.remove(Keys.FORTUNE_DATE_EPOCH) pref.remove(Keys.FORTUNE_IMAGE_ID) pref.remove(Keys.FORTUNE_SCORE) pref.remove(Keys.FORTUNE_SEEN) diff --git a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt index 853420c6..4e519ddb 100644 --- a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt +++ b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.Flow interface FortuneLocalDataSource { val fortuneIdFlow: Flow - val fortuneDateFlow: Flow + val fortuneDateEpochFlow: Flow val fortuneImageIdFlow: Flow val fortuneScoreFlow: Flow val hasUnseenFortuneFlow: Flow diff --git a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt index 0a14cb17..b8ab799f 100644 --- a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt +++ b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt @@ -5,7 +5,6 @@ import com.yapp.domain.model.FortuneCreateStatus import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import java.time.LocalDate -import java.time.format.DateTimeFormatter import javax.inject.Inject class FortuneLocalDataSourceImpl @Inject constructor( @@ -13,7 +12,7 @@ class FortuneLocalDataSourceImpl @Inject constructor( ) : FortuneLocalDataSource { override val fortuneIdFlow = userPreferences.fortuneIdFlow - override val fortuneDateFlow = userPreferences.fortuneDateFlow + override val fortuneDateEpochFlow = userPreferences.fortuneDateEpochFlow override val fortuneImageIdFlow = userPreferences.fortuneImageIdFlow override val fortuneScoreFlow = userPreferences.fortuneScoreFlow override val hasUnseenFortuneFlow = userPreferences.hasUnseenFortuneFlow @@ -22,19 +21,19 @@ class FortuneLocalDataSourceImpl @Inject constructor( override val fortuneCreateStatusFlow = combine( userPreferences.fortuneIdFlow, - userPreferences.fortuneDateFlow, + userPreferences.fortuneDateEpochFlow, userPreferences.isFortuneCreatingFlow, userPreferences.isFortuneFailedFlow, ) { fortuneId, fortuneDate, isCreating, isFailed -> when { isFailed -> FortuneCreateStatus.Failure isCreating -> FortuneCreateStatus.Creating - fortuneId != null && fortuneDate == today() -> FortuneCreateStatus.Success(fortuneId) + fortuneId != null && fortuneDate == todayEpoch() -> FortuneCreateStatus.Success(fortuneId) else -> FortuneCreateStatus.Idle } }.distinctUntilChanged() - private fun today(): String = LocalDate.now().format(DateTimeFormatter.ISO_DATE) + private fun todayEpoch(): Long = LocalDate.now().toEpochDay() override suspend fun markFortuneCreating() { userPreferences.markFortuneCreating() diff --git a/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSource.kt b/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSource.kt index 37b4fc5a..3ad851df 100644 --- a/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSource.kt +++ b/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSource.kt @@ -6,9 +6,13 @@ interface UserLocalDataSource { val userIdFlow: Flow val userNameFlow: Flow val onboardingCompletedFlow: Flow + val updateNoticeDontShowVersionFlow: Flow + val updateNoticeLastShownDateEpochFlow: Flow suspend fun saveUserId(userId: Long) suspend fun saveUserName(userName: String) suspend fun setOnboardingCompleted() + suspend fun markUpdateNoticeDontShow(version: String) + suspend fun markUpdateNoticeShownToday() suspend fun clearUserData() } diff --git a/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSourceImpl.kt b/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSourceImpl.kt index 7e7d4324..187a7a59 100644 --- a/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSourceImpl.kt +++ b/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSourceImpl.kt @@ -11,6 +11,8 @@ class UserLocalDataSourceImpl @Inject constructor( override val userIdFlow: Flow = userPreferences.userIdFlow override val userNameFlow: Flow = userPreferences.userNameFlow override val onboardingCompletedFlow: Flow = userPreferences.onboardingCompletedFlow + override val updateNoticeDontShowVersionFlow: Flow = userPreferences.updateNoticeDontShowVersionFlow + override val updateNoticeLastShownDateEpochFlow: Flow = userPreferences.updateNoticeLastShownDateEpochFlow override suspend fun saveUserId(userId: Long) { userPreferences.saveUserId(userId) @@ -24,6 +26,14 @@ class UserLocalDataSourceImpl @Inject constructor( userPreferences.setOnboardingCompleted() } + override suspend fun markUpdateNoticeDontShow(version: String) { + userPreferences.markUpdateNoticeDontShow(version) + } + + override suspend fun markUpdateNoticeShownToday() { + userPreferences.markUpdateNoticeShownToday() + } + override suspend fun clearUserData() { userPreferences.clearUserData() } diff --git a/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt b/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt index 8afe824e..1c761ba6 100644 --- a/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt +++ b/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt @@ -15,7 +15,7 @@ class FortuneRepositoryImpl @Inject constructor( ) : FortuneRepository { override val fortuneIdFlow: Flow = fortuneLocalDataSource.fortuneIdFlow - override val fortuneDateFlow: Flow = fortuneLocalDataSource.fortuneDateFlow + override val fortuneDateEpochFlow: Flow = fortuneLocalDataSource.fortuneDateEpochFlow override val fortuneImageIdFlow: Flow = fortuneLocalDataSource.fortuneImageIdFlow override val fortuneScoreFlow: Flow = fortuneLocalDataSource.fortuneScoreFlow override val hasUnseenFortuneFlow: Flow = fortuneLocalDataSource.hasUnseenFortuneFlow diff --git a/data/src/main/java/com/yapp/data/repositoryimpl/UserInfoRepositoryImpl.kt b/data/src/main/java/com/yapp/data/repositoryimpl/UserInfoRepositoryImpl.kt index d96ca6be..818e232d 100644 --- a/data/src/main/java/com/yapp/data/repositoryimpl/UserInfoRepositoryImpl.kt +++ b/data/src/main/java/com/yapp/data/repositoryimpl/UserInfoRepositoryImpl.kt @@ -17,10 +17,14 @@ class UserInfoRepositoryImpl @Inject constructor( override val userIdFlow: Flow = userLocalDataSource.userIdFlow override val userNameFlow: Flow = userLocalDataSource.userNameFlow override val onboardingCompletedFlow: Flow = userLocalDataSource.onboardingCompletedFlow + override val updateNoticeDontShowVersionFlow: Flow = userLocalDataSource.updateNoticeDontShowVersionFlow + override val updateNoticeLastShownDateEpochFlow: Flow = userLocalDataSource.updateNoticeLastShownDateEpochFlow override suspend fun saveUserId(userId: Long) = userLocalDataSource.saveUserId(userId) override suspend fun saveUserName(userName: String) = userLocalDataSource.saveUserName(userName) override suspend fun setOnboardingCompleted() = userLocalDataSource.setOnboardingCompleted() + override suspend fun markUpdateNoticeDontShow(version: String) = userLocalDataSource.markUpdateNoticeDontShow(version) + override suspend fun markUpdateNoticeShownToday() = userLocalDataSource.markUpdateNoticeShownToday() override suspend fun clearUserData() = userLocalDataSource.clearUserData() override suspend fun getUserInfo(userId: Long): Result { diff --git a/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt b/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt index 29024ae6..372fd5fe 100644 --- a/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt +++ b/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.Flow interface FortuneRepository { val fortuneIdFlow: Flow - val fortuneDateFlow: Flow + val fortuneDateEpochFlow: Flow val fortuneImageIdFlow: Flow val fortuneScoreFlow: Flow val hasUnseenFortuneFlow: Flow diff --git a/domain/src/main/java/com/yapp/domain/repository/UserInfoRepository.kt b/domain/src/main/java/com/yapp/domain/repository/UserInfoRepository.kt index bda28291..a9df412e 100644 --- a/domain/src/main/java/com/yapp/domain/repository/UserInfoRepository.kt +++ b/domain/src/main/java/com/yapp/domain/repository/UserInfoRepository.kt @@ -8,10 +8,14 @@ interface UserInfoRepository { val userIdFlow: Flow val userNameFlow: Flow val onboardingCompletedFlow: Flow + val updateNoticeDontShowVersionFlow: Flow + val updateNoticeLastShownDateEpochFlow: Flow suspend fun saveUserId(userId: Long) suspend fun saveUserName(userName: String) suspend fun setOnboardingCompleted() + suspend fun markUpdateNoticeDontShow(version: String) + suspend fun markUpdateNoticeShownToday() suspend fun clearUserData() suspend fun getUserInfo(userId: Long): Result diff --git a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerViewModel.kt b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerViewModel.kt index 1f0e6d1b..05b6c798 100644 --- a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerViewModel.kt +++ b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerViewModel.kt @@ -17,7 +17,6 @@ import org.orbitmvi.orbit.viewmodel.container import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneId -import java.time.format.DateTimeFormatter import javax.inject.Inject import kotlin.math.max @@ -45,8 +44,8 @@ class AlarmSnoozeTimerViewModel @Inject constructor( } private fun fetchIsFirstMission() = intent { - val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull() - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) + val fortuneDate = fortuneRepository.fortuneDateEpochFlow.firstOrNull() + val todayDate = LocalDate.now().toEpochDay() val isFirstMission = fortuneDate != todayDate reduce { diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts index 9ef0f667..0b90300c 100644 --- a/feature/home/build.gradle.kts +++ b/feature/home/build.gradle.kts @@ -19,4 +19,5 @@ dependencies { implementation(libs.orbit.viewmodel) implementation(libs.androidx.material.android) implementation(libs.androidx.annotation) + implementation(libs.coil.compose) } diff --git a/feature/home/src/main/AndroidManifest.xml b/feature/home/src/main/AndroidManifest.xml index 8bdb7e14..1c6dcf4e 100644 --- a/feature/home/src/main/AndroidManifest.xml +++ b/feature/home/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + diff --git a/feature/home/src/main/java/com/yapp/home/HomeContract.kt b/feature/home/src/main/java/com/yapp/home/HomeContract.kt index dee0ecef..e4d15d19 100644 --- a/feature/home/src/main/java/com/yapp/home/HomeContract.kt +++ b/feature/home/src/main/java/com/yapp/home/HomeContract.kt @@ -18,6 +18,7 @@ sealed class HomeContract { val isDeleteDialogVisible: Boolean = false, val isNoActivatedAlarmDialogVisible: Boolean = false, val isNoDailyFortuneDialogVisible: Boolean = false, + val isUpdateNoticeVisible: Boolean = false, val hasNewFortune: Boolean = false, val isToolTipVisible: Boolean = false, val pendingAlarmToggle: Pair? = null, @@ -58,6 +59,8 @@ sealed class HomeContract { data object ShowNoDailyFortuneDialog : Action() data object HideNoDailyFortuneDialog : Action() data object HideToolTip : Action() + data object OnClickDontShowAgain : Action() + data object HideUpdateNotice : Action() data object RollbackPendingAlarmToggle : Action() data object ConfirmDeletion : Action() data class DeleteSingleAlarm(val alarmId: Long) : Action() diff --git a/feature/home/src/main/java/com/yapp/home/HomeScreen.kt b/feature/home/src/main/java/com/yapp/home/HomeScreen.kt index f69d7683..10d74e52 100644 --- a/feature/home/src/main/java/com/yapp/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/yapp/home/HomeScreen.kt @@ -75,6 +75,7 @@ import com.yapp.domain.model.Alarm import com.yapp.home.alarm.component.AlarmListItem import com.yapp.home.alarm.component.AlarmListItemMenu import com.yapp.home.component.bottomsheet.AlarmListBottomSheet +import com.yapp.home.component.bottomsheet.UpdateNoticeBottomSheet import com.yapp.ui.component.dialog.OrbitDialog import com.yapp.ui.component.lottie.LottieAnimation import com.yapp.ui.component.snackbar.showCustomSnackBar @@ -245,6 +246,17 @@ fun HomeScreen( }, ) } + + if (state.isUpdateNoticeVisible) { + UpdateNoticeBottomSheet( + onDontShowAgain = { + processAction(HomeContract.Action.OnClickDontShowAgain) + }, + onClose = { + processAction(HomeContract.Action.HideUpdateNotice) + }, + ) + } } @Composable diff --git a/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt b/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt index 1834e476..57b74428 100644 --- a/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt @@ -1,5 +1,8 @@ package com.yapp.home +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import android.util.Log import androidx.lifecycle.ViewModel import com.yapp.common.util.ResourceProvider @@ -9,6 +12,7 @@ import com.yapp.domain.repository.UserInfoRepository import com.yapp.domain.usecase.AlarmUseCase import com.yapp.home.util.AlarmDateTimeFormatter import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import feature.home.R import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first @@ -21,8 +25,8 @@ import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.syntax.simple.repeatOnSubscription import org.orbitmvi.orbit.viewmodel.container import java.time.LocalDate -import java.time.format.DateTimeFormatter import javax.inject.Inject +import javax.inject.Named @HiltViewModel class HomeViewModel @Inject constructor( @@ -31,6 +35,8 @@ class HomeViewModel @Inject constructor( private val alarmDateTimeFormatter: AlarmDateTimeFormatter, private val fortuneRepository: FortuneRepository, private val userInfoRepository: UserInfoRepository, + @Named("appVersion") private val appVersion: String, + @ApplicationContext private val context: Context, ) : ViewModel(), ContainerHost { override val container: Container = container( @@ -41,6 +47,7 @@ class HomeViewModel @Inject constructor( loadAllAlarms() loadDailyFortuneState() loadUserName() + loadUpdateNoticeVisibility() } } } @@ -63,6 +70,8 @@ class HomeViewModel @Inject constructor( HomeContract.Action.ShowNoDailyFortuneDialog -> showNoDailyFortuneDialog() HomeContract.Action.HideNoDailyFortuneDialog -> hideNoDailyFortuneDialog() HomeContract.Action.HideToolTip -> hideToolTip() + HomeContract.Action.HideUpdateNotice -> hideUpdateNotice() + HomeContract.Action.OnClickDontShowAgain -> setUpdateNoticeDontShowVersion() HomeContract.Action.RollbackPendingAlarmToggle -> rollbackAlarmActivation() HomeContract.Action.ConfirmDeletion -> confirmDeletion() is HomeContract.Action.DeleteSingleAlarm -> deleteSingleAlarm(action.alarmId) @@ -332,8 +341,8 @@ class HomeViewModel @Inject constructor( } private fun loadDailyFortune() = intent { - val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull() - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) + val fortuneDate = fortuneRepository.fortuneDateEpochFlow.firstOrNull() + val todayDate = LocalDate.now().toEpochDay() if (fortuneDate != todayDate) { processAction(HomeContract.Action.ShowNoDailyFortuneDialog) @@ -344,10 +353,10 @@ class HomeViewModel @Inject constructor( } private fun loadDailyFortuneState() = intent { - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) + val todayDate = LocalDate.now().toEpochDay() combine( - fortuneRepository.fortuneDateFlow, + fortuneRepository.fortuneDateEpochFlow, fortuneRepository.fortuneScoreFlow, fortuneRepository.shouldShowFortuneToolTipFlow, ) { fortuneDate, fortuneScore, shouldShowTooltip -> @@ -366,6 +375,39 @@ class HomeViewModel @Inject constructor( } } + private fun loadUpdateNoticeVisibility() = intent { + if (!isOnlineNow()) { + reduce { state.copy(isUpdateNoticeVisible = false) } + return@intent + } + + val dontShowVersion = + userInfoRepository.updateNoticeDontShowVersionFlow.firstOrNull() + val lastShownDate = + userInfoRepository.updateNoticeLastShownDateEpochFlow.firstOrNull() + + val today = LocalDate.now().toEpochDay() + + val shouldShow = when { + dontShowVersion != null && dontShowVersion == appVersion -> false + lastShownDate != null && lastShownDate == today -> false + else -> true + } + + if (shouldShow) userInfoRepository.markUpdateNoticeShownToday() + + reduce { state.copy(isUpdateNoticeVisible = shouldShow) } + } + + private fun setUpdateNoticeDontShowVersion() = intent { + userInfoRepository.markUpdateNoticeDontShow(appVersion) + reduce { state.copy(isUpdateNoticeVisible = false) } + } + + private fun hideUpdateNotice() = intent { + reduce { state.copy(isUpdateNoticeVisible = false) } + } + private fun loadUserName() = intent { userInfoRepository.userNameFlow.first { userName -> reduce { state.copy(name = userName ?: "") } @@ -411,4 +453,13 @@ class HomeViewModel @Inject constructor( reduce { state.copy(sortOrder = sortOrder) } hideDropDownMenu() } + + private fun isOnlineNow(): Boolean { + val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val network = cm.activeNetwork ?: return false + val caps = cm.getNetworkCapabilities(network) ?: return false + + return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } } diff --git a/feature/home/src/main/java/com/yapp/home/component/bottomsheet/UpdateNoticeBottomSheet.kt b/feature/home/src/main/java/com/yapp/home/component/bottomsheet/UpdateNoticeBottomSheet.kt new file mode 100644 index 00000000..63ffb413 --- /dev/null +++ b/feature/home/src/main/java/com/yapp/home/component/bottomsheet/UpdateNoticeBottomSheet.kt @@ -0,0 +1,145 @@ +package com.yapp.home.component.bottomsheet + +import android.content.pm.PackageManager +import android.os.Build +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.yapp.designsystem.theme.OrbitTheme +import feature.home.R + +private fun resolveVersionName(ctx: android.content.Context): String { + return runCatching { + val pm = ctx.packageManager + val packageName = ctx.packageName + val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0)) + } else { + @Suppress("DEPRECATION") + pm.getPackageInfo(packageName, 0) + } + info.versionName ?: "" + }.getOrDefault("") +} + +private fun bannerUrl(versionName: String): String = + "https://www.orbitalarm.net/images/aos/$versionName/update-banner.png" + +@Composable +internal fun UpdateNoticeBottomSheet( + onDontShowAgain: () -> Unit, + onClose: () -> Unit, +) { + val context = LocalContext.current + val isPreview = LocalInspectionMode.current + + val versionName = remember(isPreview) { + if (isPreview) "preview" else resolveVersionName(context) + } + val imageUrl = remember(versionName) { bannerUrl(versionName.ifEmpty { "unknown" }) } + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFF17191F).copy(alpha = 0.85f)) + .clickable(onClick = onClose), + contentAlignment = Alignment.BottomCenter, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = OrbitTheme.colors.gray_900, + shape = RoundedCornerShape(topStart = 30.dp, topEnd = 30.dp), + ) + .clip(RoundedCornerShape(topStart = 30.dp, topEnd = 30.dp)), + ) { + if (isPreview) { + // 프리뷰용 박스 + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .background(color = OrbitTheme.colors.white), + ) + } else { + AsyncImage( + model = imageUrl, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f), + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 20.dp, start = 20.dp, end = 20.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .weight(1f) + .clickable(onClick = onDontShowAgain) + .padding(vertical = 14.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(id = R.string.update_notice_bottom_sheet_dont_show_again), + style = OrbitTheme.typography.body1SemiBold, + color = OrbitTheme.colors.white, + ) + } + Box( + modifier = Modifier + .weight(1f) + .clickable(onClick = onClose) + .padding(vertical = 14.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(id = R.string.update_notice_bottom_sheet_close), + style = OrbitTheme.typography.body1SemiBold, + color = OrbitTheme.colors.white, + ) + } + } + } + } +} + +@Preview +@Composable +private fun UpdateNoticeBottomSheetPreview() { + OrbitTheme { + UpdateNoticeBottomSheet( + onDontShowAgain = {}, + onClose = {}, + ) + } +} diff --git a/feature/home/src/main/res/values/strings.xml b/feature/home/src/main/res/values/strings.xml index 682d14b9..b2266b16 100644 --- a/feature/home/src/main/res/values/strings.xml +++ b/feature/home/src/main/res/values/strings.xml @@ -124,4 +124,7 @@ %1$d시간 %2$d분 후에 울려요 %d분 후에 울려요 곧 울려요 + + 다시 보지 않기 + 닫기 diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt index eeccad37..b5e1c45d 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt @@ -133,20 +133,27 @@ class MissionViewModel @Inject constructor( private fun completeMission(type: String) = intent { performHapticSuccess() logMissionSuccess(type) - if (state.missionMode == MissionMode.REAL) { - val fortuneCreateStaus = fortuneRepository.fortuneCreateStatusFlow.first() - - when (fortuneCreateStaus) { - is FortuneCreateStatus.Creating, is FortuneCreateStatus.Success -> { - postSideEffect(MissionContract.SideEffect.NavigateToFortune) - } - FortuneCreateStatus.Failure, FortuneCreateStatus.Idle -> { - postSideEffect(MissionContract.SideEffect.NavigateBack) - } - } - } else { + + if (state.missionMode != MissionMode.REAL) { postSideEffect(MissionContract.SideEffect.NavigateBack) + return@intent } + + val fortuneCreateStatus = fortuneRepository.fortuneCreateStatusFlow.first() + val hasUnseenFortune = fortuneRepository.hasUnseenFortuneFlow.first() + + val shouldOpenFortune = ( + fortuneCreateStatus is FortuneCreateStatus.Creating || + fortuneCreateStatus is FortuneCreateStatus.Success && hasUnseenFortune + ) + + postSideEffect( + if (shouldOpenFortune) { + MissionContract.SideEffect.NavigateToFortune + } else { + MissionContract.SideEffect.NavigateBack + }, + ) } private fun logMissionSuccess(type: String) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index db4817c9..2d1248b6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,7 +50,7 @@ hilt-work = "1.2.0" ## Third Party okhttp = "4.12.0" retrofit = "2.11.0" -coil = "2.4.0" +coil = "2.7.0" # Google Libraries Versions google-service = "4.4.2" diff --git a/project.dot.png b/project.dot.png index 070d91b3..77a616a5 100644 Binary files a/project.dot.png and b/project.dot.png differ