Skip to content

Commit

Permalink
feat(FeedDetailActivity): 댓글 하이라이팅 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
ki960213 committed Dec 14, 2023
1 parent 16efcf6 commit 6219159
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 39 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 @@ -5,14 +5,15 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearSmoothScroller
import com.emmsale.R
import com.emmsale.data.model.Comment
import com.emmsale.databinding.ActivityFeedDetailBinding
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 @@ -26,6 +27,7 @@ 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 FeedDetailActivity :
Expand All @@ -37,6 +39,12 @@ class FeedDetailActivity :

override val viewModel: FeedDetailViewModel by viewModels()

@Inject
lateinit var centerSmoothScroller: CenterSmoothScroller

@Inject
lateinit var endSmoothScroller: EndSmoothScroller

private val bottomMenuDialog: BottomMenuDialog by lazy { BottomMenuDialog(this) }

private val feedAndCommentsAdapter = FeedAndCommentsAdapter(
Expand Down Expand Up @@ -71,9 +79,21 @@ class FeedDetailActivity :

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

private fun startToEditComment(commentId: Long) {
val position = getCommentPosition(commentId) + 1 // 게시글을 포함한 멀티 뷰 타입 리사이클러 뷰라서 1을 더함

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

Expand Down Expand Up @@ -212,17 +232,39 @@ class FeedDetailActivity :
private fun observeFeedDetail() {
viewModel.feedDetailUiState.observe(this) {
val feedAndComments = listOf(it.feedUiState) + it.commentsUiState.commentUiStates
feedAndCommentsAdapter.submitList(feedAndComments) {
if (highlightCommentId == INVALID_COMMENT_ID || isNotRealFirstFetch()) return@submitList
viewModel.highlightComment(highlightCommentId)
viewModel.isAlreadyFirstFetched = true
}
feedAndCommentsAdapter.submitList(feedAndComments) { handleHighlightComment() }
}
}

private fun isNotRealFirstFetch(): Boolean =
private fun handleHighlightComment() {
if (highlightCommentId == INVALID_COMMENT_ID || isNotRealFirstEnter()) return

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

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

private fun highlightCommentOnFirstEnter() {
val position =
getCommentPosition(highlightCommentId) + 1 // 게시글을 포함한 멀티 뷰 타입 리사이클러 뷰라서 1을 더함

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

private fun getCommentPosition(commentId: Long): Int = viewModel.commentUiStates
.indexOfFirst { commentUiState ->
commentUiState.comment.id == commentId
}

private fun observeUiEvent() {
viewModel.uiEvent.observe(this, ::handleUiEvent)
}
Expand Down Expand Up @@ -274,8 +316,6 @@ class FeedDetailActivity :
binding.btiwCommentPost.clearText()
scrollToLastPosition()
}

is FeedDetailUiEvent.CommentHighlight -> highlightComment(uiEvent.commentId)
}
}

Expand All @@ -284,26 +324,10 @@ class FeedDetailActivity :
binding.rvFeedAndComments.smoothScrollToPosition(commentsCount)
}

private fun highlightComment(commentId: Long) {
val position = viewModel.commentUiStates
.indexOfFirst {
it.comment.id == commentId
}

binding.rvFeedAndComments.scrollToPosition(position + 1)
lifecycleScope.launch {
delay(200L)
binding.rvFeedAndComments.layoutManager?.startSmoothScroll(
object : LinearSmoothScroller(this@FeedDetailActivity) {
override fun getVerticalSnapPreference(): Int = SNAP_TO_START
}.apply { targetPosition = position + 1 },
)
}
}

companion object {
private const val KEY_HIGHLIGHT_COMMENT_ID = "KEY_HIGHLIGHT_COMMENT_ID"
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 @@ -28,6 +28,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.properties.Delegates
Expand Down Expand Up @@ -69,6 +70,8 @@ class FeedDetailViewModel @Inject constructor(
private val _canSubmitComment = NotNullMutableLiveData(true)
val canSubmitComment: NotNullLiveData<Boolean> = _canSubmitComment

private var unhighlightJob: Job? = null

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

Expand Down Expand Up @@ -198,12 +201,21 @@ class FeedDetailViewModel @Inject constructor(

fun startEditComment(commentId: Long) {
_editingCommentId.value = commentId
highlightComment(commentId)
unhighlightJob?.cancel()
_feedDetailUiState.value = _feedDetailUiState.value.highlightComment(commentId)
}

fun cancelEditComment() {
_editingCommentId.value = null
unhighlightComment()
_feedDetailUiState.value = _feedDetailUiState.value.unhighlightComment()
}

fun highlightCommentOnFirstEnter(commentId: Long) {
_feedDetailUiState.value = _feedDetailUiState.value.highlightComment(commentId)
unhighlightJob = viewModelScope.launch {
delay(COMMENT_HIGHLIGHTING_DURATION_ON_FIRST_ENTER)
_feedDetailUiState.value = _feedDetailUiState.value.unhighlightComment()
}
}

fun reportComment(commentId: Long): Job = command(
Expand All @@ -223,20 +235,13 @@ class FeedDetailViewModel @Inject constructor(
},
)

fun highlightComment(commentId: Long) {
_feedDetailUiState.value = _feedDetailUiState.value.highlightComment(commentId)
_uiEvent.value = FeedDetailUiEvent.CommentHighlight(commentId)
}

private fun unhighlightComment() {
_feedDetailUiState.value = _feedDetailUiState.value.unhighlightComment()
}

companion object {
const val KEY_FEED_ID: String = "KEY_FEED_ID"
private const val DEFAULT_FEED_ID: Long = -1

private const val DELETED_FEED_FETCH_ERROR_CODE = 403
private const val REPORT_DUPLICATE_ERROR_CODE = 400

private const val COMMENT_HIGHLIGHTING_DURATION_ON_FIRST_ENTER = 2000L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,4 @@ sealed interface FeedDetailUiEvent {
object CommentReportFail : FeedDetailUiEvent
object CommentReportComplete : FeedDetailUiEvent
object CommentPostComplete : FeedDetailUiEvent
data class CommentHighlight(val commentId: Long) : FeedDetailUiEvent
}

0 comments on commit 6219159

Please sign in to comment.