Skip to content

Commit

Permalink
merge: 댓글 하이라이팅 (#890)
Browse files Browse the repository at this point in the history
Related to: #882
  • Loading branch information
ki960213 authored Dec 29, 2023
1 parent aafde96 commit ad20393
Show file tree
Hide file tree
Showing 15 changed files with 286 additions and 164 deletions.
4 changes: 2 additions & 2 deletions android/2023-emmsale/app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
# is used.
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation

# ApiResponse 클래스 축소 및 난독화 해제하여 CallAdapter에서 retrofit2.Call<ApiResponse>를 반환하는 CallAdapter 만들 수 있도록 변경
-keepnames class com.emmsale.data.common.retrofit.callAdapter.ApiResponse
# ApiResponse 클래스의 타입 매개변수를 유지하기 위해 추가. 안하면 CallAdapter에서 retrofit2.Call<ApiResponse>를 반환하는 CallAdapter 만들 수 없음.
-keepnames, allowobfuscation class com.emmsale.data.common.retrofit.callAdapter.ApiResponse
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ abstract class NetworkFragment<V : ViewDataBinding>(

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
observeCommentUiEvent()
observeCommonUiEvent()
}

private fun observeCommentUiEvent() {
viewModel.networkUiEvent.observe(this) { handleCommentUiEvent(it) }
private fun observeCommonUiEvent() {
viewModel.networkUiEvent.observe(this) { handleCommonUiEvent(it) }
}

private fun handleCommentUiEvent(event: NetworkUiEvent) {
private fun handleCommonUiEvent(event: NetworkUiEvent) {
when (event) {
NetworkUiEvent.RequestFailByNetworkError -> binding.root.showSnackBar(getString(R.string.all_network_check_message))
is NetworkUiEvent.Unexpected -> showUnexpectedErrorOccurredDialog()
Expand Down
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,20 @@ class ChildCommentsActivity :

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

private fun startToEditComment(commentId: Long) {
val position = viewModel.startEditComment(commentId) ?: return

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

Expand Down Expand Up @@ -130,12 +152,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 +194,30 @@ 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
highlightCommentOnFirstEnter()
}

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

private fun highlightCommentOnFirstEnter() {
val position = viewModel.highlightCommentOnFirstEnter(highlightCommentId) ?: return

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 +271,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,34 @@ 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
/**
* @return 수정할 댓글 위치
*/
fun startEditComment(commentId: Long): Int? {
_editingComment.value = comments.value.commentUiStates
.find { it.comment.id == commentId }
?.comment
?: return null
unhighlightJob?.cancel()
_comments.value = _comments.value.highlight(commentId)
return _comments.value.getPosition(commentId)
}

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

/**
* @return 하이라이팅할 댓글 위치
*/
fun highlightCommentOnFirstEnter(commentId: Long): Int? {
_comments.value = _comments.value.highlight(commentId)
unhighlightJob = viewModelScope.launch {
delay(COMMENT_HIGHLIGHTING_DURATION_ON_FIRST_ENTER)
_comments.value = _comments.value.unhighlight()
}
return _comments.value.getPosition(commentId)
}

fun reportComment(commentId: Long): Job = command(
Expand All @@ -106,24 +136,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 ad20393

Please sign in to comment.