diff --git a/presentation/src/main/java/daily/dayo/presentation/adapter/BlockListAdapter.kt b/presentation/src/main/java/daily/dayo/presentation/adapter/BlockListAdapter.kt deleted file mode 100644 index f7abe000..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/adapter/BlockListAdapter.kt +++ /dev/null @@ -1,79 +0,0 @@ -package daily.dayo.presentation.adapter - -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.CheckBox -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.RequestManager -import daily.dayo.presentation.common.GlideLoadUtil.loadImageView -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.ItemBlockBinding -import daily.dayo.domain.model.UserBlocked - -class BlockListAdapter(private val requestManager: RequestManager) : - RecyclerView.Adapter() { - - companion object { - private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: UserBlocked, newItem: UserBlocked) = - oldItem.memberId == newItem.memberId - - override fun areContentsTheSame(oldItem: UserBlocked, newItem: UserBlocked): Boolean = - oldItem == newItem - } - } - - private val differ = AsyncListDiffer(this, diffCallback) - fun submitList(list: List) = differ.submitList(list) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BlockListViewHolder { - return BlockListViewHolder( - ItemBlockBinding.inflate(LayoutInflater.from(parent.context), parent, false) - ) - } - - override fun onBindViewHolder(holder: BlockListViewHolder, position: Int) { - val item = differ.currentList[position] - holder.bind(item) - } - - override fun getItemCount(): Int { - return differ.currentList.size - } - - interface OnItemClickListener { - fun onItemClick(checkbox: CheckBox, blockUser: UserBlocked, position: Int) - } - - private var listener: OnItemClickListener? = null - fun setOnItemClickListener(listener: OnItemClickListener) { - this.listener = listener - } - - inner class BlockListViewHolder(private val binding: ItemBlockBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun bind(blockUser: UserBlocked) { - binding.blockUser = blockUser.nickname - loadImageView( - requestManager = requestManager, - width = binding.imgBlockUserProfile.width, - height = binding.imgBlockUserProfile.height, - imgName = blockUser.profileImg ?: "", - imgView = binding.imgBlockUserProfile - ) - setUnblockButtonClickListener(blockUser) - } - - private fun setUnblockButtonClickListener(blockUser: UserBlocked) { - val pos = adapterPosition - if (pos != RecyclerView.NO_POSITION) { - binding.btnBlockUserCancel.setOnDebounceClickListener { - listener?.onItemClick(binding.btnBlockUserCancel, blockUser, pos) - } - } - } - } -} diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileOptionFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileOptionFragment.kt index 255ae38b..489d7e64 100644 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileOptionFragment.kt +++ b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileOptionFragment.kt @@ -122,17 +122,6 @@ class ProfileOptionFragment : DialogFragment() { } private fun blockUser() { - profileViewModel.requestBlockMember(args.memberId) - profileViewModel.blockSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - Toast.makeText( - requireContext(), - R.string.other_profile_block_success_message, - Toast.LENGTH_SHORT - ).show() - findNavController().navigateUp() - } - } } private fun setOptionReportUserClickListener() { diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/SettingFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/setting/SettingFragment.kt index 66334eb0..09466f99 100644 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/SettingFragment.kt +++ b/presentation/src/main/java/daily/dayo/presentation/fragment/setting/SettingFragment.kt @@ -73,10 +73,7 @@ class SettingFragment : Fragment() { private fun setBlockButtonClickListener() { binding.layoutSettingBlock.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.SettingFragment, - action = R.id.action_settingFragment_to_settingBlockFragment - ) + } } diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/block/SettingBlockFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/setting/block/SettingBlockFragment.kt deleted file mode 100644 index 2b762a30..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/block/SettingBlockFragment.kt +++ /dev/null @@ -1,98 +0,0 @@ -package daily.dayo.presentation.fragment.setting.block - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.CheckBox -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import daily.dayo.presentation.common.Status -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentSettingBlockBinding -import daily.dayo.presentation.adapter.BlockListAdapter -import daily.dayo.presentation.viewmodel.ProfileSettingViewModel -import daily.dayo.presentation.viewmodel.ProfileViewModel -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.domain.model.UserBlocked - -@AndroidEntryPoint -class SettingBlockFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val profileViewModel by viewModels() - private val profileSettingViewModel by viewModels() - private var blockListAdapter: BlockListAdapter? = null - private var glideRequestManager: RequestManager? = null - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSettingBlockBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setBackButtonClickListener() - setBlockListAdapter() - } - - override fun onResume() { - super.onResume() - setBlockList() - } - - private fun onDestroyBindingView() { - glideRequestManager = null - blockListAdapter = null - binding.rvBlock.adapter = null - } - - private fun setBlockListAdapter() { - blockListAdapter = glideRequestManager?.let { requestManager -> - BlockListAdapter(requestManager = requestManager) - } - binding.rvBlock.adapter = blockListAdapter - blockListAdapter?.setOnItemClickListener(object : - BlockListAdapter.OnItemClickListener { - override fun onItemClick(checkbox: CheckBox, blockUser: UserBlocked, position: Int) { - unblockUser(blockUser.memberId, position) - } - }) - } - - private fun setBlockList() { - profileSettingViewModel.requestBlockList() - profileSettingViewModel.blockList.observe(viewLifecycleOwner) { - when (it.status) { - Status.SUCCESS -> it.data?.let { blockList -> - blockListAdapter?.submitList(blockList) - } - else -> {} - } - } - } - - private fun unblockUser(memberId: String, position: Int) { - profileViewModel.requestUnblockMember(memberId = memberId) - profileViewModel.unblockSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - profileSettingViewModel.requestBlockList() - } - } - } - - private fun setBackButtonClickListener() { - binding.btnSettingBlockBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/main/MainNavigator.kt b/presentation/src/main/java/daily/dayo/presentation/screen/main/MainNavigator.kt index 10d5a9ba..35119b39 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/main/MainNavigator.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/main/MainNavigator.kt @@ -9,6 +9,7 @@ import androidx.navigation.compose.rememberNavController import daily.dayo.presentation.screen.home.HomeRoute import daily.dayo.presentation.screen.home.navigateHome import daily.dayo.presentation.screen.mypage.navigateBackToFolder +import daily.dayo.presentation.screen.mypage.navigateBlockedUsers import daily.dayo.presentation.screen.mypage.navigateBookmark import daily.dayo.presentation.screen.mypage.navigateFolder import daily.dayo.presentation.screen.mypage.navigateFolderCreate @@ -84,6 +85,10 @@ class MainNavigator( navController.navigateProfileEdit() } + fun navigateBlockedUsers() { + navController.navigateBlockedUsers() + } + fun navigateBookmark() { navController.navigateBookmark() } diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/main/MainScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/main/MainScreen.kt index 50bd7ab7..c52917f6 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/main/MainScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/main/MainScreen.kt @@ -143,6 +143,7 @@ internal fun MainScreen( navigateBackToFolder = { folderId -> navigator.navigateBackToFolder(folderId) } ) profileNavGraph( + snackBarHostState = snackBarHostState, onFollowMenuClick = { memberId, tabNum -> navigator.navigateFollowMenu(memberId, tabNum) }, onFolderClick = { folderId -> navigator.navigateFolder(folderId) }, onPostClick = { postId -> navigator.navigatePost(postId) }, @@ -157,6 +158,7 @@ internal fun MainScreen( coroutineScope = coroutineScope, snackBarHostState = snackBarHostState, onProfileEditClick = { navigator.navigateProfileEdit() }, + onBlockUsersClick = { navigator.navigateBlockedUsers() }, onPasswordChangeClick = { navigator.navigateChangePassword() }, onSettingNotificationClick = { navigator.navigateSettingsNotification() }, onBackClick = { navigator.popBackStack() } diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MypageNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MypageNavigation.kt index 6c893bc4..083f92ef 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MypageNavigation.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MypageNavigation.kt @@ -12,6 +12,8 @@ import daily.dayo.presentation.screen.folder.FolderCreateScreen import daily.dayo.presentation.screen.folder.FolderEditScreen import daily.dayo.presentation.screen.folder.FolderPostMoveScreen import daily.dayo.presentation.screen.folder.FolderScreen +import daily.dayo.presentation.screen.settings.BlockedUsersScreen +import kotlinx.coroutines.CoroutineScope fun NavController.navigateMyPage() { navigate(MyPageRoute.route) { @@ -23,6 +25,10 @@ fun NavController.navigateProfileEdit() { navigate(MyPageRoute.profileEdit()) } +fun NavController.navigateBlockedUsers() { + navigate(MyPageRoute.blockedUsers()) +} + fun NavController.navigateBookmark() { navigate(MyPageRoute.bookmark()) } @@ -109,6 +115,12 @@ fun NavGraphBuilder.myPageNavGraph( ) } + composable(MyPageRoute.blockedUsers()) { + BlockedUsersScreen( + onBackClick = onBackClick, + ) + } + composable(MyPageRoute.bookmark()) { BookmarkScreen( onBackClick = onBackClick @@ -190,6 +202,9 @@ object MyPageRoute { // profile edit fun profileEdit() = "$route/edit" + // blocked users + fun blockedUsers() = "$route/blockedUsers" + // follow fun follow(memberId: String, tabNum: String) = "$route/follow/$memberId/$tabNum" diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileNavigation.kt index b3dfa2e9..60b237c5 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileNavigation.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileNavigation.kt @@ -1,5 +1,6 @@ package daily.dayo.presentation.screen.profile +import androidx.compose.material3.SnackbarHostState import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType @@ -11,6 +12,7 @@ fun NavController.navigateProfile(memberId: String) { } fun NavGraphBuilder.profileNavGraph( + snackBarHostState: SnackbarHostState, onFollowMenuClick: (String, Int) -> Unit, onFolderClick: (Long) -> Unit, onPostClick: (Long) -> Unit, @@ -26,6 +28,7 @@ fun NavGraphBuilder.profileNavGraph( ) { navBackStackEntry -> val memberId = navBackStackEntry.arguments?.getString("memberId") ?: "" ProfileScreen( + externalSnackBarHostState = snackBarHostState, memberId = memberId, onFollowMenuClick = onFollowMenuClick, onFolderClick = onFolderClick, diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileScreen.kt index 5eeab49c..e72175f6 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileScreen.kt @@ -1,5 +1,6 @@ package daily.dayo.presentation.screen.profile +import android.content.Context import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource @@ -43,11 +44,13 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import daily.dayo.domain.model.Folder import daily.dayo.domain.model.Profile import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R +import daily.dayo.presentation.common.Status import daily.dayo.presentation.common.extension.clickableSingle import daily.dayo.presentation.theme.Dark import daily.dayo.presentation.theme.DayoTheme @@ -60,14 +63,17 @@ import daily.dayo.presentation.view.FolderView import daily.dayo.presentation.view.ProfileDropdownMenu import daily.dayo.presentation.view.RoundImageView import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.dialog.ConfirmDialog import daily.dayo.presentation.view.dialog.UserReportDialog import daily.dayo.presentation.viewmodel.FolderViewModel import daily.dayo.presentation.viewmodel.ProfileViewModel import daily.dayo.presentation.viewmodel.ReportViewModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @Composable fun ProfileScreen( + externalSnackBarHostState: SnackbarHostState, memberId: String, onFollowMenuClick: (String, Int) -> Unit, onFolderClick: (Long) -> Unit, @@ -76,6 +82,9 @@ fun ProfileScreen( folderViewModel: FolderViewModel = hiltViewModel(), reportViewModel: ReportViewModel = hiltViewModel() ) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val snackBarHostState = remember { SnackbarHostState() } val lifecycleOwner = LocalLifecycleOwner.current val profileInfo = profileViewModel.profileInfo.observeAsState() val folderList = folderViewModel.folderList.observeAsState() @@ -85,6 +94,24 @@ fun ProfileScreen( val onClickUserReport: (String) -> Unit = { reason -> profileInfo.value?.data?.memberId?.let { reportViewModel.requestSaveMemberReport(reason, it) } } + val onClickUserBlockSuccess by profileViewModel.blockSuccess.collectAsStateWithLifecycle() + val onClickUserBlock: (String) -> Unit = { memberId -> + profileInfo.value?.data?.memberId?.let { profileViewModel.requestBlockMember(it) } + } + + LaunchedEffect(onClickUserBlockSuccess) { + when (onClickUserBlockSuccess) { + Status.SUCCESS -> { + coroutineScope.launch { + externalSnackBarHostState.showSnackbar(context.getString(R.string.other_profile_block_success_message)) + } + onBackClick() + } + Status.ERROR -> { + } + else -> {} + } + } LaunchedEffect(Unit) { profileViewModel.requestOtherProfile(memberId) @@ -112,36 +139,43 @@ fun ProfileScreen( } ProfileScreen( + context = context, + coroutineScope = coroutineScope, + snackBarHostState = snackBarHostState, profile = profileInfo.value?.data ?: DEFAULT_PROFILE, folderList = folderList.value?.data ?: emptyList(), onFollowClick = onFollowClick, onFollowMenuClick = onFollowMenuClick, onFolderClick = onFolderClick, onClickUserReport = onClickUserReport, + onClickUserBlock = onClickUserBlock, onBackClick = onBackClick ) } @Composable private fun ProfileScreen( + context: Context, + coroutineScope: CoroutineScope, + snackBarHostState: SnackbarHostState, profile: Profile, folderList: List, onFollowClick: () -> Unit, onFollowMenuClick: (String, Int) -> Unit, onFolderClick: (Long) -> Unit, onClickUserReport: (String) -> Unit, - onBackClick: () -> Unit + onClickUserBlock: (String) -> Unit, + onBackClick: () -> Unit, ) { - val context = LocalContext.current - val coroutineScope = rememberCoroutineScope() - val snackBarHostState = remember { SnackbarHostState() } - var showDialog by remember { mutableStateOf(false) } + var showReportDialog by remember { mutableStateOf(false) } + var showBlockDialog by remember { mutableStateOf(false) } Scaffold( topBar = { ProfileTopNavigation( - onUserReportClick = { showDialog = true }, - onBackClick = onBackClick + onUserReportClick = { showReportDialog = true }, + onUserBlockClick = { showBlockDialog = true }, + onBackClick = onBackClick, ) }, snackbarHost = { SnackbarHost(snackBarHostState) }, @@ -174,18 +208,30 @@ private fun ProfileScreen( } ) - if (showDialog) { + if (showReportDialog) { UserReportDialog( - onClickCancel = { showDialog = !showDialog }, + onClickCancel = { showReportDialog = false }, onClickConfirm = { reason -> onClickUserReport(reason) - showDialog = !showDialog + showReportDialog = false coroutineScope.launch { snackBarHostState.showSnackbar(context.getString(R.string.report_user_alert_message)) } } ) } + + if (showBlockDialog) { + ConfirmDialog( + title = stringResource(id = R.string.other_profile_block_message), + description = stringResource(id = R.string.other_profile_block_explanation_message), + onClickCancel = { showBlockDialog = false }, + onClickConfirm = { + onClickUserBlock(profile.memberId ?: "") + showBlockDialog = false + }, + ) + } } @Composable @@ -353,7 +399,8 @@ private fun UserDiary(folder: Folder, onFolderClick: (Long) -> Unit) { @Composable private fun ProfileTopNavigation( onUserReportClick: () -> Unit, - onBackClick: () -> Unit + onUserBlockClick: () -> Unit, + onBackClick: () -> Unit, ) { var showProfileOption by remember { mutableStateOf(false) } @@ -387,6 +434,10 @@ private fun ProfileTopNavigation( onUserReportClick = { onUserReportClick() showProfileOption = !showProfileOption + }, + onUserBlockClick = { + onUserBlockClick() + showProfileOption = !showProfileOption } ) } @@ -397,13 +448,17 @@ private fun ProfileTopNavigation( @Composable private fun PreviewProfileScreen() { ProfileScreen( + context = LocalContext.current, + coroutineScope = rememberCoroutineScope(), + snackBarHostState = remember { SnackbarHostState() }, profile = DEFAULT_PROFILE, folderList = emptyList(), onFollowClick = { }, onFollowMenuClick = { _, _ -> }, onFolderClick = { }, onClickUserReport = { }, - onBackClick = { } + onClickUserBlock = { }, + onBackClick = { }, ) } diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt new file mode 100644 index 00000000..b4861f2e --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt @@ -0,0 +1,265 @@ +package daily.dayo.presentation.screen.settings + +import android.content.Context +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.size.Size +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.view.DayoOutlinedButton +import daily.dayo.presentation.view.FilledRoundedCornerButton +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.RoundImageView +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.viewmodel.ProfileSettingViewModel +import daily.dayo.presentation.viewmodel.ProfileViewModel +import kotlinx.coroutines.launch + +@Composable +fun BlockedUsersScreen( + onBackClick: () -> Unit, + profileViewModel: ProfileViewModel = hiltViewModel(), + profileSettingViewModel: ProfileSettingViewModel = hiltViewModel(), +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val snackBarHostState = remember { SnackbarHostState() } + val blockedUsers by profileSettingViewModel.blockList.collectAsStateWithLifecycle() + val unblockSuccess by profileViewModel.unblockSuccess.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + profileSettingViewModel.requestBlockList() + } + + LaunchedEffect(unblockSuccess) { + unblockSuccess?.let { state -> + when (state) { + Status.SUCCESS -> { + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.other_profile_unblock_success_message)) + } + profileSettingViewModel.requestBlockList() + } + + Status.ERROR -> { + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.other_profile_unblock_fail_message)) + } + } + + Status.LOADING -> { + + } + } + } + } + + Scaffold( + topBar = { BlockedUsersActionbarLayout(onBackClick = onBackClick) }, + snackbarHost = { SnackbarHost(snackBarHostState) }, + content = { innerPadding -> + Column( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .padding(innerPadding) + .fillMaxSize() + .padding(top = 12.dp, start = 20.dp, end = 20.dp) + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(vertical = 16.dp) + ) { + if (blockedUsers.status != Status.ERROR) { + blockedUsers.data.orEmpty().let { blockedUsers -> + if (blockedUsers.isEmpty()) { + item { + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = 164.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_blocked_users_empty), + contentDescription = null, + modifier = Modifier + .width(136.dp) + .wrapContentHeight() + .padding(6.5.dp) + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = stringResource(R.string.blocked_users_empty_description), + color = Gray3_9FA5AE, + style = DayoTheme.typography.b3, + modifier = Modifier + .wrapContentSize() + ) + } + } + } else { + itemsIndexed( + blockedUsers, + key = { _, user -> user.memberId } + ) { _, user -> + // Nickname이 null인 경우는 없을 것 같지만, null일 경우 보이지 않도록 처리 + user.nickname?.let { nickname -> + BlockedUser( + userId = user.memberId, + imageFileName = user.profileImg, + nickName = nickname, + onUnblockClick = { userId -> + coroutineScope.launch { + profileViewModel.requestUnblockMember(userId) + } + }, + context = context, + ) + } + } + } + } + } else { + item { + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = 164.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_blocked_users_empty), + contentDescription = null, + modifier = Modifier + .width(136.dp) + .wrapContentHeight() + .padding(6.5.dp) + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = stringResource(R.string.blocked_users_error_description), + color = Gray3_9FA5AE, + style = DayoTheme.typography.b3, + modifier = Modifier + .wrapContentSize() + ) + Spacer(modifier = Modifier.height(20.dp)) + FilledRoundedCornerButton( + modifier = Modifier + .padding(horizontal = 20.dp) + .wrapContentSize(), + onClick = { profileSettingViewModel.requestBlockList() }, + label = stringResource(R.string.re_try) + ) + } + } + } + } + } + } + ) +} + +@Preview +@Composable +fun BlockedUser( + userId: String = "", + imageFileName: String = "", + nickName: String = "", + onUnblockClick: (String) -> Unit = {}, + context: Context = LocalContext.current, +) { + Row( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxWidth() + .height(38.dp) + .padding(bottom = 2.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + RoundImageView( + imageUrl = "${BuildConfig.BASE_URL}/images/${imageFileName}", + context = context, + modifier = Modifier + .size(36.dp) + .aspectRatio(1f), + imageDescription = "User Profile Image", + imageSize = Size(36, 36), + roundSize = 18.dp, + placeholderResId = R.drawable.ic_profile_default_user_profile, + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = nickName, + style = DayoTheme.typography.b6.copy(color = Dark), + maxLines = 1, + modifier = Modifier.align(Alignment.CenterVertically) + ) + Spacer(modifier = Modifier.weight(1f)) + DayoOutlinedButton( + label = stringResource(R.string.blocked_users_unblock), + onClick = { onUnblockClick(userId) }, + modifier = Modifier.height(36.dp), + ) + } +} + + +@Preview +@Composable +fun BlockedUsersActionbarLayout( + onBackClick: () -> Unit = {}, +) { + TopNavigation( + leftIcon = { + NoRippleIconButton( + onClick = { onBackClick() }, + iconContentDescription = stringResource(R.string.back_sign), + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + }, + title = stringResource(R.string.blocked_users_title), + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsNavigation.kt index 1d59eb3b..204ebd3d 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsNavigation.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsNavigation.kt @@ -22,6 +22,7 @@ fun NavGraphBuilder.settingsNavGraph( coroutineScope: CoroutineScope, snackBarHostState: SnackbarHostState, onProfileEditClick: () -> Unit, + onBlockUsersClick: () -> Unit, onBackClick: () -> Unit, onSettingNotificationClick: () -> Unit, onPasswordChangeClick: () -> Unit, @@ -31,6 +32,7 @@ fun NavGraphBuilder.settingsNavGraph( onProfileEditClick = onProfileEditClick, onBackClick = onBackClick, onPasswordChangeClick = onPasswordChangeClick, + onBlockUsersClick = onBlockUsersClick, onSettingNotificationClick = onSettingNotificationClick, ) } diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsScreen.kt index 124a423e..a8da46d2 100644 --- a/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsScreen.kt +++ b/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsScreen.kt @@ -58,6 +58,7 @@ fun SettingsScreen( onProfileEditClick: () -> Unit, onBackClick: () -> Unit, onPasswordChangeClick: () -> Unit, + onBlockUsersClick: () -> Unit, onSettingNotificationClick: () -> Unit, profileViewModel: ProfileViewModel = hiltViewModel() ) { @@ -73,6 +74,7 @@ fun SettingsScreen( onBackClick = onBackClick, onSettingNotificationClick = onSettingNotificationClick, onPasswordChangeClick = onPasswordChangeClick, + onBlockUsersClick = onBlockUsersClick ) } @@ -83,6 +85,7 @@ private fun SettingsScreen( onBackClick: () -> Unit, onSettingNotificationClick: () -> Unit, onPasswordChangeClick: () -> Unit, + onBlockUsersClick: () -> Unit, ) { Scaffold( topBar = { @@ -114,7 +117,7 @@ private fun SettingsScreen( val settingMenus = listOf( SettingItem(R.string.setting_menu_change_password, R.drawable.ic_setting_password_change, onClickMenu = onPasswordChangeClick), - SettingItem(R.string.setting_menu_block_user, R.drawable.ic_block, onClickMenu = {}), + SettingItem(R.string.setting_menu_block_user, R.drawable.ic_block, onClickMenu = onBlockUsersClick), SettingItem(R.string.setting_menu_notification, R.drawable.ic_notification, onClickMenu = onSettingNotificationClick), null, // Divider SettingItem(R.string.setting_menu_notice, R.drawable.ic_setting_notice, onClickMenu = {}), @@ -270,6 +273,7 @@ private fun PreviewSettingsScreen() { onBackClick = {}, onProfileEditClick = {}, onPasswordChangeClick = {}, + onBlockUsersClick = {}, onSettingNotificationClick = {} ) } diff --git a/presentation/src/main/java/daily/dayo/presentation/view/DropdownMenu.kt b/presentation/src/main/java/daily/dayo/presentation/view/DropdownMenu.kt index 87fa20e0..47a0fba1 100644 --- a/presentation/src/main/java/daily/dayo/presentation/view/DropdownMenu.kt +++ b/presentation/src/main/java/daily/dayo/presentation/view/DropdownMenu.kt @@ -22,7 +22,13 @@ import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.theme.Red_FF4545 @Composable -fun MyPostDropdownMenu(postId: Long, expanded: Boolean, onDismissRequest: () -> Unit, onPostModifyClick: (Long) -> Unit, onPostDeleteClick: (Long) -> Unit) { +fun MyPostDropdownMenu( + postId: Long, + expanded: Boolean, + onDismissRequest: () -> Unit, + onPostModifyClick: (Long) -> Unit, + onPostDeleteClick: (Long) -> Unit +) { DayoTheme(shapes = DayoTheme.shapes.copy(extraSmall = RoundedCornerShape(16.dp))) { DropdownMenu( expanded = expanded, @@ -87,7 +93,11 @@ fun MyPostDropdownMenu(postId: Long, expanded: Boolean, onDismissRequest: () -> } @Composable -fun OthersPostDropdownMenu(expanded: Boolean, onDismissRequest: () -> Unit, onPostReportClick: () -> Unit) { +fun OthersPostDropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + onPostReportClick: () -> Unit +) { DayoTheme(shapes = DayoTheme.shapes.copy(extraSmall = RoundedCornerShape(16.dp))) { DropdownMenu( expanded = expanded, @@ -122,7 +132,12 @@ fun OthersPostDropdownMenu(expanded: Boolean, onDismissRequest: () -> Unit, onPo } @Composable -fun ProfileDropdownMenu(expanded: Boolean, onDismissRequest: () -> Unit, onUserReportClick: () -> Unit) { +fun ProfileDropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + onUserReportClick: () -> Unit, + onUserBlockClick: () -> Unit, +) { DayoTheme(shapes = DayoTheme.shapes.copy(extraSmall = RoundedCornerShape(16.dp))) { DropdownMenu( expanded = expanded, @@ -132,7 +147,7 @@ fun ProfileDropdownMenu(expanded: Boolean, onDismissRequest: () -> Unit, onUserR DropdownMenuItem( modifier = Modifier .padding(horizontal = 6.dp) - .clip(RoundedCornerShape(12.dp)), + .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)), text = { Row( modifier = Modifier.width(128.dp), @@ -152,6 +167,29 @@ fun ProfileDropdownMenu(expanded: Boolean, onDismissRequest: () -> Unit, onUserR }, onClick = onUserReportClick ) + DropdownMenuItem( + modifier = Modifier + .padding(horizontal = 6.dp) + .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)), + text = { + Row( + modifier = Modifier.width(128.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_menu_block), + contentDescription = stringResource(R.string.other_profile_option_block_user), + tint = Dark, + modifier = Modifier.padding(end = 8.dp) + ) + Text( + text = stringResource(R.string.other_profile_option_block_user), + style = DayoTheme.typography.b6.copy(Dark) + ) + } + }, + onClick = onUserBlockClick + ) } } } diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileSettingViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileSettingViewModel.kt index 5b3368f1..40c50f95 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileSettingViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileSettingViewModel.kt @@ -43,8 +43,9 @@ class ProfileSettingViewModel @Inject constructor( private val _isUpdateSuccess = MutableStateFlow(null) val isUpdateSuccess: StateFlow get() = _isUpdateSuccess - private val _blockList = MutableLiveData>>() - val blockList: LiveData>> get() = _blockList + private val _blockList = + MutableStateFlow>>(Resource.loading(emptyList())) + val blockList: StateFlow>> get() = _blockList private val _isNicknameDuplicate = MutableSharedFlow() val isNicknameDuplicate = _isNicknameDuplicate.asSharedFlow() @@ -80,7 +81,7 @@ class ProfileSettingViewModel @Inject constructor( fun requestUpdateMyProfileWithResizedFile( nickname: String, - profileImg: Bitmap?= null, + profileImg: Bitmap? = null, profileImgTempDir: String? = null, isReset: Boolean = false ) { @@ -108,22 +109,25 @@ class ProfileSettingViewModel @Inject constructor( } fun requestBlockList() = viewModelScope.launch { + val currentData = _blockList.value.data + _blockList.emit(Resource.loading(currentData)) + requestBlockListUseCase().let { response -> when (response) { is NetworkResponse.Success -> { - _blockList.postValue(Resource.success(response.body?.data)) + _blockList.emit(Resource.success(response.body?.data)) } is NetworkResponse.NetworkError -> { - _blockList.postValue(Resource.error(response.exception.toString(), null)) + _blockList.emit(Resource.error(response.exception.toString(), null)) } is NetworkResponse.ApiError -> { - _blockList.postValue(Resource.error(response.error.toString(), null)) + _blockList.emit(Resource.error(response.error.toString(), null)) } is NetworkResponse.UnknownError -> { - _blockList.postValue(Resource.error(response.throwable.toString(), null)) + _blockList.emit(Resource.error(response.throwable.toString(), null)) } } } diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileViewModel.kt index 40c32286..3601d84e 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileViewModel.kt @@ -23,8 +23,10 @@ import daily.dayo.domain.usecase.member.RequestMyProfileUseCase import daily.dayo.domain.usecase.member.RequestOtherProfileUseCase import daily.dayo.presentation.common.Event import daily.dayo.presentation.common.Resource +import daily.dayo.presentation.common.Status import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest @@ -65,11 +67,11 @@ class ProfileViewModel @Inject constructor( private val _likePostList = MutableLiveData>() val likePostList: LiveData> get() = _likePostList - private val _blockSuccess = MutableLiveData>() - val blockSuccess: LiveData> get() = _blockSuccess + private val _blockSuccess = MutableStateFlow(null) + val blockSuccess: StateFlow get() = _blockSuccess - private val _unblockSuccess = MutableLiveData>() - val unblockSuccess: LiveData> get() = _unblockSuccess + private val _unblockSuccess = MutableStateFlow(null) + val unblockSuccess: StateFlow get() = _unblockSuccess fun requestMyProfile() = viewModelScope.launch { requestMyProfileUseCase().let { ApiResponse -> @@ -250,44 +252,46 @@ class ProfileViewModel @Inject constructor( } fun requestBlockMember(memberId: String) = viewModelScope.launch { + _blockSuccess.emit(Status.LOADING) requestBlockMemberUseCase(memberId)?.let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { - _blockSuccess.postValue(Event(true)) + _blockSuccess.emit(Status.SUCCESS) } is NetworkResponse.NetworkError -> { - _blockSuccess.postValue(Event(false)) + _blockSuccess.emit(Status.ERROR) } is NetworkResponse.ApiError -> { - _blockSuccess.postValue(Event(false)) + _blockSuccess.emit(Status.ERROR) } is NetworkResponse.UnknownError -> { - _blockSuccess.postValue(Event(false)) + _blockSuccess.emit(Status.ERROR) } } } } fun requestUnblockMember(memberId: String) = viewModelScope.launch { + _unblockSuccess.emit(Status.LOADING) requestUnblockMemberUseCase(memberId).let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { - _unblockSuccess.postValue(Event(true)) + _unblockSuccess.emit(Status.SUCCESS) } is NetworkResponse.NetworkError -> { - _unblockSuccess.postValue(Event(false)) + _unblockSuccess.emit(Status.ERROR) } is NetworkResponse.ApiError -> { - _unblockSuccess.postValue(Event(false)) + _unblockSuccess.emit(Status.ERROR) } is NetworkResponse.UnknownError -> { - _unblockSuccess.postValue(Event(false)) + _unblockSuccess.emit(Status.ERROR) } } } diff --git a/presentation/src/main/res/drawable/ic_blocked_users_empty.xml b/presentation/src/main/res/drawable/ic_blocked_users_empty.xml new file mode 100644 index 00000000..648d468e --- /dev/null +++ b/presentation/src/main/res/drawable/ic_blocked_users_empty.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_menu_block.xml b/presentation/src/main/res/drawable/ic_menu_block.xml new file mode 100644 index 00000000..b905adfc --- /dev/null +++ b/presentation/src/main/res/drawable/ic_menu_block.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/presentation/src/main/res/layout/fragment_setting_block.xml b/presentation/src/main/res/layout/fragment_setting_block.xml deleted file mode 100644 index 5aee26fb..00000000 --- a/presentation/src/main/res/layout/fragment_setting_block.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/item_block.xml b/presentation/src/main/res/layout/item_block.xml deleted file mode 100644 index 12e1c38c..00000000 --- a/presentation/src/main/res/layout/item_block.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/navigation/nav_graph.xml b/presentation/src/main/res/navigation/nav_graph.xml index a69bd0db..4cc2b1fe 100644 --- a/presentation/src/main/res/navigation/nav_graph.xml +++ b/presentation/src/main/res/navigation/nav_graph.xml @@ -538,14 +538,6 @@ app:popEnterAnim="@anim/translate_from_left_in" app:popExitAnim="@anim/translate_to_right_out" /> - - - - 뒤로가기 잠시만 기다려 주세요 닫기 + 재시도 %d%s @@ -346,6 +347,8 @@ 사용자를 차단할까요? 사용자를 차단하면\n 서로 게시물을 볼 수 없어요. 사용자가 차단되었어요. + 사용자가 차단해제되었어요. + 차단 해제에 실패했어요. 다시 시도해주세요 사용자 신고 로그인 정보 환경설정 @@ -537,6 +540,12 @@ 탈퇴 사유를 작성해주세요. (100자 이내) 계정 삭제시 정보가 회원님의 모든 콘텐츠와 활동기록이 함께 삭제됩니다. 삭제된 정보는 복구할 수 없습니다. + + 차단 관리 + 아직 차단한 유저가 없어요. + 로딩에 실패 했어요. + 차단 해제 + 정보