feat(chat): 대화 탭 DM 시작 진입점을 추가한다
This commit is contained in:
@@ -12,6 +12,9 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.settings.notification.MemberRole
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.recipient.DmRecipientPickerActivity
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentV2MainChatBinding
|
||||
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
|
||||
import kr.co.vividnext.sodalive.search.SearchActivity
|
||||
@@ -51,12 +54,19 @@ class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
|
||||
private var isChatPullRefreshing = false
|
||||
private var refreshingChatFilter: ChatRoomFilter? = null
|
||||
private var lastRenderedChatContentState: ChatRoomListUiState.Content? = null
|
||||
private var shouldRefreshChatRoomsOnResume = false
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
shouldRefreshChatRoomsOnResume = false
|
||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||
|
||||
setupTitleBar()
|
||||
val isCreator = SharedPreferenceManager.role == MemberRole.CREATOR.name
|
||||
binding.btnNewDm.isVisible = isCreator
|
||||
binding.btnNewDm.setOnClickListener {
|
||||
if (isCreator) openWithLoginGuard(DmRecipientPickerActivity::class.java)
|
||||
}
|
||||
val initialFilter = getInitialFilter() ?: ChatRoomFilter.ALL
|
||||
selectedFilter = initialFilter
|
||||
setupFilterTabs(initialFilter)
|
||||
@@ -67,6 +77,17 @@ class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
|
||||
viewModel.loadFirstPage(initialFilter)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (!shouldRefreshChatRoomsOnResume) {
|
||||
shouldRefreshChatRoomsOnResume = true
|
||||
return
|
||||
}
|
||||
cancelChatPullRefresh()
|
||||
bindChatRooms(emptyList(), showEmpty = false)
|
||||
viewModel.loadFirstPage(selectedFilter)
|
||||
}
|
||||
|
||||
fun selectFilter(filter: ChatRoomFilter) {
|
||||
if (refreshingChatFilter != null && refreshingChatFilter != filter) {
|
||||
cancelChatPullRefresh()
|
||||
|
||||
@@ -58,4 +58,17 @@
|
||||
tools:text="아직 나눈 대화가 없어요.\n크리에이터와 이야기를 시작해 보세요." />
|
||||
</FrameLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
<ImageButton
|
||||
android:id="@+id/btn_new_dm"
|
||||
android:layout_width="66dp"
|
||||
android:layout_height="66dp"
|
||||
android:layout_margin="@dimen/spacing_14"
|
||||
android:background="@drawable/bg_creator_channel_owner_fab"
|
||||
android:contentDescription="@string/screen_chat_floating_button"
|
||||
android:elevation="8dp"
|
||||
android:padding="@dimen/spacing_14"
|
||||
android:src="@drawable/ic_plus_no_bg"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -110,6 +110,28 @@ class ChatMainFragmentLayoutTest {
|
||||
assertTrue(source.contains("binding.swipeChatRooms.isRefreshing = false"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `채팅방에서 복귀하면 현재 filter 첫 페이지로 목록을 갱신한다`() {
|
||||
val source = chatMainFragmentSource()
|
||||
val onResume = source.substringBetween(
|
||||
"override fun onResume()",
|
||||
"private fun setupTitleBar()"
|
||||
)
|
||||
|
||||
assertTrue(source.contains("private var shouldRefreshChatRoomsOnResume = false"))
|
||||
val onViewCreated = source.substringBetween(
|
||||
"override fun onViewCreated",
|
||||
"viewModel.loadFirstPage(initialFilter)"
|
||||
)
|
||||
assertTrue(onViewCreated.contains("shouldRefreshChatRoomsOnResume = false"))
|
||||
assertTrue(onResume.contains("super.onResume()"))
|
||||
assertTrue(onResume.contains("if (!shouldRefreshChatRoomsOnResume)"))
|
||||
assertTrue(onResume.contains("shouldRefreshChatRoomsOnResume = true"))
|
||||
assertTrue(onResume.contains("cancelChatPullRefresh()"))
|
||||
assertTrue(onResume.contains("bindChatRooms(emptyList(), showEmpty = false)"))
|
||||
assertTrue(onResume.contains("viewModel.loadFirstPage(selectedFilter)"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `채팅 empty 상태에서 pull refresh loading은 empty message를 숨기지 않는다`() {
|
||||
val source = chatMainFragmentSource()
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.chat.recipient
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
class DmRecipientPickerSourceTest {
|
||||
@Test
|
||||
fun `대화 탭은 크리에이터에게만 수신자 선택 진입을 연결한다`() {
|
||||
val source = source("main/chat/ChatMainFragment.kt")
|
||||
assertTrue(source.contains("SharedPreferenceManager.role == MemberRole.CREATOR.name"))
|
||||
assertTrue(source.contains("binding.btnNewDm.isVisible = isCreator"))
|
||||
assertTrue(source.contains("if (isCreator)"))
|
||||
assertTrue(source.contains("openWithLoginGuard(DmRecipientPickerActivity::class.java)"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `수신자 화면은 팔로워와 검색 및 빈 결과 상태를 연결한다`() {
|
||||
val source = source("main/chat/recipient/DmRecipientPickerActivity.kt")
|
||||
assertTrue(source.contains("viewModel.enter()"))
|
||||
assertTrue(source.contains("viewModel.onSearchQueryChanged"))
|
||||
assertTrue(source.contains("DmRecipientPickerMode.FOLLOWERS"))
|
||||
assertTrue(source.contains("state.searchResultCount"))
|
||||
assertTrue(source.contains("state.isSearchEmpty"))
|
||||
assertTrue(source.contains("adapter.submitItems(state.recipients)"))
|
||||
val factory = source("main/chat/recipient/DmRecipientPickerFactory.kt")
|
||||
assertTrue(factory.contains("getFollowerList("))
|
||||
assertTrue(factory.contains("SharedPreferenceManager.userId"))
|
||||
assertTrue(factory.contains("searchUser("))
|
||||
assertTrue(factory.contains("observeOn(AndroidSchedulers.mainThread())"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `수신자 선택은 공통 확인 팝업에서만 요청하고 취소는 부작용이 없다`() {
|
||||
val source = source("main/chat/recipient/DmRecipientPickerActivity.kt")
|
||||
val confirmation = source.substringAfter("private fun confirmRecipient(").substringBefore("private fun openRoom(")
|
||||
assertTrue(confirmation.contains("V2ModalDialog("))
|
||||
assertTrue(confirmation.contains("R.string.dm_recipient_confirm_title"))
|
||||
assertTrue(confirmation.contains("getString(R.string.dm_recipient_confirm_message, recipient.nickname)"))
|
||||
assertTrue(confirmation.contains("R.string.cancel"))
|
||||
assertTrue(confirmation.contains("R.string.dm_recipient_send"))
|
||||
assertTrue(confirmation.contains("confirmButtonClick = { openRoom(recipient.id) }"))
|
||||
assertFalse(confirmation.contains("cancelButtonClick"))
|
||||
assertFalse(confirmation.contains("createOrGetRoom"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `보내기는 recipient 경로로 생성한 방 아이디로 기존 DM 화면을 연다`() {
|
||||
val source = source("main/chat/recipient/DmRecipientPickerActivity.kt")
|
||||
assertTrue(source.contains("createOrGetRoomByRecipientId(SharedPreferenceManager.token, recipientId)"))
|
||||
assertTrue(source.contains("response.success && data != null"))
|
||||
assertTrue(source.contains("DmChatRoomActivity.newIntentByRoomId(this, data.roomId)"))
|
||||
assertFalse(source.contains("newIntentByCreatorId"))
|
||||
assertTrue(
|
||||
file("app/src/main/AndroidManifest.xml").readText().contains(
|
||||
"kr.co.vividnext.sodalive.v2.main.chat.recipient.DmRecipientPickerActivity"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `수신자 화면 layout은 Figma 검색 toolbar와 검색결과 count를 사용한다`() {
|
||||
val layout = file("app/src/main/res/layout/activity_dm_recipient_picker.xml").readText()
|
||||
|
||||
assertFalse(layout.contains("layout=\"@layout/detail_toolbar\""))
|
||||
assertTrue(layout.contains("android:id=\"@+id/btn_back\""))
|
||||
assertTrue(layout.contains("android:id=\"@+id/search_container\""))
|
||||
assertTrue(layout.contains("android:id=\"@+id/iv_search\""))
|
||||
assertTrue(layout.contains("android:id=\"@+id/et_search_nickname\""))
|
||||
assertTrue(layout.contains("android:id=\"@+id/tv_search_result_count\""))
|
||||
assertTrue(layout.contains("@string/dm_recipient_search_result_count"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `검색 empty 문구는 목록 콘텐츠 영역에서 전환된다`() {
|
||||
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
.parse(file("app/src/main/res/layout/activity_dm_recipient_picker.xml"))
|
||||
val content = requireNotNull(document.findById("recipient_content_container"))
|
||||
val empty = requireNotNull(document.findById("tv_search_empty"))
|
||||
val list = requireNotNull(document.findById("rv_recipients"))
|
||||
|
||||
assertSame(content, empty.parentNode)
|
||||
assertSame(content, list.parentNode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `새 수신자 문구는 지원 언어 전체에 정의된다`() {
|
||||
val names = listOf(
|
||||
"dm_recipient_search_hint",
|
||||
"dm_recipient_followers",
|
||||
"dm_recipient_search_result_count",
|
||||
"dm_recipient_search_empty",
|
||||
"dm_recipient_confirm_title",
|
||||
"dm_recipient_confirm_message",
|
||||
"dm_recipient_send"
|
||||
)
|
||||
for (directory in listOf("values", "values-en", "values-ja")) {
|
||||
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
.parse(file("app/src/main/res/$directory/strings.xml"))
|
||||
val strings = document.getElementsByTagName("string")
|
||||
val available = (0 until strings.length).map {
|
||||
strings.item(it).attributes.getNamedItem("name").nodeValue
|
||||
}
|
||||
names.forEach { assertTrue("$directory/$it", it in available) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun source(path: String): String {
|
||||
val file = file("app/src/main/java/kr/co/vividnext/sodalive/v2/$path")
|
||||
assertTrue("수신자 연결 소스가 필요함: $path", file.exists())
|
||||
return file.readText()
|
||||
}
|
||||
|
||||
private fun file(path: String): File =
|
||||
if (File("app").isDirectory) File(path) else File("../$path")
|
||||
|
||||
private fun org.w3c.dom.Document.findById(id: String): org.w3c.dom.Node? {
|
||||
val nodes = getElementsByTagName("*")
|
||||
return (0 until nodes.length).map { nodes.item(it) }
|
||||
.firstOrNull { node ->
|
||||
val value = node.attributes?.getNamedItem("android:id")?.nodeValue
|
||||
value == "@+id/$id" || value == "@id/$id"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user