feat(chat): 채팅 탭 화면 동작을 연결한다
This commit is contained in:
@@ -1,8 +1,123 @@
|
||||
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.chat.talk.room.ChatRoomActivity
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
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>(
|
||||
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.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import java.io.File
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], application = Application::class)
|
||||
@@ -83,11 +84,80 @@ class ChatMainFragmentLayoutTest {
|
||||
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 {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
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 {
|
||||
if (id != View.NO_ID) {
|
||||
val idName = runCatching { resources.getResourceEntryName(id) }.getOrNull()
|
||||
|
||||
Reference in New Issue
Block a user