Skip to content

Commit

Permalink
merge: 댓글 하이라이팅 (#883)
Browse files Browse the repository at this point in the history
Related to: #882
  • Loading branch information
ki960213 authored Dec 29, 2023
1 parent e8b18a9 commit ce92124
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 157 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.emmsale.presentation.common.layoutManager

import android.content.Context
import androidx.recyclerview.widget.LinearSmoothScroller
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

class CenterSmoothScroller @Inject constructor(
@ApplicationContext context: Context,
) : LinearSmoothScroller(context) {

override fun calculateDtToFit(
viewStart: Int,
viewEnd: Int,
boxStart: Int,
boxEnd: Int,
snapPreference: Int,
): Int = (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.emmsale.presentation.common.layoutManager

import android.content.Context
import androidx.recyclerview.widget.LinearSmoothScroller
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

class EndSmoothScroller @Inject constructor(
@ApplicationContext context: Context,
) : LinearSmoothScroller(context) {

override fun getVerticalSnapPreference(): Int = SNAP_TO_END
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class SubTextInputWindow @JvmOverloads constructor(
init {
applyStyledAttributes(attrs)
addView(binding.root)
isClickable = true
background = context.getColor(R.color.white).toDrawable()
elevation = 5f.dp
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.emmsale.R
import com.emmsale.data.model.Comment
import com.emmsale.databinding.ActivityChildCommentsBinding
import com.emmsale.presentation.base.NetworkActivity
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.layoutManager.CenterSmoothScroller
import com.emmsale.presentation.common.layoutManager.EndSmoothScroller
import com.emmsale.presentation.common.recyclerView.DividerItemDecoration
import com.emmsale.presentation.common.views.InfoDialog
import com.emmsale.presentation.common.views.WarningDialog
Expand All @@ -24,18 +27,26 @@ import com.emmsale.presentation.ui.childCommentList.ChildCommentsViewModel.Compa
import com.emmsale.presentation.ui.childCommentList.recyclerView.CommentsAdapter
import com.emmsale.presentation.ui.childCommentList.uiState.ChildCommentsUiEvent
import com.emmsale.presentation.ui.feedDetail.FeedDetailActivity
import com.emmsale.presentation.ui.feedDetail.uiState.CommentsUiState
import com.emmsale.presentation.ui.profile.ProfileActivity
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject

@AndroidEntryPoint
class ChildCommentsActivity :
NetworkActivity<ActivityChildCommentsBinding>(R.layout.activity_child_comments) {

override val viewModel: ChildCommentsViewModel by viewModels()

@Inject
lateinit var centerSmoothScroller: CenterSmoothScroller

@Inject
lateinit var endSmoothScroller: EndSmoothScroller

private val commentsAdapter: CommentsAdapter = CommentsAdapter(
onCommentClick = { comment -> viewModel.unhighlight(comment.id) },
onCommentClick = { },
onAuthorImageClick = { authorId -> ProfileActivity.startActivity(this, authorId) },
onCommentMenuClick = ::showCommentMenuDialog,
)
Expand Down Expand Up @@ -63,9 +74,21 @@ class ChildCommentsActivity :

private fun BottomMenuDialog.addCommentUpdateButton(commentId: Long) {
addMenuItemBelow(context.getString(R.string.all_update_button_label)) {
viewModel.setEditMode(true, commentId)
viewModel.startEditComment(commentId)
binding.stiwCommentUpdate.requestFocusOnEditText()
showKeyboard()
startToEditComment(commentId)
}
}

private fun startToEditComment(commentId: Long) {
val position = viewModel.comments.value.getPosition(commentId)

lifecycleScope.launch {
delay(KEYBOARD_SHOW_WAITING_TIME)
binding.rvChildcommentsChildcomments
.layoutManager
?.startSmoothScroll(endSmoothScroller.apply { targetPosition = position })
}
}

Expand Down Expand Up @@ -130,12 +153,12 @@ class ChildCommentsActivity :
hideKeyboard()
}
binding.onCommentUpdateCancelButtonClick = {
viewModel.setEditMode(false)
viewModel.cancelEditComment()
hideKeyboard()
}
binding.onUpdatedCommentSubmitButtonClick = {
val commentId = viewModel.editingCommentId.value
if (commentId != null) viewModel.updateComment(commentId, it)
val comment = viewModel.editingComment.value
if (comment != null) viewModel.updateComment(comment.id, it)
hideKeyboard()
}
}
Expand Down Expand Up @@ -172,23 +195,31 @@ class ChildCommentsActivity :

private fun observeComments() {
viewModel.comments.observe(this) {
commentsAdapter.submitList(it.commentUiStates) { scrollToIfFirstFetch(it) }
commentsAdapter.submitList(it.commentUiStates) { handleHighlightComment() }
}
}

private fun scrollToIfFirstFetch(commentUiState: CommentsUiState) {
fun cantScroll(): Boolean =
viewModel.isAlreadyFirstFetched || commentUiState.commentUiStates.isEmpty()
private fun handleHighlightComment() {
if (highlightCommentId == INVALID_COMMENT_ID || isNotRealFirstEnter()) return

if (highlightCommentId == INVALID_COMMENT_ID || cantScroll()) return
val position = viewModel.comments.value.commentUiStates
.indexOfFirst {
it.comment.id == highlightCommentId
}
binding.rvChildcommentsChildcomments.scrollToPosition(position)

viewModel.highlight(highlightCommentId)
viewModel.isAlreadyFirstFetched = true
viewModel.highlightCommentOnFirstEnter(highlightCommentId)
highlightCommentOnFirstEnter()
}

private fun isNotRealFirstEnter(): Boolean =
viewModel.isAlreadyFirstFetched || viewModel.comments.value.commentUiStates.isEmpty()

private fun highlightCommentOnFirstEnter() {
val position = viewModel.comments.value.getPosition(highlightCommentId)

binding.rvChildcommentsChildcomments.scrollToPosition(position)
lifecycleScope.launch {
delay(100L) // 버그 때문에
binding.rvChildcommentsChildcomments
.layoutManager
?.startSmoothScroll(centerSmoothScroller.apply { targetPosition = position })
}
}

private fun observeUiEvent() {
Expand Down Expand Up @@ -242,6 +273,7 @@ class ChildCommentsActivity :
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
private const val KEYBOARD_SHOW_WAITING_TIME = 300L

fun startActivity(
context: Context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import com.emmsale.data.model.Comment
import com.emmsale.data.repository.interfaces.CommentRepository
import com.emmsale.data.repository.interfaces.TokenRepository
import com.emmsale.presentation.base.RefreshableViewModel
Expand All @@ -14,6 +16,8 @@ import com.emmsale.presentation.ui.childCommentList.uiState.ChildCommentsUiEvent
import com.emmsale.presentation.ui.feedDetail.uiState.CommentsUiState
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

Expand All @@ -36,16 +40,16 @@ class ChildCommentsViewModel @Inject constructor(
private val _comments = NotNullMutableLiveData(CommentsUiState())
val comments: NotNullLiveData<CommentsUiState> = _comments

private val _editingCommentId = MutableLiveData<Long?>()
val editingCommentId: LiveData<Long?> = _editingCommentId
private val _editingComment = MutableLiveData<Comment?>()
val editingComment: LiveData<Comment?> = _editingComment

val editingCommentContent: LiveData<String?> = _editingCommentId.map { commentId ->
if (commentId == null) null else _comments.value[commentId]?.comment?.content
}
val isEditingComment: LiveData<Boolean> = _editingComment.map { it != null }

private val _canSubmitComment = NotNullMutableLiveData(true)
val canSubmitComment: NotNullLiveData<Boolean> = _canSubmitComment

private var unhighlightJob: Job? = null

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

Expand All @@ -69,7 +73,7 @@ class ChildCommentsViewModel @Inject constructor(

fun updateComment(commentId: Long, content: String): Job = commandAndRefresh(
command = { commentRepository.updateComment(commentId, content) },
onSuccess = { _editingCommentId.value = null },
onSuccess = { _editingComment.value = null },
onFailure = { _, _ -> _uiEvent.value = ChildCommentsUiEvent.CommentUpdateFail },
onStart = { _canSubmitComment.value = false },
onFinish = { _canSubmitComment.value = true },
Expand All @@ -80,8 +84,26 @@ class ChildCommentsViewModel @Inject constructor(
onFailure = { _, _ -> _uiEvent.value = ChildCommentsUiEvent.CommentDeleteFail },
)

fun setEditMode(isEditMode: Boolean, commentId: Long = INVALID_COMMENT_ID) {
_editingCommentId.value = if (isEditMode) commentId else null
fun startEditComment(commentId: Long) {
_editingComment.value = comments.value.commentUiStates
.find { it.comment.id == commentId }
?.comment
?: return
unhighlightJob?.cancel()
_comments.value = _comments.value.highlight(commentId)
}

fun cancelEditComment() {
_editingComment.value = null
_comments.value = _comments.value.unhighlight()
}

fun highlightCommentOnFirstEnter(commentId: Long) {
_comments.value = _comments.value.highlight(commentId)
unhighlightJob = viewModelScope.launch {
delay(COMMENT_HIGHLIGHTING_DURATION_ON_FIRST_ENTER)
_comments.value = _comments.value.unhighlight()
}
}

fun reportComment(commentId: Long): Job = command(
Expand All @@ -106,24 +128,12 @@ class ChildCommentsViewModel @Inject constructor(
onSuccess = { _comments.value = CommentsUiState(uid, it) },
)

fun highlight(commentId: Long) {
val comment = _comments.value.commentUiStates.find { it.comment.id == commentId } ?: return
if (comment.isHighlight) return
_comments.value = _comments.value.highlight(commentId)
}

fun unhighlight(commentId: Long) {
val comment = _comments.value.commentUiStates.find { it.comment.id == commentId } ?: return
if (!comment.isHighlight) return
_comments.value = _comments.value.unhighlight()
}

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 COMMENT_HIGHLIGHTING_DURATION_ON_FIRST_ENTER = 2000L
}
}
Loading

0 comments on commit ce92124

Please sign in to comment.