Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions common/resource/src/main/res/raw/ticket_open.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ interface LocalDataSource {
val refreshToken: Flow<String>
val accessTokenExpiresIn: Flow<Long>

val capsuleIds: Flow<Set<String>>

suspend fun setTokens(
accessToken: String,
refreshToken: String,
accessTokenExpiresIn: Long,
)

suspend fun clearTokens()

suspend fun addCapsuleId(capsuleId: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
Expand All @@ -17,6 +18,8 @@ class LocalDataSourceImpl @Inject constructor(
private val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token")

private val ACCESS_TOKEN_EXPIRES_IN_KEY = longPreferencesKey("access_token_expires_in")

private val CAPSULE_ID_KEY = stringSetPreferencesKey("capsule_id")
}

override val accessToken: Flow<String> =
Expand All @@ -34,6 +37,11 @@ class LocalDataSourceImpl @Inject constructor(
preferences[ACCESS_TOKEN_EXPIRES_IN_KEY] ?: 0L
}

override val capsuleIds: Flow<Set<String>> =
dataStore.data.map { preferences ->
preferences[CAPSULE_ID_KEY] ?: emptySet()
}

override suspend fun setTokens(
accessToken: String,
refreshToken: String,
Expand All @@ -48,7 +56,15 @@ class LocalDataSourceImpl @Inject constructor(

override suspend fun clearTokens() {
dataStore.edit { preferences ->
preferences.clear()
preferences.remove(ACCESS_TOKEN_KEY)
preferences.remove(REFRESH_TOKEN_KEY)
preferences.remove(ACCESS_TOKEN_EXPIRES_IN_KEY)
}
}

override suspend fun addCapsuleId(capsuleId: String) {
dataStore.edit { preferences ->
(preferences[CAPSULE_ID_KEY] ?: emptySet()) + capsuleId
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import kotlinx.coroutines.flow.Flow

interface LocalRepository {
val accessToken: Flow<String>
val capsuleIds: Flow<Set<Long>>

suspend fun clearTokens()
suspend fun addCapsuleId(capsuleId: Long)
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
package com.idiotfrogs.data.repository.local

import com.idiotfrogs.local.LocalDataSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject

class LocalRepositoryImpl @Inject constructor(
private val localDataSource: LocalDataSource
): LocalRepository {
override val accessToken = localDataSource.accessToken

override val capsuleIds = localDataSource.capsuleIds
.map { ids -> ids.mapNotNull { it.toLongOrNull() }.toSet() }

override suspend fun clearTokens() {
localDataSource.clearTokens()
}

override suspend fun addCapsuleId(capsuleId: Long) {
localDataSource.addCapsuleId(capsuleIds.toString())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.idiotfrogs.domain.usecase.timecapsule

import com.idiotfrogs.data.repository.local.LocalRepository
import javax.inject.Inject

class AddViewedTimeCapsuleUseCase @Inject constructor(
private val localRepository: LocalRepository
) {
suspend fun invoke(capsuleId: Long) {
localRepository.addCapsuleId(capsuleId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ class GetMyTimeCapsuleUseCase @Inject constructor(
) {
suspend operator fun invoke(): Result<Map<TimeCapsuleRole, List<MyTimeCapsuleResponse>>> =
safeCatching {
timeCapsuleRepository.getMyTimeCapsule()
.filter { it.timeCapsuleStatus != TimeCapsuleStatus.OPENED }
.groupBy { it.role }
timeCapsuleRepository.getMyTimeCapsule().groupBy { it.role }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.idiotfrogs.domain.usecase.timecapsule

import com.idiotfrogs.data.repository.timecapsule.TimeCapsuleRepository
import com.idiotfrogs.model.timecapsule.MyTimeCapsuleResponse
import com.idiotfrogs.model.timecapsule.TimeCapsuleRole
import com.idiotfrogs.model.timecapsule.TimeCapsuleStatus
import com.idiotfrogs.util.safeCatching
import javax.inject.Inject

class GetOpenedMyTimeCapsuleUseCase @Inject constructor(
private val timeCapsuleRepository: TimeCapsuleRepository
) {
suspend operator fun invoke(): Result<Map<TimeCapsuleRole, List<MyTimeCapsuleResponse>>> =
safeCatching {
timeCapsuleRepository.getMyTimeCapsule()
.filter { it.timeCapsuleStatus == TimeCapsuleStatus.OPENED }
.groupBy { it.role }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.idiotfrogs.domain.usecase.timecapsule

import com.idiotfrogs.data.repository.local.LocalRepository
import javax.inject.Inject

class GetViewedTimeCapsuleUseCase @Inject constructor(
private val localRepository: LocalRepository
) {
val capsuleIds = localRepository.capsuleIds
}
1 change: 1 addition & 0 deletions feature/home/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ dependencies {
androidTestImplementation(libs.androidx.test.espresso)

implementation(libs.kotlinx.datetime)
implementation(libs.lottie.compose)
}
80 changes: 79 additions & 1 deletion feature/home/src/main/java/com/idiotfrogs/home/HomeScreen.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.idiotfrogs.home

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
Expand Down Expand Up @@ -29,11 +32,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
Expand All @@ -42,6 +47,8 @@ import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition
import com.idiotfrogs.designsystem.component.MSDim
import com.idiotfrogs.designsystem.component.MSLoadingOverlay
import com.idiotfrogs.designsystem.component.MSMenuFab
Expand All @@ -59,6 +66,8 @@ import com.idiotfrogs.model.timecapsule.TimeCapsuleRole
import com.idiotfrogs.navigation.LocalComposeMSNavigator
import com.idiotfrogs.navigation.Routes
import com.idiotfrogs.extension.toYearMonthDay
import com.idiotfrogs.home.component.OpenAnimation
import com.idiotfrogs.home.component.OpenInteraction
import com.idiotfrogs.model.timecapsule.TimeCapsuleStatus
import com.idiotfrogs.resource.R
import dev.chrisbanes.haze.hazeSource
Expand All @@ -67,13 +76,19 @@ import kotlinx.coroutines.delay
import org.orbitmvi.orbit.compose.collectAsState
import org.orbitmvi.orbit.compose.collectSideEffect

enum class OpenStep { INTERACTION, ANIMATION, NONE }

@Composable
fun HomeRoute(
viewModel: HomeViewModel = hiltViewModel()
) {
val navigator = LocalComposeMSNavigator.current
val uiState by viewModel.collectAsState()
var showToast by remember { mutableStateOf(false) }
var currentOpenStep by remember { mutableStateOf(OpenStep.NONE) }
var clickedCapsuleId by remember { mutableLongStateOf(-1L) }

val visibleState = remember { MutableTransitionState(false) }

LaunchedEffect(showToast) {
if (!showToast) return@LaunchedEffect
Expand All @@ -87,6 +102,10 @@ fun HomeRoute(
HomeSideEffect.NavigateToProfile -> navigator.navigate(Routes.Profile)
is HomeSideEffect.NavigateToDetail -> navigator.navigate(Routes.Detail(it.id))
HomeSideEffect.ShowToast -> showToast = true
is HomeSideEffect.ShowOpenAnimation -> {
clickedCapsuleId = it.id
currentOpenStep = OpenStep.INTERACTION
}
}
}

Expand All @@ -101,6 +120,60 @@ fun HomeRoute(
}

MSLoadingOverlay(visible = uiState.data != null && uiState.isLoading)

// 로티를 미리 로드해둬야 함
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.ticket_open))
val ready = composition != null

// 만약 클릭한 캡슐이 있다면 파생해서 해당 캡슐 데이터를 얻음
val clickedCapsule by remember {
derivedStateOf {
if (clickedCapsuleId == -1L) {
null
} else {
uiState.data?.capsules?.flatMap { it.value }?.find { it.timeCapsuleId == clickedCapsuleId }
}
}
}

LaunchedEffect(currentOpenStep, ready) {
if (currentOpenStep == OpenStep.ANIMATION && ready) {
visibleState.targetState = true
}
}

when (currentOpenStep) {
OpenStep.INTERACTION -> {
OpenInteraction(
image = clickedCapsule?.mainImageUrl,
onFinish = { currentOpenStep = OpenStep.ANIMATION}
)
}
// 가드 조건(if) - 매칭된 조건에 대한 추가 검사 제공
OpenStep.ANIMATION if ready -> {
// 배경 지정 안하면 홈 화면이 비쳐보임
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
AnimatedVisibility(
visibleState = visibleState,
enter = fadeIn(tween(500))
) {
OpenAnimation(
image = clickedCapsule?.mainImageUrl,
composition = { composition!! }, // 위에서 체크해서 non-null, 람다를 통한 지연 읽기
confirmClick = {
// id가 유효하지 않은 경우 해당 페이지 예외 발생 -> 뒤로 이동
navigator.navigate(Routes.Detail(clickedCapsuleId))
}
)
}
}
}
else -> Unit
}
}
}

Expand Down Expand Up @@ -265,7 +338,12 @@ fun HomeScreen(
items(data) {
HomeTicket(
modifier = Modifier.noRippleClickable {
onAction(HomeAction.TimeCapsuleClicked(it.timeCapsuleId))
onAction(
HomeAction.TimeCapsuleClicked(
it.timeCapsuleId,
it.timeCapsuleStatus
)
)
},
buried = it.timeCapsuleStatus == TimeCapsuleStatus.BURIED,
createdAt = it.createdAt.toYearMonthDay(),
Expand Down
22 changes: 19 additions & 3 deletions feature/home/src/main/java/com/idiotfrogs/home/HomeViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package com.idiotfrogs.home
import androidx.compose.runtime.Immutable
import com.idiotfrogs.domain.usecase.auth.PutFcmTokenUseCase
import com.idiotfrogs.domain.usecase.timecapsule.GetMyTimeCapsuleUseCase
import com.idiotfrogs.domain.usecase.timecapsule.GetViewedTimeCapsuleUseCase
import com.idiotfrogs.domain.usecase.timecapsule.RequestCollaboratorUseCase
import com.idiotfrogs.domain.usecase.user.GetMyProfileUseCase
import com.idiotfrogs.model.timecapsule.MyTimeCapsuleResponse
import com.idiotfrogs.model.timecapsule.PendingCollaboratorsRequest
import com.idiotfrogs.model.timecapsule.TimeCapsuleRole
import com.idiotfrogs.model.timecapsule.TimeCapsuleStatus
import com.idiotfrogs.model.user.ProfileResponse
import com.idiotfrogs.notification.FcmTokenProvider
import com.idiotfrogs.util.base.DataUiState
Expand All @@ -16,6 +18,7 @@ import com.idiotfrogs.util.sideEffect.RefreshEvent
import com.idiotfrogs.util.sideEffect.RefreshSideEffect
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first
import org.orbitmvi.orbit.Container
import org.orbitmvi.orbit.viewmodel.container
import javax.inject.Inject
Expand All @@ -27,7 +30,8 @@ class HomeViewModel @Inject constructor(
private val requestCollaboratorUseCase: RequestCollaboratorUseCase,
private val fcmTokenProvider: FcmTokenProvider,
private val putFcmTokenUseCase: PutFcmTokenUseCase,
): BaseViewModel<HomeUiState, HomeSideEffect, HomeAction>() {
private val getViewedTimeCapsuleUseCase: GetViewedTimeCapsuleUseCase,
) : BaseViewModel<HomeUiState, HomeSideEffect, HomeAction>() {

override val container: Container<HomeUiState, HomeSideEffect> = container(
initialState = HomeUiState(),
Expand Down Expand Up @@ -95,12 +99,23 @@ class HomeViewModel @Inject constructor(
}
}

private fun checkTimeCapsule(capsuleId: Long, timeCapsuleStatus: TimeCapsuleStatus) = safeLaunch {
val capsuleIds = getViewedTimeCapsuleUseCase.capsuleIds.first()

// 만약 티켓이 오픈된 이후 처음 확인하는 거라면
if (capsuleId !in capsuleIds && timeCapsuleStatus == TimeCapsuleStatus.OPENED) {
intent { postSideEffect(HomeSideEffect.ShowOpenAnimation(capsuleId)) }
} else {
intent { postSideEffect(HomeSideEffect.NavigateToDetail(capsuleId)) }
}
}

override fun onAction(action: HomeAction) {
intent {
when (action) {
HomeAction.CreateClicked -> postSideEffect(HomeSideEffect.NavigateToCreate)
HomeAction.ProfileClicked -> postSideEffect(HomeSideEffect.NavigateToProfile)
is HomeAction.TimeCapsuleClicked -> postSideEffect(HomeSideEffect.NavigateToDetail(action.id))
is HomeAction.TimeCapsuleClicked -> checkTimeCapsule(action.id, action.timeCapsuleStatus)
is HomeAction.JoinCodeSubmitted -> requestCollaborator(PendingCollaboratorsRequest(action.code))
HomeAction.Refresh -> fetchHome()
}
Expand All @@ -124,7 +139,7 @@ data class HomeData(
sealed interface HomeAction {
data object CreateClicked : HomeAction
data object ProfileClicked : HomeAction
data class TimeCapsuleClicked(val id: Long) : HomeAction
data class TimeCapsuleClicked(val id: Long, val timeCapsuleStatus: TimeCapsuleStatus) : HomeAction
data class JoinCodeSubmitted(val code: String) : HomeAction
data object Refresh : HomeAction
}
Expand All @@ -135,4 +150,5 @@ sealed interface HomeSideEffect {
data class NavigateToDetail(val id: Long) : HomeSideEffect

data object ShowToast : HomeSideEffect
data class ShowOpenAnimation(val id: Long) : HomeSideEffect
}
Loading
Loading