diff --git a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt index 9cebb70..b38adf0 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt @@ -163,6 +163,7 @@ interface RestAPI { @Query("size") size: Int?, @Query("date") date: String?, @Query("memberId") memberId: String?, + @Query("type") type: String? = null, @Query("sort") sort: String? = "DESC", ): ApiResponse> diff --git a/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetFeedsRepository.kt b/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetFeedsRepository.kt index ae7058d..989904e 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetFeedsRepository.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetFeedsRepository.kt @@ -61,7 +61,8 @@ class GetFeedPageSource @Inject constructor( date = arguments.get("date"), memberId = arguments.get("memberId"), page = loadParams.key ?: 1, - size = loadParams.loadSize + size = loadParams.loadSize, + type = null, ) return posts.mapSuccess { Pagination( diff --git a/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetPostsRepository.kt b/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetPostsRepository.kt index f21aeb5..391a58b 100644 --- a/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetPostsRepository.kt +++ b/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetPostsRepository.kt @@ -49,7 +49,8 @@ class GetPostPagingSource @Inject constructor( date = null, memberId = arguments.get("memberId"), page = loadParams.key ?: 1, - size = loadParams.loadSize + size = loadParams.loadSize, + type = arguments.get("type"), ) } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt index 7a1fc5a..82e2e5b 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt @@ -206,10 +206,9 @@ fun SurvivalFeedTab( writerName = item.authorName, time = gapBetweenNow(time = item.createdAt), onTap = { onTapContent(item.postId) }, + isMission = false, ) } - - } } } @@ -246,6 +245,7 @@ fun MissionFeedTab( writerName = item.authorName, time = gapBetweenNow(time = item.createdAt), onTap = { onTapContent(item.postId) }, + isMission = true, ) } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt index 2b8c25f..35850c8 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageFeedElement.kt @@ -1,5 +1,6 @@ package com.no5ing.bbibbi.presentation.feature.view.main.home +import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape @@ -19,9 +21,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import com.no5ing.bbibbi.R import com.no5ing.bbibbi.presentation.component.MicroTextBubbleBox import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.presentation.theme.bbibbiTypo @@ -33,6 +37,7 @@ fun HomePageFeedElement( imageUrl: String, writerName: String, time: String, + isMission: Boolean, onTap: () -> Unit = {}, ) { Column( @@ -41,7 +46,9 @@ fun HomePageFeedElement( .clickable { onTap() }, horizontalAlignment = Alignment.CenterHorizontally ) { - Box { + Box( + contentAlignment = Alignment.TopStart, + ) { AsyncImage( model = asyncImagePainter(source = imageUrl), contentDescription = null, @@ -51,11 +58,17 @@ fun HomePageFeedElement( .aspectRatio(1.0f) .clip(RoundedCornerShape(24.dp)) ) -// MicroTextBubbleBox( -// text = postContent, -// alignment = Alignment.BottomCenter, -// modifier = Modifier.padding(bottom = 10.dp) -// ) + if (isMission) { + Box( + modifier = Modifier.padding(12.dp) + ) { + Image( + painter = painterResource(id = R.drawable.mission_diamond), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + } } Row( diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt index 009ae26..6861cc8 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageStoryBar.kt @@ -39,6 +39,8 @@ import com.no5ing.bbibbi.presentation.feature.uistate.family.MainFeedStoryElemen import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.presentation.theme.bbibbiTypo import com.no5ing.bbibbi.util.LocalSessionState +import com.no5ing.bbibbi.util.gapBetweenNow +import com.no5ing.bbibbi.util.gapUntilNext import kotlinx.coroutines.flow.StateFlow @Composable @@ -113,6 +115,7 @@ fun StoryBarIcon( isUploaded: Boolean, rank: Int, ) { + val canUpload = gapUntilNext() > 0 Column( modifier = Modifier .width(64.dp) @@ -174,7 +177,7 @@ fun StoryBarIcon( ) } } - if (member.shouldShowPickIcon && !isInDeferredPickState) { + if (member.shouldShowPickIcon && !isInDeferredPickState && canUpload) { Box( contentAlignment = Alignment.BottomEnd, modifier = Modifier diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt index 12dd6e9..2a50adc 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageUploadButton.kt @@ -12,12 +12,14 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme @@ -197,7 +199,10 @@ fun WaitingMembersPop( Box(modifier = Modifier.padding(bottom = 30.dp)) { Box( modifier = Modifier - .background(MaterialTheme.bbibbiScheme.mainYellow, shape = RoundedCornerShape(6.dp)) + .background( + MaterialTheme.bbibbiScheme.mainYellow, + shape = RoundedCornerShape(12.dp) + ) .padding( vertical = 10.dp, horizontal = 14.dp @@ -207,11 +212,20 @@ fun WaitingMembersPop( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - pickers.take(3).forEach { - MiniCircledIcon( - noImageLetter = it.displayName.first().toString(), - imageUrl = it.imageUrl, - ) + val pickersShattered = pickers.take(3) + Row { + Box { + pickersShattered.reversed().forEachIndexed { rawIdx, it -> + MiniCircledIcon( + noImageLetter = it.displayName.first().toString(), + imageUrl = it.imageUrl, + modifier = Modifier.offset( + x = 16.dp * (pickersShattered.size - 1 - rawIdx) + ) + ) + } + } + Spacer(modifier = Modifier.width(16.dp.times(pickersShattered.size - 1))) } if(pickers.size == 1) { Text( @@ -267,27 +281,28 @@ fun MiniCircledIcon( modifier: Modifier = Modifier, noImageLetter: String, imageUrl: String?, - opacity: Float = 1.0f, - backgroundColor: Color = MaterialTheme.bbibbiScheme.backgroundSecondary, onTap: () -> Unit = {}, ) { - Box { + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .size(24.dp) + .background(MaterialTheme.bbibbiScheme.mainYellow, CircleShape) + ) { + + } if (imageUrl != null) { AsyncImage( model = asyncImagePainter(source = imageUrl), contentDescription = null, contentScale = ContentScale.Crop, - modifier = modifier - .size(24.dp) + modifier = Modifier + .size(20.dp) .clip(CircleShape) - .background(backgroundColor) - .border( - width = 2.dp, - color = MaterialTheme.bbibbiScheme.mainYellow, - shape = CircleShape - ) .clickable { onTap() }, - alpha = opacity, ) } else { Box( @@ -295,25 +310,20 @@ fun MiniCircledIcon( contentAlignment = Alignment.Center, ) { Box( - modifier = modifier - .size(24.dp) + modifier = Modifier + .size(20.dp) .clip(CircleShape) .background( MaterialTheme.bbibbiScheme - .backgroundHover - .copy(alpha = opacity) - ) - .border( - width = 2.dp, - color = MaterialTheme.bbibbiScheme.mainYellow, - shape = CircleShape + .backgroundHover, + CircleShape ) ) Box(modifier = Modifier.align(Alignment.Center)) { Text( text = noImageLetter, fontSize = 8.sp, - color = MaterialTheme.bbibbiScheme.white.copy(alpha = opacity), + color = MaterialTheme.bbibbiScheme.white, fontWeight = FontWeight.SemiBold, ) } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePage.kt index 1c91518..c8277a4 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePage.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePage.kt @@ -31,6 +31,7 @@ import com.no5ing.bbibbi.presentation.component.snackBarWarning import com.no5ing.bbibbi.presentation.feature.view.common.AlbumCameraSelectDialog import com.no5ing.bbibbi.presentation.feature.view_model.members.ChangeProfileImageViewModel import com.no5ing.bbibbi.presentation.feature.view_model.members.FamilyMemberViewModel +import com.no5ing.bbibbi.presentation.feature.view_model.post.FamilyMissionPostsViewModel import com.no5ing.bbibbi.presentation.feature.view_model.post.FamilyPostsViewModel import com.no5ing.bbibbi.util.LocalSessionState import com.no5ing.bbibbi.util.LocalSnackbarHostState @@ -51,6 +52,7 @@ fun ProfilePage( familyMemberViewModel: FamilyMemberViewModel = hiltViewModel(), profileImageChangeViewModel: ChangeProfileImageViewModel = hiltViewModel(), familyPostsViewModel: FamilyPostsViewModel = hiltViewModel(), + familyMissionPostsViewModel: FamilyMissionPostsViewModel = hiltViewModel(), memberState: State> = familyMemberViewModel.uiState.collectAsState(), ) { val sessionState = LocalSessionState.current @@ -60,6 +62,7 @@ fun ProfilePage( val snackBarHost = LocalSnackbarHostState.current LaunchedEffect(Unit) { familyPostsViewModel.invoke(Arguments(arguments = mapOf("memberId" to memberId))) + familyMissionPostsViewModel.invoke(Arguments(arguments = mapOf("memberId" to memberId))) familyMemberViewModel.invoke(Arguments(resourceId = memberId)) } LaunchedEffect(changeableUriState.value) { @@ -146,6 +149,7 @@ fun ProfilePage( ProfilePageContent( onTapContent = onTapPost, postItemsState = familyPostsViewModel.uiState, + missionItemState = familyMissionPostsViewModel.uiState, ) } } @@ -171,7 +175,8 @@ fun ProfilePagePreview() { ) ProfilePageContent( onTapContent = {}, - postItemsState = MutableStateFlow(PagingData.empty()) + postItemsState = MutableStateFlow(PagingData.empty()), + missionItemState = MutableStateFlow(PagingData.empty()) ) } } diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePageContent.kt index d332ca5..a5bcd9b 100644 --- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePageContent.kt +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/profile/ProfilePageContent.kt @@ -1,5 +1,6 @@ package com.no5ing.bbibbi.presentation.feature.view.main.profile +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -17,6 +18,8 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.PullRefreshIndicator @@ -26,6 +29,10 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -39,18 +46,75 @@ import androidx.paging.compose.collectAsLazyPagingItems import coil.compose.AsyncImage import com.no5ing.bbibbi.R import com.no5ing.bbibbi.data.model.post.Post +import com.no5ing.bbibbi.data.model.post.PostType import com.no5ing.bbibbi.presentation.component.MicroTextBubbleBox +import com.no5ing.bbibbi.presentation.feature.view.common.PostTypeSwitchButton import com.no5ing.bbibbi.presentation.theme.bbibbiScheme import com.no5ing.bbibbi.presentation.theme.bbibbiTypo import com.no5ing.bbibbi.util.asyncImagePainter import com.no5ing.bbibbi.util.toLocalizedDate import kotlinx.coroutines.flow.StateFlow -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalFoundationApi::class) @Composable fun ProfilePageContent( onTapContent: (Post) -> Unit = {}, postItemsState: StateFlow>, + missionItemState: StateFlow>, + postViewTypeState: MutableState = remember { mutableStateOf(PostType.SURVIVAL) } +) { + val pagerState = rememberPagerState { 2 } + LaunchedEffect(pagerState.currentPage) { + val type = if (pagerState.currentPage == 0) PostType.SURVIVAL else PostType.MISSION + if (type != postViewTypeState.value) { + postViewTypeState.value = type + } + } + LaunchedEffect(postViewTypeState.value) { + val page = if (postViewTypeState.value == PostType.SURVIVAL) 0 else 1 + pagerState.animateScrollToPage(page) + } + Column { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + PostTypeSwitchButton( + isLocked = false, + state = postViewTypeState + ) + } + Spacer(modifier = Modifier.height(16.dp)) + HorizontalPager( + state = pagerState, + verticalAlignment = Alignment.Top, + ) { + when(it) { + 0 -> { + SurvivalProfilePageFeed( + postItemsState = postItemsState, + onTapContent = onTapContent + ) + } + 1 -> { + MissionProfilePageFeed( + postItemsState = missionItemState, + onTapContent = onTapContent + ) + } + } + + } + + } + +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun SurvivalProfilePageFeed( + postItemsState: StateFlow>, + onTapContent: (Post) -> Unit = {}, ) { val postItems = postItemsState.collectAsLazyPagingItems() val pullRefreshStyle = rememberPullRefreshState( @@ -98,6 +162,7 @@ fun ProfilePageContent( time = toLocalizedDate(time = item.createdAt), onTap = { onTapContent(item) }, postContent = item.content, + isMission = false, ) } } @@ -113,6 +178,73 @@ fun ProfilePageContent( } } +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun MissionProfilePageFeed( + postItemsState: StateFlow>, + onTapContent: (Post) -> Unit = {}, +) { + val postItems = postItemsState.collectAsLazyPagingItems() + val pullRefreshStyle = rememberPullRefreshState( + refreshing = postItems.loadState.refresh is LoadState.Loading, + onRefresh = { + postItems.refresh() + } + ) + Box(modifier = Modifier.pullRefresh(pullRefreshStyle)) { + if (postItems.itemCount == 0) { + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Image( + painter = painterResource(R.drawable.no_uploaded_image), + contentDescription = null, // 필수 param + modifier = Modifier + .size(width = 126.dp, height = 118.dp), + contentScale = ContentScale.FillWidth, + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = stringResource(id = R.string.profile_image_not_exists), + color = MaterialTheme.bbibbiScheme.textSecondary, + style = MaterialTheme.bbibbiTypo.bodyOneRegular, + ) + + } + } else { + LazyVerticalGrid( + modifier = Modifier.fillMaxSize(), + columns = GridCells.Fixed(count = 2), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(postItems.itemCount) { + val item = postItems[it] ?: throw RuntimeException() + ProfilePageContentItem( + imageUrl = item.imageUrl, + emojiCnt = item.emojiCount, + commentCnt = item.commentCount, + time = toLocalizedDate(time = item.createdAt), + onTap = { onTapContent(item) }, + postContent = item.content, + isMission = true, + ) + } + } + } + + PullRefreshIndicator( + refreshing = postItems.loadState.refresh is LoadState.Loading, + state = pullRefreshStyle, + modifier = Modifier.align(Alignment.TopCenter), + backgroundColor = MaterialTheme.bbibbiScheme.backgroundSecondary, + contentColor = MaterialTheme.bbibbiScheme.iconSelected, + ) + } +} @Composable fun ProfilePageContentItem( imageUrl: String, @@ -120,6 +252,7 @@ fun ProfilePageContentItem( emojiCnt: Int, commentCnt: Int, time: String, + isMission: Boolean, onTap: () -> Unit = {}, ) { Column( @@ -143,6 +276,17 @@ fun ProfilePageContentItem( alignment = Alignment.BottomCenter, modifier = Modifier.padding(bottom = 10.dp) ) + if (isMission) { + Box( + modifier = Modifier.padding(12.dp) + ) { + Image( + painter = painterResource(id = R.drawable.mission_diamond), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + } } Column( modifier = Modifier.padding(vertical = 8.dp, horizontal = 20.dp), diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/FamilyMissionPostsViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/FamilyMissionPostsViewModel.kt new file mode 100644 index 0000000..d94bdba --- /dev/null +++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/FamilyMissionPostsViewModel.kt @@ -0,0 +1,44 @@ +package com.no5ing.bbibbi.presentation.feature.view_model.post + +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.no5ing.bbibbi.data.model.post.Post +import com.no5ing.bbibbi.data.repository.Arguments +import com.no5ing.bbibbi.data.repository.post.GetPostsRepository +import com.no5ing.bbibbi.presentation.feature.view_model.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@HiltViewModel +class FamilyMissionPostsViewModel @Inject constructor( + private val getPostsRepository: GetPostsRepository, +) : BaseViewModel>() { + override fun initState(): PagingData { + return PagingData.empty() + } + + override fun invoke(arguments: Arguments) { + withMutexScope(Dispatchers.IO) { + getPostsRepository + .fetch(arguments.copy(arguments = arguments.arguments + mapOf("type" to "MISSION"))) + .cachedIn(viewModelScope) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = PagingData.empty() + ).collectLatest { + setState(it) + } + } + } + + override fun release() { + super.release() + getPostsRepository.closeResources() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/mission_diamond.png b/app/src/main/res/drawable/mission_diamond.png new file mode 100644 index 0000000..dc7c6e4 Binary files /dev/null and b/app/src/main/res/drawable/mission_diamond.png differ