diff --git a/core/design-system/src/main/java/com/easyhz/noffice/core/design_system/component/banner/Banner.kt b/core/design-system/src/main/java/com/easyhz/noffice/core/design_system/component/banner/Banner.kt index 60bf6255..ebcf09cd 100644 --- a/core/design-system/src/main/java/com/easyhz/noffice/core/design_system/component/banner/Banner.kt +++ b/core/design-system/src/main/java/com/easyhz/noffice/core/design_system/component/banner/Banner.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.ParagraphStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle @@ -20,6 +21,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.easyhz.noffice.core.design_system.R import com.easyhz.noffice.core.design_system.extension.screenHorizonPadding +import com.easyhz.noffice.core.design_system.extension.skeletonEffect import com.easyhz.noffice.core.design_system.theme.Green100 import com.easyhz.noffice.core.design_system.theme.Title1 import com.easyhz.noffice.core.design_system.theme.Title2 @@ -34,7 +36,8 @@ fun Banner( val bannerIntro = stringResource(id = R.string.banner_intro) val bannerOutro = stringResource(id = R.string.banner_outro) val annotatedString = remember(userName, date, bannerIntro, bannerOutro) { - buildAnnotatedString { + if (userName.isBlank()) AnnotatedString("") + else buildAnnotatedString { withStyle(style = ParagraphStyle(lineHeight = 32.sp)) { withStyle(style = Title1.toSpanStyle()) { append(userName) @@ -59,7 +62,8 @@ fun Banner( brush = Brush.verticalGradient(listOf(White, Green100)), ) ) { - Text(text = annotatedString, + Text( + text = annotatedString, modifier = Modifier .align(Alignment.BottomStart) .screenHorizonPadding() @@ -68,6 +72,21 @@ fun Banner( } } +@Composable +fun SkeletonBanner( + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .fillMaxWidth() + .heightIn(min = 108.dp) + .background( + brush = Brush.verticalGradient(listOf(White, Green100)), + ) + .skeletonEffect() + ) +} + @Preview @Composable private fun BannerPrev() { diff --git a/feature/home/src/main/java/com/easyhz/noffice/feature/home/component/notice/NoticeView.kt b/feature/home/src/main/java/com/easyhz/noffice/feature/home/component/notice/NoticeView.kt index 5350b623..572c478d 100644 --- a/feature/home/src/main/java/com/easyhz/noffice/feature/home/component/notice/NoticeView.kt +++ b/feature/home/src/main/java/com/easyhz/noffice/feature/home/component/notice/NoticeView.kt @@ -18,7 +18,9 @@ import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import com.easyhz.noffice.core.design_system.R import com.easyhz.noffice.core.design_system.component.banner.Banner +import com.easyhz.noffice.core.design_system.component.banner.SkeletonBanner import com.easyhz.noffice.core.design_system.component.card.ItemCard +import com.easyhz.noffice.core.design_system.component.skeleton.SkeletonProvider import com.easyhz.noffice.core.design_system.extension.screenHorizonPadding import com.easyhz.noffice.core.design_system.util.card.CardDetailInfo import com.easyhz.noffice.core.design_system.util.card.CardExceptionType @@ -33,6 +35,8 @@ fun NoticeView( name: String, dayOfWeek: String, organizationList: LazyPagingItems, + isLoading: Boolean, + isRefreshing: Boolean, navigateToAnnouncementDetail: (Int, Int, String) -> Unit, ) { LazyColumn( @@ -40,12 +44,15 @@ fun NoticeView( contentPadding = PaddingValues(bottom = 48.dp) ) { item { - Banner(userName = name, date = dayOfWeek) + SkeletonProvider(isLoading = isLoading, skeletonContent = { SkeletonBanner() }) { + Banner(userName = name, date = dayOfWeek) + } } items(organizationList.itemCount) { index -> organizationList[index]?.let { OrganizationSection( organization = it, + isRefreshing = isRefreshing, navigateToAnnouncementDetail = {id, title -> navigateToAnnouncementDetail(it.id, id, title) } ) } @@ -58,12 +65,19 @@ private fun OrganizationSection( modifier: Modifier = Modifier, noticeViewModel: NoticeViewModel = hiltViewModel(), organization: Organization, + isRefreshing: Boolean, navigateToAnnouncementDetail: (Int, String) -> Unit, ) { val announcementList = noticeViewModel.getAnnouncementStateByOrganization(organizationId = organization.id).collectAsLazyPagingItems() LaunchedEffect(organization.id) { noticeViewModel.fetchAnnouncementByOrganization(organization.id) } + + LaunchedEffect(key1 = isRefreshing) { + if(!isRefreshing) return@LaunchedEffect + noticeViewModel.refreshAnnouncementByOrganization(organization.id) + } + Column(modifier) { OrganizationHeader( modifier = Modifier diff --git a/feature/home/src/main/java/com/easyhz/noffice/feature/home/component/viewmodel/NoticeViewModel.kt b/feature/home/src/main/java/com/easyhz/noffice/feature/home/component/viewmodel/NoticeViewModel.kt index 536f6023..d2344fb1 100644 --- a/feature/home/src/main/java/com/easyhz/noffice/feature/home/component/viewmodel/NoticeViewModel.kt +++ b/feature/home/src/main/java/com/easyhz/noffice/feature/home/component/viewmodel/NoticeViewModel.kt @@ -40,4 +40,10 @@ class NoticeViewModel @Inject constructor( } } + fun refreshAnnouncementByOrganization(id: Int) { + if (_isDataLoaded[id] == false) return + _isDataLoaded[id] = false + fetchAnnouncementByOrganization(id) + } + } \ No newline at end of file diff --git a/feature/home/src/main/java/com/easyhz/noffice/feature/home/contract/home/HomeIntent.kt b/feature/home/src/main/java/com/easyhz/noffice/feature/home/contract/home/HomeIntent.kt index e488bdc2..f54389e5 100644 --- a/feature/home/src/main/java/com/easyhz/noffice/feature/home/contract/home/HomeIntent.kt +++ b/feature/home/src/main/java/com/easyhz/noffice/feature/home/contract/home/HomeIntent.kt @@ -8,4 +8,5 @@ sealed class HomeIntent: UiIntent() { data class ChangeTopBarMenu(val topBarMenu: HomeTopBarMenu): HomeIntent() data class ClickTopBarIconMenu(val iconMenu: TopBarIconMenu): HomeIntent() data class JoinToOrganization(val organizationId: Int): HomeIntent() + data object Refresh: HomeIntent() } \ No newline at end of file diff --git a/feature/home/src/main/java/com/easyhz/noffice/feature/home/contract/home/HomeSideEffect.kt b/feature/home/src/main/java/com/easyhz/noffice/feature/home/contract/home/HomeSideEffect.kt index 32263eb7..5908a52d 100644 --- a/feature/home/src/main/java/com/easyhz/noffice/feature/home/contract/home/HomeSideEffect.kt +++ b/feature/home/src/main/java/com/easyhz/noffice/feature/home/contract/home/HomeSideEffect.kt @@ -8,4 +8,5 @@ sealed class HomeSideEffect: UiSideEffect() { data object NavigateToMyPage: HomeSideEffect() data class NavigateToOrganizationJoin(val organizationSignUpInformation: OrganizationSignUpInformation): HomeSideEffect() data class ShowSnackBar(@StringRes val stringId: Int): HomeSideEffect() + data object Refresh: HomeSideEffect() } \ No newline at end of file diff --git a/feature/home/src/main/java/com/easyhz/noffice/feature/home/contract/home/HomeState.kt b/feature/home/src/main/java/com/easyhz/noffice/feature/home/contract/home/HomeState.kt index 1b992aed..b9291b54 100644 --- a/feature/home/src/main/java/com/easyhz/noffice/feature/home/contract/home/HomeState.kt +++ b/feature/home/src/main/java/com/easyhz/noffice/feature/home/contract/home/HomeState.kt @@ -9,7 +9,8 @@ data class HomeState( val userInfo: UserInfo, val name: String, val dayOfWeek: String, - val isLoading: Boolean + val isJoinLoading: Boolean, + val isInitLoading: Boolean, ): UiState() { companion object { fun init() = HomeState( @@ -23,7 +24,8 @@ data class HomeState( ), name = "", dayOfWeek = "", - isLoading = false + isJoinLoading = false, + isInitLoading = true ) } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/easyhz/noffice/feature/home/screen/home/HomeScreen.kt b/feature/home/src/main/java/com/easyhz/noffice/feature/home/screen/home/HomeScreen.kt index 613bd1bc..7184afe8 100644 --- a/feature/home/src/main/java/com/easyhz/noffice/feature/home/screen/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/easyhz/noffice/feature/home/screen/home/HomeScreen.kt @@ -4,15 +4,22 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.LoadState @@ -24,6 +31,7 @@ import com.easyhz.noffice.core.design_system.component.loading.LoadingScreenProv import com.easyhz.noffice.core.design_system.component.scaffold.NofficeScaffold import com.easyhz.noffice.core.design_system.component.topBar.HomeTopBar import com.easyhz.noffice.core.design_system.extension.screenHorizonPadding +import com.easyhz.noffice.core.design_system.theme.Green500 import com.easyhz.noffice.core.design_system.util.exception.ExceptionType import com.easyhz.noffice.core.model.organization.OrganizationSignUpInformation import com.easyhz.noffice.feature.home.component.notice.NoticeView @@ -33,6 +41,7 @@ import com.easyhz.noffice.feature.home.contract.home.HomeSideEffect import com.easyhz.noffice.feature.home.permission.checkNotificationPermission import com.easyhz.noffice.feature.home.util.HomeTopBarMenu +@OptIn(ExperimentalMaterialApi::class) @Composable fun HomeScreen( modifier: Modifier = Modifier, @@ -46,6 +55,13 @@ fun HomeScreen( val organizationList = viewModel.organizationState.collectAsLazyPagingItems() val organizationIdToJoin = remember { DeepLinkManager.organizationIdToJoin } val context = LocalContext.current + val isRefreshing = remember(organizationList.loadState.refresh) { + organizationList.loadState.refresh == LoadState.Loading + } + val pullRefreshState = rememberPullRefreshState( + refreshing = !uiState.isInitLoading && isRefreshing, + onRefresh = { viewModel.postIntent(HomeIntent.Refresh) } + ) val requestPermissionLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> if (isGranted) { @@ -61,12 +77,11 @@ fun HomeScreen( launcher = requestPermissionLauncher ) { } } - LaunchedEffect(key1 = organizationIdToJoin) { viewModel.postIntent(HomeIntent.JoinToOrganization(organizationIdToJoin)) } LoadingScreenProvider( - isLoading = uiState.isLoading + isLoading = uiState.isJoinLoading ) { NofficeScaffold( modifier = modifier, @@ -81,34 +96,46 @@ fun HomeScreen( } } ) { paddingValues -> - if(organizationList.itemCount == 0 && organizationList.loadState.refresh != LoadState.Loading) { - ExceptionView( - modifier = Modifier.fillMaxSize(), - type = ExceptionType.NO_ORGANIZATION - ) - } - Crossfade( - targetState = uiState.topBarMenu, - animationSpec = tween(500), - label = "TopBarMenu" - ) { screen -> - when (screen) { - HomeTopBarMenu.NOTICE -> { - NoticeView( - modifier = Modifier.padding(top = paddingValues.calculateTopPadding()), - dayOfWeek = uiState.dayOfWeek, - name = uiState.name, - organizationList = organizationList, - navigateToAnnouncementDetail = navigateToAnnouncementDetail - ) - } + Box(modifier = Modifier + .pullRefresh(pullRefreshState) + .padding(top = paddingValues.calculateTopPadding())) { + if(organizationList.itemCount == 0 && !isRefreshing) { + ExceptionView( + modifier = Modifier.fillMaxSize(), + type = ExceptionType.NO_ORGANIZATION + ) + } + Crossfade( + targetState = uiState.topBarMenu, + animationSpec = tween(500), + label = "TopBarMenu" + ) { screen -> + when (screen) { + HomeTopBarMenu.NOTICE -> { + NoticeView( + dayOfWeek = uiState.dayOfWeek, + name = uiState.name, + organizationList = organizationList, + isLoading = uiState.isInitLoading, + isRefreshing = isRefreshing, + navigateToAnnouncementDetail = navigateToAnnouncementDetail + ) + } - HomeTopBarMenu.TASK -> { - TaskView(modifier = Modifier - .padding(top = paddingValues.calculateTopPadding()) - .screenHorizonPadding()) + HomeTopBarMenu.TASK -> { + TaskView(modifier = Modifier + .screenHorizonPadding()) + } } } + PullRefreshIndicator( + refreshing = isRefreshing, + contentColor = Green500, + state = pullRefreshState, + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 20.dp) + ) } } } @@ -125,6 +152,9 @@ fun HomeScreen( withDismissAction = true ) } + is HomeSideEffect.Refresh -> { + organizationList.refresh() + } } } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/easyhz/noffice/feature/home/screen/home/HomeViewModel.kt b/feature/home/src/main/java/com/easyhz/noffice/feature/home/screen/home/HomeViewModel.kt index 6020a2bb..ff968699 100644 --- a/feature/home/src/main/java/com/easyhz/noffice/feature/home/screen/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/easyhz/noffice/feature/home/screen/home/HomeViewModel.kt @@ -9,6 +9,7 @@ import com.easyhz.noffice.core.common.error.HttpError import com.easyhz.noffice.core.common.error.handleError import com.easyhz.noffice.core.common.manager.DeepLinkManager import com.easyhz.noffice.core.common.util.DateFormat +import com.easyhz.noffice.core.common.util.errorLogging import com.easyhz.noffice.core.design_system.util.topBar.TopBarIconMenu import com.easyhz.noffice.core.model.organization.Organization import com.easyhz.noffice.domain.home.usecase.member.FetchUserInfoUseCase @@ -45,6 +46,7 @@ class HomeViewModel @Inject constructor( is HomeIntent.ChangeTopBarMenu -> { onChangeTopBarMenu(intent.topBarMenu) } is HomeIntent.ClickTopBarIconMenu -> { onClickTopBarIconMenu(intent.iconMenu) } is HomeIntent.JoinToOrganization -> { joinToOrganization(intent.organizationId) } + is HomeIntent.Refresh -> { refresh() } } } @@ -58,7 +60,9 @@ class HomeViewModel @Inject constructor( fetchUserInfoUseCase.invoke(Unit).onSuccess { reduce { copy(userInfo = it, name = it.alias) } }.onFailure { - // TODO FAIL 처리 + errorLogging(this.javaClass.name, "fetchUserInfo", it) + }.also { + reduce { copy(isInitLoading = false)} } } @@ -95,7 +99,7 @@ class HomeViewModel @Inject constructor( /* 조직 가입 */ private fun joinToOrganization(id: Int) = viewModelScope.launch { if (id == -1) return@launch - reduce { copy(isLoading = true) } + reduce { copy(isJoinLoading = true) } fetchOrganizationSignUpInfoUseCase.invoke(id).onSuccess { delay(500) postSideEffect { HomeSideEffect.NavigateToOrganizationJoin(it) } @@ -107,10 +111,14 @@ class HomeViewModel @Inject constructor( showSnackBar(messageResId) }.also { DeepLinkManager.setOrganizationIdToJoin(-1) - reduce { copy(isLoading = false) } + reduce { copy(isJoinLoading = false) } } } + private fun refresh() { + postSideEffect { HomeSideEffect.Refresh } + } + private fun showSnackBar(@StringRes stringId: Int) { postSideEffect { HomeSideEffect.ShowSnackBar(stringId)