feat(chat): 채팅 탭 화면 동작을 연결한다
This commit is contained in:
@@ -1,8 +1,123 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.main.chat
|
package kr.co.vividnext.sodalive.v2.main.chat
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
|
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
|
||||||
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.databinding.FragmentV2MainChatBinding
|
import kr.co.vividnext.sodalive.databinding.FragmentV2MainChatBinding
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomFilter
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiItem
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiState
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomType
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.ui.ChatRoomListAdapter
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
|
class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
|
||||||
FragmentV2MainChatBinding::inflate
|
FragmentV2MainChatBinding::inflate
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PAGINATION_THRESHOLD = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
private val viewModel: ChatMainViewModel by viewModel()
|
||||||
|
private val chatRoomListAdapter = ChatRoomListAdapter { onChatRoomClick(it) }
|
||||||
|
private lateinit var loadingDialog: LoadingDialog
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||||
|
|
||||||
|
setupTitleBar()
|
||||||
|
setupFilterTabs()
|
||||||
|
setupChatRooms()
|
||||||
|
bindViewModel()
|
||||||
|
|
||||||
|
binding.btnChatFloating.setOnClickListener { }
|
||||||
|
viewModel.loadFirstPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupTitleBar() {
|
||||||
|
binding.viewChatTitleBar.tvTitleBarTitle.setText(R.string.tab_chat)
|
||||||
|
binding.viewChatTitleBar.ivTitleBarMenu.setImageResource(R.drawable.ic_bar_cash)
|
||||||
|
binding.viewChatTitleBar.llTitleBarActions.addView(
|
||||||
|
ImageView(requireContext()).apply {
|
||||||
|
setImageResource(R.drawable.ic_bar_search)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupFilterTabs() {
|
||||||
|
binding.viewChatFilterTabs.root.setMenus(
|
||||||
|
listOf(
|
||||||
|
getString(R.string.screen_chat_filter_all),
|
||||||
|
getString(R.string.screen_chat_filter_ai),
|
||||||
|
getString(R.string.screen_chat_filter_dm)
|
||||||
|
),
|
||||||
|
selectedIndex = 0
|
||||||
|
)
|
||||||
|
binding.viewChatFilterTabs.root.setOnTabSelectedListener { index ->
|
||||||
|
viewModel.selectFilter(ChatRoomFilter.fromTabIndex(index))
|
||||||
|
binding.rvChatRooms.scrollToPosition(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupChatRooms() {
|
||||||
|
val layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
binding.rvChatRooms.layoutManager = layoutManager
|
||||||
|
binding.rvChatRooms.adapter = chatRoomListAdapter
|
||||||
|
binding.rvChatRooms.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
if (dy <= 0) return
|
||||||
|
|
||||||
|
val lastVisiblePosition = layoutManager.findLastVisibleItemPosition()
|
||||||
|
val shouldLoadNext = lastVisiblePosition >=
|
||||||
|
chatRoomListAdapter.itemCount - PAGINATION_THRESHOLD
|
||||||
|
if (shouldLoadNext) {
|
||||||
|
viewModel.loadNextPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindViewModel() {
|
||||||
|
viewModel.chatRoomStateLiveData.observe(viewLifecycleOwner) { state ->
|
||||||
|
when (state) {
|
||||||
|
is ChatRoomListUiState.Content -> {
|
||||||
|
chatRoomListAdapter.submitItems(state.items)
|
||||||
|
if (!state.isAppending) {
|
||||||
|
binding.rvChatRooms.scrollToPosition(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChatRoomListUiState.Empty -> chatRoomListAdapter.submitItems(emptyList())
|
||||||
|
is ChatRoomListUiState.Error -> chatRoomListAdapter.submitItems(emptyList())
|
||||||
|
ChatRoomListUiState.Loading -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModel.isLoading.observe(viewLifecycleOwner) {
|
||||||
|
if (it) {
|
||||||
|
loadingDialog.show(screenWidth)
|
||||||
|
} else {
|
||||||
|
loadingDialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModel.toastLiveData.observe(viewLifecycleOwner) {
|
||||||
|
val text = it?.message ?: it?.resId?.let { resId -> getString(resId) }
|
||||||
|
if (!text.isNullOrBlank()) {
|
||||||
|
Toast.makeText(requireContext(), text, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onChatRoomClick(item: ChatRoomListUiItem) {
|
||||||
|
if (item.chatType != ChatRoomType.AI) return
|
||||||
|
startActivity(ChatRoomActivity.newIntent(requireContext(), item.roomId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
@Config(sdk = [28], application = Application::class)
|
@Config(sdk = [28], application = Application::class)
|
||||||
@@ -83,11 +84,80 @@ class ChatMainFragmentLayoutTest {
|
|||||||
assertEquals(ConstraintLayout.LayoutParams.PARENT_ID, buttonParams.bottomToBottom)
|
assertEquals(ConstraintLayout.LayoutParams.PARENT_ID, buttonParams.bottomToBottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `채팅 fragment source는 화면 초기 구성과 첫 페이지 로드를 연결한다`() {
|
||||||
|
val source = chatMainFragmentSource()
|
||||||
|
|
||||||
|
assertTrue(source.contains("private val viewModel: ChatMainViewModel by viewModel()"))
|
||||||
|
assertTrue(source.contains("ChatRoomListAdapter"))
|
||||||
|
assertTrue(source.contains("LoadingDialog(requireActivity(), layoutInflater)"))
|
||||||
|
assertTrue(source.contains("tvTitleBarTitle.setText(R.string.tab_chat)"))
|
||||||
|
assertTrue(source.contains("ivTitleBarMenu.setImageResource(R.drawable.ic_bar_cash)"))
|
||||||
|
assertTrue(source.contains("llTitleBarActions.addView"))
|
||||||
|
assertTrue(source.contains("setImageResource(R.drawable.ic_bar_search)"))
|
||||||
|
assertTrue(source.contains("setMenus("))
|
||||||
|
assertTrue(source.contains("R.string.screen_chat_filter_all"))
|
||||||
|
assertTrue(source.contains("R.string.screen_chat_filter_ai"))
|
||||||
|
assertTrue(source.contains("R.string.screen_chat_filter_dm"))
|
||||||
|
assertTrue(source.contains("viewModel.loadFirstPage()"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `채팅 fragment source는 filter list pagination observe를 연결한다`() {
|
||||||
|
val source = chatMainFragmentSource()
|
||||||
|
|
||||||
|
assertTrue(source.contains("setOnTabSelectedListener"))
|
||||||
|
assertTrue(source.contains("ChatRoomFilter.fromTabIndex(index)"))
|
||||||
|
assertTrue(source.contains("LinearLayoutManager(requireContext())"))
|
||||||
|
assertTrue(source.contains("rvChatRooms.adapter = chatRoomListAdapter"))
|
||||||
|
assertTrue(source.contains("addOnScrollListener(object : RecyclerView.OnScrollListener()"))
|
||||||
|
assertTrue(source.contains("findLastVisibleItemPosition()"))
|
||||||
|
assertTrue(source.contains("viewModel.loadNextPage()"))
|
||||||
|
assertTrue(source.contains("chatRoomStateLiveData.observe(viewLifecycleOwner)"))
|
||||||
|
assertTrue(source.contains("ChatRoomListUiState.Content"))
|
||||||
|
assertTrue(source.contains("ChatRoomListUiState.Empty"))
|
||||||
|
assertTrue(source.contains("ChatRoomListUiState.Error"))
|
||||||
|
assertTrue(source.contains("viewModel.isLoading.observe(viewLifecycleOwner)"))
|
||||||
|
assertTrue(source.contains("viewModel.toastLiveData.observe(viewLifecycleOwner)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `채팅 fragment source는 AI 항목만 채팅방으로 이동한다`() {
|
||||||
|
val source = chatMainFragmentSource()
|
||||||
|
|
||||||
|
assertTrue(source.contains("private fun onChatRoomClick(item: ChatRoomListUiItem)"))
|
||||||
|
assertTrue(source.contains("if (item.chatType != ChatRoomType.AI) return"))
|
||||||
|
assertTrue(source.contains("ChatRoomActivity.newIntent(requireContext(), item.roomId)"))
|
||||||
|
assertTrue(source.contains("btnChatFloating.setOnClickListener { }"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `채팅 fragment source는 탭 전환 및 첫 페이지 로딩 시 스크롤을 최상단으로 이동시킨다`() {
|
||||||
|
val source = chatMainFragmentSource()
|
||||||
|
|
||||||
|
assertTrue(source.contains("setOnTabSelectedListener"))
|
||||||
|
assertTrue(source.contains("rvChatRooms.scrollToPosition(0)"))
|
||||||
|
assertTrue(source.contains("!state.isAppending"))
|
||||||
|
}
|
||||||
|
|
||||||
private fun inflateView(layoutResId: Int): View {
|
private fun inflateView(layoutResId: Int): View {
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
return LayoutInflater.from(context).inflate(layoutResId, null, false)
|
return LayoutInflater.from(context).inflate(layoutResId, null, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun chatMainFragmentSource(): String = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
private fun projectFile(relativePath: String): File {
|
||||||
|
val candidates = listOf(
|
||||||
|
File(relativePath),
|
||||||
|
File("../$relativePath")
|
||||||
|
)
|
||||||
|
return candidates.firstOrNull { it.exists() }
|
||||||
|
?: error("Project file not found: $relativePath")
|
||||||
|
}
|
||||||
|
|
||||||
private fun View.containsViewIdContaining(idNamePart: String): Boolean {
|
private fun View.containsViewIdContaining(idNamePart: String): Boolean {
|
||||||
if (id != View.NO_ID) {
|
if (id != View.NO_ID) {
|
||||||
val idName = runCatching { resources.getResourceEntryName(id) }.getOrNull()
|
val idName = runCatching { resources.getResourceEntryName(id) }.getOrNull()
|
||||||
|
|||||||
Reference in New Issue
Block a user