Skip to content

Commit

Permalink
merge: 프로필 화면 및 프로필 수정 화면 디자인 변경 적용 (#911)
Browse files Browse the repository at this point in the history
Related to: #812
  • Loading branch information
ki960213 authored Feb 7, 2024
1 parent 3f53d2b commit a3e41a2
Show file tree
Hide file tree
Showing 27 changed files with 546 additions and 588 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ fun MemberResponse.toData() = Member(
name = name,
description = description,
profileImageUrl = imageUrl,
activities = activities.toData(),
activities = activities.toData().toSet(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ data class Member(
val name: String = "",
val description: String = "",
val profileImageUrl: String = "",
val activities: List<Activity> = emptyList(),
val activities: Set<Activity> = emptySet(),
) {
val fields: List<Activity>
get() = activities.filter { it.activityType == ActivityType.INTEREST_FIELD }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class DefaultMemberRepository @Inject constructor(
activitiesApiResponse: ApiResponse<List<Activity>>,
): ApiResponse<Member> {
return when (activitiesApiResponse) {
is Success -> Success(member.copy(activities = activitiesApiResponse.data))
is Success -> Success(member.copy(activities = activitiesApiResponse.data.toSet()))
is Failure -> Failure(activitiesApiResponse.code, activitiesApiResponse.message)
NetworkError -> NetworkError
is Unexpected -> Unexpected(activitiesApiResponse.error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import com.emmsale.R
import com.emmsale.data.model.Activity
import com.emmsale.databinding.FragmentEditmyprofileClubsAddBottomDialogBinding
import com.emmsale.presentation.common.views.ActivityTag
import com.emmsale.presentation.common.views.activityChipOf
import com.emmsale.presentation.ui.editMyProfile.uiState.ActivityUiState
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.chip.ChipGroup
import dagger.hilt.android.AndroidEntryPoint
Expand Down Expand Up @@ -39,7 +39,6 @@ class ClubsAddBottomDialogFragment : BottomSheetDialogFragment() {
super.onViewCreated(view, savedInstanceState)
initDataBinding()
setupUiLogic()
viewModel.fetchUnselectedActivities()
}

override fun getTheme(): Int = R.style.RoundBottomSheetDialogStyle
Expand All @@ -59,25 +58,25 @@ class ClubsAddBottomDialogFragment : BottomSheetDialogFragment() {
}

private fun setupClubsUiLogic() {
viewModel.activities.observe(viewLifecycleOwner) { allActivities ->
viewModel.clubs.observe(viewLifecycleOwner) { clubs ->
binding.cgEditmyprofileclubsdialogClubs.removeAllViews()
binding.cgEditmyprofileclubsdialogClubs.addChips(allActivities.clubs)
binding.cgEditmyprofileclubsdialogClubs.addChips(clubs)
}
}

private fun ChipGroup.addChips(clubs: List<ActivityUiState>) {
private fun ChipGroup.addChips(clubs: List<Activity>) {
clubs.forEach { club ->
val chip = getActivityTag(club)
addView(chip)
}
}

private fun getActivityTag(club: ActivityUiState): ActivityTag {
private fun getActivityTag(club: Activity): ActivityTag {
return activityChipOf {
text = club.activity.name
isChecked = club.isSelected
setOnCheckedChangeListener { _, _ ->
viewModel.toggleActivitySelection(club.activity.id)
text = club.name
isChecked = club in viewModel.selectedClubs.value!!
setOnCheckedChangeListener { _, isChecked ->
viewModel.setActivitySelection(club.id, isChecked)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.view.isVisible
import com.emmsale.R
import com.emmsale.databinding.ActivityEditMyProfileBinding
import com.emmsale.presentation.base.NetworkActivity
Expand Down Expand Up @@ -88,10 +89,11 @@ class EditMyProfileActivity :

private fun setupDataBinding() {
binding.viewModel = viewModel
binding.onFieldTagsAddButtonClick = ::showFieldTags
binding.onFieldAddButtonClick = ::showFieldTags
binding.onEducationAddButtonClick = ::showEducations
binding.onClubAddButtonClick = ::showClubs
binding.onProfileImageUpdateUiClick = ::startToEditProfileImage
binding.onProfileImageUiClick = ::startToEditProfileImage
binding.onDescriptionEditButtonClick = ::startToEditDescription
}

private fun showFieldTags() {
Expand Down Expand Up @@ -129,6 +131,20 @@ class EditMyProfileActivity :
)
}

private fun startToEditDescription() {
showKeyboard(binding.etEditmyprofileDescription)
binding.etEditmyprofileDescription.moveCursorToLast()
}

private fun showKeyboard(view: EditText) {
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
if (view.requestFocus()) imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
}

private fun EditText.moveCursorToLast() {
setSelection(text.length)
}

private fun setupBackPressedDispatcher() {
onBackPressedDispatcher.addCallback(
this,
Expand Down Expand Up @@ -159,9 +175,6 @@ class EditMyProfileActivity :
}

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
binding.ivEditmyprofileDescriptionPlaceholderIcon.isVisible =
s?.length == 0 || s == null

if (s.toString().contains('\n')) {
binding.etEditmyprofileDescription.setText(
s.toString().filterNot { it == '\n' },
Expand Down Expand Up @@ -202,7 +215,10 @@ class EditMyProfileActivity :
WarningDialog(
context = this,
title = getString(R.string.editmyprofile_activity_remove_warning_title),
message = getString(R.string.editmyprofile_activity_remove_warning_message),
message = getString(
R.string.editmyprofile_activity_remove_warning_message,
viewModel.activities.value.first { it.id == activityId }.name,
),
positiveButtonLabel = getString(R.string.all_delete_button_label),
negativeButtonLabel = getString(R.string.all_cancel),
onPositiveButtonClick = { viewModel.removeActivity(activityId) },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.emmsale.presentation.ui.editMyProfile

import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import com.emmsale.data.model.Activity
import com.emmsale.data.model.ActivityType
import com.emmsale.data.model.Member
import com.emmsale.data.repository.interfaces.ActivityRepository
import com.emmsale.data.repository.interfaces.MemberRepository
Expand All @@ -10,7 +14,6 @@ import com.emmsale.presentation.base.RefreshableViewModel
import com.emmsale.presentation.common.livedata.NotNullLiveData
import com.emmsale.presentation.common.livedata.NotNullMutableLiveData
import com.emmsale.presentation.common.livedata.SingleLiveEvent
import com.emmsale.presentation.ui.editMyProfile.uiState.ActivitiesUiState
import com.emmsale.presentation.ui.editMyProfile.uiState.EditMyProfileUiEvent
import com.emmsale.presentation.ui.editMyProfile.uiState.EditMyProfileUiState
import dagger.hilt.android.lifecycle.HiltViewModel
Expand All @@ -31,35 +34,97 @@ class EditMyProfileViewModel @Inject constructor(
private val _profile = NotNullMutableLiveData(EditMyProfileUiState(Member()))
val profile: NotNullLiveData<EditMyProfileUiState> = _profile

private val _activities = NotNullMutableLiveData(listOf<Activity>())
val activities: NotNullLiveData<List<Activity>> = _activities

val fields: LiveData<List<Activity>> = _activities.map {
it.filter { activity -> activity.activityType == ActivityType.INTEREST_FIELD }
}

val clubs: LiveData<List<Activity>> = _activities.map {
it.filter { activity -> activity.activityType == ActivityType.CLUB }
}

val educations: LiveData<List<Activity>> = _activities.map {
it.filter { activity -> activity.activityType == ActivityType.EDUCATION }
}

private val _selectedActivityIds = NotNullMutableLiveData(setOf<Long>())

val selectedFields = MediatorLiveData(setOf<Activity>()).apply {
addSource(_selectedActivityIds) { value = getSelectedFields() }
addSource(fields) { value = getSelectedFields() }
}

val selectedClubs = MediatorLiveData(setOf<Activity>()).apply {
addSource(_selectedActivityIds) { value = getSelectedClubs() }
addSource(clubs) { value = getSelectedClubs() }
}

val selectedEducations = MediatorLiveData(setOf<Activity>()).apply {
addSource(_selectedActivityIds) { value = getSelectedEducations() }
addSource(educations) { value = getSelectedEducations() }
}

val isFieldsSelectionChanged = MediatorLiveData(false).apply {
addSource(_profile) { value = selectedFields.value != _profile.value.member.fields.toSet() }
addSource(selectedFields) {
value = selectedFields.value != _profile.value.member.fields.toSet()
}
}

val isClubsSelectionChanged = MediatorLiveData(false).apply {
addSource(_profile) { value = selectedClubs.value != _profile.value.member.clubs.toSet() }
addSource(selectedClubs) {
value = selectedClubs.value != _profile.value.member.clubs.toSet()
}
}

val isEducationsSelectionChanged = MediatorLiveData(false).apply {
addSource(_profile) {
value = selectedEducations.value != _profile.value.member.educations.toSet()
}
addSource(selectedEducations) {
value = selectedEducations.value != _profile.value.member.educations.toSet()
}
}

private val _uiEvent = SingleLiveEvent<EditMyProfileUiEvent>()
val uiEvent: LiveData<EditMyProfileUiEvent> = _uiEvent

private val _activities = NotNullMutableLiveData(ActivitiesUiState())
val activities: NotNullLiveData<ActivitiesUiState> = _activities
private fun getSelectedFields(): Set<Activity> =
fields.value?.filter { it.id in _selectedActivityIds.value }?.toSet() ?: emptySet()

private fun getSelectedClubs(): Set<Activity> =
clubs.value?.filter { it.id in _selectedActivityIds.value }?.toSet() ?: emptySet()

private fun getSelectedEducations(): Set<Activity> =
educations.value?.filter { it.id in _selectedActivityIds.value }?.toSet() ?: emptySet()

init {
fetchProfile()
fetchActivities()
}

private fun fetchProfile(): Job = fetchData(
fetchData = { memberRepository.getMember(uid) },
onSuccess = { _profile.value = EditMyProfileUiState(it) },
onSuccess = ::onProfileFetchSuccess,
)

private fun fetchActivities(): Job = fetchData(
fetchData = { activityRepository.getActivities() },
onSuccess = { _activities.value = it },
)

override fun refresh(): Job = refreshData(
refresh = { memberRepository.getMember(uid) },
onSuccess = { _profile.value = EditMyProfileUiState(it) },
onSuccess = ::onProfileFetchSuccess,
)

fun fetchUnselectedActivities(): Job = fetchData(
fetchData = { activityRepository.getActivities() },
onSuccess = {
_activities.value = _activities.value.fetchUnselectedActivities(
allActivities = it,
myActivities = profile.value.member.activities,
)
},
)
private fun onProfileFetchSuccess(member: Member) {
_profile.value = EditMyProfileUiState(member)
_selectedActivityIds.value = member.activities.map(Activity::id).toSet()
}

fun updateProfileImage(profileImageFile: File): Job = command(
command = { memberRepository.updateMemberProfileImage(uid, profileImageFile) },
Expand All @@ -78,36 +143,67 @@ class EditMyProfileViewModel @Inject constructor(
onFailure = { _, _ -> _uiEvent.value = EditMyProfileUiEvent.ActivityRemoveFail },
)

fun addSelectedFields() {
fun updateFields() {
viewModelScope.launch {
val selectedFieldIds =
activities.value.fields.filter { it.isSelected }.map { it.activity.id }
updateMemberActivities(selectedFieldIds)
val addFieldIds = selectedFields.value
?.filter { it !in _profile.value.member.fields }
?.map(Activity::id)
?: emptyList()
val removeFieldIds = _profile.value.member.fields
.filter { it !in (selectedFields.value ?: emptyList()) }
.map(Activity::id)
if (addFieldIds.isNotEmpty()) addActivities(addFieldIds)
if (removeFieldIds.isNotEmpty()) removeActivities(removeFieldIds)
}
}

fun addSelectedEducations() {
viewModelScope.launch {
val selectedEducationIds =
activities.value.educations.filter { it.isSelected }.map { it.activity.id }
updateMemberActivities(selectedEducationIds)
val addEducationIds = selectedEducations.value
?.filter { it !in _profile.value.member.educations }
?.map(Activity::id)
?: emptyList()
val removeEducationIds = _profile.value.member.educations
.filter { it !in (selectedEducations.value ?: emptyList()) }
.map(Activity::id)
if (addEducationIds.isNotEmpty()) addActivities(addEducationIds)
if (removeEducationIds.isNotEmpty()) removeActivities(removeEducationIds)
}
}

fun addSelectedClubs() {
viewModelScope.launch {
val selectedClubIds =
activities.value.clubs.filter { it.isSelected }.map { it.activity.id }
updateMemberActivities(selectedClubIds)
val addClubIds = selectedClubs.value
?.filter { it !in _profile.value.member.clubs }
?.map(Activity::id)
?: emptyList()
val removeClubIds = _profile.value.member.clubs
.filter { it !in (selectedClubs.value ?: emptyList()) }
.map(Activity::id)
if (addClubIds.isNotEmpty()) addActivities(addClubIds)
if (removeClubIds.isNotEmpty()) removeActivities(removeClubIds)
}
}

private suspend fun updateMemberActivities(activityIds: List<Long>): Job = commandAndRefresh(
private suspend fun addActivities(activityIds: List<Long>): Job = commandAndRefresh(
command = { memberRepository.addMemberActivities(activityIds) },
onFailure = { _, _ -> _uiEvent.value = EditMyProfileUiEvent.ActivitiesAddFail },
)

fun toggleActivitySelection(activityId: Long) {
_activities.value = activities.value.toggleIsSelected(activityId)
private suspend fun removeActivities(activityIds: List<Long>): Job = commandAndRefresh(
command = { memberRepository.deleteMemberActivities(activityIds) },
onFailure = { _, _ -> _uiEvent.value = EditMyProfileUiEvent.ActivityRemoveFail },
)

fun setActivitySelection(activityId: Long, isSelected: Boolean) {
if (isSelected) {
_selectedActivityIds.value += activityId
} else {
_selectedActivityIds.value -= activityId
}
}

companion object {
const val MAX_FIELDS_COUNT = 4
}
}
Loading

0 comments on commit a3e41a2

Please sign in to comment.