feat(chat): DM 채팅방 ViewModel을 추가한다
This commit is contained in:
@@ -0,0 +1,308 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.chat.dm
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.orhanobut.logger.Logger
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||||
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.CreateDmChatRoomResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatMessageResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatMessagesPageResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRoomOpenResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.SendDmChatMessageResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.DmChatMessageStatus
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.DmChatMessageUiItem
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.DmChatRoomUiState
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.mergeByMessageId
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.sortByCreatedAtAndMessageId
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.toUiItem
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.toUiItems
|
||||||
|
|
||||||
|
class DmChatRoomViewModel(
|
||||||
|
private val repository: DmChatRepository
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private var currentRoomId: Long = 0L
|
||||||
|
private var opponentNickname: String = ""
|
||||||
|
private var opponentProfileImageUrl: String = ""
|
||||||
|
private var currentMessages: List<DmChatMessageUiItem> = emptyList()
|
||||||
|
private var hasMore: Boolean = false
|
||||||
|
private var nextCursor: Long? = null
|
||||||
|
private var isLoadingOlder: Boolean = false
|
||||||
|
private var isSending: Boolean = false
|
||||||
|
private var localMessageSequence: Long = 0L
|
||||||
|
|
||||||
|
private val _chatRoomStateLiveData = MutableLiveData<DmChatRoomUiState>()
|
||||||
|
val chatRoomStateLiveData: LiveData<DmChatRoomUiState>
|
||||||
|
get() = _chatRoomStateLiveData
|
||||||
|
|
||||||
|
private val _finishEventLiveData = MutableLiveData<Boolean>()
|
||||||
|
val finishEventLiveData: LiveData<Boolean>
|
||||||
|
get() = _finishEventLiveData
|
||||||
|
|
||||||
|
private val _prependedMessageCountLiveData = MutableLiveData(0)
|
||||||
|
val prependedMessageCountLiveData: LiveData<Int>
|
||||||
|
get() = _prependedMessageCountLiveData
|
||||||
|
|
||||||
|
fun enter(roomId: Long, creatorId: Long) {
|
||||||
|
when {
|
||||||
|
roomId > 0L -> openRoom(roomId)
|
||||||
|
creatorId > 0L -> createRoomAndOpen(creatorId)
|
||||||
|
else -> _finishEventLiveData.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadOlderMessages() {
|
||||||
|
val roomId = currentRoomId
|
||||||
|
val cursor = nextCursor
|
||||||
|
if (roomId <= 0L || !hasMore || cursor == null || isLoadingOlder) return
|
||||||
|
|
||||||
|
isLoadingOlder = true
|
||||||
|
emitContent()
|
||||||
|
|
||||||
|
compositeDisposable.add(
|
||||||
|
repository.getMessages(
|
||||||
|
token = authToken(),
|
||||||
|
roomId = roomId,
|
||||||
|
cursor = cursor
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ handleOlderMessagesResult(it) },
|
||||||
|
{
|
||||||
|
isLoadingOlder = false
|
||||||
|
it.message?.let { message -> Logger.e(message) }
|
||||||
|
emitContent()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendText(text: String) {
|
||||||
|
val trimmed = text.trim()
|
||||||
|
if (trimmed.isBlank() || currentRoomId <= 0L || isSending) return
|
||||||
|
|
||||||
|
val localId = nextLocalId()
|
||||||
|
val localItem = DmChatMessageUiItem(
|
||||||
|
messageId = null,
|
||||||
|
localId = localId,
|
||||||
|
mine = true,
|
||||||
|
textMessage = trimmed,
|
||||||
|
senderNickname = "",
|
||||||
|
senderProfileImageUrl = "",
|
||||||
|
createdAt = System.currentTimeMillis(),
|
||||||
|
status = DmChatMessageStatus.SENDING
|
||||||
|
)
|
||||||
|
currentMessages = currentMessages + localItem
|
||||||
|
isSending = true
|
||||||
|
emitContent()
|
||||||
|
|
||||||
|
sendLocalMessage(localId = localId, text = trimmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retry(localId: String) {
|
||||||
|
val failedItem = currentMessages.firstOrNull {
|
||||||
|
it.localId == localId && it.status == DmChatMessageStatus.FAILED
|
||||||
|
} ?: return
|
||||||
|
if (isSending || currentRoomId <= 0L) return
|
||||||
|
|
||||||
|
currentMessages = currentMessages.map {
|
||||||
|
if (it.localId == localId) it.copy(status = DmChatMessageStatus.SENDING) else it
|
||||||
|
}
|
||||||
|
isSending = true
|
||||||
|
emitContent()
|
||||||
|
|
||||||
|
sendLocalMessage(localId = localId, text = failedItem.textMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRealtimeMessage(message: DmChatMessageResponse) {
|
||||||
|
val item = message.toUiItem() ?: return
|
||||||
|
currentMessages = currentMessages.mergeByMessageId(listOf(item))
|
||||||
|
emitContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncLatestMessagesAfterReconnect() {
|
||||||
|
val roomId = currentRoomId
|
||||||
|
if (roomId <= 0L) return
|
||||||
|
|
||||||
|
compositeDisposable.add(
|
||||||
|
repository.getMessages(
|
||||||
|
token = authToken(),
|
||||||
|
roomId = roomId,
|
||||||
|
cursor = null
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
val data = it.data
|
||||||
|
if (it.success && data != null) {
|
||||||
|
currentMessages = currentMessages.mergeByMessageId(data.messages.toUiItems())
|
||||||
|
emitContent()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ it.message?.let { message -> Logger.e(message) } }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRoomAndOpen(creatorId: Long) {
|
||||||
|
_chatRoomStateLiveData.value = DmChatRoomUiState.Loading
|
||||||
|
compositeDisposable.add(
|
||||||
|
repository.createOrGetRoom(token = authToken(), creatorId = creatorId)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.flatMap { response ->
|
||||||
|
val data = response.requireData()
|
||||||
|
repository.openRoom(token = authToken(), roomId = data.roomId)
|
||||||
|
}
|
||||||
|
.subscribe(
|
||||||
|
{ handleOpenRoomResult(it) },
|
||||||
|
{ handleError(it) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openRoom(roomId: Long) {
|
||||||
|
_chatRoomStateLiveData.value = DmChatRoomUiState.Loading
|
||||||
|
compositeDisposable.add(
|
||||||
|
repository.openRoom(token = authToken(), roomId = roomId)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ handleOpenRoomResult(it) },
|
||||||
|
{ handleError(it) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendLocalMessage(localId: String, text: String) {
|
||||||
|
compositeDisposable.add(
|
||||||
|
repository.sendTextMessage(
|
||||||
|
token = authToken(),
|
||||||
|
roomId = currentRoomId,
|
||||||
|
textMessage = text
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ handleSendResult(localId, it) },
|
||||||
|
{
|
||||||
|
it.message?.let { message -> Logger.e(message) }
|
||||||
|
markLocalMessageFailed(localId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleOpenRoomResult(response: ApiResponse<DmChatRoomOpenResponse>) {
|
||||||
|
val data = response.data
|
||||||
|
if (!response.success || data == null) {
|
||||||
|
showError(response.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentRoomId = data.roomId
|
||||||
|
opponentNickname = data.opponentNickname
|
||||||
|
opponentProfileImageUrl = data.opponentProfileImageUrl
|
||||||
|
currentMessages = data.messages.toUiItems().sortByCreatedAtAndMessageId()
|
||||||
|
hasMore = data.hasMore
|
||||||
|
nextCursor = data.nextCursor
|
||||||
|
isLoadingOlder = false
|
||||||
|
emitContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleOlderMessagesResult(response: ApiResponse<DmChatMessagesPageResponse>) {
|
||||||
|
isLoadingOlder = false
|
||||||
|
val data = response.data
|
||||||
|
if (!response.success || data == null) {
|
||||||
|
emitContent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val beforeIds = currentMessages.mapNotNull { it.messageId }.toSet()
|
||||||
|
currentMessages = currentMessages.mergeByMessageId(data.messages.toUiItems())
|
||||||
|
hasMore = data.hasMore
|
||||||
|
nextCursor = data.nextCursor
|
||||||
|
_prependedMessageCountLiveData.value = currentMessages.count {
|
||||||
|
it.messageId != null && it.messageId !in beforeIds
|
||||||
|
}
|
||||||
|
emitContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSendResult(
|
||||||
|
localId: String,
|
||||||
|
response: ApiResponse<SendDmChatMessageResponse>
|
||||||
|
) {
|
||||||
|
val message = response.data?.message
|
||||||
|
val sentItem = if (response.success && message != null) message.toUiItem() else null
|
||||||
|
if (sentItem == null) {
|
||||||
|
markLocalMessageFailed(localId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isSending = false
|
||||||
|
currentMessages = currentMessages.map {
|
||||||
|
if (it.localId == localId) sentItem else it
|
||||||
|
}.deduplicateSentMessage(sentItem.messageId).sortByCreatedAtAndMessageId()
|
||||||
|
emitContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<DmChatMessageUiItem>.deduplicateSentMessage(messageId: Long?): List<DmChatMessageUiItem> {
|
||||||
|
if (messageId == null) return this
|
||||||
|
var found = false
|
||||||
|
return filter { item ->
|
||||||
|
if (item.messageId != messageId) return@filter true
|
||||||
|
if (found) return@filter false
|
||||||
|
found = true
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun markLocalMessageFailed(localId: String) {
|
||||||
|
isSending = false
|
||||||
|
currentMessages = currentMessages.map {
|
||||||
|
if (it.localId == localId) it.copy(status = DmChatMessageStatus.FAILED) else it
|
||||||
|
}
|
||||||
|
emitContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleError(throwable: Throwable) {
|
||||||
|
throwable.message?.let { Logger.e(it) }
|
||||||
|
showError(throwable.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showError(message: String?) {
|
||||||
|
_chatRoomStateLiveData.value = DmChatRoomUiState.Error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitContent() {
|
||||||
|
if (currentRoomId <= 0L) return
|
||||||
|
_chatRoomStateLiveData.value = DmChatRoomUiState.Content(
|
||||||
|
roomId = currentRoomId,
|
||||||
|
opponentNickname = opponentNickname,
|
||||||
|
opponentProfileImageUrl = opponentProfileImageUrl,
|
||||||
|
messages = currentMessages,
|
||||||
|
hasMore = hasMore,
|
||||||
|
nextCursor = nextCursor,
|
||||||
|
isLoadingOlder = isLoadingOlder
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nextLocalId(): String {
|
||||||
|
localMessageSequence += 1L
|
||||||
|
return "local-$localMessageSequence"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun authToken(): String = SharedPreferenceManager.token
|
||||||
|
|
||||||
|
private fun ApiResponse<CreateDmChatRoomResponse>.requireData(): CreateDmChatRoomResponse {
|
||||||
|
if (success && data != null) return data
|
||||||
|
throw IllegalStateException(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,3 +16,19 @@ data class DmChatMessageUiItem(
|
|||||||
val createdAt: Long,
|
val createdAt: Long,
|
||||||
val status: DmChatMessageStatus
|
val status: DmChatMessageStatus
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sealed class DmChatRoomUiState {
|
||||||
|
data object Loading : DmChatRoomUiState()
|
||||||
|
|
||||||
|
data class Content(
|
||||||
|
val roomId: Long,
|
||||||
|
val opponentNickname: String,
|
||||||
|
val opponentProfileImageUrl: String,
|
||||||
|
val messages: List<DmChatMessageUiItem>,
|
||||||
|
val hasMore: Boolean,
|
||||||
|
val nextCursor: Long?,
|
||||||
|
val isLoadingOlder: Boolean = false
|
||||||
|
) : DmChatRoomUiState()
|
||||||
|
|
||||||
|
data class Error(val message: String?) : DmChatRoomUiState()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.chat.dm
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
|
||||||
|
import io.reactivex.rxjava3.core.Scheduler
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import io.reactivex.rxjava3.subjects.SingleSubject
|
||||||
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest.Companion.message
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest.Companion.messagesPage
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest.Companion.openResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModelTest.Companion.requireValue
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatMessagesPageResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.DmChatRoomUiState
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(sdk = [28], application = Application::class)
|
||||||
|
class DmChatPaginationStateTest {
|
||||||
|
|
||||||
|
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||||
|
private lateinit var api: FakeDmChatApi
|
||||||
|
private lateinit var viewModel: DmChatRoomViewModel
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
setImmediateRxSchedulers()
|
||||||
|
SharedPreferenceManager.resetForTest()
|
||||||
|
SharedPreferenceManager.init(context)
|
||||||
|
SharedPreferenceManager.token = "test-token"
|
||||||
|
api = FakeDmChatApi()
|
||||||
|
viewModel = DmChatRoomViewModel(repository = DmChatRepository(api))
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
RxJavaPlugins.reset()
|
||||||
|
RxAndroidPlugins.reset()
|
||||||
|
SharedPreferenceManager.resetForTest()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `hasMore가 false이면 과거 메시지를 요청하지 않는다`() {
|
||||||
|
api.enqueueOpenSuccess(openResponse(roomId = 10L, hasMore = false, nextCursor = 1L))
|
||||||
|
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||||
|
|
||||||
|
viewModel.loadOlderMessages()
|
||||||
|
|
||||||
|
assertTrue(api.messagesCalls.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `nextCursor가 null이면 과거 메시지를 요청하지 않는다`() {
|
||||||
|
api.enqueueOpenSuccess(openResponse(roomId = 10L, hasMore = true, nextCursor = null))
|
||||||
|
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||||
|
|
||||||
|
viewModel.loadOlderMessages()
|
||||||
|
|
||||||
|
assertTrue(api.messagesCalls.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `과거 메시지 로딩 중 중복 요청은 무시한다`() {
|
||||||
|
val pendingOlder = SingleSubject.create<ApiResponse<DmChatMessagesPageResponse>>()
|
||||||
|
api.enqueueOpenSuccess(openResponse(roomId = 10L, hasMore = true, nextCursor = 50L))
|
||||||
|
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||||
|
api.enqueueMessagesSuccessSubject(pendingOlder)
|
||||||
|
|
||||||
|
viewModel.loadOlderMessages()
|
||||||
|
viewModel.loadOlderMessages()
|
||||||
|
|
||||||
|
assertEquals(1, api.messagesCalls.size)
|
||||||
|
val loadingState = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
assertTrue(loadingState.isLoadingOlder)
|
||||||
|
|
||||||
|
pendingOlder.onSuccess(ApiResponse(success = true, data = messagesPage(messages = emptyList())))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `과거 메시지 요청 실패는 로딩 상태를 해제하고 기존 목록을 유지한다`() {
|
||||||
|
api.enqueueOpenSuccess(
|
||||||
|
openResponse(
|
||||||
|
roomId = 10L,
|
||||||
|
messages = listOf(message(messageId = 2L, createdAt = 200L, textMessage = "현재")),
|
||||||
|
hasMore = true,
|
||||||
|
nextCursor = 50L
|
||||||
|
)
|
||||||
|
)
|
||||||
|
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||||
|
api.enqueueMessages(Single.error(IllegalStateException("network")))
|
||||||
|
|
||||||
|
viewModel.loadOlderMessages()
|
||||||
|
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
assertFalse(state.isLoadingOlder)
|
||||||
|
assertEquals(listOf(2L), state.messages.map { it.messageId })
|
||||||
|
assertEquals(1, api.messagesCalls.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `과거 메시지는 cursor로 요청하고 기존 목록 상단에 prepend하며 중복을 제거한다`() {
|
||||||
|
api.enqueueOpenSuccess(
|
||||||
|
openResponse(
|
||||||
|
roomId = 10L,
|
||||||
|
messages = listOf(message(messageId = 2L, createdAt = 200L, textMessage = "현재")),
|
||||||
|
hasMore = true,
|
||||||
|
nextCursor = 50L
|
||||||
|
)
|
||||||
|
)
|
||||||
|
api.enqueueMessagesSuccess(
|
||||||
|
messagesPage(
|
||||||
|
messages = listOf(
|
||||||
|
message(messageId = 1L, createdAt = 100L, textMessage = "과거"),
|
||||||
|
message(messageId = 2L, createdAt = 200L, textMessage = "중복")
|
||||||
|
),
|
||||||
|
hasMore = false,
|
||||||
|
nextCursor = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||||
|
|
||||||
|
viewModel.loadOlderMessages()
|
||||||
|
|
||||||
|
assertEquals(listOf(MessagesCall("Bearer test-token", 10L, 50L, 20)), api.messagesCalls)
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
assertEquals(listOf(1L, 2L), state.messages.map { it.messageId })
|
||||||
|
assertEquals(listOf("과거", "현재"), state.messages.map { it.textMessage })
|
||||||
|
assertFalse(state.hasMore)
|
||||||
|
assertEquals(1, viewModel.prependedMessageCountLiveData.requireValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setImmediateRxSchedulers() {
|
||||||
|
val trampoline = { _: Scheduler -> Schedulers.trampoline() }
|
||||||
|
RxJavaPlugins.setIoSchedulerHandler(trampoline)
|
||||||
|
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
|
||||||
|
RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun FakeDmChatApi.enqueueMessagesSuccessSubject(
|
||||||
|
subject: SingleSubject<ApiResponse<DmChatMessagesPageResponse>>
|
||||||
|
) = enqueueMessages(subject)
|
||||||
|
}
|
||||||
@@ -0,0 +1,451 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.chat.dm
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
|
||||||
|
import io.reactivex.rxjava3.core.Scheduler
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import io.reactivex.rxjava3.subjects.SingleSubject
|
||||||
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.CreateDmChatRoomRequest
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.CreateDmChatRoomResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatApi
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatMessageResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatMessagesPageResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRoomOpenResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.SendDmChatMessageResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.SendDmTextMessageRequest
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.DmChatMessageStatus
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.DmChatRoomUiState
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(sdk = [28], application = Application::class)
|
||||||
|
class DmChatRoomViewModelTest {
|
||||||
|
|
||||||
|
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||||
|
private lateinit var api: FakeDmChatApi
|
||||||
|
private lateinit var viewModel: DmChatRoomViewModel
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
setImmediateRxSchedulers()
|
||||||
|
SharedPreferenceManager.resetForTest()
|
||||||
|
SharedPreferenceManager.init(context)
|
||||||
|
SharedPreferenceManager.token = "test-token"
|
||||||
|
api = FakeDmChatApi()
|
||||||
|
viewModel = DmChatRoomViewModel(repository = DmChatRepository(api))
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
RxJavaPlugins.reset()
|
||||||
|
RxAndroidPlugins.reset()
|
||||||
|
SharedPreferenceManager.resetForTest()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `roomId 진입은 create 없이 openRoom을 호출하고 메시지를 정렬한다`() {
|
||||||
|
api.enqueueOpenSuccess(
|
||||||
|
openResponse(
|
||||||
|
roomId = 10L,
|
||||||
|
messages = listOf(
|
||||||
|
message(messageId = 2L, createdAt = 200L, textMessage = "둘"),
|
||||||
|
message(messageId = 1L, createdAt = 100L, textMessage = "하나")
|
||||||
|
),
|
||||||
|
hasMore = true,
|
||||||
|
nextCursor = 1L
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||||
|
|
||||||
|
assertTrue(api.createCalls.isEmpty())
|
||||||
|
assertEquals(listOf(OpenCall("Bearer test-token", 10L, 20)), api.openCalls)
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
assertEquals(10L, state.roomId)
|
||||||
|
assertEquals("상대", state.opponentNickname)
|
||||||
|
assertEquals(listOf(1L, 2L), state.messages.map { it.messageId })
|
||||||
|
assertTrue(state.hasMore)
|
||||||
|
assertEquals(1L, state.nextCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `creatorId 진입은 createOrGetRoom 후 반환 roomId로 openRoom을 호출한다`() {
|
||||||
|
api.enqueueCreateSuccess(CreateDmChatRoomResponse(roomId = 12L))
|
||||||
|
api.enqueueOpenSuccess(openResponse(roomId = 12L))
|
||||||
|
|
||||||
|
viewModel.enter(roomId = 0L, creatorId = 99L)
|
||||||
|
|
||||||
|
assertEquals(listOf(CreateCall("Bearer test-token", CreateDmChatRoomRequest(creatorId = 99L))), api.createCalls)
|
||||||
|
assertEquals(listOf(OpenCall("Bearer test-token", 12L, 20)), api.openCalls)
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
assertEquals(12L, state.roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `유효하지 않은 진입 값은 종료 이벤트를 발행한다`() {
|
||||||
|
viewModel.enter(roomId = 0L, creatorId = 0L)
|
||||||
|
|
||||||
|
assertTrue(viewModel.finishEventLiveData.requireValue() == true)
|
||||||
|
assertTrue(api.createCalls.isEmpty())
|
||||||
|
assertTrue(api.openCalls.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `blank 전송은 요청하지 않고 pending 메시지를 추가하지 않는다`() {
|
||||||
|
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||||
|
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||||
|
|
||||||
|
viewModel.sendText(" ")
|
||||||
|
|
||||||
|
assertTrue(api.sendCalls.isEmpty())
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
assertTrue(state.messages.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `전송 직후 pending을 추가하고 성공 시 서버 메시지로 교체한다`() {
|
||||||
|
val pendingSend = SingleSubject.create<ApiResponse<SendDmChatMessageResponse>>()
|
||||||
|
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||||
|
api.enqueueSend(pendingSend)
|
||||||
|
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||||
|
|
||||||
|
viewModel.sendText(" 안녕 ")
|
||||||
|
val sendingState = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
|
||||||
|
assertEquals(listOf(SendCall("Bearer test-token", 10L, SendDmTextMessageRequest("안녕"))), api.sendCalls)
|
||||||
|
assertEquals(DmChatMessageStatus.SENDING, sendingState.messages.single().status)
|
||||||
|
assertEquals("안녕", sendingState.messages.single().textMessage)
|
||||||
|
|
||||||
|
pendingSend.onSuccess(
|
||||||
|
ApiResponse(
|
||||||
|
success = true,
|
||||||
|
data = SendDmChatMessageResponse(
|
||||||
|
message = message(messageId = 30L, mine = true, textMessage = "안녕"),
|
||||||
|
deliveredRealtime = true,
|
||||||
|
pushSent = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val sentState = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
assertEquals(listOf(30L), sentState.messages.map { it.messageId })
|
||||||
|
assertEquals(DmChatMessageStatus.SENT, sentState.messages.single().status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `전송 중 새 전송 중복 요청은 무시한다`() {
|
||||||
|
val pendingSend = SingleSubject.create<ApiResponse<SendDmChatMessageResponse>>()
|
||||||
|
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||||
|
api.enqueueSend(pendingSend)
|
||||||
|
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||||
|
|
||||||
|
viewModel.sendText("안녕")
|
||||||
|
viewModel.sendText("안녕")
|
||||||
|
|
||||||
|
assertEquals(1, api.sendCalls.size)
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
assertEquals(1, state.messages.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `전송 실패는 pending 메시지를 FAILED로 바꾸고 retry 성공 시 교체한다`() {
|
||||||
|
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||||
|
api.enqueueSend(Single.error(IllegalStateException("network")))
|
||||||
|
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||||
|
|
||||||
|
viewModel.sendText("안녕")
|
||||||
|
val failedState = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
val failedItem = failedState.messages.single()
|
||||||
|
assertEquals(DmChatMessageStatus.FAILED, failedItem.status)
|
||||||
|
|
||||||
|
api.enqueueSendSuccess(message(messageId = 40L, mine = true, textMessage = "안녕"))
|
||||||
|
viewModel.retry(failedItem.localId!!)
|
||||||
|
|
||||||
|
val retriedState = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
assertEquals(listOf(40L), retriedState.messages.map { it.messageId })
|
||||||
|
assertEquals(DmChatMessageStatus.SENT, retriedState.messages.single().status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `retry 중 SSE echo가 먼저 와도 성공 교체 후 messageId 중복을 남기지 않는다`() {
|
||||||
|
val pendingRetry = SingleSubject.create<ApiResponse<SendDmChatMessageResponse>>()
|
||||||
|
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||||
|
api.enqueueSend(Single.error(IllegalStateException("network")))
|
||||||
|
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||||
|
viewModel.sendText("안녕")
|
||||||
|
val failedState = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
val failedItem = failedState.messages.single()
|
||||||
|
api.enqueueSend(pendingRetry)
|
||||||
|
|
||||||
|
viewModel.retry(failedItem.localId!!)
|
||||||
|
viewModel.onRealtimeMessage(message(messageId = 45L, mine = true, textMessage = "안녕"))
|
||||||
|
pendingRetry.onSuccess(
|
||||||
|
ApiResponse(
|
||||||
|
success = true,
|
||||||
|
data = SendDmChatMessageResponse(
|
||||||
|
message = message(messageId = 45L, mine = true, textMessage = "안녕"),
|
||||||
|
deliveredRealtime = true,
|
||||||
|
pushSent = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val retriedState = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
assertEquals(listOf(45L), retriedState.messages.map { it.messageId })
|
||||||
|
assertEquals(1, retriedState.messages.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SSE 메시지는 messageId 중복을 제거하고 최신 메시지를 추가한다`() {
|
||||||
|
api.enqueueOpenSuccess(openResponse(roomId = 10L, messages = listOf(message(messageId = 1L, textMessage = "기존"))))
|
||||||
|
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||||
|
|
||||||
|
viewModel.onRealtimeMessage(message(messageId = 1L, textMessage = "중복"))
|
||||||
|
viewModel.onRealtimeMessage(message(messageId = 2L, createdAt = 200L, textMessage = "신규"))
|
||||||
|
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
assertEquals(listOf(1L, 2L), state.messages.map { it.messageId })
|
||||||
|
assertEquals(listOf("기존", "신규"), state.messages.map { it.textMessage })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SSE echo가 전송 성공보다 먼저 와도 성공 교체 후 messageId 중복을 남기지 않는다`() {
|
||||||
|
val pendingSend = SingleSubject.create<ApiResponse<SendDmChatMessageResponse>>()
|
||||||
|
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||||
|
api.enqueueSend(pendingSend)
|
||||||
|
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||||
|
|
||||||
|
viewModel.sendText("안녕")
|
||||||
|
viewModel.onRealtimeMessage(message(messageId = 50L, mine = true, textMessage = "안녕"))
|
||||||
|
pendingSend.onSuccess(
|
||||||
|
ApiResponse(
|
||||||
|
success = true,
|
||||||
|
data = SendDmChatMessageResponse(
|
||||||
|
message = message(messageId = 50L, mine = true, textMessage = "안녕"),
|
||||||
|
deliveredRealtime = true,
|
||||||
|
pushSent = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
assertEquals(listOf(50L), state.messages.map { it.messageId })
|
||||||
|
assertEquals(1, state.messages.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `재연결 후 최신 메시지 동기화는 getMessages 결과를 병합한다`() {
|
||||||
|
api.enqueueOpenSuccess(openResponse(roomId = 10L, messages = listOf(message(messageId = 1L, textMessage = "기존"))))
|
||||||
|
api.enqueueMessagesSuccess(
|
||||||
|
messagesPage(
|
||||||
|
messages = listOf(message(messageId = 2L, createdAt = 200L, textMessage = "동기화"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||||
|
|
||||||
|
viewModel.syncLatestMessagesAfterReconnect()
|
||||||
|
|
||||||
|
assertEquals(listOf(MessagesCall("Bearer test-token", 10L, null, 20)), api.messagesCalls)
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
assertEquals(listOf(1L, 2L), state.messages.map { it.messageId })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `재연결 후 최신 메시지 동기화 실패는 기존 메시지를 유지한다`() {
|
||||||
|
api.enqueueOpenSuccess(openResponse(roomId = 10L, messages = listOf(message(messageId = 1L, textMessage = "기존"))))
|
||||||
|
api.enqueueMessages(Single.error(IllegalStateException("network")))
|
||||||
|
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||||
|
|
||||||
|
viewModel.syncLatestMessagesAfterReconnect()
|
||||||
|
|
||||||
|
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||||
|
assertEquals(listOf(1L), state.messages.map { it.messageId })
|
||||||
|
assertEquals(listOf("기존"), state.messages.map { it.textMessage })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setImmediateRxSchedulers() {
|
||||||
|
val trampoline = { _: Scheduler -> Schedulers.trampoline() }
|
||||||
|
RxJavaPlugins.setIoSchedulerHandler(trampoline)
|
||||||
|
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
|
||||||
|
RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun openResponse(
|
||||||
|
roomId: Long,
|
||||||
|
messages: List<DmChatMessageResponse> = emptyList(),
|
||||||
|
hasMore: Boolean = false,
|
||||||
|
nextCursor: Long? = null
|
||||||
|
) = DmChatRoomOpenResponse(
|
||||||
|
roomId = roomId,
|
||||||
|
opponentNickname = "상대",
|
||||||
|
opponentProfileImageUrl = "https://example.com/profile.png",
|
||||||
|
messages = messages,
|
||||||
|
hasMore = hasMore,
|
||||||
|
nextCursor = nextCursor
|
||||||
|
)
|
||||||
|
|
||||||
|
fun messagesPage(
|
||||||
|
messages: List<DmChatMessageResponse>,
|
||||||
|
hasMore: Boolean = false,
|
||||||
|
nextCursor: Long? = null
|
||||||
|
) = DmChatMessagesPageResponse(
|
||||||
|
messages = messages,
|
||||||
|
hasMore = hasMore,
|
||||||
|
nextCursor = nextCursor
|
||||||
|
)
|
||||||
|
|
||||||
|
fun message(
|
||||||
|
messageId: Long,
|
||||||
|
messageType: String = "TEXT",
|
||||||
|
mine: Boolean = false,
|
||||||
|
createdAt: Long = 100L,
|
||||||
|
textMessage: String? = "메시지"
|
||||||
|
) = DmChatMessageResponse(
|
||||||
|
messageId = messageId,
|
||||||
|
messageType = messageType,
|
||||||
|
mine = mine,
|
||||||
|
createdAt = createdAt,
|
||||||
|
textMessage = textMessage,
|
||||||
|
voiceMessageUrl = null,
|
||||||
|
senderId = if (mine) 1L else 2L,
|
||||||
|
senderNickname = if (mine) "나" else "상대",
|
||||||
|
senderProfileImageUrl = "https://example.com/profile.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
fun <T> LiveData<T>.requireValue(): T? {
|
||||||
|
var value: T? = null
|
||||||
|
val observer = Observer<T> { value = it }
|
||||||
|
observeForever(observer)
|
||||||
|
removeObserver(observer)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CreateCall(
|
||||||
|
val authHeader: String,
|
||||||
|
val request: CreateDmChatRoomRequest
|
||||||
|
)
|
||||||
|
|
||||||
|
data class OpenCall(
|
||||||
|
val authHeader: String,
|
||||||
|
val roomId: Long,
|
||||||
|
val limit: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MessagesCall(
|
||||||
|
val authHeader: String,
|
||||||
|
val roomId: Long,
|
||||||
|
val cursor: Long?,
|
||||||
|
val limit: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SendCall(
|
||||||
|
val authHeader: String,
|
||||||
|
val roomId: Long,
|
||||||
|
val request: SendDmTextMessageRequest
|
||||||
|
)
|
||||||
|
|
||||||
|
class FakeDmChatApi : DmChatApi {
|
||||||
|
val createCalls = mutableListOf<CreateCall>()
|
||||||
|
val openCalls = mutableListOf<OpenCall>()
|
||||||
|
val messagesCalls = mutableListOf<MessagesCall>()
|
||||||
|
val sendCalls = mutableListOf<SendCall>()
|
||||||
|
|
||||||
|
private val createResponses = ArrayDeque<Single<ApiResponse<CreateDmChatRoomResponse>>>()
|
||||||
|
private val openResponses = ArrayDeque<Single<ApiResponse<DmChatRoomOpenResponse>>>()
|
||||||
|
private val messagesResponses = ArrayDeque<Single<ApiResponse<DmChatMessagesPageResponse>>>()
|
||||||
|
private val sendResponses = ArrayDeque<Single<ApiResponse<SendDmChatMessageResponse>>>()
|
||||||
|
|
||||||
|
fun enqueueCreateSuccess(response: CreateDmChatRoomResponse) {
|
||||||
|
createResponses.addLast(Single.just(ApiResponse(success = true, data = response)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enqueueOpenSuccess(response: DmChatRoomOpenResponse) {
|
||||||
|
openResponses.addLast(Single.just(ApiResponse(success = true, data = response)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enqueueMessagesSuccess(response: DmChatMessagesPageResponse) {
|
||||||
|
messagesResponses.addLast(Single.just(ApiResponse(success = true, data = response)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enqueueMessages(response: Single<ApiResponse<DmChatMessagesPageResponse>>) {
|
||||||
|
messagesResponses.addLast(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enqueueSend(response: Single<ApiResponse<SendDmChatMessageResponse>>) {
|
||||||
|
sendResponses.addLast(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enqueueSendSuccess(message: DmChatMessageResponse) {
|
||||||
|
sendResponses.addLast(
|
||||||
|
Single.just(
|
||||||
|
ApiResponse(
|
||||||
|
success = true,
|
||||||
|
data = SendDmChatMessageResponse(
|
||||||
|
message = message,
|
||||||
|
deliveredRealtime = true,
|
||||||
|
pushSent = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createDmChatRoom(
|
||||||
|
authHeader: String,
|
||||||
|
request: CreateDmChatRoomRequest
|
||||||
|
): Single<ApiResponse<CreateDmChatRoomResponse>> {
|
||||||
|
createCalls.add(CreateCall(authHeader, request))
|
||||||
|
return createResponses.removeFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openDmChatRoom(
|
||||||
|
authHeader: String,
|
||||||
|
roomId: Long,
|
||||||
|
limit: Int
|
||||||
|
): Single<ApiResponse<DmChatRoomOpenResponse>> {
|
||||||
|
openCalls.add(OpenCall(authHeader, roomId, limit))
|
||||||
|
return openResponses.removeFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDmChatMessages(
|
||||||
|
authHeader: String,
|
||||||
|
roomId: Long,
|
||||||
|
cursor: Long?,
|
||||||
|
limit: Int
|
||||||
|
): Single<ApiResponse<DmChatMessagesPageResponse>> {
|
||||||
|
messagesCalls.add(MessagesCall(authHeader, roomId, cursor, limit))
|
||||||
|
return messagesResponses.removeFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sendDmTextMessage(
|
||||||
|
authHeader: String,
|
||||||
|
roomId: Long,
|
||||||
|
request: SendDmTextMessageRequest
|
||||||
|
): Single<ApiResponse<SendDmChatMessageResponse>> {
|
||||||
|
sendCalls.add(SendCall(authHeader, roomId, request))
|
||||||
|
return sendResponses.removeFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disconnectRealtime(
|
||||||
|
authHeader: String,
|
||||||
|
roomId: Long
|
||||||
|
): Single<ApiResponse<Boolean>> = Single.just(ApiResponse(success = true, data = true))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user