diff --git a/app/src/main/java/com/idle/care/notification/NotificationService.kt b/app/src/main/java/com/idle/care/notification/NotificationService.kt index 7050ccbe..cfd2c78e 100644 --- a/app/src/main/java/com/idle/care/notification/NotificationService.kt +++ b/app/src/main/java/com/idle/care/notification/NotificationService.kt @@ -3,8 +3,8 @@ package com.idle.care.notification import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import com.idle.domain.model.error.ErrorHelper -import com.idle.domain.repositorry.TokenRepository import com.idle.domain.repositorry.ProfileRepository +import com.idle.domain.repositorry.TokenRepository import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope @@ -40,10 +40,12 @@ class NotificationService : FirebaseMessagingService() { scope.launch { val userType = profileRepository.getMyUserType() - tokenRepository.postDeviceToken( - deviceToken = token, - userType = userType, - ) + if (userType.isNotEmpty()) { + tokenRepository.postDeviceToken( + deviceToken = token, + userType = userType, + ) + } } } diff --git a/core/data/src/main/java/com/idle/data/repository/ChatRepositoryImpl.kt b/core/data/src/main/java/com/idle/data/repository/ChatRepositoryImpl.kt index 4197bd70..21962d0a 100644 --- a/core/data/src/main/java/com/idle/data/repository/ChatRepositoryImpl.kt +++ b/core/data/src/main/java/com/idle/data/repository/ChatRepositoryImpl.kt @@ -95,22 +95,42 @@ class ChatRepositoryImpl @Inject constructor( messageId = messageId, ) }.mapCatching { response -> - response.map { - val message = it.toVO() - if (!localChatDataSource.isChatRoomExist(message.roomId, myId)) { - localChatDataSource.insertChatRoom( - myId = myId, - chatRoom = ChatRoom( - id = message.roomId, - opponentId = if (myId == message.senderId) message.receiverId else message.senderId, - lastMessage = message.content, - lastMessageTime = message.createdAt, - unReadMessageCount = 1, + val messages = response.chatMessageInfos + val readSequence = response.sequence + + messages.lastOrNull()?.let { + localChatDataSource.readMessages( + roomId = roomId, + myId = myId, + senderId = it.senderId ?: return@let, + sequence = readSequence, + ) + } + + messages + .sortedBy { it.sequence } + .map { + val message = it.toVO() + + if (!localChatDataSource.isChatRoomExist(message.roomId, myId)) { + localChatDataSource.insertChatRoom( + myId = myId, + chatRoom = ChatRoom( + id = message.roomId, + opponentId = if (myId == message.senderId) message.receiverId else message.senderId, + lastMessage = message.content, + lastMessageTime = message.createdAt, + unReadMessageCount = 1, + ) ) - ) + } + + val maxSeq = + localChatDataSource.getMaxLocalSequence(roomId, myId) ?: Int.MIN_VALUE + if (message.sequence > maxSeq) { + localChatDataSource.insertMessage(message, myId) + } } - localChatDataSource.insertMessage(message, myId) - } }.getOrThrow() } @@ -138,6 +158,7 @@ class ChatRepositoryImpl @Inject constructor( chatDataSource.subscribeChatMessage(userId) .map { val message = it.toVO() + when (message) { is ChatMessage -> { if (!localChatDataSource.isChatRoomExist( @@ -165,9 +186,11 @@ class ChatRepositoryImpl @Inject constructor( roomId = message.chatroomId, myId = userId, senderId = message.opponentId, + sequence = message.sequence, ) } } + message }.retryWhen { cause, attempt -> if (cause is IOException && attempt < MAX_RETRY_ATTEMPTS) { @@ -201,18 +224,21 @@ class ChatRepositoryImpl @Inject constructor( myId: String, opponentId: String, userType: UserType, + sequence: Int, ): Result = chatDataSource.readMessage( userType = userType, readMessageRequest = ReadMessageRequest( chatroomId = chatroomId, opponentId = opponentId, + sequence = sequence, ) ).onSuccess { localChatDataSource.readMessages( roomId = chatroomId, myId = myId, senderId = opponentId, + sequence = sequence, ) } } diff --git a/core/database/src/main/java/com/idle/database/dao/MessagesDao.kt b/core/database/src/main/java/com/idle/database/dao/MessagesDao.kt index 4c17927b..6679f7d0 100644 --- a/core/database/src/main/java/com/idle/database/dao/MessagesDao.kt +++ b/core/database/src/main/java/com/idle/database/dao/MessagesDao.kt @@ -1,15 +1,14 @@ package com.idle.database.dao import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Upsert import com.idle.database.model.MessageEntity @Dao interface MessagesDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertMessage(messages: MessageEntity) + @Upsert + suspend fun upsertMessage(messages: MessageEntity) @Query( """ @@ -25,7 +24,7 @@ interface MessagesDao { roomId: String, myId: String, lastMessageId: String?, - limit: Int = 50 + limit: Int = 50, ): List @Query( @@ -36,12 +35,14 @@ interface MessagesDao { AND myId = :myId AND senderId = :opponentId AND isRead = 0 + AND sequence <= :sequence """ ) suspend fun readMessages( roomId: String, myId: String, opponentId: String, + sequence: Int, ) @Query( @@ -60,4 +61,14 @@ interface MessagesDao { roomId: String, messageId: String, ): Boolean + + @Query( + """ + SELECT MAX(sequence) + FROM message + WHERE roomId = :roomId + AND myId = :myId + """ + ) + suspend fun getMaxLocalSequence(roomId: String, myId: String): Int? } diff --git a/core/database/src/main/java/com/idle/database/model/MessageEntity.kt b/core/database/src/main/java/com/idle/database/model/MessageEntity.kt index 6dfc2882..2e7f40fd 100644 --- a/core/database/src/main/java/com/idle/database/model/MessageEntity.kt +++ b/core/database/src/main/java/com/idle/database/model/MessageEntity.kt @@ -3,23 +3,23 @@ package com.idle.database.model import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Index -import androidx.room.PrimaryKey import com.idle.domain.model.chat.ChatMessage import java.time.LocalDateTime @Entity( tableName = "message", - indices = [Index(value = ["roomId", "id"], unique = false)], - foreignKeys = arrayOf( + primaryKeys = ["myId", "id"], + indices = [Index(value = ["roomId", "sequence"])], + foreignKeys = [ ForeignKey( entity = ChatRoomEntity::class, parentColumns = ["id", "myId"], childColumns = ["roomId", "myId"], ) - ) + ] ) data class MessageEntity( - @PrimaryKey val id: String, + val id: String, val roomId: String, val myId: String, val senderId: String, @@ -27,6 +27,7 @@ data class MessageEntity( val content: String, val createdAt: LocalDateTime, val isRead: Boolean, + val sequence: Int, ) { internal fun toDomain() = ChatMessage( id = id, @@ -36,6 +37,7 @@ data class MessageEntity( content = content, createdAt = createdAt, isRead = isRead, + sequence = sequence, ) } @@ -48,4 +50,5 @@ internal fun ChatMessage.toMessageEntity(myId: String) = MessageEntity( content = content, createdAt = createdAt, isRead = isRead, + sequence = sequence, ) diff --git a/core/database/src/main/java/com/idle/database/source/LocalChatDataSource.kt b/core/database/src/main/java/com/idle/database/source/LocalChatDataSource.kt index 244ac3fb..d5628223 100644 --- a/core/database/src/main/java/com/idle/database/source/LocalChatDataSource.kt +++ b/core/database/src/main/java/com/idle/database/source/LocalChatDataSource.kt @@ -15,7 +15,7 @@ class LocalChatDataSource @Inject constructor( private val chatRoomsDao: ChatRoomsDao, ) { suspend fun insertMessage(message: ChatMessage, myId: String) = - messagesDao.insertMessage(message.toMessageEntity(myId)) + messagesDao.upsertMessage(message.toMessageEntity(myId)) suspend fun getMessages( roomId: String, @@ -32,7 +32,8 @@ class LocalChatDataSource @Inject constructor( roomId: String, myId: String, senderId: String, - ): Unit = messagesDao.readMessages(roomId, myId, senderId) + sequence: Int, + ): Unit = messagesDao.readMessages(roomId, myId, senderId, sequence) suspend fun isMessageExist(roomId: String, myId: String, messageId: String): Boolean = messagesDao.isMessageExist( @@ -41,6 +42,12 @@ class LocalChatDataSource @Inject constructor( messageId = messageId, ) + suspend fun getMaxLocalSequence(roomId: String, myId: String): Int? = + messagesDao.getMaxLocalSequence( + roomId = roomId, + myId = myId, + ) + suspend fun insertChatRoom(myId: String, chatRoom: ChatRoom) = chatRoomsDao.insertChatRoom( ChatRoomEntity( diff --git a/core/domain/src/main/kotlin/com/idle/domain/model/chat/Message.kt b/core/domain/src/main/kotlin/com/idle/domain/model/chat/Message.kt index f7f0b6a3..7e195b5a 100644 --- a/core/domain/src/main/kotlin/com/idle/domain/model/chat/Message.kt +++ b/core/domain/src/main/kotlin/com/idle/domain/model/chat/Message.kt @@ -2,7 +2,7 @@ package com.idle.domain.model.chat import java.time.LocalDateTime -sealed class Message +sealed class Message(open val sequence: Int) data class ChatMessage( val id: String, @@ -12,9 +12,11 @@ data class ChatMessage( val content: String, val createdAt: LocalDateTime, val isRead: Boolean, -) : Message() + override val sequence: Int, +) : Message(sequence) data class ReadMessage( val opponentId: String, val chatroomId: String, -) : Message() + override val sequence: Int, +) : Message(sequence) diff --git a/core/domain/src/main/kotlin/com/idle/domain/repositorry/ChatRepository.kt b/core/domain/src/main/kotlin/com/idle/domain/repositorry/ChatRepository.kt index c3bfdd62..b3e4b85d 100644 --- a/core/domain/src/main/kotlin/com/idle/domain/repositorry/ChatRepository.kt +++ b/core/domain/src/main/kotlin/com/idle/domain/repositorry/ChatRepository.kt @@ -54,5 +54,6 @@ interface ChatRepository { myId: String, opponentId: String, userType: UserType, + sequence: Int, ): Result } diff --git a/core/network/src/main/java/com/idle/network/api/ChatApi.kt b/core/network/src/main/java/com/idle/network/api/ChatApi.kt index 222a5d94..d2690575 100644 --- a/core/network/src/main/java/com/idle/network/api/ChatApi.kt +++ b/core/network/src/main/java/com/idle/network/api/ChatApi.kt @@ -1,7 +1,7 @@ package com.idle.network.api -import com.idle.network.model.chat.ChatMessageResponse import com.idle.network.model.chat.GenerateChatRoomResponse +import com.idle.network.model.chat.GetChatMessageResponse import com.idle.network.model.chat.GetChatRoomResponse import retrofit2.Response import retrofit2.http.GET @@ -26,11 +26,11 @@ interface ChatApi { suspend fun getWorkerChatRoomMessages( @Path("chatroom-id") chatRoomId: String, @Query("message-id") messageId: String?, - ): Response> + ): Response @GET("api/v2/chat/center/chatrooms/{chatroom-id}/messages") suspend fun getCenterChatRoomMessages( @Path("chatroom-id") chatRoomId: String, @Query("message-id") messageId: String?, - ): Response> + ): Response } diff --git a/core/network/src/main/java/com/idle/network/model/chat/ChatMessageResponse.kt b/core/network/src/main/java/com/idle/network/model/chat/ChatMessageResponse.kt index 505526cf..3cebc789 100644 --- a/core/network/src/main/java/com/idle/network/model/chat/ChatMessageResponse.kt +++ b/core/network/src/main/java/com/idle/network/model/chat/ChatMessageResponse.kt @@ -13,6 +13,7 @@ data class ChatMessageResponse( val receiverId: String?, val content: String?, val createdAt: String?, + val sequence: Int?, override val type: String = MESSAGE_TYPE, ) : ChatResponse() { override fun toVO() = ChatMessage( @@ -24,6 +25,7 @@ data class ChatMessageResponse( createdAt = createdAt?.let { LocalDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME) } ?: LocalDateTime.MIN, isRead = false, + sequence = sequence ?: -1 ) } diff --git a/core/network/src/main/java/com/idle/network/model/chat/GetChatMessageResponse.kt b/core/network/src/main/java/com/idle/network/model/chat/GetChatMessageResponse.kt new file mode 100644 index 00000000..2f452d59 --- /dev/null +++ b/core/network/src/main/java/com/idle/network/model/chat/GetChatMessageResponse.kt @@ -0,0 +1,13 @@ +package com.idle.network.model.chat + +import com.idle.domain.model.chat.ChatMessage +import kotlinx.serialization.Serializable + +@Serializable +data class GetChatMessageResponse( + val chatMessageInfos: List = emptyList(), + val sequence: Int = 0, +) { + fun toVO(): Pair, Int> = + chatMessageInfos.map(ChatMessageResponse::toVO) to sequence +} diff --git a/core/network/src/main/java/com/idle/network/model/chat/ReadMessageRequest.kt b/core/network/src/main/java/com/idle/network/model/chat/ReadMessageRequest.kt index 545929b6..1964ab62 100644 --- a/core/network/src/main/java/com/idle/network/model/chat/ReadMessageRequest.kt +++ b/core/network/src/main/java/com/idle/network/model/chat/ReadMessageRequest.kt @@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable data class ReadMessageRequest( val chatroomId: String, val opponentId: String, + val sequence: Int, ) diff --git a/core/network/src/main/java/com/idle/network/model/chat/ReadMessageResponse.kt b/core/network/src/main/java/com/idle/network/model/chat/ReadMessageResponse.kt index 4898faf5..44f0af89 100644 --- a/core/network/src/main/java/com/idle/network/model/chat/ReadMessageResponse.kt +++ b/core/network/src/main/java/com/idle/network/model/chat/ReadMessageResponse.kt @@ -7,10 +7,12 @@ import kotlinx.serialization.Serializable data class ReadMessageResponse( val readByUserId: String?, val chatroomId: String?, + val sequence: Int?, override val type: String = READ_TYPE, ) : ChatResponse() { override fun toVO() = ReadMessage( opponentId = readByUserId ?: "", chatroomId = chatroomId ?: "", + sequence = sequence ?: -1 ) } diff --git a/core/network/src/main/java/com/idle/network/source/ChatDataSource.kt b/core/network/src/main/java/com/idle/network/source/ChatDataSource.kt index 721c1522..ee47b040 100644 --- a/core/network/src/main/java/com/idle/network/source/ChatDataSource.kt +++ b/core/network/src/main/java/com/idle/network/source/ChatDataSource.kt @@ -1,13 +1,12 @@ package com.idle.network.source -import android.util.Log import com.idle.domain.model.auth.UserType import com.idle.network.BuildConfig import com.idle.network.api.ChatApi import com.idle.network.di.TokenManager -import com.idle.network.model.chat.ChatMessageResponse import com.idle.network.model.chat.ChatResponse import com.idle.network.model.chat.GenerateChatRoomResponse +import com.idle.network.model.chat.GetChatMessageResponse import com.idle.network.model.chat.GetChatRoomResponse import com.idle.network.model.chat.ReadMessageRequest import com.idle.network.model.chat.SendMessageRequest @@ -47,7 +46,7 @@ class ChatDataSource @Inject constructor( suspend fun getWorkerChatRoomMessages( roomId: String, messageId: String?, - ): Result> = + ): Result = safeApiCall { chatApi.getWorkerChatRoomMessages( chatRoomId = roomId, @@ -58,7 +57,7 @@ class ChatDataSource @Inject constructor( suspend fun getCenterChatRoomMessages( roomId: String, messageId: String?, - ): Result> = + ): Result = safeApiCall { chatApi.getCenterChatRoomMessages( chatRoomId = roomId, @@ -92,7 +91,6 @@ class ChatDataSource @Inject constructor( connectionAttempts++ connectWebSocket().getOrThrow() } else { - Log.d("test connect", throwable.stackTraceToString()) throw throwable } } @@ -101,7 +99,6 @@ class ChatDataSource @Inject constructor( session?.disconnect() Result.success(Unit) } catch (e: Exception) { - Log.d("test disconnect", e.stackTraceToString()) Result.failure(e) } @@ -110,7 +107,6 @@ class ChatDataSource @Inject constructor( StompSubscribeHeaders(destination = "/sub/${userId}"), chatResponseSerializer, )?.map { - Log.d("test", it.toString()) it } ?: flow { throw IOException("웹소켓을 먼저 연결해주세요.") } @@ -119,9 +115,7 @@ class ChatDataSource @Inject constructor( sendMessageRequest: SendMessageRequest ): Result = runCatching { - Log.d("test", "/pub/send/${userType.apiValue.lowercase()}") - - session?.convertAndSend( + val result = session?.convertAndSend( headers = StompSendHeaders(destination = "/pub/send/${userType.apiValue.lowercase()}"), body = sendMessageRequest, serializer = SendMessageRequest.serializer(), diff --git a/feature/chatting-detail/src/main/java/com/idle/chatting_detail/ChattingDetailViewModel.kt b/feature/chatting-detail/src/main/java/com/idle/chatting_detail/ChattingDetailViewModel.kt index 4b0561e5..57a677f4 100644 --- a/feature/chatting-detail/src/main/java/com/idle/chatting_detail/ChattingDetailViewModel.kt +++ b/feature/chatting-detail/src/main/java/com/idle/chatting_detail/ChattingDetailViewModel.kt @@ -187,6 +187,7 @@ class ChattingDetailViewModel @Inject constructor( myId = myId, opponentId = opponentId, userType = myUserType, + sequence = _chatMessages.value?.lastOrNull()?.sequence ?: return ).onSuccess { _chatMessages.value = _chatMessages.value?.map { if (it.receiverId == myId) it.copy(isRead = true) else it