diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/CommonUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/CommonUiEvent.kt new file mode 100644 index 000000000..b2d975e82 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/CommonUiEvent.kt @@ -0,0 +1,6 @@ +package com.emmsale.presentation.common + +sealed interface CommonUiEvent { + data class Unexpected(val errorMessage: String) : CommonUiEvent + object RequestFailByNetworkError : CommonUiEvent +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/KeyboardHider.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/KeyboardHider.kt similarity index 58% rename from android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/KeyboardHider.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/KeyboardHider.kt index 0309e8d13..dde4755d7 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/KeyboardHider.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/KeyboardHider.kt @@ -1,19 +1,15 @@ -package com.emmsale.presentation.ui.messageList +package com.emmsale.presentation.common -import android.content.Context +import android.app.Activity import android.view.MotionEvent -import android.view.View -import android.view.inputmethod.InputMethodManager +import com.emmsale.presentation.common.extension.hideKeyboard import kotlin.math.abs class KeyboardHider( - private val targetView: View, + private val activity: Activity, private val sensitivity: Float = DEFAULT_SENSITIVITY, private val willConsumeTouchEvent: Boolean = CONSUMED_TOUCH_EVENT, ) { - private val imm by lazy { - targetView.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - } private var startY: Float = -1F private var movedY: Float = 0F @@ -22,7 +18,7 @@ class KeyboardHider( when (event.action) { MotionEvent.ACTION_DOWN -> startY = event.y MotionEvent.ACTION_MOVE -> movedY = abs(event.y - startY) - MotionEvent.ACTION_UP -> if (canHideKeyboard()) hideKeyboard() + MotionEvent.ACTION_UP -> if (canHideKeyboard()) activity.hideKeyboard() } return willConsumeTouchEvent } @@ -31,12 +27,6 @@ class KeyboardHider( return movedY < sensitivity } - private fun hideKeyboard() { - if (targetView.onCheckIsTextEditor()) { - imm.hideSoftInputFromWindow(targetView.windowToken, 0) - } - } - companion object { private const val DEFAULT_SENSITIVITY: Float = 10F private const val CONSUMED_TOUCH_EVENT = false diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/ScreenUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/ScreenUiState.kt new file mode 100644 index 000000000..75db655eb --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/ScreenUiState.kt @@ -0,0 +1,5 @@ +package com.emmsale.presentation.common + +enum class ScreenUiState { + NONE, LOADING, NETWORK_ERROR +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/bindingadapter/SwipeRefreshLayoutBindingAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/bindingadapter/SwipeRefreshLayoutBindingAdapter.kt index ada9f0011..c33967cdc 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/bindingadapter/SwipeRefreshLayoutBindingAdapter.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/bindingadapter/SwipeRefreshLayoutBindingAdapter.kt @@ -3,8 +3,11 @@ package com.emmsale.presentation.common.bindingadapter import androidx.annotation.ColorInt import androidx.databinding.BindingAdapter import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch -@BindingAdapter("onRefresh") +@BindingAdapter("app:onRefresh") fun SwipeRefreshLayout.setOnRefresh(onRefresh: () -> Unit) { setOnRefreshListener { onRefresh() @@ -12,6 +15,23 @@ fun SwipeRefreshLayout.setOnRefresh(onRefresh: () -> Unit) { } } +@BindingAdapter("app:onRefresh1") +fun SwipeRefreshLayout.setOnRefresh1(onRefreshListener: OnRefreshListener) { + setOnRefreshListener { + val refreshJob = onRefreshListener.onRefresh() + if (refreshJob.isCancelled) return@setOnRefreshListener + + MainScope().launch { + refreshJob.join() + isRefreshing = false + } + } +} + +fun interface OnRefreshListener { + fun onRefresh(): Job +} + @BindingAdapter("app:swipeRefreshColor") fun SwipeRefreshLayout.setSwipeRefreshColor(@ColorInt color: Int) { setColorSchemeColors(color) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/bindingadapter/ViewBindingAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/bindingadapter/ViewBindingAdapter.kt index 5dabf2d48..9cfaf228b 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/bindingadapter/ViewBindingAdapter.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/bindingadapter/ViewBindingAdapter.kt @@ -4,15 +4,38 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.updateLayoutParams import androidx.databinding.BindingAdapter +import com.emmsale.presentation.common.extension.dp + +private const val DP_UNIT = "dp" @BindingAdapter("app:visible") fun View.setVisible(visible: Boolean) { visibility = if (visible) View.VISIBLE else View.GONE } -@BindingAdapter("app:layoutMarginTop") +@BindingAdapter("app:layout_marginTop") fun View.setLayoutMarginTop(dimen: Float) { updateLayoutParams { topMargin = dimen.toInt() } } + +@BindingAdapter("app:layout_marginTop") +fun View.setLayoutMarginTop(margin: String) { + validateMarginFormat(margin) + updateLayoutParams { + topMargin = margin.toDimen() + } +} + +private fun validateMarginFormat(margin: String) { + require(margin.endsWith(DP_UNIT) || margin.all { it.isDigit() }) { + "숫자만 이루어져 있거나 숫자 뒤에 $DP_UNIT 문자열만 올 수 있습니다." + } +} + +private fun String.toDimen(): Int = if (endsWith(DP_UNIT)) { + removeSuffix(DP_UNIT).toInt().dp +} else { + toInt() +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/extension/ActivityExt.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/extension/ActivityExt.kt new file mode 100644 index 000000000..88fe10cc5 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/extension/ActivityExt.kt @@ -0,0 +1,24 @@ +package com.emmsale.presentation.common.extension + +import android.app.Activity +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import androidx.appcompat.app.AppCompatActivity + +private const val KEYBOARD_SHOW_DELAY: Long = 100 + +fun Activity.hideKeyboard() { + val imm = getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(window.decorView.windowToken, 0) +} + +fun Activity.showKeyboard() { + val imm = getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager + currentFocus?.postDelayed({ + imm.showSoftInput(currentFocus, InputMethodManager.SHOW_IMPLICIT) + if (currentFocus is EditText) { + val editText = currentFocus as EditText + editText.setSelection(editText.text.length) + } + }, KEYBOARD_SHOW_DELAY) +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/extension/FloatExt.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/extension/FloatExt.kt new file mode 100644 index 000000000..506450ac3 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/extension/FloatExt.kt @@ -0,0 +1,6 @@ +package com.emmsale.presentation.common.extension + +import android.content.res.Resources + +val Float.dp: Float + get() = (this * Resources.getSystem().displayMetrics.density) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/livedata/SingleLiveEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/livedata/SingleLiveEvent.kt new file mode 100644 index 000000000..c7ec09b05 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/livedata/SingleLiveEvent.kt @@ -0,0 +1,26 @@ +package com.emmsale.presentation.common.livedata + +import androidx.annotation.MainThread +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import java.util.concurrent.atomic.AtomicBoolean + +class SingleLiveEvent : MutableLiveData() { + + private val mPending: AtomicBoolean = AtomicBoolean(false) + + override fun observe(owner: LifecycleOwner, observer: Observer) { + super.observe(owner) { t -> + if (mPending.compareAndSet(true, false)) { + observer.onChanged(t) + } + } + } + + @MainThread + override fun setValue(t: T?) { + mPending.set(true) + super.setValue(t) + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/recyclerView/DividerItemDecoration.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/recyclerView/DividerItemDecoration.kt new file mode 100644 index 000000000..7b65bc2f2 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/recyclerView/DividerItemDecoration.kt @@ -0,0 +1,39 @@ +package com.emmsale.presentation.common.recyclerView + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import androidx.annotation.ColorRes +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.emmsale.R +import com.emmsale.presentation.common.extension.dp + +class DividerItemDecoration( + context: Context, + private val dividerHeight: Float = 0.5f.dp, + @ColorRes private val dividerColor: Int = R.color.light_gray, +) : RecyclerView.ItemDecoration() { + private val paint = Paint().apply { + color = ContextCompat.getColor(context, dividerColor) + style = Paint.Style.FILL + } + + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + super.onDraw(c, parent, state) + + val left = parent.paddingStart.toFloat() + val right = parent.width - parent.paddingEnd.toFloat() + for (i in 0 until parent.childCount - 1) { + val child = parent.getChildAt(i) + val params = child.layoutParams as RecyclerView.LayoutParams + + val top = child.bottom.toFloat() + params.bottomMargin + val bottom = top + dividerHeight + + val dividerRect = RectF(left, top, right, bottom) + c.drawRect(dividerRect, paint) + } + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/FeedDetailImageItemDecoration.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/recyclerView/IntervalItemDecoration.kt similarity index 63% rename from android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/FeedDetailImageItemDecoration.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/recyclerView/IntervalItemDecoration.kt index a5b0e8612..2bd90d734 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/FeedDetailImageItemDecoration.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/recyclerView/IntervalItemDecoration.kt @@ -1,12 +1,12 @@ -package com.emmsale.presentation.ui.feedDetail.recyclerView +package com.emmsale.presentation.common.recyclerView import android.graphics.Rect import android.view.View import androidx.recyclerview.widget.RecyclerView -import com.emmsale.presentation.common.extension.dp -class FeedDetailImageItemDecoration( - private val divWidth: Int = 10.dp, +class IntervalItemDecoration( + private val width: Int = 0, + private val height: Int = 0, ) : RecyclerView.ItemDecoration() { override fun getItemOffsets( outRect: Rect, @@ -17,6 +17,7 @@ class FeedDetailImageItemDecoration( super.getItemOffsets(outRect, view, parent, state) val position = parent.getChildAdapterPosition(view) - if (position > 0) outRect.left = divWidth + if (position > 0) outRect.left = width + if (position > 0) outRect.top = height } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/BasicTextInputWindow.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/BasicTextInputWindow.kt new file mode 100644 index 000000000..ed1dd6108 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/BasicTextInputWindow.kt @@ -0,0 +1,69 @@ +package com.emmsale.presentation.common.views + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.res.use +import androidx.databinding.BindingAdapter +import com.emmsale.R +import com.emmsale.databinding.LayoutBasicInputWindowBinding +import com.emmsale.presentation.common.extension.dp +import com.emmsale.presentation.common.views.BasicTextInputWindow.OnSubmitListener +import kotlin.properties.Delegates.observable + +class BasicTextInputWindow @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : ConstraintLayout(context, attrs) { + + private val binding: LayoutBasicInputWindowBinding by lazy { + LayoutBasicInputWindowBinding.inflate(LayoutInflater.from(context), this, false) + } + + var isSubmitEnabled: Boolean by observable(false) { _, _, newValue -> + binding.isSubmitEnabled = newValue + } + + var onSubmitListener: OnSubmitListener by observable(OnSubmitListener { }) { _, _, newValue -> + binding.onSubmitListener = newValue + } + + init { + applyStyledAttributes(attrs) + setPadding(17.dp, 8.dp, 17.dp, 8.dp) + isClickable = true + addView(binding.root) + } + + private fun applyStyledAttributes(attrs: AttributeSet?) { + context.theme.obtainStyledAttributes( + attrs, + R.styleable.BasicTextInputWindow, + 0, + 0, + ).use { + binding.etBasicInput.hint = it.getString(R.styleable.BasicTextInputWindow_hint) + binding.tvSubmitButton.text = + it.getString(R.styleable.BasicTextInputWindow_submitButtonLabel) + } + } + + fun clearText() { + binding.etBasicInput.text.clear() + } + + fun interface OnSubmitListener { + fun onSubmit(text: String) + } +} + +@BindingAdapter("app:onSubmit") +fun BasicTextInputWindow.setOnSubmitListener(onSubmitListener: OnSubmitListener) { + this.onSubmitListener = onSubmitListener +} + +@BindingAdapter("app:isSubmitEnabled") +fun BasicTextInputWindow.setIsSubmitEnabled(isSubmitEnabled: Boolean) { + this.isSubmitEnabled = isSubmitEnabled +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/InfoDialog.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/InfoDialog.kt index b091f5848..af9c921a7 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/InfoDialog.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/InfoDialog.kt @@ -15,6 +15,8 @@ class InfoDialog( private val title: String, private val message: String, private val buttonLabel: String = context.getString(R.string.all_okay), + private val onButtonClick: (() -> Unit)? = null, + private val cancelable: Boolean = true, ) : Dialog(context) { private val binding: DialogInfoBinding by lazy { DialogInfoBinding.inflate(layoutInflater) } @@ -22,6 +24,7 @@ class InfoDialog( super.onCreate(savedInstanceState) setContentView(binding.root) + setCancelable(cancelable) initDialogWindow() initDataBinding() } @@ -39,6 +42,7 @@ class InfoDialog( binding.message = message binding.buttonLabel = buttonLabel binding.onButtonClick = { + onButtonClick?.invoke() dismiss() } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/NetworkErrorView.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/NetworkErrorView.kt new file mode 100644 index 000000000..4f02d2936 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/NetworkErrorView.kt @@ -0,0 +1,41 @@ +package com.emmsale.presentation.common.views + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.graphics.drawable.toDrawable +import androidx.databinding.BindingAdapter +import com.emmsale.R +import com.emmsale.databinding.LayoutNetworkErrorBinding +import com.emmsale.presentation.common.views.NetworkErrorView.OnRefreshListener +import kotlin.properties.Delegates.observable + +class NetworkErrorView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : ConstraintLayout(context, attrs) { + + private val binding: LayoutNetworkErrorBinding by lazy { + LayoutNetworkErrorBinding.inflate(LayoutInflater.from(context), this, false) + } + + var onRefreshListener: OnRefreshListener by observable(OnRefreshListener { }) { _, _, newValue -> + binding.onRefreshListener = newValue + } + + init { + addView(binding.root) + background = context.getColor(R.color.white).toDrawable() + isClickable = true + } + + fun interface OnRefreshListener { + fun onRefresh() + } +} + +@BindingAdapter("app:onRefresh") +fun NetworkErrorView.setOnRefreshListener(onRefreshListener: OnRefreshListener) { + this.onRefreshListener = onRefreshListener +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/SubTextInputWindow.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/SubTextInputWindow.kt new file mode 100644 index 000000000..841ab7e50 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/SubTextInputWindow.kt @@ -0,0 +1,94 @@ +package com.emmsale.presentation.common.views + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.res.use +import androidx.core.graphics.drawable.toDrawable +import androidx.databinding.BindingAdapter +import com.emmsale.R +import com.emmsale.databinding.LayoutSubTextInputWindowBinding +import com.emmsale.presentation.common.extension.dp +import com.emmsale.presentation.common.views.SubTextInputWindow.OnCancelListener +import com.emmsale.presentation.common.views.SubTextInputWindow.OnSubmitListener +import kotlin.properties.Delegates.observable + +class SubTextInputWindow @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : ConstraintLayout(context, attrs) { + + private val binding: LayoutSubTextInputWindowBinding by lazy { + LayoutSubTextInputWindowBinding.inflate(LayoutInflater.from(context), this, false) + } + + var text: String by observable("") { _, _, newValue -> + binding.etSubTextInput.setText(newValue) + } + + var isSubmitEnabled: Boolean by observable(false) { _, _, newValue -> + binding.isSubmitEnabled = newValue + } + + var onSubmitListener: OnSubmitListener by observable(OnSubmitListener { }) { _, _, newValue -> + binding.onSubmitListener = newValue + } + + var onCancelListener: OnCancelListener by observable(OnCancelListener { }) { _, _, newValue -> + binding.onCancelListener = newValue + } + + init { + applyStyledAttributes(attrs) + addView(binding.root) + background = context.getColor(R.color.white).toDrawable() + elevation = 5f.dp + } + + private fun applyStyledAttributes(attrs: AttributeSet?) { + context.theme.obtainStyledAttributes( + attrs, + R.styleable.SubTextInputWindow, + 0, + 0, + ).use { + binding.tvSubmitButton.text = + it.getString(R.styleable.SubTextInputWindow_submitButtonLabel) + binding.tvCancelButton.text = + it.getString(R.styleable.SubTextInputWindow_cancelButtonLabel) + } + } + + fun requestFocusOnEditText() { + binding.etSubTextInput.requestFocus() + } + + fun interface OnSubmitListener { + fun onSubmit(text: String) + } + + fun interface OnCancelListener { + fun onCancel() + } +} + +@BindingAdapter("app:text") +fun SubTextInputWindow.setText(text: String?) { + if (text != null) this.text = text +} + +@BindingAdapter("app:isSubmitEnabled") +fun SubTextInputWindow.setIsSubmitEnabled(isSubmitEnabled: Boolean) { + this.isSubmitEnabled = isSubmitEnabled +} + +@BindingAdapter("app:onSubmit") +fun SubTextInputWindow.setOnSubmitListener(onSubmitListener: OnSubmitListener) { + this.onSubmitListener = onSubmitListener +} + +@BindingAdapter("app:onCancel") +fun SubTextInputWindow.setOnCancelListener(onCancelListener: OnCancelListener) { + this.onCancelListener = onCancelListener +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/service/KerdyFirebaseMessagingService.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/service/KerdyFirebaseMessagingService.kt index 77aac6402..4d44ce6d5 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/service/KerdyFirebaseMessagingService.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/service/KerdyFirebaseMessagingService.kt @@ -130,7 +130,7 @@ class KerdyFirebaseMessagingService : FirebaseMessagingService() { val intent = if (isUpdateNeeded) { SplashActivity.getIntent(this) } else { - ChildCommentActivity.getIntent(this, feedId, parentCommentId, true) + ChildCommentActivity.getIntent(this, feedId, parentCommentId, childCommentId, false) } baseContext.showNotification( diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentActivity.kt index 5687c175a..5615c6198 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentActivity.kt @@ -1,26 +1,29 @@ package com.emmsale.presentation.ui.childCommentList +import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.inputmethod.InputMethodManager import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible import com.emmsale.R import com.emmsale.databinding.ActivityChildCommentsBinding -import com.emmsale.presentation.common.Event +import com.emmsale.presentation.common.CommonUiEvent +import com.emmsale.presentation.common.extension.hideKeyboard +import com.emmsale.presentation.common.extension.showKeyboard import com.emmsale.presentation.common.extension.showSnackBar import com.emmsale.presentation.common.extension.showToast +import com.emmsale.presentation.common.recyclerView.DividerItemDecoration import com.emmsale.presentation.common.views.InfoDialog import com.emmsale.presentation.common.views.WarningDialog import com.emmsale.presentation.common.views.bottomMenuDialog.BottomMenuDialog import com.emmsale.presentation.common.views.bottomMenuDialog.MenuItemType import com.emmsale.presentation.ui.childCommentList.ChildCommentViewModel.Companion.KEY_FEED_ID import com.emmsale.presentation.ui.childCommentList.ChildCommentViewModel.Companion.KEY_PARENT_COMMENT_ID -import com.emmsale.presentation.ui.childCommentList.recyclerView.ChildCommentRecyclerViewDivider import com.emmsale.presentation.ui.childCommentList.uiState.ChildCommentsUiEvent -import com.emmsale.presentation.ui.childCommentList.uiState.CommentsUiState +import com.emmsale.presentation.ui.childCommentList.uiState.ChildCommentsUiState import com.emmsale.presentation.ui.feedDetail.FeedDetailActivity import com.emmsale.presentation.ui.feedDetail.recyclerView.CommentsAdapter import com.emmsale.presentation.ui.profile.ProfileActivity @@ -29,33 +32,39 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class ChildCommentActivity : AppCompatActivity() { private val binding by lazy { ActivityChildCommentsBinding.inflate(layoutInflater) } - private val viewModel: ChildCommentViewModel by viewModels() - private val inputMethodManager: InputMethodManager by lazy { - getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - } + private val viewModel: ChildCommentViewModel by viewModels() private val commentsAdapter: CommentsAdapter = CommentsAdapter( - onParentCommentClick = {}, - onProfileImageClick = ::showProfile, + onCommentClick = { comment -> viewModel.unhighlight(comment.id) }, + onAuthorImageClick = { authorId -> ProfileActivity.startActivity(this, authorId) }, onCommentMenuClick = ::showCommentMenuDialog, ) private val bottomMenuDialog: BottomMenuDialog by lazy { BottomMenuDialog(this) } + private val highlightCommentId: Long by lazy { + intent.getLongExtra(KEY_HIGHLIGHT_COMMENT_ID, INVALID_COMMENT_ID) + } + + private val fromPostDetail: Boolean by lazy { + intent.getBooleanExtra(KEY_FROM_POST_DETAIL, true) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) - initDataBinding() - initBackPressedDispatcher() - initToolbar() - initChildCommentsRecyclerView() - initEditComment() - setupUiLogic() + + setupDataBinding() + setupBackPressedDispatcher() + setupToolbar() + setupChildCommentsRecyclerView() + observeComments() + observeUiEvent() } - override fun onStart() { - super.onStart() + override fun onRestart() { + super.onRestart() viewModel.refresh() } @@ -72,17 +81,19 @@ class ChildCommentActivity : AppCompatActivity() { private fun BottomMenuDialog.addCommentUpdateButton(commentId: Long) { addMenuItemBelow(context.getString(R.string.all_update_button_label)) { - editComment(commentId) + viewModel.setEditMode(true, commentId) + binding.stiwCommentUpdate.requestFocusOnEditText() + showKeyboard() } } private fun BottomMenuDialog.addCommentDeleteButton(commentId: Long) { addMenuItemBelow(context.getString(R.string.all_delete_button_label)) { - onCommentDeleteButtonClick(commentId) + showDeleteCommentConfirmDialog(commentId) } } - private fun onCommentDeleteButtonClick(commentId: Long) { + private fun showDeleteCommentConfirmDialog(commentId: Long) { val context = binding.root.context WarningDialog( context = context, @@ -90,7 +101,7 @@ class ChildCommentActivity : AppCompatActivity() { message = context.getString(R.string.commentdeletedialog_message), positiveButtonLabel = context.getString(R.string.commentdeletedialog_positive_button_label), negativeButtonLabel = context.getString(R.string.commentdeletedialog_negative_button_label), - onPositiveButtonClick = { deleteComment(commentId) }, + onPositiveButtonClick = { viewModel.deleteComment(commentId) }, ).show() } @@ -98,24 +109,10 @@ class ChildCommentActivity : AppCompatActivity() { addMenuItemBelow( context.getString(R.string.all_report_button_label), MenuItemType.IMPORTANT, - ) { reportComment(commentId) } - } - - private fun showProfile(authorId: Long) { - ProfileActivity.startActivity(this, authorId) + ) { showReportConfirmDialog(commentId) } } - private fun editComment(commentId: Long) { - viewModel.setEditMode(true, commentId) - binding.etChildcommentsCommentUpdate.requestFocus() - showKeyboard() - } - - private fun deleteComment(commentId: Long) { - viewModel.deleteComment(commentId) - } - - private fun reportComment(commentId: Long) { + private fun showReportConfirmDialog(commentId: Long) { WarningDialog( context = this, title = getString(R.string.all_report_dialog_title), @@ -126,79 +123,82 @@ class ChildCommentActivity : AppCompatActivity() { ).show() } - private fun initDataBinding() { + private fun setupDataBinding() { binding.viewModel = viewModel binding.lifecycleOwner = this - binding.cancelUpdateComment = ::cancelUpdateComment - binding.updateComment = ::updateComment - } - - private fun cancelUpdateComment() { - viewModel.setEditMode(false) - hideKeyboard() - } - - private fun updateComment() { - val commentId = viewModel.editingCommentId.value ?: return - val content = binding.etChildcommentsCommentUpdate.text.toString() - viewModel.updateComment(commentId, content) - hideKeyboard() + binding.onCommentSubmitButtonClick = { + viewModel.postChildComment(it) + hideKeyboard() + } + binding.onCommentUpdateCancelButtonClick = { + viewModel.setEditMode(false) + hideKeyboard() + } + binding.onUpdatedCommentSubmitButtonClick = { + val commentId = viewModel.editingCommentId.value + if (commentId != null) viewModel.updateComment(commentId, it) + hideKeyboard() + } } - private fun initBackPressedDispatcher() { - onBackPressedDispatcher.addCallback(this, ChildCommentOnBackPressedCallback()) + private fun setupBackPressedDispatcher() { + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (!fromPostDetail) { + FeedDetailActivity.startActivity( + this@ChildCommentActivity, + viewModel.feedId, + ) + } + finish() + } + }, + ) } - private fun initToolbar() { + private fun setupToolbar() { binding.tbChildcommentsToolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } } - private fun initChildCommentsRecyclerView() { + @SuppressLint("ClickableViewAccessibility") + private fun setupChildCommentsRecyclerView() { binding.rvChildcommentsChildcomments.apply { adapter = commentsAdapter itemAnimator = null - addItemDecoration(ChildCommentRecyclerViewDivider(this@ChildCommentActivity)) + addItemDecoration(DividerItemDecoration(this@ChildCommentActivity)) } } - private fun setupUiLogic() { - setupCommentsUiLogic() - setupEditingCommentUiLogic() - setUpUiEvent() - } - - private fun setupCommentsUiLogic() { + private fun observeComments() { viewModel.comments.observe(this) { - handleChildComments(it) + commentsAdapter.submitList(it.comments) { scrollToIfFirstFetch(it) } } } - private fun setupEditingCommentUiLogic() { - viewModel.editingCommentContent.observe(this) { - if (it == null) return@observe - binding.etChildcommentsCommentUpdate.setText(it) - } - } + private fun scrollToIfFirstFetch(childCommentUiState: ChildCommentsUiState) { + fun cantScroll(): Boolean = + viewModel.isAlreadyFirstFetched || childCommentUiState.comments.isEmpty() - private fun handleChildComments(comments: CommentsUiState) { - commentsAdapter.submitList(comments.comments) - } + if (highlightCommentId == INVALID_COMMENT_ID || cantScroll()) return + val position = viewModel.comments.value.comments + .indexOfFirst { commentUiState -> + commentUiState.comment.id == highlightCommentId + } + binding.rvChildcommentsChildcomments.scrollToPosition(position) - private fun initEditComment() { - binding.tvChildcommentsPostchildcommentbutton.setOnClickListener { - onChildCommentSave() - } + viewModel.highlight(highlightCommentId) + viewModel.isAlreadyFirstFetched = true } - private fun setUpUiEvent() { - viewModel.uiEvent.observe(this) { - handleUiEvent(it) - } + private fun observeUiEvent() { + viewModel.uiEvent.observe(this) { handleUiEvent(it) } + viewModel.commonUiEvent.observe(this) { handleBaseUiEvent(it) } } - private fun handleUiEvent(event: Event) { - val content = event.getContentIfNotHandled() ?: return - when (content) { + private fun handleUiEvent(event: ChildCommentsUiEvent) { + when (event) { ChildCommentsUiEvent.CommentReportFail -> binding.root.showSnackBar(getString(R.string.all_report_fail_message)) ChildCommentsUiEvent.CommentReportComplete -> InfoDialog( context = this, @@ -217,41 +217,55 @@ class ChildCommentActivity : AppCompatActivity() { ChildCommentsUiEvent.CommentPostFail -> binding.root.showSnackBar(getString(R.string.comments_comments_posting_error_message)) ChildCommentsUiEvent.CommentUpdateFail -> binding.root.showSnackBar(getString(R.string.comments_comments_update_error_message)) ChildCommentsUiEvent.CommentDeleteFail -> binding.root.showSnackBar(getString(R.string.comments_comments_delete_error_message)) - ChildCommentsUiEvent.None -> {} - is ChildCommentsUiEvent.UnexpectedError -> showToast(content.errorMessage) - ChildCommentsUiEvent.CommentPostComplete -> scrollToLastPosition() - } - } + ChildCommentsUiEvent.CommentPostComplete -> { + smoothScrollToLastPosition() + binding.btiwCommentPost.clearText() + } - private fun onChildCommentSave() { - viewModel.saveChildComment(binding.etChildcommentsEditchildcommentcontent.text.toString()) - binding.etChildcommentsEditchildcommentcontent.text.clear() - hideKeyboard() - } + ChildCommentsUiEvent.CommentUpdateComplete -> + binding.stiwCommentUpdate.isVisible = false - private fun scrollToLastPosition() { - binding.rvChildcommentsChildcomments.smoothScrollToPosition(viewModel.comments.value.comments.size) + ChildCommentsUiEvent.IllegalCommentFetch -> InfoDialog( + context = this, + title = getString(R.string.all_fetch_fail_title), + message = getString(R.string.comments_not_exist_comment_message), + buttonLabel = getString(R.string.all_okay), + onButtonClick = { finish() }, + cancelable = false, + ).show() + } } - private fun hideKeyboard() { - inputMethodManager.hideSoftInputFromWindow(binding.root.windowToken, 0) + private fun handleBaseUiEvent(event: CommonUiEvent) { + when (event) { + CommonUiEvent.RequestFailByNetworkError -> binding.root.showSnackBar(getString(R.string.all_network_check_message)) + is CommonUiEvent.Unexpected -> showToast(event.errorMessage) + } } - private fun showKeyboard() { - val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager - @Suppress("DEPRECATION") - imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) + private fun smoothScrollToLastPosition() { + binding.rvChildcommentsChildcomments.smoothScrollToPosition(viewModel.comments.value.comments.size) } companion object { - private const val KEY_FROM_NOTIFICATION = "KEY_FROM_NOTIFICATION" + private const val KEY_HIGHLIGHT_COMMENT_ID = "KEY_HIGHLIGHT_COMMENT_ID" + private const val KEY_FROM_POST_DETAIL = "KEY_FROM_POST_DETAIL" + private const val INVALID_COMMENT_ID: Long = -1 - fun startActivity(context: Context, feedId: Long, parentCommentId: Long) { - val intent = Intent(context, ChildCommentActivity::class.java).apply { - putExtra(KEY_FEED_ID, feedId) - putExtra(KEY_PARENT_COMMENT_ID, parentCommentId) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - } + fun startActivity( + context: Context, + feedId: Long, + parentCommentId: Long, + highlightCommentId: Long = INVALID_COMMENT_ID, + fromPostDetail: Boolean = true, + ) { + val intent = getIntent( + context = context, + feedId = feedId, + parentCommentId = parentCommentId, + highlightCommentId = highlightCommentId, + fromPostDetail = fromPostDetail, + ) context.startActivity(intent) } @@ -259,22 +273,13 @@ class ChildCommentActivity : AppCompatActivity() { context: Context, feedId: Long, parentCommentId: Long, - fromNotification: Boolean = false, - ): Intent = - Intent(context, ChildCommentActivity::class.java).apply { - putExtra(KEY_FEED_ID, feedId) - putExtra(KEY_PARENT_COMMENT_ID, parentCommentId) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - if (fromNotification) putExtra(KEY_FROM_NOTIFICATION, true) - } - } - - inner class ChildCommentOnBackPressedCallback : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - if (intent.getBooleanExtra(KEY_FROM_NOTIFICATION, false)) { - FeedDetailActivity.startActivity(this@ChildCommentActivity, viewModel.feedId) - } - finish() - } + highlightCommentId: Long = INVALID_COMMENT_ID, + fromPostDetail: Boolean = true, + ) = Intent(context, ChildCommentActivity::class.java) + .putExtra(KEY_FEED_ID, feedId) + .putExtra(KEY_PARENT_COMMENT_ID, parentCommentId) + .putExtra(KEY_HIGHLIGHT_COMMENT_ID, highlightCommentId) + .putExtra(KEY_FROM_POST_DETAIL, fromPostDetail) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentViewModel.kt index 0979ec649..8bcf1af65 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentViewModel.kt @@ -12,170 +12,199 @@ import com.emmsale.data.common.retrofit.callAdapter.Success import com.emmsale.data.common.retrofit.callAdapter.Unexpected import com.emmsale.data.repository.interfaces.CommentRepository import com.emmsale.data.repository.interfaces.TokenRepository -import com.emmsale.presentation.common.Event -import com.emmsale.presentation.common.FetchResult +import com.emmsale.presentation.common.CommonUiEvent +import com.emmsale.presentation.common.ScreenUiState import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable +import com.emmsale.presentation.common.livedata.SingleLiveEvent import com.emmsale.presentation.ui.childCommentList.uiState.ChildCommentsUiEvent -import com.emmsale.presentation.ui.childCommentList.uiState.CommentsUiState -import com.emmsale.presentation.ui.feedDetail.uiState.CommentUiState +import com.emmsale.presentation.ui.childCommentList.uiState.ChildCommentsUiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.properties.Delegates.vetoable @HiltViewModel class ChildCommentViewModel @Inject constructor( stateHandle: SavedStateHandle, private val tokenRepository: TokenRepository, private val commentRepository: CommentRepository, -) : ViewModel(), Refreshable { +) : ViewModel() { + + var isAlreadyFirstFetched: Boolean by vetoable(false) { _, _, newValue -> + newValue + } + private val parentCommentId = stateHandle.get(KEY_PARENT_COMMENT_ID)!! + val feedId = stateHandle.get(KEY_FEED_ID)!! private val uid: Long by lazy { tokenRepository.getMyUid()!! } - private val _comments = NotNullMutableLiveData(CommentsUiState.Loading) - val comments: NotNullLiveData = _comments + private val _screenUiState = NotNullMutableLiveData(ScreenUiState.LOADING) + val screenUiState: NotNullLiveData = _screenUiState + + private val _comments = NotNullMutableLiveData(ChildCommentsUiState()) + val comments: NotNullLiveData = _comments private val _editingCommentId = MutableLiveData() val editingCommentId: LiveData = _editingCommentId - val editingCommentContent = _editingCommentId.map { - _comments.value.comments.find { commentUiState -> commentUiState.comment.id == it }?.comment?.content - } - private val _uiEvent: NotNullMutableLiveData> = - NotNullMutableLiveData(Event(ChildCommentsUiEvent.None)) - val uiEvent: NotNullLiveData> = _uiEvent - - override fun refresh() { - viewModelScope.launch { - when (val result = commentRepository.getComment(parentCommentId)) { - is Failure, NetworkError -> - _comments.value = _comments.value.copy(fetchResult = FetchResult.ERROR) - - is Success -> - _comments.value = _comments.value.copy( - fetchResult = FetchResult.SUCCESS, - comments = listOf(CommentUiState.create(uid, result.data)) + - result.data.childComments.map { - CommentUiState.create(uid, it) - }, - ) - - is Unexpected -> - _uiEvent.value = - Event(ChildCommentsUiEvent.UnexpectedError(result.error.toString())) - } - } + val editingCommentContent: LiveData = _editingCommentId.map { + _comments.value.comments + .find { commentUiState -> commentUiState.comment.id == it } + ?.comment + ?.content } - fun saveChildComment(content: String) { - viewModelScope.launch { - _comments.value = _comments.value.copy(fetchResult = FetchResult.LOADING) - when (val result = commentRepository.saveComment(content, feedId, parentCommentId)) { - is Failure -> { - _uiEvent.value = Event(ChildCommentsUiEvent.CommentPostFail) - _comments.value = _comments.value.copy(fetchResult = FetchResult.SUCCESS) - } + private val _commonUiEvent = SingleLiveEvent() + val commonUiEvent: LiveData = _commonUiEvent - NetworkError -> - _comments.value = _comments.value.copy(fetchResult = FetchResult.ERROR) + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent - is Success -> { - refresh() - _uiEvent.value = Event(ChildCommentsUiEvent.CommentPostComplete) - } + init { + fetchComments() + } - is Unexpected -> - _uiEvent.value = - Event(ChildCommentsUiEvent.UnexpectedError(result.error.toString())) + private fun fetchComments(): Job = viewModelScope.launch { + _screenUiState.value = ScreenUiState.LOADING + when (val result = commentRepository.getComment(parentCommentId)) { + is Failure -> _uiEvent.value = ChildCommentsUiEvent.IllegalCommentFetch + NetworkError -> { + _screenUiState.value = ScreenUiState.NETWORK_ERROR + return@launch } + + is Success -> _comments.value = ChildCommentsUiState.create(uid, result.data) + is Unexpected -> + _commonUiEvent.value = CommonUiEvent.Unexpected(result.error.toString()) } + _screenUiState.value = ScreenUiState.NONE } - fun updateComment(commentId: Long, content: String) { - viewModelScope.launch { - _comments.value = _comments.value.copy(fetchResult = FetchResult.LOADING) - when (val result = commentRepository.updateComment(commentId, content)) { - is Failure -> { - _uiEvent.value = Event(ChildCommentsUiEvent.CommentUpdateFail) - _comments.value = _comments.value.copy(fetchResult = FetchResult.SUCCESS) - } - - NetworkError -> - _comments.value = _comments.value.copy(fetchResult = FetchResult.ERROR) - - is Success -> { - _editingCommentId.value = null - refresh() - } - - is Unexpected -> - _uiEvent.value = - Event(ChildCommentsUiEvent.UnexpectedError(result.error.toString())) + fun postChildComment(content: String): Job = viewModelScope.launch { + val loadingJob = launch { + delay(LOADING_DELAY) + _screenUiState.value = ScreenUiState.LOADING + } + when (val result = commentRepository.saveComment(content, feedId, parentCommentId)) { + is Failure -> _uiEvent.value = ChildCommentsUiEvent.CommentPostFail + NetworkError -> _commonUiEvent.value = CommonUiEvent.RequestFailByNetworkError + is Success -> { + refresh().join() + _uiEvent.value = ChildCommentsUiEvent.CommentPostComplete } + + is Unexpected -> + _commonUiEvent.value = CommonUiEvent.Unexpected(result.error.toString()) } + loadingJob.cancel() + _screenUiState.value = ScreenUiState.NONE } - fun deleteComment(commentId: Long) { - viewModelScope.launch { - _comments.value = _comments.value.copy(fetchResult = FetchResult.LOADING) - when (val result = commentRepository.deleteComment(commentId)) { - is Failure -> { - _uiEvent.value = Event(ChildCommentsUiEvent.CommentDeleteFail) - _comments.value = _comments.value.copy(fetchResult = FetchResult.SUCCESS) - } - - NetworkError -> - _comments.value = _comments.value.copy(fetchResult = FetchResult.ERROR) + fun updateComment(commentId: Long, content: String): Job = viewModelScope.launch { + val loadingJob = launch { + delay(LOADING_DELAY) + _screenUiState.value = ScreenUiState.LOADING + } + when (val result = commentRepository.updateComment(commentId, content)) { + is Failure -> _uiEvent.value = ChildCommentsUiEvent.CommentUpdateFail + NetworkError -> _commonUiEvent.value = CommonUiEvent.RequestFailByNetworkError - is Success -> refresh() - is Unexpected -> - _uiEvent.value = - Event(ChildCommentsUiEvent.UnexpectedError(result.error.toString())) + is Success -> { + refresh().join() + _editingCommentId.value = null } + + is Unexpected -> + _commonUiEvent.value = CommonUiEvent.Unexpected(result.error.toString()) } + loadingJob.cancel() + _screenUiState.value = ScreenUiState.NONE } - fun setEditMode(isEditMode: Boolean, commentId: Long = -1) { - if (!isEditMode) { - _editingCommentId.value = null - return + fun deleteComment(commentId: Long): Job = viewModelScope.launch { + val loadingJob = launch { + delay(LOADING_DELAY) + _screenUiState.value = ScreenUiState.LOADING + } + when (val result = commentRepository.deleteComment(commentId)) { + is Failure -> _uiEvent.value = ChildCommentsUiEvent.CommentDeleteFail + NetworkError -> _commonUiEvent.value = CommonUiEvent.RequestFailByNetworkError + is Success -> refresh().join() + is Unexpected -> + _commonUiEvent.value = CommonUiEvent.Unexpected(result.error.toString()) } - _editingCommentId.value = commentId + loadingJob.cancel() + _screenUiState.value = ScreenUiState.NONE } - fun reportComment(commentId: Long) { - viewModelScope.launch { - _comments.value = _comments.value.copy(fetchResult = FetchResult.LOADING) - val authorId = - _comments.value.comments.find { it.comment.id == commentId }!!.comment.authorId - - when (val result = commentRepository.reportComment(commentId, authorId, uid)) { - is Failure -> { - if (result.code == REPORT_DUPLICATE_ERROR_CODE) { - _uiEvent.value = Event(ChildCommentsUiEvent.CommentReportDuplicate) - } else { - _uiEvent.value = Event(ChildCommentsUiEvent.CommentReportFail) - } - _comments.value = _comments.value.copy(fetchResult = FetchResult.SUCCESS) + fun setEditMode(isEditMode: Boolean, commentId: Long = INVALID_COMMENT_ID) { + _editingCommentId.value = if (isEditMode) commentId else null + } + + fun reportComment(commentId: Long): Job = viewModelScope.launch { + val loadingJob = launch { + delay(LOADING_DELAY) + _screenUiState.value = ScreenUiState.LOADING + } + val authorId = + _comments.value.comments.find { it.comment.id == commentId }!!.comment.authorId + when (val result = commentRepository.reportComment(commentId, authorId, uid)) { + is Failure -> { + if (result.code == REPORT_DUPLICATE_ERROR_CODE) { + _uiEvent.value = ChildCommentsUiEvent.CommentReportDuplicate + } else { + _uiEvent.value = ChildCommentsUiEvent.CommentReportFail } + } - NetworkError -> - _comments.value = _comments.value.copy(fetchResult = FetchResult.ERROR) + NetworkError -> _commonUiEvent.value = CommonUiEvent.RequestFailByNetworkError + is Success -> _uiEvent.value = ChildCommentsUiEvent.CommentReportComplete + is Unexpected -> + _commonUiEvent.value = CommonUiEvent.Unexpected(result.error.toString()) + } + loadingJob.cancel() + _screenUiState.value = ScreenUiState.NONE + } - is Success -> _uiEvent.value = Event(ChildCommentsUiEvent.CommentReportComplete) - is Unexpected -> - _uiEvent.value = - Event(ChildCommentsUiEvent.UnexpectedError(result.error.toString())) + fun refresh(): Job = viewModelScope.launch { + when (val result = commentRepository.getComment(parentCommentId)) { + is Failure -> {} + NetworkError -> { + _commonUiEvent.value = CommonUiEvent.RequestFailByNetworkError + return@launch } + + is Success -> _comments.value = ChildCommentsUiState.create(uid, result.data) + is Unexpected -> + _commonUiEvent.value = CommonUiEvent.Unexpected(result.error.toString()) } + _screenUiState.value = ScreenUiState.NONE + } + + fun highlight(commentId: Long) { + val comment = _comments.value.comments.find { it.comment.id == commentId } ?: return + if (comment.isHighlight) return + _comments.value = _comments.value.highlight(commentId) + } + + fun unhighlight(commentId: Long) { + val comment = _comments.value.comments.find { it.comment.id == commentId } ?: return + if (!comment.isHighlight) return + _comments.value = _comments.value.unhighlight(commentId) } companion object { const val KEY_FEED_ID = "KEY_FEED_ID" const val KEY_PARENT_COMMENT_ID = "KEY_PARENT_COMMENT_ID" + private const val INVALID_COMMENT_ID: Long = -1 + private const val REPORT_DUPLICATE_ERROR_CODE = 400 + + private const val LOADING_DELAY: Long = 1000 } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/ChildCommentAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/ChildCommentAdapter.kt deleted file mode 100644 index 51470feb3..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/ChildCommentAdapter.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.emmsale.presentation.ui.childCommentList.recyclerView - -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.emmsale.presentation.ui.childCommentList.uiState.CommentUiState - -class ChildCommentAdapter( - private val showProfile: (authorId: Long) -> Unit, - private val editComment: (commentId: Long) -> Unit, - private val deleteComment: (commentId: Long) -> Unit, - private val reportComment: (commentId: Long) -> Unit, -) : ListAdapter(diffUtil) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return if (viewType == PARENT_COMMENT_VIEW_TYPE) { - ParentCommentViewHolder.create( - parent, - showProfile, - editComment, - deleteComment, - reportComment, - ) - } else { - ChildCommentViewHolder.create( - parent, - showProfile, - editComment, - deleteComment, - reportComment, - ) - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is ParentCommentViewHolder -> holder.bind(getItem(position)) - is ChildCommentViewHolder -> holder.bind(getItem(position)) - } - } - - override fun getItemViewType(position: Int): Int = - if (position == 0) PARENT_COMMENT_VIEW_TYPE else CHILD_COMMENT_VIEW_TYPE - - companion object { - private const val PARENT_COMMENT_VIEW_TYPE = 1 - private const val CHILD_COMMENT_VIEW_TYPE = 2 - - private val diffUtil = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: CommentUiState, - newItem: CommentUiState, - ): Boolean = oldItem.id == newItem.id - - override fun areContentsTheSame( - oldItem: CommentUiState, - newItem: CommentUiState, - ): Boolean = oldItem == newItem - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/ChildCommentRecyclerViewDivider.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/ChildCommentRecyclerViewDivider.kt deleted file mode 100644 index 41c0761dd..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/ChildCommentRecyclerViewDivider.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.emmsale.presentation.ui.childCommentList.recyclerView - -import android.content.Context -import android.graphics.Canvas -import android.graphics.drawable.Drawable -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.RecyclerView -import com.emmsale.R - -class ChildCommentRecyclerViewDivider(context: Context) : RecyclerView.ItemDecoration() { - private val divider: Drawable by lazy { - ContextCompat.getDrawable(context, R.drawable.bg_all_vertical_divider) - ?: throw IllegalStateException("bg_all_vertical_divider 리소스를 찾을 수 없습니다. drawable 리소스를 확인해주세요.") - } - - override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { - super.onDraw(c, parent, state) - - val left = parent.paddingStart - val right = parent.width - parent.paddingEnd - - for (i in 0 until parent.childCount - 1) { - val child = parent.getChildAt(i) - val params = child.layoutParams as RecyclerView.LayoutParams - - val top = child.bottom + params.bottomMargin - val bottom = top + divider.intrinsicHeight - - divider.setBounds(left, top, right, bottom) - divider.draw(c) - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/ChildCommentViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/ChildCommentViewHolder.kt deleted file mode 100644 index 0307fd416..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/ChildCommentViewHolder.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.emmsale.presentation.ui.childCommentList.recyclerView - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.emmsale.R -import com.emmsale.databinding.ItemChildcommentsChildcommentBinding -import com.emmsale.presentation.common.views.WarningDialog -import com.emmsale.presentation.common.views.bottomMenuDialog.BottomMenuDialog -import com.emmsale.presentation.common.views.bottomMenuDialog.MenuItemType -import com.emmsale.presentation.ui.childCommentList.uiState.CommentUiState - -class ChildCommentViewHolder( - private val binding: ItemChildcommentsChildcommentBinding, - private val showProfile: (authorId: Long) -> Unit, - private val editComment: (commentId: Long) -> Unit, - private val deleteComment: (commentId: Long) -> Unit, - private val reportComment: (commentId: Long) -> Unit, -) : RecyclerView.ViewHolder(binding.root) { - - private val bottomMenuDialog = BottomMenuDialog(binding.root.context) - - init { - binding.ivChildcommentAuthorImage.setOnClickListener { - if (binding.comment?.isDeleted == true) return@setOnClickListener - showProfile(binding.comment?.authorId ?: return@setOnClickListener) - } - } - - fun bind(comment: CommentUiState) { - binding.comment = comment - - initMenuButton(comment) - } - - private fun initMenuButton(comment: CommentUiState) { - bottomMenuDialog.resetMenu() - if (comment.isUpdatable) bottomMenuDialog.addUpdateButton() - if (comment.isDeletable) bottomMenuDialog.addDeleteButton() - if (comment.isReportable) bottomMenuDialog.addReportButton() - - binding.ivChildcommentMenubutton.setOnClickListener { bottomMenuDialog.show() } - } - - private fun BottomMenuDialog.addUpdateButton() { - addMenuItemBelow(context.getString(R.string.all_update_button_label)) { - editComment(binding.comment?.id ?: return@addMenuItemBelow) - } - } - - private fun BottomMenuDialog.addDeleteButton() { - addMenuItemBelow(context.getString(R.string.all_delete_button_label)) { onDeleteButtonClick() } - } - - private fun onDeleteButtonClick() { - val context = binding.root.context - WarningDialog( - context = context, - title = context.getString(R.string.commentdeletedialog_title), - message = context.getString(R.string.commentdeletedialog_message), - positiveButtonLabel = context.getString(R.string.commentdeletedialog_positive_button_label), - negativeButtonLabel = context.getString(R.string.commentdeletedialog_negative_button_label), - onPositiveButtonClick = { - deleteComment(binding.comment?.id ?: return@WarningDialog) - }, - ).show() - } - - private fun BottomMenuDialog.addReportButton() { - addMenuItemBelow( - context.getString(R.string.all_report_button_label), - MenuItemType.IMPORTANT, - ) { reportComment(binding.comment?.id ?: return@addMenuItemBelow) } - } - - companion object { - fun create( - parent: ViewGroup, - showProfile: (authorId: Long) -> Unit, - editComment: (commentId: Long) -> Unit, - deleteComment: (commentId: Long) -> Unit, - reportComment: (commentId: Long) -> Unit, - ): ChildCommentViewHolder { - val binding = ItemChildcommentsChildcommentBinding - .inflate(LayoutInflater.from(parent.context), parent, false) - - return ChildCommentViewHolder( - binding, - showProfile, - editComment, - deleteComment, - reportComment, - ) - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/ParentCommentViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/ParentCommentViewHolder.kt deleted file mode 100644 index 2919b346a..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/ParentCommentViewHolder.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.emmsale.presentation.ui.childCommentList.recyclerView - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.emmsale.R -import com.emmsale.databinding.ItemChildcommentsParentcommentBinding -import com.emmsale.presentation.common.views.WarningDialog -import com.emmsale.presentation.common.views.bottomMenuDialog.BottomMenuDialog -import com.emmsale.presentation.common.views.bottomMenuDialog.MenuItemType -import com.emmsale.presentation.ui.childCommentList.uiState.CommentUiState - -class ParentCommentViewHolder( - private val binding: ItemChildcommentsParentcommentBinding, - private val showProfile: (authorId: Long) -> Unit, - private val editComment: (commentId: Long) -> Unit, - private val deleteComment: (commentId: Long) -> Unit, - private val reportComment: (commentId: Long) -> Unit, -) : RecyclerView.ViewHolder(binding.root) { - - private val bottomMenuDialog = BottomMenuDialog(binding.root.context) - - init { - binding.ivChildcommentsParentCommentAuthorImage.setOnClickListener { - if (binding.comment?.isDeleted == true) return@setOnClickListener - showProfile(binding.comment?.authorId ?: return@setOnClickListener) - } - } - - fun bind(comment: CommentUiState) { - binding.comment = comment - - initMenuButton(comment) - } - - private fun initMenuButton(comment: CommentUiState) { - bottomMenuDialog.resetMenu() - if (comment.isUpdatable) bottomMenuDialog.addUpdateButton() - if (comment.isDeletable) bottomMenuDialog.addDeleteButton() - if (comment.isReportable) bottomMenuDialog.addReportButton() - - binding.ivChildcommentsParentcommentmenubutton.setOnClickListener { bottomMenuDialog.show() } - } - - private fun BottomMenuDialog.addUpdateButton() { - addMenuItemBelow(context.getString(R.string.all_update_button_label)) { - editComment(binding.comment?.id ?: return@addMenuItemBelow) - } - } - - private fun BottomMenuDialog.addDeleteButton() { - addMenuItemBelow(context.getString(R.string.all_delete_button_label)) { onDeleteButtonClick() } - } - - private fun onDeleteButtonClick() { - val context = binding.root.context - WarningDialog( - context = context, - title = context.getString(R.string.commentdeletedialog_title), - message = context.getString(R.string.commentdeletedialog_message), - positiveButtonLabel = context.getString(R.string.commentdeletedialog_positive_button_label), - negativeButtonLabel = context.getString(R.string.commentdeletedialog_negative_button_label), - onPositiveButtonClick = { - deleteComment(binding.comment?.id ?: return@WarningDialog) - }, - ).show() - } - - private fun BottomMenuDialog.addReportButton() { - addMenuItemBelow( - context.getString(R.string.all_report_button_label), - MenuItemType.IMPORTANT, - ) { reportComment(binding.comment?.id ?: return@addMenuItemBelow) } - } - - companion object { - fun create( - parent: ViewGroup, - showProfile: (authorId: Long) -> Unit, - updateComment: (commentId: Long) -> Unit, - deleteComment: (commendId: Long) -> Unit, - reportComment: (commentId: Long) -> Unit, - ): ParentCommentViewHolder { - val binding = ItemChildcommentsParentcommentBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false, - ) - - return ParentCommentViewHolder( - binding, - showProfile, - updateComment, - deleteComment, - reportComment, - ) - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/ChildCommentsUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/ChildCommentsUiEvent.kt index 3bcc95c2c..d0b274375 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/ChildCommentsUiEvent.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/ChildCommentsUiEvent.kt @@ -1,8 +1,6 @@ package com.emmsale.presentation.ui.childCommentList.uiState sealed interface ChildCommentsUiEvent { - object None : ChildCommentsUiEvent - data class UnexpectedError(val errorMessage: String) : ChildCommentsUiEvent object CommentPostFail : ChildCommentsUiEvent object CommentUpdateFail : ChildCommentsUiEvent object CommentDeleteFail : ChildCommentsUiEvent @@ -10,4 +8,6 @@ sealed interface ChildCommentsUiEvent { object CommentReportFail : ChildCommentsUiEvent object CommentReportComplete : ChildCommentsUiEvent object CommentPostComplete : ChildCommentsUiEvent + object CommentUpdateComplete : ChildCommentsUiEvent + object IllegalCommentFetch : ChildCommentsUiEvent } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/ChildCommentsUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/ChildCommentsUiState.kt index e969a87f0..615e781f5 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/ChildCommentsUiState.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/ChildCommentsUiState.kt @@ -1,52 +1,25 @@ package com.emmsale.presentation.ui.childCommentList.uiState import com.emmsale.data.model.Comment +import com.emmsale.presentation.ui.feedDetail.uiState.CommentUiState -data class ChildCommentsUiState( - val isLoading: Boolean, - val isError: Boolean, - val parentComment: CommentUiState, - val childComments: List, -) { +data class ChildCommentsUiState(val comments: List = emptyList()) { - fun changeToLoadingState(): ChildCommentsUiState = copy( - isLoading = true, - isError = false, + fun highlight(commentId: Long) = copy( + comments = comments.map { if (it.comment.id == commentId) it.highlight() else it }, ) - fun changeToErrorState(): ChildCommentsUiState = copy( - isLoading = false, - isError = true, - ) - - fun changeChildCommentsState( - comment: Comment, - loginMemberId: Long, - ): ChildCommentsUiState = copy( - isLoading = false, - isError = false, - parentComment = CommentUiState.create(comment, loginMemberId), - childComments = comment.childComments.map { CommentUiState.create(it, loginMemberId) }, + fun unhighlight(commentId: Long) = copy( + comments = comments.map { if (it.comment.id == commentId) it.unhighlight() else it }, ) companion object { - val FIRST_LOADING = ChildCommentsUiState( - isLoading = true, - isError = false, - parentComment = CommentUiState( - authorId = -1, - authorName = "", - authorImageUrl = "", - lastModifiedDate = "", - isUpdated = false, - id = -1, - content = "", - isUpdatable = false, - isDeletable = false, - isReportable = false, - isDeleted = false, - ), - childComments = listOf(), + fun create( + uid: Long, + parentComment: Comment, + ) = ChildCommentsUiState( + comments = listOf(CommentUiState.create(uid, parentComment)) + + parentComment.childComments.map { CommentUiState.create(uid, it) }, ) } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/CommentUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/CommentUiState.kt deleted file mode 100644 index 84c6e0379..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/CommentUiState.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.emmsale.presentation.ui.childCommentList.uiState - -import com.emmsale.data.model.Comment -import java.time.format.DateTimeFormatter - -data class CommentUiState( - val authorId: Long, - val authorName: String, - val authorImageUrl: String, - val lastModifiedDate: String, - val isUpdated: Boolean, - val id: Long, - val content: String, - val isUpdatable: Boolean, - val isDeletable: Boolean, - val isReportable: Boolean, - val isDeleted: Boolean, -) { - companion object { - private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") - - fun create( - comment: Comment, - loginMemberId: Long, - ) = CommentUiState( - authorId = comment.authorId, - authorName = comment.authorName, - authorImageUrl = comment.authorImageUrl, - lastModifiedDate = comment.updatedAt.format(dateTimeFormatter), - isUpdated = comment.createdAt != comment.updatedAt, - id = comment.id, - content = comment.content, - isUpdatable = comment.authorId == loginMemberId, - isDeletable = comment.authorId == loginMemberId, - isReportable = comment.authorId != loginMemberId, - isDeleted = comment.deleted, - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/CommentsUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/CommentsUiState.kt deleted file mode 100644 index d12df16af..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/CommentsUiState.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.emmsale.presentation.ui.childCommentList.uiState - -import com.emmsale.presentation.common.FetchResult -import com.emmsale.presentation.common.FetchResultUiState -import com.emmsale.presentation.ui.feedDetail.uiState.CommentUiState - -data class CommentsUiState( - override val fetchResult: FetchResult, - val comments: List, -) : FetchResultUiState() { - companion object { - val Loading: CommentsUiState = CommentsUiState( - fetchResult = FetchResult.LOADING, - comments = emptyList(), - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/CommentFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/CommentFragment.kt deleted file mode 100644 index 6b105988c..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/CommentFragment.kt +++ /dev/null @@ -1,224 +0,0 @@ -package com.emmsale.presentation.ui.commentList - -import android.content.Context -import android.content.Context.INPUT_METHOD_SERVICE -import android.os.Bundle -import android.view.View -import android.view.inputmethod.InputMethodManager -import androidx.fragment.app.viewModels -import com.emmsale.R -import com.emmsale.databinding.FragmentCommentsBinding -import com.emmsale.presentation.base.BaseFragment -import com.emmsale.presentation.common.extension.showSnackBar -import com.emmsale.presentation.common.firebase.analytics.FirebaseAnalyticsDelegate -import com.emmsale.presentation.common.firebase.analytics.FirebaseAnalyticsDelegateImpl -import com.emmsale.presentation.common.views.InfoDialog -import com.emmsale.presentation.common.views.WarningDialog -import com.emmsale.presentation.ui.childCommentList.ChildCommentActivity -import com.emmsale.presentation.ui.commentList.CommentViewModel.Companion.KEY_EVENT_ID -import com.emmsale.presentation.ui.commentList.recyclerView.CommentRecyclerViewDivider -import com.emmsale.presentation.ui.commentList.recyclerView.CommentsAdapter -import com.emmsale.presentation.ui.commentList.uiState.CommentsUiEvent -import com.emmsale.presentation.ui.commentList.uiState.CommentsUiState -import com.emmsale.presentation.ui.eventDetail.EventDetailActivity -import com.emmsale.presentation.ui.login.LoginActivity -import com.emmsale.presentation.ui.profile.ProfileActivity -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class CommentFragment : - BaseFragment(), - FirebaseAnalyticsDelegate by FirebaseAnalyticsDelegateImpl("comment") { - override val layoutResId: Int = R.layout.fragment_comments - private val viewModel: CommentViewModel by viewModels() - - private val inputMethodManager: InputMethodManager by lazy { - requireContext().getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager - } - - private lateinit var eventDetailActivity: EventDetailActivity - private var isSaveButtonClick = false - - override fun onAttach(context: Context) { - super.onAttach(context) - eventDetailActivity = context as EventDetailActivity - registerScreen(this) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initDataBinding() - initCommentsRecyclerView() - setupUiLogic() - } - - override fun onStart() { - super.onStart() - viewModel.refresh() - } - - private fun initDataBinding() { - binding.viewModel = viewModel - binding.cancelUpdateComment = ::cancelUpdateComment - binding.updateComment = ::updateComment - } - - private fun cancelUpdateComment() { - viewModel.setEditMode(false) - hideKeyboard() - } - - private fun updateComment() { - val commentId = viewModel.editingCommentId.value ?: return - val content = binding.etCommentsCommentUpdate.text.toString() - viewModel.updateComment(commentId, content) - hideKeyboard() - } - - private fun initCommentsRecyclerView() { - binding.rvCommentsComments.apply { - adapter = CommentsAdapter( - showProfile = ::showProfile, - showChildComments = ::showChildComments, - editComment = ::editComment, - deleteComment = ::deleteComment, - reportComment = ::reportComment, - ) - itemAnimator = null - addItemDecoration(CommentRecyclerViewDivider(requireContext())) - } - } - - private fun showProfile(authorId: Long) { - ProfileActivity.startActivity(context ?: return, authorId) - } - - private fun showChildComments(commentId: Long) { - ChildCommentActivity.startActivity(requireContext(), viewModel.eventId, commentId) - } - - private fun editComment(commentId: Long) { - viewModel.setEditMode(true, commentId) - binding.etCommentsCommentUpdate.requestFocus() - showKeyboard() - } - - private fun deleteComment(commentId: Long) { - viewModel.deleteComment(commentId) - } - - private fun reportComment(commentId: Long) { - val context = context ?: return - WarningDialog( - context = context, - title = context.getString(R.string.all_report_dialog_title), - message = context.getString(R.string.comments_comment_report_dialog_message), - positiveButtonLabel = context.getString(R.string.all_report_dialog_positive_button_label), - negativeButtonLabel = context.getString(R.string.commentdeletedialog_negative_button_label), - onPositiveButtonClick = { - viewModel.reportComment(commentId) - }, - ).show() - } - - private fun setupUiLogic() { - setupLoginUiLogic() - setupCommentsUiLogic() - setupEditingCommentUiLogic() - setupEventUiLogic() - } - - private fun setupLoginUiLogic() { - viewModel.isLogin.observe(viewLifecycleOwner) { - handleNotLogin(it) - } - } - - private fun setupCommentsUiLogic() { - viewModel.comments.observe(viewLifecycleOwner) { - handleComments(it) - handleCommentEditing() - } - } - - private fun handleNotLogin(isLogin: Boolean) { - if (!isLogin) { - LoginActivity.startActivity(requireContext()) - activity?.finish() - } - } - - private fun handleComments(comments: CommentsUiState) { - (binding.rvCommentsComments.adapter as CommentsAdapter).submitList(comments.comments) - } - - private fun handleCommentEditing() { - binding.tvCommentsPostcommentbutton.setOnClickListener { onCommentSave() } - } - - private fun onCommentSave() { - isSaveButtonClick = true - viewModel.saveComment(binding.etCommentsPostComment.text.toString()) - binding.etCommentsPostComment.apply { - text.clear() - } - hideKeyboard() - } - - private fun hideKeyboard() { - inputMethodManager.hideSoftInputFromWindow(binding.root.windowToken, 0) - } - - private fun showKeyboard() { - @Suppress("DEPRECATION") - inputMethodManager.toggleSoftInput( - InputMethodManager.SHOW_FORCED, - InputMethodManager.HIDE_IMPLICIT_ONLY, - ) - } - - private fun setupEditingCommentUiLogic() { - viewModel.editingCommentContent.observe(viewLifecycleOwner) { - if (it == null) return@observe - binding.etCommentsCommentUpdate.setText(it) - } - } - - private fun setupEventUiLogic() { - viewModel.event.observe(viewLifecycleOwner) { - handleEvents(it) - } - } - - private fun handleEvents(event: CommentsUiEvent?) { - if (event == null) return - when (event) { - CommentsUiEvent.REPORT_ERROR -> binding.root.showSnackBar(R.string.all_report_fail_message) - CommentsUiEvent.REPORT_COMPLETE -> InfoDialog( - context = context ?: return, - title = getString(R.string.all_report_complete_dialog_title), - message = getString(R.string.all_report_complete_dialog_message), - buttonLabel = getString(R.string.all_okay), - ).show() - - CommentsUiEvent.POST_ERROR -> binding.root.showSnackBar(getString(R.string.comments_comments_posting_error_message)) - CommentsUiEvent.UPDATE_ERROR -> binding.root.showSnackBar(getString(R.string.comments_comments_update_error_message)) - CommentsUiEvent.DELETE_ERROR -> binding.root.showSnackBar(getString(R.string.comments_comments_delete_error_message)) - CommentsUiEvent.REPORT_DUPLICATE -> InfoDialog( - context = context ?: return, - title = getString(R.string.all_report_duplicate_dialog_title), - message = getString(R.string.all_report_duplicate_message), - buttonLabel = getString(R.string.all_okay), - ).show() - } - viewModel.removeEvent() - } - - companion object { - fun create(eventId: Long): CommentFragment = CommentFragment().apply { - arguments = Bundle().apply { - putLong(KEY_EVENT_ID, eventId) - } - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/CommentViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/CommentViewModel.kt deleted file mode 100644 index 17a2d2b1a..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/CommentViewModel.kt +++ /dev/null @@ -1,151 +0,0 @@ -package com.emmsale.presentation.ui.commentList - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.map -import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected -import com.emmsale.data.repository.interfaces.CommentRepository -import com.emmsale.data.repository.interfaces.TokenRepository -import com.emmsale.presentation.common.firebase.analytics.logComment -import com.emmsale.presentation.common.livedata.NotNullLiveData -import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable -import com.emmsale.presentation.ui.commentList.uiState.CommentsUiEvent -import com.emmsale.presentation.ui.commentList.uiState.CommentsUiState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class CommentViewModel @Inject constructor( - stateHandle: SavedStateHandle, - private val tokenRepository: TokenRepository, - private val commentRepository: CommentRepository, -) : ViewModel(), Refreshable { - val eventId = requireNotNull(stateHandle.get(KEY_EVENT_ID)) { - "[ERROR] 컨퍼런스의 댓글 프래그먼트는 컨퍼런스 아이디를 알아야 합니다. 로직을 다시 확인해주세요" - } - - private val _isLogin = NotNullMutableLiveData(true) - val isLogin: NotNullLiveData = _isLogin - - private val _comments = NotNullMutableLiveData(CommentsUiState.Loading) - val comments: NotNullLiveData = _comments - - private val _editingCommentId = MutableLiveData() - val editingCommentId: LiveData = _editingCommentId - val editingCommentContent = - _editingCommentId.map { _comments.value.comments.find { comment -> comment.id == it }?.content } - - private val _event = MutableLiveData(null) - val event: LiveData = _event - - override fun refresh() { - _comments.value = _comments.value.changeToLoadingState() - viewModelScope.launch { - val token = tokenRepository.getToken() - if (token == null) { - _isLogin.value = false - return@launch - } - - when (val result = commentRepository.getComments(eventId)) { - is Failure, NetworkError -> - _comments.value = _comments.value.changeToFetchingErrorState() - - is Success -> - _comments.value = - _comments.value.changeCommentsState(result.data, token.uid) - - is Unexpected -> throw Throwable(result.error) - } - } - } - - fun saveComment(content: String) { - viewModelScope.launch { - when (val result = commentRepository.saveComment(content, eventId)) { - is Failure, NetworkError -> { - _event.value = CommentsUiEvent.POST_ERROR - logComment(content, eventId) - } - - is Success -> refresh() - is Unexpected -> throw Throwable(result.error) - } - } - } - - fun updateComment(commentId: Long, content: String) { - viewModelScope.launch { - when (val result = commentRepository.updateComment(commentId, content)) { - is Failure, NetworkError -> _event.value = CommentsUiEvent.UPDATE_ERROR - is Success -> { - _editingCommentId.value = null - refresh() - } - - is Unexpected -> throw Throwable(result.error) - } - } - } - - fun deleteComment(commentId: Long) { - viewModelScope.launch { - when (val result = commentRepository.deleteComment(commentId)) { - is Failure, NetworkError -> _event.value = CommentsUiEvent.DELETE_ERROR - is Success -> refresh() - is Unexpected -> throw Throwable(result.error) - } - } - } - - fun setEditMode(isEditMode: Boolean, commentId: Long = -1) { - if (!isEditMode) { - _editingCommentId.value = null - return - } - _editingCommentId.value = commentId - } - - fun reportComment(commentId: Long) { - viewModelScope.launch { - val token = tokenRepository.getToken() - if (token == null) { - _isLogin.value = false - return@launch - } - val authorId = - _comments.value.comments.find { it.id == commentId }?.authorId ?: return@launch - when (val result = commentRepository.reportComment(commentId, authorId, token.uid)) { - is Failure -> { - if (result.code == REPORT_DUPLICATE_ERROR_CODE) { - _event.value = CommentsUiEvent.REPORT_DUPLICATE - } else { - _event.value = CommentsUiEvent.REPORT_ERROR - } - } - - NetworkError -> _event.value = CommentsUiEvent.REPORT_ERROR - is Success -> _event.value = CommentsUiEvent.REPORT_COMPLETE - is Unexpected -> throw Throwable(result.error) - } - } - } - - fun removeEvent() { - _event.value = null - } - - companion object { - const val KEY_EVENT_ID = "KEY_EVENT_ID" - - private const val REPORT_DUPLICATE_ERROR_CODE = 400 - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/recyclerView/CommentRecyclerViewDivider.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/recyclerView/CommentRecyclerViewDivider.kt deleted file mode 100644 index 519b4b208..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/recyclerView/CommentRecyclerViewDivider.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.emmsale.presentation.ui.commentList.recyclerView - -import android.content.Context -import android.graphics.Canvas -import android.graphics.drawable.Drawable -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.RecyclerView -import com.emmsale.R - -class CommentRecyclerViewDivider(context: Context) : RecyclerView.ItemDecoration() { - private val divider: Drawable by lazy { - ContextCompat.getDrawable(context, R.drawable.bg_all_vertical_divider) - ?: throw IllegalStateException("bg_all_vertical_divider 리소스를 찾을 수 없습니다. drawable 리소스를 확인해주세요.") - } - - override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { - super.onDraw(c, parent, state) - - val left = parent.paddingStart - val right = parent.width - parent.paddingEnd - - for (i in 0 until parent.childCount - 1) { - val child = parent.getChildAt(i) - val params = child.layoutParams as RecyclerView.LayoutParams - - val top = child.bottom + params.bottomMargin - val bottom = top + divider.intrinsicHeight - - divider.setBounds(left, top, right, bottom) - divider.draw(c) - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/recyclerView/CommentViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/recyclerView/CommentViewHolder.kt deleted file mode 100644 index 8a66299e1..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/recyclerView/CommentViewHolder.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.emmsale.presentation.ui.commentList.recyclerView - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.emmsale.R -import com.emmsale.databinding.ItemCommentsCommentsBinding -import com.emmsale.presentation.common.views.WarningDialog -import com.emmsale.presentation.common.views.bottomMenuDialog.BottomMenuDialog -import com.emmsale.presentation.common.views.bottomMenuDialog.MenuItemType -import com.emmsale.presentation.ui.commentList.uiState.CommentUiState - -class CommentViewHolder( - private val binding: ItemCommentsCommentsBinding, - private val showProfile: (authorId: Long) -> Unit, - private val showChildComments: (parentCommentId: Long) -> Unit, - private val editComment: (commentId: Long) -> Unit, - private val deleteComment: (commentId: Long) -> Unit, - private val reportComment: (commentId: Long) -> Unit, -) : RecyclerView.ViewHolder(binding.root) { - - private val bottomMenuDialog = BottomMenuDialog(binding.root.context) - - init { - binding.root.setOnClickListener { - showChildComments(binding.comment?.id ?: return@setOnClickListener) - } - binding.ivCommentAuthorImage.setOnClickListener { - showProfile(binding.comment?.authorId ?: return@setOnClickListener) - } - } - - fun bind(comment: CommentUiState) { - binding.comment = comment - - initMenuButton(comment) - } - - private fun initMenuButton(comment: CommentUiState) { - bottomMenuDialog.resetMenu() - if (comment.isUpdatable) bottomMenuDialog.addUpdateButton() - if (comment.isDeletable) bottomMenuDialog.addDeleteButton() - if (comment.isReportable) bottomMenuDialog.addReportButton() - - binding.ivCommentMenubutton.setOnClickListener { bottomMenuDialog.show() } - } - - private fun BottomMenuDialog.addUpdateButton() { - addMenuItemBelow(context.getString(R.string.all_update_button_label)) { - editComment(binding.comment?.id ?: return@addMenuItemBelow) - } - } - - private fun BottomMenuDialog.addDeleteButton() { - addMenuItemBelow(context.getString(R.string.all_delete_button_label)) { onDeleteButtonClick() } - } - - private fun onDeleteButtonClick() { - val context = binding.root.context - WarningDialog( - context = context, - title = context.getString(R.string.commentdeletedialog_title), - message = context.getString(R.string.commentdeletedialog_message), - positiveButtonLabel = context.getString(R.string.commentdeletedialog_positive_button_label), - negativeButtonLabel = context.getString(R.string.commentdeletedialog_negative_button_label), - onPositiveButtonClick = { - deleteComment(binding.comment?.id ?: return@WarningDialog) - }, - ).show() - } - - private fun BottomMenuDialog.addReportButton() { - addMenuItemBelow( - context.getString(R.string.all_report_button_label), - MenuItemType.IMPORTANT, - ) { reportComment(binding.comment?.id ?: return@addMenuItemBelow) } - } - - companion object { - fun create( - parent: ViewGroup, - showProfile: (authorId: Long) -> Unit, - showChildComments: (parentCommentId: Long) -> Unit, - editComment: (commentId: Long) -> Unit, - deleteComment: (commentId: Long) -> Unit, - reportComment: (commentId: Long) -> Unit, - ): CommentViewHolder { - val binding = ItemCommentsCommentsBinding - .inflate(LayoutInflater.from(parent.context), parent, false) - - return CommentViewHolder( - binding, - showProfile, - showChildComments, - editComment, - deleteComment, - reportComment, - ) - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/recyclerView/CommentsAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/recyclerView/CommentsAdapter.kt deleted file mode 100644 index 63518695b..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/recyclerView/CommentsAdapter.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.emmsale.presentation.ui.commentList.recyclerView - -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import com.emmsale.presentation.ui.commentList.uiState.CommentUiState - -class CommentsAdapter( - private val showProfile: (authorId: Long) -> Unit, - private val showChildComments: (parentCommentId: Long) -> Unit, - private val editComment: (commentId: Long) -> Unit, - private val deleteComment: (commentId: Long) -> Unit, - private val reportComment: (commentId: Long) -> Unit, -) : ListAdapter(diffUtil) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentViewHolder { - return CommentViewHolder.create( - parent = parent, - showProfile = showProfile, - showChildComments = showChildComments, - editComment = editComment, - deleteComment = deleteComment, - reportComment = reportComment, - ) - } - - override fun onBindViewHolder(holder: CommentViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - companion object { - private val diffUtil = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: CommentUiState, - newItem: CommentUiState, - ): Boolean = oldItem.id == newItem.id - - override fun areContentsTheSame( - oldItem: CommentUiState, - newItem: CommentUiState, - ): Boolean = oldItem == newItem - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/uiState/CommentUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/uiState/CommentUiState.kt deleted file mode 100644 index 6c9ea029a..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/uiState/CommentUiState.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.emmsale.presentation.ui.commentList.uiState - -import com.emmsale.data.model.Comment -import java.time.format.DateTimeFormatter - -data class CommentUiState( - val authorId: Long, - val authorImageUrl: String, - val authorName: String, - val lastModifiedDate: String, - val isUpdated: Boolean, - val id: Long, - val content: String, - val childCommentsCount: Int, - val isUpdatable: Boolean, - val isDeletable: Boolean, - val isReportable: Boolean, - val isDeleted: Boolean, -) { - companion object { - private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") - - fun create( - comment: Comment, - loginMemberId: Long, - ) = CommentUiState( - authorId = comment.authorId, - authorImageUrl = comment.authorImageUrl, - authorName = comment.authorName, - lastModifiedDate = comment.updatedAt.format(dateTimeFormatter), - isUpdated = comment.createdAt != comment.updatedAt, - id = comment.id, - content = comment.content, - childCommentsCount = comment.childComments.size, - isUpdatable = comment.authorId == loginMemberId, - isDeletable = comment.authorId == loginMemberId, - isReportable = comment.authorId != loginMemberId, - isDeleted = comment.deleted, - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/uiState/CommentsUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/uiState/CommentsUiEvent.kt deleted file mode 100644 index c392ffcfd..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/uiState/CommentsUiEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.emmsale.presentation.ui.commentList.uiState - -enum class CommentsUiEvent { - POST_ERROR, UPDATE_ERROR, DELETE_ERROR, REPORT_ERROR, REPORT_COMPLETE, REPORT_DUPLICATE -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/uiState/CommentsUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/uiState/CommentsUiState.kt deleted file mode 100644 index 760f131ab..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/commentList/uiState/CommentsUiState.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.emmsale.presentation.ui.commentList.uiState - -import com.emmsale.data.model.Comment - -data class CommentsUiState( - val isLoading: Boolean, - val isError: Boolean, - val comments: List, -) { - - fun changeToLoadingState(): CommentsUiState = copy( - isLoading = true, - isError = false, - ) - - fun changeToFetchingErrorState(): CommentsUiState = copy( - isLoading = false, - isError = true, - ) - - fun changeCommentsState(comments: List, loginMemberId: Long): CommentsUiState = copy( - isLoading = false, - isError = false, - comments = comments.map { - CommentUiState.create(comment = it, loginMemberId = loginMemberId) - }, - ) - - companion object { - val Loading = CommentsUiState( - isLoading = true, - isError = false, - comments = listOf(), - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailActivity.kt index a346aceac..12ece7cda 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailActivity.kt @@ -7,17 +7,19 @@ import android.view.inputmethod.InputMethodManager import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.ConcatAdapter +import androidx.recyclerview.widget.RecyclerView import com.emmsale.R import com.emmsale.databinding.ActivityFeedDetailBinding import com.emmsale.presentation.common.Event +import com.emmsale.presentation.common.extension.showKeyboard import com.emmsale.presentation.common.extension.showSnackBar import com.emmsale.presentation.common.extension.showToast +import com.emmsale.presentation.common.recyclerView.DividerItemDecoration import com.emmsale.presentation.common.views.InfoDialog import com.emmsale.presentation.common.views.WarningDialog import com.emmsale.presentation.common.views.bottomMenuDialog.BottomMenuDialog import com.emmsale.presentation.common.views.bottomMenuDialog.MenuItemType import com.emmsale.presentation.ui.childCommentList.ChildCommentActivity -import com.emmsale.presentation.ui.commentList.recyclerView.CommentRecyclerViewDivider import com.emmsale.presentation.ui.feedDetail.FeedDetailViewModel.Companion.KEY_FEED_ID import com.emmsale.presentation.ui.feedDetail.recyclerView.CommentsAdapter import com.emmsale.presentation.ui.feedDetail.recyclerView.FeedDetailAdapter @@ -28,8 +30,13 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class FeedDetailActivity : AppCompatActivity() { private val binding by lazy { ActivityFeedDetailBinding.inflate(layoutInflater) } + private val viewModel: FeedDetailViewModel by viewModels() + private val highlightCommentId: Long by lazy { + intent.getLongExtra(KEY_HIGHLIGHT_COMMENT_ID, INVALID_COMMENT_ID) + } + private val inputMethodManager: InputMethodManager by lazy { getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager } @@ -37,10 +44,30 @@ class FeedDetailActivity : AppCompatActivity() { private val feedDetailAdapter: FeedDetailAdapter = FeedDetailAdapter(::showProfile) private val commentsAdapter: CommentsAdapter = CommentsAdapter( - onParentCommentClick = ::showChildComments, - onProfileImageClick = ::showProfile, + onCommentClick = { comment -> + ChildCommentActivity.startActivity( + context = this, + feedId = comment.feedId, + parentCommentId = comment.parentId ?: comment.id, + highlightCommentId = comment.id, + ) + }, + onAuthorImageClick = ::showProfile, onCommentMenuClick = ::showCommentMenuDialog, - ) + ).apply { + registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + if (highlightCommentId == INVALID_COMMENT_ID || viewModel.isAlreadyFirstFetched || itemCount == 0) return + val position = viewModel.feedDetail.value.comments + .indexOfFirst { it.comment.id == highlightCommentId } + FEED_DETAIL_COUNT + binding.rvFeeddetailFeedAndComments.scrollToPosition(position) + + viewModel.highlightComment(highlightCommentId) + + viewModel.isAlreadyFirstFetched = true + } + }) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -72,14 +99,6 @@ class FeedDetailActivity : AppCompatActivity() { inputMethodManager.hideSoftInputFromWindow(binding.root.windowToken, 0) } - private fun showKeyboard() { - @Suppress("DEPRECATION") - inputMethodManager.toggleSoftInput( - InputMethodManager.SHOW_FORCED, - InputMethodManager.HIDE_IMPLICIT_ONLY, - ) - } - private fun cancelUpdateComment() { viewModel.setEditMode(false) hideKeyboard() @@ -155,7 +174,7 @@ class FeedDetailActivity : AppCompatActivity() { commentsAdapter, ) itemAnimator = null - addItemDecoration(CommentRecyclerViewDivider(this@FeedDetailActivity)) + addItemDecoration(DividerItemDecoration(this@FeedDetailActivity)) } } @@ -163,10 +182,6 @@ class FeedDetailActivity : AppCompatActivity() { ProfileActivity.startActivity(this, authorId) } - private fun showChildComments(commentId: Long) { - ChildCommentActivity.startActivity(this, viewModel.feedId, commentId) - } - private fun showCommentMenuDialog(isWrittenByLoginUser: Boolean, commentId: Long) { bottomMenuDialog.resetMenu() if (isWrittenByLoginUser) { @@ -303,10 +318,25 @@ class FeedDetailActivity : AppCompatActivity() { } companion object { - fun startActivity(context: Context, feedId: Long) { - val intent = Intent(context, FeedDetailActivity::class.java) - .putExtra(KEY_FEED_ID, feedId) - context.startActivity(intent) + private const val KEY_HIGHLIGHT_COMMENT_ID = "KEY_HIGHLIGHT_COMMENT_ID" + private const val INVALID_COMMENT_ID: Long = -1 + private const val FEED_DETAIL_COUNT: Int = 1 + + fun startActivity( + context: Context, + feedId: Long, + highlightCommentId: Long = INVALID_COMMENT_ID, + ) { + context.startActivity(getIntent(context, feedId, highlightCommentId)) } + + fun getIntent( + context: Context, + feedId: Long, + highlightCommentId: Long = INVALID_COMMENT_ID, + ) = Intent(context, FeedDetailActivity::class.java) + .putExtra(KEY_FEED_ID, feedId) + .putExtra(KEY_HIGHLIGHT_COMMENT_ID, highlightCommentId) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailViewModel.kt index 5eef6f577..c42de4d1c 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailViewModel.kt @@ -25,6 +25,7 @@ import com.emmsale.presentation.ui.feedDetail.uiState.FeedDetailUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.properties.Delegates @HiltViewModel class FeedDetailViewModel @Inject constructor( @@ -33,6 +34,10 @@ class FeedDetailViewModel @Inject constructor( private val commentRepository: CommentRepository, private val tokenRepository: TokenRepository, ) : ViewModel(), Refreshable { + var isAlreadyFirstFetched: Boolean by Delegates.vetoable(false) { _, _, newValue -> + newValue + } + val feedId = savedStateHandle[KEY_FEED_ID] ?: DEFAULT_FEED_ID private val uid: Long by lazy { tokenRepository.getMyUid()!! } @@ -236,6 +241,10 @@ class FeedDetailViewModel @Inject constructor( } } + fun highlightComment(commentId: Long) { + _feedDetail.value = _feedDetail.value.highlightComment(commentId) + } + companion object { const val KEY_FEED_ID: String = "KEY_FEED_ID" private const val DEFAULT_FEED_ID: Long = -1 diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/CommentsAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/CommentsAdapter.kt index 16a3c63ae..a8e32b011 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/CommentsAdapter.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/CommentsAdapter.kt @@ -2,54 +2,24 @@ package com.emmsale.presentation.ui.feedDetail.recyclerView import android.view.ViewGroup import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.emmsale.presentation.ui.feedDetail.recyclerView.viewHolder.ChildCommentViewHolder +import com.emmsale.data.model.Comment import com.emmsale.presentation.ui.feedDetail.recyclerView.viewHolder.CommentViewHolder -import com.emmsale.presentation.ui.feedDetail.recyclerView.viewHolder.DeletedChildCommentViewHolder -import com.emmsale.presentation.ui.feedDetail.recyclerView.viewHolder.DeletedCommentViewHolder import com.emmsale.presentation.ui.feedDetail.uiState.CommentUiState class CommentsAdapter( - private val onParentCommentClick: (parentCommentId: Long) -> Unit, - private val onProfileImageClick: (authorId: Long) -> Unit, + private val onCommentClick: (comment: Comment) -> Unit, + private val onAuthorImageClick: (authorId: Long) -> Unit, private val onCommentMenuClick: (isWrittenByLoginUser: Boolean, commentId: Long) -> Unit, -) : ListAdapter(CommentDiffUtil) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = - when (CommentsViewType.of(viewType)) { - CommentsViewType.COMMENT -> CommentViewHolder.from( - parent = parent, - onClick = onParentCommentClick, - onProfileImageClick = onProfileImageClick, - onCommentMenuClick = onCommentMenuClick, - ) +) : ListAdapter(CommentDiffUtil) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentViewHolder = + CommentViewHolder( + parent = parent, + onCommentClick = onCommentClick, + onAuthorImageClick = onAuthorImageClick, + onCommentMenuClick = onCommentMenuClick, + ) - CommentsViewType.CHILD_COMMENT -> ChildCommentViewHolder.from( - parent = parent, - onProfileImageClick = onProfileImageClick, - onCommentMenuClick = onCommentMenuClick, - ) - - CommentsViewType.DELETED_COMMENT -> DeletedCommentViewHolder.from( - parent = parent, - onClick = onParentCommentClick, - ) - - CommentsViewType.DELETED_CHILD_COMMENT -> DeletedChildCommentViewHolder.from(parent) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - if (holder is CommentViewHolder) holder.bind(getItem(position)) - if (holder is ChildCommentViewHolder) holder.bind(getItem(position)) - if (holder is DeletedCommentViewHolder) holder.bind(getItem(position)) - } - - override fun getItemViewType(position: Int): Int { - val comment = getItem(position).comment - return when { - !comment.deleted && comment.parentId == null -> CommentsViewType.COMMENT.typeNumber - comment.deleted && comment.parentId == null -> CommentsViewType.DELETED_COMMENT.typeNumber - !comment.deleted -> CommentsViewType.CHILD_COMMENT.typeNumber - else -> CommentsViewType.DELETED_CHILD_COMMENT.typeNumber - } + override fun onBindViewHolder(holder: CommentViewHolder, position: Int) { + holder.bind(getItem(position)) } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/CommentsViewType.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/CommentsViewType.kt deleted file mode 100644 index 6ae86f1a9..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/CommentsViewType.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.emmsale.presentation.ui.feedDetail.recyclerView - -enum class CommentsViewType(val typeNumber: Int) { - COMMENT(0), - CHILD_COMMENT(1), - DELETED_COMMENT(2), - DELETED_CHILD_COMMENT(3), - ; - - companion object { - fun of(typeNumber: Int): CommentsViewType = CommentsViewType.values() - .find { it.typeNumber == typeNumber } - ?: throw IllegalArgumentException("CommentsViewType에는 타입 번호가 ${typeNumber}인 뷰 타입이 없습니다.") - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/ChildCommentViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/ChildCommentViewHolder.kt deleted file mode 100644 index e3b766ea4..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/ChildCommentViewHolder.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.emmsale.presentation.ui.feedDetail.recyclerView.viewHolder - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.emmsale.databinding.ItemAllChildCommentBinding -import com.emmsale.presentation.ui.feedDetail.uiState.CommentUiState - -class ChildCommentViewHolder( - private val binding: ItemAllChildCommentBinding, - onProfileImageClick: (authorId: Long) -> Unit, - onCommentMenuClick: (isWrittenByLoginUser: Boolean, commentId: Long) -> Unit, -) : RecyclerView.ViewHolder(binding.root) { - - init { - binding.onProfileImageClick = onProfileImageClick - binding.onCommentMenuClick = onCommentMenuClick - } - - fun bind(uiState: CommentUiState) { - binding.uiState = uiState - } - - companion object { - fun from( - parent: ViewGroup, - onProfileImageClick: (authorId: Long) -> Unit, - onCommentMenuClick: (isWrittenByLoginUser: Boolean, commentId: Long) -> Unit, - ): ChildCommentViewHolder { - val binding = ItemAllChildCommentBinding - .inflate(LayoutInflater.from(parent.context), parent, false) - return ChildCommentViewHolder(binding, onProfileImageClick, onCommentMenuClick) - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/CommentViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/CommentViewHolder.kt index 20ee06dac..9bf260d3b 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/CommentViewHolder.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/CommentViewHolder.kt @@ -3,36 +3,28 @@ package com.emmsale.presentation.ui.feedDetail.recyclerView.viewHolder import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import com.emmsale.R +import com.emmsale.data.model.Comment import com.emmsale.databinding.ItemAllCommentBinding import com.emmsale.presentation.ui.feedDetail.uiState.CommentUiState class CommentViewHolder( - private val binding: ItemAllCommentBinding, - onClick: (commentId: Long) -> Unit, - onProfileImageClick: (authorId: Long) -> Unit, + parent: ViewGroup, + onCommentClick: (comment: Comment) -> Unit, + onAuthorImageClick: (authorId: Long) -> Unit, onCommentMenuClick: (isWrittenByLoginUser: Boolean, commentId: Long) -> Unit, -) : RecyclerView.ViewHolder(binding.root) { +) : RecyclerView.ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_all_comment, parent, false), +) { + private val binding = ItemAllCommentBinding.bind(itemView) init { - binding.onClick = onClick - binding.onProfileImageClick = onProfileImageClick + binding.onCommentClick = onCommentClick + binding.onAuthorImageClick = onAuthorImageClick binding.onCommentMenuClick = onCommentMenuClick } fun bind(commentUiState: CommentUiState) { binding.uiState = commentUiState } - - companion object { - fun from( - parent: ViewGroup, - onClick: (commentId: Long) -> Unit, - onProfileImageClick: (authorId: Long) -> Unit, - onCommentMenuClick: (isWrittenByLoginUser: Boolean, commentId: Long) -> Unit, - ): CommentViewHolder { - val binding = ItemAllCommentBinding - .inflate(LayoutInflater.from(parent.context), parent, false) - return CommentViewHolder(binding, onClick, onProfileImageClick, onCommentMenuClick) - } - } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/DeletedChildCommentViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/DeletedChildCommentViewHolder.kt deleted file mode 100644 index 301c7b23d..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/DeletedChildCommentViewHolder.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.emmsale.presentation.ui.feedDetail.recyclerView.viewHolder - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.emmsale.R - -class DeletedChildCommentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - - companion object { - fun from(parent: ViewGroup): DeletedChildCommentViewHolder { - val layoutInflater = LayoutInflater.from(parent.context) - val itemView = - layoutInflater.inflate(R.layout.item_all_deleted_child_comment, parent, false) - return DeletedChildCommentViewHolder(itemView) - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/DeletedCommentViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/DeletedCommentViewHolder.kt deleted file mode 100644 index f926296ac..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/DeletedCommentViewHolder.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.emmsale.presentation.ui.feedDetail.recyclerView.viewHolder - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.emmsale.databinding.ItemAllDeletedCommentBinding -import com.emmsale.presentation.ui.feedDetail.uiState.CommentUiState - -class DeletedCommentViewHolder( - private val binding: ItemAllDeletedCommentBinding, - onClick: (commentId: Long) -> Unit, -) : RecyclerView.ViewHolder(binding.root) { - - init { - binding.onClick = onClick - } - - fun bind(commentUiState: CommentUiState) { - binding.uiState = commentUiState - } - - companion object { - fun from(parent: ViewGroup, onClick: (commentId: Long) -> Unit): DeletedCommentViewHolder { - val binding = ItemAllDeletedCommentBinding - .inflate(LayoutInflater.from(parent.context), parent, false) - return DeletedCommentViewHolder(binding, onClick) - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/FeedDetailViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/FeedDetailViewHolder.kt index 1f6be5e3c..1b70d140f 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/FeedDetailViewHolder.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/FeedDetailViewHolder.kt @@ -4,7 +4,8 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.emmsale.databinding.ItemFeeddetailFeedDetailBinding -import com.emmsale.presentation.ui.feedDetail.recyclerView.FeedDetailImageItemDecoration +import com.emmsale.presentation.common.extension.dp +import com.emmsale.presentation.common.recyclerView.IntervalItemDecoration import com.emmsale.presentation.ui.feedDetail.recyclerView.FeedDetailImagesAdapter import com.emmsale.presentation.ui.feedDetail.uiState.FeedDetailUiState @@ -20,7 +21,7 @@ class FeedDetailViewHolder( binding.rvFeeddetailFeedDetailImages.apply { adapter = imageUrlsAdapter itemAnimator = null - addItemDecoration(FeedDetailImageItemDecoration()) + addItemDecoration(IntervalItemDecoration(width = IMAGE_INTERVAL)) } } @@ -30,6 +31,8 @@ class FeedDetailViewHolder( } companion object { + private val IMAGE_INTERVAL: Int = 10.dp + fun from( parent: ViewGroup, onProfileImageClick: (authorId: Long) -> Unit, diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/CommentUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/CommentUiState.kt index eb98f23b6..7d3b973a5 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/CommentUiState.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/CommentUiState.kt @@ -4,16 +4,23 @@ import com.emmsale.data.model.Comment data class CommentUiState( val isWrittenByLoginUser: Boolean, + val isHighlight: Boolean, val comment: Comment, ) { val isUpdated: Boolean = comment.createdAt != comment.updatedAt val childCommentsCount = comment.childComments.count { !it.deleted } + fun highlight() = copy(isHighlight = true) + + fun unhighlight() = copy(isHighlight = false) + companion object { - fun create(uid: Long, comment: Comment): CommentUiState = CommentUiState( - isWrittenByLoginUser = uid == comment.authorId, - comment = comment, - ) + fun create(uid: Long, comment: Comment, isHighlight: Boolean = false): CommentUiState = + CommentUiState( + isWrittenByLoginUser = uid == comment.authorId, + isHighlight = isHighlight, + comment = comment, + ) } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiState.kt index f83120f0f..8f1dc3805 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiState.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiState.kt @@ -16,6 +16,14 @@ data class FeedDetailUiState( val commentsCount: Int = comments.count { !it.comment.deleted } + fun highlightComment(commentId: Long) = copy( + comments = comments.map { if (it.comment.id == commentId) it.highlight() else it }, + ) + + fun unhighlightComment(commentId: Long) = copy( + comments = comments.map { if (it.comment.id == commentId) it.unhighlight() else it }, + ) + companion object { val Loading: FeedDetailUiState = FeedDetailUiState( fetchResult = FetchResult.LOADING, diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/MessageListActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/MessageListActivity.kt index 1fddcccdf..1918ec74a 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/MessageListActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/MessageListActivity.kt @@ -15,6 +15,7 @@ import com.emmsale.R import com.emmsale.databinding.ActivityMessageListBinding import com.emmsale.presentation.common.EventObserver import com.emmsale.presentation.common.FetchResult +import com.emmsale.presentation.common.KeyboardHider import com.emmsale.presentation.common.extension.showSnackBar import com.emmsale.presentation.ui.messageList.MessageListViewModel.Companion.KEY_OTHER_UID import com.emmsale.presentation.ui.messageList.MessageListViewModel.Companion.KEY_ROOM_ID @@ -30,7 +31,7 @@ import kotlinx.coroutines.launch class MessageListActivity : AppCompatActivity() { private val binding by lazy { ActivityMessageListBinding.inflate(layoutInflater) } private val viewModel: MessageListViewModel by viewModels() - private val keyboardHider by lazy { KeyboardHider(binding.etMessageInput) } + private val keyboardHider by lazy { KeyboardHider(this) } private val messageListAdapter by lazy { MessageListAdapter(onProfileClick = ::navigateToProfile) } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/MyCommentsActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/MyCommentsActivity.kt index 493326f6c..79f1aff47 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/MyCommentsActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/MyCommentsActivity.kt @@ -8,8 +8,8 @@ import androidx.appcompat.app.AppCompatActivity import com.emmsale.R import com.emmsale.databinding.ActivityMyCommentsBinding import com.emmsale.presentation.common.extension.showSnackBar +import com.emmsale.presentation.common.recyclerView.DividerItemDecoration import com.emmsale.presentation.ui.childCommentList.ChildCommentActivity -import com.emmsale.presentation.ui.commentList.recyclerView.CommentRecyclerViewDivider import com.emmsale.presentation.ui.login.LoginActivity import com.emmsale.presentation.ui.myCommentList.recyclerView.MyCommentsAdapter import com.emmsale.presentation.ui.myCommentList.uiState.MyCommentsUiState @@ -42,17 +42,23 @@ class MyCommentsActivity : AppCompatActivity() { private fun initMyCommentsRecyclerView() { binding.rvMycommentsMycomments.apply { - adapter = MyCommentsAdapter(::showChildComments) + adapter = MyCommentsAdapter(::navigateToChildComments) itemAnimator = null - addItemDecoration(CommentRecyclerViewDivider(this@MyCommentsActivity)) + addItemDecoration(DividerItemDecoration(this@MyCommentsActivity)) } } - private fun showChildComments(eventId: Long, commentId: Long) { - ChildCommentActivity.startActivity(this, eventId, commentId) + private fun navigateToChildComments(eventId: Long, parentCommentId: Long, commentId: Long) { + ChildCommentActivity.startActivity( + context = this, + feedId = eventId, + parentCommentId = parentCommentId, + highlightCommentId = commentId, + fromPostDetail = false, + ) } - fun setupUiLogic() { + private fun setupUiLogic() { setupLoginUiLogic() setupCommentsUiLogic() } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentViewHolder.kt index 0bf81f43e..ca47e4a66 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentViewHolder.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentViewHolder.kt @@ -8,22 +8,11 @@ import com.emmsale.presentation.ui.myCommentList.uiState.MyCommentUiState class MyCommentViewHolder( private val binding: ItemMycommentsCommentBinding, - private val showChildComments: (eventId: Long, commentId: Long) -> Unit, + onClick: (eventId: Long, parentCommentId: Long, commentId: Long) -> Unit, ) : RecyclerView.ViewHolder(binding.root) { init { - binding.root.setOnClickListener { - showChildComments( - binding.comment?.eventId ?: return@setOnClickListener, - if (binding.comment?.parentId == null) { - binding.comment?.id - ?: return@setOnClickListener - } else { - binding.comment?.parentId - ?: return@setOnClickListener - }, - ) - } + binding.onClick = onClick } fun bind(comment: MyCommentUiState) { @@ -33,12 +22,12 @@ class MyCommentViewHolder( companion object { fun create( parent: ViewGroup, - showChildComments: (eventId: Long, commentId: Long) -> Unit, + onClick: (eventId: Long, parentCommentId: Long, commentId: Long) -> Unit, ): MyCommentViewHolder { val binding = ItemMycommentsCommentBinding .inflate(LayoutInflater.from(parent.context), parent, false) - return MyCommentViewHolder(binding, showChildComments) + return MyCommentViewHolder(binding, onClick) } } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentsAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentsAdapter.kt index 86f6d3690..30c2aaa4e 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentsAdapter.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentsAdapter.kt @@ -6,11 +6,11 @@ import androidx.recyclerview.widget.ListAdapter import com.emmsale.presentation.ui.myCommentList.uiState.MyCommentUiState class MyCommentsAdapter( - private val showChildComments: (eventId: Long, commentId: Long) -> Unit, + private val onClick: (eventId: Long, parentCommentId: Long, commentId: Long) -> Unit, ) : ListAdapter(diffUtil) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyCommentViewHolder { - return MyCommentViewHolder.create(parent, showChildComments) + return MyCommentViewHolder.create(parent, onClick) } override fun onBindViewHolder(holder: MyCommentViewHolder, position: Int) { diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/uiState/MyCommentUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/uiState/MyCommentUiState.kt index 219da1ef2..d30db4c89 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/uiState/MyCommentUiState.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/uiState/MyCommentUiState.kt @@ -5,8 +5,8 @@ import java.time.format.DateTimeFormatter data class MyCommentUiState( val id: Long, - val eventId: Long, - val eventName: String, + val feedId: Long, + val feedTitle: String, val authorId: Long, val parentId: Long?, val content: String, @@ -20,8 +20,8 @@ data class MyCommentUiState( fun from(comment: Comment) = MyCommentUiState( id = comment.id, - eventId = comment.feedId, - eventName = comment.feedTitle, + feedId = comment.feedId, + feedTitle = comment.feedTitle, authorId = comment.authorId, parentId = comment.parentId, content = comment.content, diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/PrimaryNotificationFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/PrimaryNotificationFragment.kt index 152f7c0d3..250ed43a7 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/PrimaryNotificationFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/PrimaryNotificationFragment.kt @@ -93,6 +93,7 @@ class PrimaryNotificationFragment : BaseFragment navigateToCommentScreen( feedId = notification.feedId, parentCommentId = notification.parentCommentId, + commentId = notification.commentId, ) } } @@ -101,8 +102,14 @@ class PrimaryNotificationFragment : BaseFragment + + diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_child_comments.xml b/android/2023-emmsale/app/src/main/res/layout/activity_child_comments.xml index 4a1af1c2c..f0e416bc5 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_child_comments.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_child_comments.xml @@ -1,26 +1,31 @@ - + + + + name="onCommentSubmitButtonClick" + type="kotlin.jvm.functions.Function1<String, Unit>" /> + + + tools:listitem="@layout/item_all_comment" /> - - - - - - - - - - - - - + + - - - - - - - - + app:layout_constraintBottom_toBottomOf="parent" + app:text="@{viewModel.editingCommentContent}" + app:visible="@{viewModel.editingCommentId != null}" + app:isSubmitEnabled="@{viewModel.screenUiState != ScreenUiState.LOADING}" + app:submitButtonLabel="@string/all_update_button_label" + app:cancelButtonLabel="@string/all_cancel" + app:onCancel="@{() -> onCommentUpdateCancelButtonClick.invoke()}" + app:onSubmit="@{(content) -> onUpdatedCommentSubmitButtonClick.invoke(content)}" /> - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:visible="@{viewModel.screenUiState == ScreenUiState.NETWORK_ERROR}" + app:onRefresh="@{() -> viewModel.refresh()}" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_feed_detail.xml b/android/2023-emmsale/app/src/main/res/layout/activity_feed_detail.xml index 711b33fba..3abc3840f 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_feed_detail.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_feed_detail.xml @@ -61,7 +61,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - tools:listitem="@layout/item_comments_comments" /> + tools:listitem="@layout/item_all_comment" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/2023-emmsale/app/src/main/res/layout/item_all_child_comment.xml b/android/2023-emmsale/app/src/main/res/layout/item_all_child_comment.xml deleted file mode 100644 index dc491290f..000000000 --- a/android/2023-emmsale/app/src/main/res/layout/item_all_child_comment.xml +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/layout/item_all_comment.xml b/android/2023-emmsale/app/src/main/res/layout/item_all_comment.xml index 6e0cdee0c..e27532260 100644 --- a/android/2023-emmsale/app/src/main/res/layout/item_all_comment.xml +++ b/android/2023-emmsale/app/src/main/res/layout/item_all_comment.xml @@ -11,89 +11,125 @@ + + - - + + + android:paddingHorizontal="17dp" + android:background="@{uiState.highlight ? @color/comment_highlight_color : @android:color/transparent}" + android:onClick="@{() -> onCommentClick.invoke(uiState.comment)}"> + + + + + + + + + tools:text="김커디" + tools:layout_marginTop="14dp"/> + app:layout_constraintStart_toStartOf="@+id/tv_comment_author_name" + tools:text="2023.08.03" + android:layout_marginTop="5dp" + app:layout_constraintTop_toBottomOf="@+id/tv_comment_author_name" /> + app:visible="@{uiState.isUpdated && !uiState.comment.deleted}" + app:layout_constraintBottom_toBottomOf="@+id/tv_comment_last_modified_date" + app:layout_constraintStart_toEndOf="@+id/tv_comment_last_modified_date" + app:layout_constraintTop_toTopOf="@+id/tv_comment_last_modified_date" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/item_all_deleted_child_comment.xml b/android/2023-emmsale/app/src/main/res/layout/item_all_deleted_child_comment.xml deleted file mode 100644 index f4fd4ad4f..000000000 --- a/android/2023-emmsale/app/src/main/res/layout/item_all_deleted_child_comment.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/layout/item_all_deleted_comment.xml b/android/2023-emmsale/app/src/main/res/layout/item_all_deleted_comment.xml deleted file mode 100644 index f46cac864..000000000 --- a/android/2023-emmsale/app/src/main/res/layout/item_all_deleted_comment.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/layout/item_childcomments_childcomment.xml b/android/2023-emmsale/app/src/main/res/layout/item_childcomments_childcomment.xml deleted file mode 100644 index b1633aef4..000000000 --- a/android/2023-emmsale/app/src/main/res/layout/item_childcomments_childcomment.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/2023-emmsale/app/src/main/res/layout/item_childcomments_parentcomment.xml b/android/2023-emmsale/app/src/main/res/layout/item_childcomments_parentcomment.xml deleted file mode 100644 index d10cd0d78..000000000 --- a/android/2023-emmsale/app/src/main/res/layout/item_childcomments_parentcomment.xml +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/layout/item_comments_comments.xml b/android/2023-emmsale/app/src/main/res/layout/item_comments_comments.xml deleted file mode 100644 index 596f25339..000000000 --- a/android/2023-emmsale/app/src/main/res/layout/item_comments_comments.xml +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/2023-emmsale/app/src/main/res/layout/item_my_message.xml b/android/2023-emmsale/app/src/main/res/layout/item_my_message.xml index b0a7e431d..73ebcb430 100644 --- a/android/2023-emmsale/app/src/main/res/layout/item_my_message.xml +++ b/android/2023-emmsale/app/src/main/res/layout/item_my_message.xml @@ -41,7 +41,7 @@ android:text="@{message.message}" android:textColor="@color/my_message_text_color" android:textSize="12sp" - app:layoutMarginTop="@{message.first ? @dimen/my_first_message_top_margin : @dimen/my_not_fisrt_message_top_margin}" + app:layout_marginTop="@{message.first ? @dimen/my_first_message_top_margin : @dimen/my_not_fisrt_message_top_margin}" tools:text="행사 같이 가시는거 어떠세요?" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/item_mycomments_comment.xml b/android/2023-emmsale/app/src/main/res/layout/item_mycomments_comment.xml index 70769ebf3..271f6c721 100644 --- a/android/2023-emmsale/app/src/main/res/layout/item_mycomments_comment.xml +++ b/android/2023-emmsale/app/src/main/res/layout/item_mycomments_comment.xml @@ -7,14 +7,21 @@ + + + + + app:layout_marginTop="@{message.shownProfile ? @dimen/other_message_with_profile_layout_top_margin : @dimen/other_message_without_profile_layout_top_margin}"> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/layout/layout_network_error.xml b/android/2023-emmsale/app/src/main/res/layout/layout_network_error.xml new file mode 100644 index 000000000..db2a02104 --- /dev/null +++ b/android/2023-emmsale/app/src/main/res/layout/layout_network_error.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/layout/layout_sub_text_input_window.xml b/android/2023-emmsale/app/src/main/res/layout/layout_sub_text_input_window.xml new file mode 100644 index 000000000..63e58a4df --- /dev/null +++ b/android/2023-emmsale/app/src/main/res/layout/layout_sub_text_input_window.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/values/colors.xml b/android/2023-emmsale/app/src/main/res/values/colors.xml index 3fe888185..fd045ae74 100644 --- a/android/2023-emmsale/app/src/main/res/values/colors.xml +++ b/android/2023-emmsale/app/src/main/res/values/colors.xml @@ -28,4 +28,5 @@ #1A5FEDFF #B1B1B1 #4D000000 + #44D9D9D9 diff --git a/android/2023-emmsale/app/src/main/res/values/custom_views.xml b/android/2023-emmsale/app/src/main/res/values/custom_views.xml index 2dcb8bbf6..d60971aaf 100644 --- a/android/2023-emmsale/app/src/main/res/values/custom_views.xml +++ b/android/2023-emmsale/app/src/main/res/values/custom_views.xml @@ -21,4 +21,14 @@ + + + + + + + + + + diff --git a/android/2023-emmsale/app/src/main/res/values/strings.xml b/android/2023-emmsale/app/src/main/res/values/strings.xml index 5d336be1d..3e56e079a 100644 --- a/android/2023-emmsale/app/src/main/res/values/strings.xml +++ b/android/2023-emmsale/app/src/main/res/values/strings.xml @@ -25,6 +25,8 @@ 중복 신고 이미 신고하셨습니다. 네트워크 에러 아이콘 + 조회 실패 + 네트워크 연결을 확인해주세요. 인터넷 연결이 불안정합니다. 인터넷 연결이 불안정하여\n데이터를 불러올 수 없습니다. 재시도 @@ -143,7 +145,7 @@ 댓글의 메뉴 버튼 삭제 삭제된 댓글입니다. - 댓글 작성자의 사진 + 댓글 작성자의 사진 답글 %d 댓글을 입력하세요. @@ -156,6 +158,7 @@ 댓글을 수정하는 데 실패했어요 😥 댓글을 삭제하는 데 실패했어요 😥. 해당 댓글을 신고 하시겠습니까? + 존재하지 않는 댓글입니다. 수정 삭제 diff --git a/android/2023-emmsale/app/src/main/res/values/themes.xml b/android/2023-emmsale/app/src/main/res/values/themes.xml index 7212f3fe0..84fe633ac 100644 --- a/android/2023-emmsale/app/src/main/res/values/themes.xml +++ b/android/2023-emmsale/app/src/main/res/values/themes.xml @@ -9,6 +9,7 @@ true @style/bottomNavigationViewStyle @style/basicTextFontStyle + @style/basicTextFontStyle @style/ThemeOverlay.MaterialComponents.BottomSheetDialog