diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 02329cae9..eb7673c8d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -110,7 +110,11 @@ dependencies { implementation(libs.bundles.datastore) implementation(libs.bundles.animation) implementation(libs.bundles.stomp) + implementation(libs.bundles.room) testImplementation(libs.bundles.test) androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.assertj) androidTestImplementation(libs.androidx.espresso.core) + annotationProcessor(libs.room.compiler) + kapt(libs.room.compiler) } diff --git a/android/app/src/androidTest/java/com/happy/friendogly/ChatMessageDatabaseTest.kt b/android/app/src/androidTest/java/com/happy/friendogly/ChatMessageDatabaseTest.kt new file mode 100644 index 000000000..4252d9c69 --- /dev/null +++ b/android/app/src/androidTest/java/com/happy/friendogly/ChatMessageDatabaseTest.kt @@ -0,0 +1,101 @@ +package com.happy.friendogly + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import com.happy.friendogly.local.model.ChatMemberEntity +import com.happy.friendogly.local.model.ChatMessageEntity +import com.happy.friendogly.local.room.ChatMessageDao +import com.happy.friendogly.local.room.ChatMessageDatabase +import com.happy.friendogly.local.room.ChatRoomDao +import com.happy.friendogly.local.room.MessageTypeEntity +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.time.LocalDateTime + +class ChatMessageDatabaseTest { + private lateinit var chatMessageDao: ChatMessageDao + private lateinit var chatRoomDao: ChatRoomDao + + private lateinit var db: ChatMessageDatabase + + @Before + fun setUp() { + db = + Room.inMemoryDatabaseBuilder( + context = ApplicationProvider.getApplicationContext(), + klass = ChatMessageDatabase::class.java, + ).build() + + chatMessageDao = db.chatMessageDao() + chatRoomDao = db.chatRoomDao() + } + + @After + fun closeDb() { + db.close() + } + + @Test + fun `can_save_chat_message_and_get_the_data`() { + // when + runBlocking { + chatMessageDao.insert( + DUMMY_MY_MESSAGE, + ) + } + + // then + runBlocking { + val actual: List = chatMessageDao.getAll() + assertThat(actual).contains(DUMMY_MY_MESSAGE) + } + } + + @Test + fun `can_update_chatRoom_message`() { + // when + runBlocking { + chatRoomDao.addMessageToChatRoom(2, DUMMY_MY_MESSAGE) + chatRoomDao.addMessageToChatRoom(2, DUMMY_CHAT_MESSAGE) + } + + runBlocking { + val messages = chatRoomDao.getMessagesByRoomId(2) + assertThat(messages).contains(DUMMY_MY_MESSAGE, DUMMY_CHAT_MESSAGE) + } + } + + companion object { + private val DUMMY_MY_MESSAGE = + ChatMessageEntity( + createdAt = LocalDateTime.of(2024, 6, 12, 14, 2), + member = + ChatMemberEntity( + 1, + "벼리", + "", + ), + content = "", + type = MessageTypeEntity.CHAT, + id = 1, + ) + + private val DUMMY_CHAT_MESSAGE = + ChatMessageEntity( + createdAt = LocalDateTime.of(2024, 6, 12, 14, 2), + member = + ChatMemberEntity( + 2, + "벼리", + "", + ), + content = "ZZZZZZZZ", + type = MessageTypeEntity.CHAT, + id = 2, + ) + } +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e3b7a0466..57233066c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -71,6 +71,7 @@ - \ No newline at end of file + diff --git a/android/app/src/main/java/com/happy/friendogly/application/di/AppModule.kt b/android/app/src/main/java/com/happy/friendogly/application/di/AppModule.kt index 4694d8ce8..1744cc972 100644 --- a/android/app/src/main/java/com/happy/friendogly/application/di/AppModule.kt +++ b/android/app/src/main/java/com/happy/friendogly/application/di/AppModule.kt @@ -1,6 +1,7 @@ package com.happy.friendogly.application.di import android.content.Context +import androidx.room.Room import com.happy.friendogly.BuildConfig import com.happy.friendogly.analytics.AnalyticsHelper import com.happy.friendogly.crashlytics.CrashlyticsHelper @@ -45,14 +46,17 @@ import com.happy.friendogly.domain.repository.WebSocketRepository import com.happy.friendogly.domain.repository.WoofRepository import com.happy.friendogly.domain.usecase.ConnectWebsocketUseCase import com.happy.friendogly.domain.usecase.DeleteAddressUseCase -import com.happy.friendogly.domain.usecase.DeleteAlarmSettingUseCase +import com.happy.friendogly.domain.usecase.DeleteChatAlarmUseCase import com.happy.friendogly.domain.usecase.DeleteClubMemberUseCase import com.happy.friendogly.domain.usecase.DeleteTokenUseCase +import com.happy.friendogly.domain.usecase.DeleteWoofAlarmUseCase import com.happy.friendogly.domain.usecase.DisconnectWebsocketUseCase import com.happy.friendogly.domain.usecase.GetAddressUseCase -import com.happy.friendogly.domain.usecase.GetAlarmSettingUseCase +import com.happy.friendogly.domain.usecase.GetChatAlarmUseCase import com.happy.friendogly.domain.usecase.GetChatListUseCase import com.happy.friendogly.domain.usecase.GetChatMemberUseCase +import com.happy.friendogly.domain.usecase.GetChatMessagesUseCase +import com.happy.friendogly.domain.usecase.GetChatRoomClubUseCase import com.happy.friendogly.domain.usecase.GetClubUseCase import com.happy.friendogly.domain.usecase.GetFootprintInfoUseCase import com.happy.friendogly.domain.usecase.GetFootprintMarkBtnInfoUseCase @@ -65,6 +69,7 @@ import com.happy.friendogly.domain.usecase.GetNearFootprintsUseCase import com.happy.friendogly.domain.usecase.GetPetsMineUseCase import com.happy.friendogly.domain.usecase.GetPetsUseCase import com.happy.friendogly.domain.usecase.GetSearchingClubsUseCase +import com.happy.friendogly.domain.usecase.GetWoofAlarmUseCase import com.happy.friendogly.domain.usecase.KakaoLoginUseCase import com.happy.friendogly.domain.usecase.PatchWalkStatusUseCase import com.happy.friendogly.domain.usecase.PostClubMemberUseCase @@ -78,14 +83,18 @@ import com.happy.friendogly.domain.usecase.PublishLeaveUseCase import com.happy.friendogly.domain.usecase.PublishSendMessageUseCase import com.happy.friendogly.domain.usecase.SaveAddressUseCase import com.happy.friendogly.domain.usecase.SaveAlamTokenUseCase -import com.happy.friendogly.domain.usecase.SaveAlarmSettingUseCase +import com.happy.friendogly.domain.usecase.SaveChatAlarmUseCase +import com.happy.friendogly.domain.usecase.SaveChatMessageUseCase import com.happy.friendogly.domain.usecase.SaveJwtTokenUseCase +import com.happy.friendogly.domain.usecase.SaveWoofAlarmUseCase import com.happy.friendogly.domain.usecase.SubScribeMessageUseCase import com.happy.friendogly.kakao.source.KakaoLoginDataSourceImpl import com.happy.friendogly.local.di.AddressModule -import com.happy.friendogly.local.di.AlarmModule import com.happy.friendogly.local.di.AlarmTokenModule +import com.happy.friendogly.local.di.ChatAlarmModule import com.happy.friendogly.local.di.TokenManager +import com.happy.friendogly.local.di.WoofAlarmModule +import com.happy.friendogly.local.room.ChatMessageDatabase import com.happy.friendogly.local.source.AddressDataSourceImpl import com.happy.friendogly.local.source.AlarmSettingDataSourceImpl import com.happy.friendogly.local.source.TokenDataSourceImpl @@ -115,8 +124,12 @@ class AppModule(context: Context) { private val authenticationListener: AuthenticationListener = AuthenticationListenerImpl(context, tokenManager) private val addressModule = AddressModule(context) - private val alarmModule = AlarmModule(context) + private val chatAlarmModule = ChatAlarmModule(context) + private val woofAlarmModule = WoofAlarmModule(context) private val alarmTokenModule = AlarmTokenModule(context) + private val chatDataBase = + Room.databaseBuilder(context, ChatMessageDatabase::class.java, "chat").build() + private val chatRoomDao = chatDataBase.chatRoomDao() // service private val authService = @@ -202,7 +215,10 @@ class AppModule(context: Context) { WebSocketDataSourceImpl(service = webSocketService) private val chatDataSource: ChatDataSource = ChatDataSourceImpl(service = chatService) private val alarmSettingDataSource: AlarmSettingDataSource = - AlarmSettingDataSourceImpl(alarmModule = alarmModule) + AlarmSettingDataSourceImpl( + chatAlarmModule = chatAlarmModule, + woofAlarmModule = woofAlarmModule, + ) private val alarmTokenDataSource: AlarmTokenDataSource = AlamTokenDataSourceImpl(service = alarmTokenService) @@ -218,9 +234,10 @@ class AppModule(context: Context) { private val petRepository: PetRepository = PetRepositoryImpl(source = petDataSource) private val addressRepository: AddressRepository = AddressRepositoryImpl(addressDataSource = addressDataSource) - val webSocketRepository: WebSocketRepository = + private val webSocketRepository: WebSocketRepository = WebSocketRepositoryImpl(source = webSocketDataSource) - private val chatRepository: ChatRepository = ChatRepositoryImpl(source = chatDataSource) + private val chatRepository: ChatRepository = + ChatRepositoryImpl(source = chatDataSource, chatRoomDao = chatRoomDao) private val alarmSettingRepository: AlarmSettingRepository = AlarmSettingRepositoryImpl(source = alarmSettingDataSource) private val alarmTokenRepository: AlarmTokenRepository = @@ -270,6 +287,12 @@ class AppModule(context: Context) { val getChatListUseCase: GetChatListUseCase = GetChatListUseCase(repository = chatRepository) val getChatMemberUseCase: GetChatMemberUseCase = GetChatMemberUseCase(repository = chatRepository) + val saveChatMessageUseCase: SaveChatMessageUseCase = + SaveChatMessageUseCase(repository = chatRepository) + val getChatMessagesUseCase: GetChatMessagesUseCase = + GetChatMessagesUseCase(repository = chatRepository) + val getChatRoomClubUseCase: GetChatRoomClubUseCase = + GetChatRoomClubUseCase(repository = chatRepository) val publishEnterUseCase: PublishEnterUseCase = PublishEnterUseCase(repository = webSocketRepository) val publishSendUseCase: PublishSendMessageUseCase = @@ -282,12 +305,18 @@ class AppModule(context: Context) { ConnectWebsocketUseCase(repository = webSocketRepository) val disconnectWebsocketUseCase: DisconnectWebsocketUseCase = DisconnectWebsocketUseCase(repository = webSocketRepository) - val deleteAlarmSettingUseCase: DeleteAlarmSettingUseCase = - DeleteAlarmSettingUseCase(repository = alarmSettingRepository) - val saveAlarmSettingUseCase: SaveAlarmSettingUseCase = - SaveAlarmSettingUseCase(repository = alarmSettingRepository) - val getAlarmSettingUseCase: GetAlarmSettingUseCase = - GetAlarmSettingUseCase(repository = alarmSettingRepository) + val deleteChatAlarmUseCase: DeleteChatAlarmUseCase = + DeleteChatAlarmUseCase(repository = alarmSettingRepository) + val saveChatAlarmUseCase: SaveChatAlarmUseCase = + SaveChatAlarmUseCase(repository = alarmSettingRepository) + val saveWoofAlarmUseCase: SaveWoofAlarmUseCase = + SaveWoofAlarmUseCase(repository = alarmSettingRepository) + val getWoofAlarmUseCase: GetWoofAlarmUseCase = + GetWoofAlarmUseCase(repository = alarmSettingRepository) + val deleteWoofAlarmUseCase: DeleteWoofAlarmUseCase = + DeleteWoofAlarmUseCase(repository = alarmSettingRepository) + val getChatAlarmUseCase: GetChatAlarmUseCase = + GetChatAlarmUseCase(repository = alarmSettingRepository) val saveAlarmTokenUseCase: SaveAlamTokenUseCase = SaveAlamTokenUseCase(repository = alarmTokenRepository) diff --git a/android/app/src/main/java/com/happy/friendogly/data/mapper/ChatMemberMapper.kt b/android/app/src/main/java/com/happy/friendogly/data/mapper/ChatMemberMapper.kt index c75d673f0..33cda6b83 100644 --- a/android/app/src/main/java/com/happy/friendogly/data/mapper/ChatMemberMapper.kt +++ b/android/app/src/main/java/com/happy/friendogly/data/mapper/ChatMemberMapper.kt @@ -9,9 +9,9 @@ import com.happy.friendogly.domain.model.ChatRooms fun ChatMemberDto.toDomain(): ChatMember = ChatMember( isOwner = isOwner, - memberId = memberId, - memberName = memberName, - memberProfileImageUrl = memberProfileImageUrl, + id = memberId, + name = memberName, + profileImageUrl = memberProfileImageUrl, ) fun ChatRoomListDto.toDomain(): ChatRooms = diff --git a/android/app/src/main/java/com/happy/friendogly/data/mapper/ClubChatRoomMapper.kt b/android/app/src/main/java/com/happy/friendogly/data/mapper/ClubChatRoomMapper.kt new file mode 100644 index 000000000..53633c2a9 --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/data/mapper/ClubChatRoomMapper.kt @@ -0,0 +1,11 @@ +package com.happy.friendogly.data.mapper + +import com.happy.friendogly.data.model.ChatRoomClubDto +import com.happy.friendogly.domain.model.ChatRoomClub + +fun ChatRoomClubDto.toDomain(): ChatRoomClub = + ChatRoomClub( + clubId = clubId, + allowedGender = allowedGender.map { it.toDomain() }, + allowedSize = allowedSize.map { it.toDomain() }, + ) diff --git a/android/app/src/main/java/com/happy/friendogly/data/mapper/MessageMapper.kt b/android/app/src/main/java/com/happy/friendogly/data/mapper/MessageMapper.kt index bc9f34a95..547ea5dee 100644 --- a/android/app/src/main/java/com/happy/friendogly/data/mapper/MessageMapper.kt +++ b/android/app/src/main/java/com/happy/friendogly/data/mapper/MessageMapper.kt @@ -2,29 +2,51 @@ package com.happy.friendogly.data.mapper import com.happy.friendogly.data.model.MessageDto import com.happy.friendogly.domain.model.ChatComponent +import com.happy.friendogly.domain.model.ChatMember import com.happy.friendogly.domain.model.Message fun MessageDto.toOther(): Message.Other = Message.Other( - memberId = senderMemberId, - name = senderName, + member = + ChatMember( + id = senderMemberId, + name = senderName, + profileImageUrl = profilePictureUrl ?: "", + ), content = content ?: "", - dateTime = createdAt, - profileUrl = profilePictureUrl, + createdAt = createdAt, ) fun MessageDto.toMine(): Message.Mine = Message.Mine( + member = + ChatMember( + id = senderMemberId, + name = senderName, + profileImageUrl = profilePictureUrl ?: "", + ), content = content ?: "", - dateTime = createdAt, + createdAt = createdAt, ) fun MessageDto.toEnter(): ChatComponent.Enter = ChatComponent.Enter( - name = senderName, + member = + ChatMember( + id = senderMemberId, + name = senderName, + profileImageUrl = profilePictureUrl ?: "", + ), + createdAt = createdAt, ) fun MessageDto.toLeave(): ChatComponent.Leave = ChatComponent.Leave( - name = senderName, + member = + ChatMember( + id = senderMemberId, + name = senderName, + profileImageUrl = profilePictureUrl ?: "", + ), + createdAt = createdAt, ) diff --git a/android/app/src/main/java/com/happy/friendogly/data/model/ChatRoomClubDto.kt b/android/app/src/main/java/com/happy/friendogly/data/model/ChatRoomClubDto.kt new file mode 100644 index 000000000..19283f2ff --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/data/model/ChatRoomClubDto.kt @@ -0,0 +1,7 @@ +package com.happy.friendogly.data.model + +data class ChatRoomClubDto( + val clubId: Long, + val allowedGender: List, + val allowedSize: List, +) diff --git a/android/app/src/main/java/com/happy/friendogly/data/repository/AlarmSettingRepositoryImpl.kt b/android/app/src/main/java/com/happy/friendogly/data/repository/AlarmSettingRepositoryImpl.kt index 304971384..79f8a9deb 100644 --- a/android/app/src/main/java/com/happy/friendogly/data/repository/AlarmSettingRepositoryImpl.kt +++ b/android/app/src/main/java/com/happy/friendogly/data/repository/AlarmSettingRepositoryImpl.kt @@ -3,10 +3,17 @@ package com.happy.friendogly.data.repository import com.happy.friendogly.data.source.AlarmSettingDataSource import com.happy.friendogly.domain.repository.AlarmSettingRepository -class AlarmSettingRepositoryImpl(private val source: AlarmSettingDataSource) : AlarmSettingRepository { - override suspend fun saveAlarm(isSet: Boolean): Result = source.saveAlarm(isSet) +class AlarmSettingRepositoryImpl(private val source: AlarmSettingDataSource) : + AlarmSettingRepository { + override suspend fun saveChatSetting(isSet: Boolean): Result = source.saveChatSetting(isSet) - override suspend fun getAlarm(): Result = source.getAlarm() + override suspend fun getChatSetting(): Result = source.getChatSetting() - override suspend fun deleteAlarm(): Result = source.deleteAlarm() + override suspend fun deleteChatSetting(): Result = source.deleteChatSetting() + + override suspend fun saveWoofSetting(isSet: Boolean): Result = source.saveWoofSetting(isSet) + + override suspend fun getWoofSetting(): Result = source.getWoofSetting() + + override suspend fun deleteWoofSetting(): Result = source.deleteWoofSetting() } diff --git a/android/app/src/main/java/com/happy/friendogly/data/repository/ChatRepositoryImpl.kt b/android/app/src/main/java/com/happy/friendogly/data/repository/ChatRepositoryImpl.kt index 49501a983..d6cac641d 100644 --- a/android/app/src/main/java/com/happy/friendogly/data/repository/ChatRepositoryImpl.kt +++ b/android/app/src/main/java/com/happy/friendogly/data/repository/ChatRepositoryImpl.kt @@ -2,15 +2,50 @@ package com.happy.friendogly.data.repository import com.happy.friendogly.data.mapper.toDomain import com.happy.friendogly.data.source.ChatDataSource +import com.happy.friendogly.domain.model.ChatComponent import com.happy.friendogly.domain.model.ChatMember +import com.happy.friendogly.domain.model.ChatRoomClub import com.happy.friendogly.domain.model.ChatRooms +import com.happy.friendogly.domain.model.Message import com.happy.friendogly.domain.repository.ChatRepository +import com.happy.friendogly.local.mapper.toData +import com.happy.friendogly.local.mapper.toDomain +import com.happy.friendogly.local.room.ChatRoomDao -class ChatRepositoryImpl(private val source: ChatDataSource) : ChatRepository { +class ChatRepositoryImpl( + private val source: ChatDataSource, + private val chatRoomDao: ChatRoomDao, +) : ChatRepository { override suspend fun getChatList(): Result = source.getChatList().mapCatching { it.toDomain() } override suspend fun getMembers(chatRoomId: Long): Result> = source.getMembers(chatRoomId).mapCatching { member -> member.map { it.toDomain() } } + + override suspend fun saveMessage( + chatRoomId: Long, + chat: ChatComponent, + ): Result = + runCatching { + val message = + when (chat) { + is ChatComponent.Date -> chat.toData() + is ChatComponent.Enter -> chat.toData() + is ChatComponent.Leave -> chat.toData() + is Message.Mine -> chat.toData() + is Message.Other -> chat.toData() + } + chatRoomDao.addMessageToChatRoom(chatRoomId, message) + } + + override suspend fun getChatMessages( + chatRoomId: Long, + myMemberId: Long, + ): Result> = + runCatching { + chatRoomDao.getMessagesByRoomId(chatRoomId).map { it.toDomain(myMemberId) } + } + + override suspend fun getChatClub(chatRoomId: Long): Result = source.getClubs(chatRoomId).mapCatching { it.toDomain() } } diff --git a/android/app/src/main/java/com/happy/friendogly/data/source/AlarmSettingDataSource.kt b/android/app/src/main/java/com/happy/friendogly/data/source/AlarmSettingDataSource.kt index 88747e3c1..396e2a6b2 100644 --- a/android/app/src/main/java/com/happy/friendogly/data/source/AlarmSettingDataSource.kt +++ b/android/app/src/main/java/com/happy/friendogly/data/source/AlarmSettingDataSource.kt @@ -1,9 +1,15 @@ package com.happy.friendogly.data.source interface AlarmSettingDataSource { - suspend fun saveAlarm(isSet: Boolean): Result + suspend fun saveChatSetting(isSet: Boolean): Result - suspend fun getAlarm(): Result + suspend fun getChatSetting(): Result - suspend fun deleteAlarm(): Result + suspend fun deleteChatSetting(): Result + + suspend fun saveWoofSetting(isSet: Boolean): Result + + suspend fun getWoofSetting(): Result + + suspend fun deleteWoofSetting(): Result } diff --git a/android/app/src/main/java/com/happy/friendogly/data/source/ChatDataSource.kt b/android/app/src/main/java/com/happy/friendogly/data/source/ChatDataSource.kt index 7eda46136..6a8d15a78 100644 --- a/android/app/src/main/java/com/happy/friendogly/data/source/ChatDataSource.kt +++ b/android/app/src/main/java/com/happy/friendogly/data/source/ChatDataSource.kt @@ -1,10 +1,13 @@ package com.happy.friendogly.data.source import com.happy.friendogly.data.model.ChatMemberDto +import com.happy.friendogly.data.model.ChatRoomClubDto import com.happy.friendogly.data.model.ChatRoomListDto interface ChatDataSource { suspend fun getChatList(): Result suspend fun getMembers(chatRoomId: Long): Result> + + suspend fun getClubs(chatRoomId: Long): Result } diff --git a/android/app/src/main/java/com/happy/friendogly/domain/model/ChatComponent.kt b/android/app/src/main/java/com/happy/friendogly/domain/model/ChatComponent.kt index b081e46ca..a880a64c1 100644 --- a/android/app/src/main/java/com/happy/friendogly/domain/model/ChatComponent.kt +++ b/android/app/src/main/java/com/happy/friendogly/domain/model/ChatComponent.kt @@ -1,11 +1,13 @@ package com.happy.friendogly.domain.model -import java.time.LocalDate +import java.time.LocalDateTime sealed interface ChatComponent { - data class Date(val created: LocalDate) : ChatComponent + val createdAt: LocalDateTime - data class Enter(val name: String) : ChatComponent + data class Date(override val createdAt: LocalDateTime) : ChatComponent - data class Leave(val name: String) : ChatComponent + data class Enter(val member: ChatMember, override val createdAt: LocalDateTime) : ChatComponent + + data class Leave(val member: ChatMember, override val createdAt: LocalDateTime) : ChatComponent } diff --git a/android/app/src/main/java/com/happy/friendogly/domain/model/ChatMember.kt b/android/app/src/main/java/com/happy/friendogly/domain/model/ChatMember.kt index 658c76fad..fe8109511 100644 --- a/android/app/src/main/java/com/happy/friendogly/domain/model/ChatMember.kt +++ b/android/app/src/main/java/com/happy/friendogly/domain/model/ChatMember.kt @@ -1,8 +1,8 @@ package com.happy.friendogly.domain.model data class ChatMember( - val isOwner: Boolean, - val memberId: Long, - val memberName: String, - val memberProfileImageUrl: String, + val isOwner: Boolean = false, + val id: Long, + val name: String, + val profileImageUrl: String, ) diff --git a/android/app/src/main/java/com/happy/friendogly/domain/model/ChatRoomClub.kt b/android/app/src/main/java/com/happy/friendogly/domain/model/ChatRoomClub.kt new file mode 100644 index 000000000..0bcfcde3b --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/domain/model/ChatRoomClub.kt @@ -0,0 +1,7 @@ +package com.happy.friendogly.domain.model + +data class ChatRoomClub( + val clubId: Long, + val allowedGender: List, + val allowedSize: List, +) diff --git a/android/app/src/main/java/com/happy/friendogly/domain/model/Message.kt b/android/app/src/main/java/com/happy/friendogly/domain/model/Message.kt index d8b14f7d1..66874ac8a 100644 --- a/android/app/src/main/java/com/happy/friendogly/domain/model/Message.kt +++ b/android/app/src/main/java/com/happy/friendogly/domain/model/Message.kt @@ -7,14 +7,13 @@ sealed interface Message : ChatComponent { data class Mine( override val content: String, - val dateTime: LocalDateTime, + val member: ChatMember, + override val createdAt: LocalDateTime, ) : Message data class Other( - val memberId: Long, - val name: String, + val member: ChatMember, override val content: String, - val dateTime: LocalDateTime, - val profileUrl: String?, + override val createdAt: LocalDateTime, ) : Message } diff --git a/android/app/src/main/java/com/happy/friendogly/domain/repository/AlarmSettingRepository.kt b/android/app/src/main/java/com/happy/friendogly/domain/repository/AlarmSettingRepository.kt index 81e6dc1ec..60d9e63b8 100644 --- a/android/app/src/main/java/com/happy/friendogly/domain/repository/AlarmSettingRepository.kt +++ b/android/app/src/main/java/com/happy/friendogly/domain/repository/AlarmSettingRepository.kt @@ -1,9 +1,15 @@ package com.happy.friendogly.domain.repository interface AlarmSettingRepository { - suspend fun saveAlarm(isSet: Boolean): Result + suspend fun saveChatSetting(isSet: Boolean): Result - suspend fun getAlarm(): Result + suspend fun getChatSetting(): Result - suspend fun deleteAlarm(): Result + suspend fun deleteChatSetting(): Result + + suspend fun saveWoofSetting(isSet: Boolean): Result + + suspend fun getWoofSetting(): Result + + suspend fun deleteWoofSetting(): Result } diff --git a/android/app/src/main/java/com/happy/friendogly/domain/repository/ChatRepository.kt b/android/app/src/main/java/com/happy/friendogly/domain/repository/ChatRepository.kt index e8cbff7e8..ed186dec5 100644 --- a/android/app/src/main/java/com/happy/friendogly/domain/repository/ChatRepository.kt +++ b/android/app/src/main/java/com/happy/friendogly/domain/repository/ChatRepository.kt @@ -1,10 +1,24 @@ package com.happy.friendogly.domain.repository +import com.happy.friendogly.domain.model.ChatComponent import com.happy.friendogly.domain.model.ChatMember +import com.happy.friendogly.domain.model.ChatRoomClub import com.happy.friendogly.domain.model.ChatRooms interface ChatRepository { suspend fun getChatList(): Result suspend fun getMembers(chatRoomId: Long): Result> + + suspend fun saveMessage( + chatRoomId: Long, + chat: ChatComponent, + ): Result + + suspend fun getChatMessages( + chatRoomId: Long, + myMemberId: Long, + ): Result> + + suspend fun getChatClub(chatRoomId: Long): Result } diff --git a/android/app/src/main/java/com/happy/friendogly/domain/usecase/DeleteAlarmSettingUseCase.kt b/android/app/src/main/java/com/happy/friendogly/domain/usecase/DeleteChatAlarmUseCase.kt similarity index 83% rename from android/app/src/main/java/com/happy/friendogly/domain/usecase/DeleteAlarmSettingUseCase.kt rename to android/app/src/main/java/com/happy/friendogly/domain/usecase/DeleteChatAlarmUseCase.kt index b0eea3c9c..ae070479b 100644 --- a/android/app/src/main/java/com/happy/friendogly/domain/usecase/DeleteAlarmSettingUseCase.kt +++ b/android/app/src/main/java/com/happy/friendogly/domain/usecase/DeleteChatAlarmUseCase.kt @@ -2,8 +2,8 @@ package com.happy.friendogly.domain.usecase import com.happy.friendogly.domain.repository.AlarmSettingRepository -class DeleteAlarmSettingUseCase( +class DeleteChatAlarmUseCase( private val repository: AlarmSettingRepository, ) { - suspend operator fun invoke(): Result = repository.deleteAlarm() + suspend operator fun invoke(): Result = repository.deleteChatSetting() } diff --git a/android/app/src/main/java/com/happy/friendogly/domain/usecase/DeleteWoofAlarmUseCase.kt b/android/app/src/main/java/com/happy/friendogly/domain/usecase/DeleteWoofAlarmUseCase.kt new file mode 100644 index 000000000..2c89e9645 --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/domain/usecase/DeleteWoofAlarmUseCase.kt @@ -0,0 +1,9 @@ +package com.happy.friendogly.domain.usecase + +import com.happy.friendogly.domain.repository.AlarmSettingRepository + +class DeleteWoofAlarmUseCase( + private val repository: AlarmSettingRepository, +) { + suspend operator fun invoke(): Result = repository.deleteWoofSetting() +} diff --git a/android/app/src/main/java/com/happy/friendogly/domain/usecase/GetAlarmSettingUseCase.kt b/android/app/src/main/java/com/happy/friendogly/domain/usecase/GetChatAlarmUseCase.kt similarity index 84% rename from android/app/src/main/java/com/happy/friendogly/domain/usecase/GetAlarmSettingUseCase.kt rename to android/app/src/main/java/com/happy/friendogly/domain/usecase/GetChatAlarmUseCase.kt index 4c577cbb5..4b970c83e 100644 --- a/android/app/src/main/java/com/happy/friendogly/domain/usecase/GetAlarmSettingUseCase.kt +++ b/android/app/src/main/java/com/happy/friendogly/domain/usecase/GetChatAlarmUseCase.kt @@ -2,8 +2,8 @@ package com.happy.friendogly.domain.usecase import com.happy.friendogly.domain.repository.AlarmSettingRepository -class GetAlarmSettingUseCase( +class GetChatAlarmUseCase( private val repository: AlarmSettingRepository, ) { - suspend operator fun invoke(): Result = repository.getAlarm() + suspend operator fun invoke(): Result = repository.getChatSetting() } diff --git a/android/app/src/main/java/com/happy/friendogly/domain/usecase/GetChatMessagesUseCase.kt b/android/app/src/main/java/com/happy/friendogly/domain/usecase/GetChatMessagesUseCase.kt new file mode 100644 index 000000000..5b506c707 --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/domain/usecase/GetChatMessagesUseCase.kt @@ -0,0 +1,13 @@ +package com.happy.friendogly.domain.usecase + +import com.happy.friendogly.domain.model.ChatComponent +import com.happy.friendogly.domain.repository.ChatRepository + +class GetChatMessagesUseCase( + private val repository: ChatRepository, +) { + suspend operator fun invoke( + chatRoomId: Long, + myMemberId: Long, + ): Result> = repository.getChatMessages(chatRoomId, myMemberId) +} diff --git a/android/app/src/main/java/com/happy/friendogly/domain/usecase/GetChatRoomClubUseCase.kt b/android/app/src/main/java/com/happy/friendogly/domain/usecase/GetChatRoomClubUseCase.kt new file mode 100644 index 000000000..313799138 --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/domain/usecase/GetChatRoomClubUseCase.kt @@ -0,0 +1,10 @@ +package com.happy.friendogly.domain.usecase + +import com.happy.friendogly.domain.model.ChatRoomClub +import com.happy.friendogly.domain.repository.ChatRepository + +class GetChatRoomClubUseCase( + private val repository: ChatRepository, +) { + suspend operator fun invoke(chatRoomId: Long): Result = repository.getChatClub(chatRoomId) +} diff --git a/android/app/src/main/java/com/happy/friendogly/domain/usecase/GetWoofAlarmUseCase.kt b/android/app/src/main/java/com/happy/friendogly/domain/usecase/GetWoofAlarmUseCase.kt new file mode 100644 index 000000000..8e8bc52b3 --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/domain/usecase/GetWoofAlarmUseCase.kt @@ -0,0 +1,9 @@ +package com.happy.friendogly.domain.usecase + +import com.happy.friendogly.domain.repository.AlarmSettingRepository + +class GetWoofAlarmUseCase( + private val repository: AlarmSettingRepository, +) { + suspend operator fun invoke(): Result = repository.getWoofSetting() +} diff --git a/android/app/src/main/java/com/happy/friendogly/domain/usecase/SaveAlarmSettingUseCase.kt b/android/app/src/main/java/com/happy/friendogly/domain/usecase/SaveChatAlarmUseCase.kt similarity index 79% rename from android/app/src/main/java/com/happy/friendogly/domain/usecase/SaveAlarmSettingUseCase.kt rename to android/app/src/main/java/com/happy/friendogly/domain/usecase/SaveChatAlarmUseCase.kt index 0213c6d68..96a343594 100644 --- a/android/app/src/main/java/com/happy/friendogly/domain/usecase/SaveAlarmSettingUseCase.kt +++ b/android/app/src/main/java/com/happy/friendogly/domain/usecase/SaveChatAlarmUseCase.kt @@ -2,8 +2,8 @@ package com.happy.friendogly.domain.usecase import com.happy.friendogly.domain.repository.AlarmSettingRepository -class SaveAlarmSettingUseCase( +class SaveChatAlarmUseCase( private val repository: AlarmSettingRepository, ) { - suspend operator fun invoke(isSet: Boolean): Result = repository.saveAlarm(isSet) + suspend operator fun invoke(isSet: Boolean): Result = repository.saveChatSetting(isSet) } diff --git a/android/app/src/main/java/com/happy/friendogly/domain/usecase/SaveChatMessageUseCase.kt b/android/app/src/main/java/com/happy/friendogly/domain/usecase/SaveChatMessageUseCase.kt new file mode 100644 index 000000000..a9879a268 --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/domain/usecase/SaveChatMessageUseCase.kt @@ -0,0 +1,13 @@ +package com.happy.friendogly.domain.usecase + +import com.happy.friendogly.domain.model.ChatComponent +import com.happy.friendogly.domain.repository.ChatRepository + +class SaveChatMessageUseCase( + private val repository: ChatRepository, +) { + suspend operator fun invoke( + chatRoomId: Long, + chatComponent: ChatComponent, + ): Result = repository.saveMessage(chatRoomId, chatComponent) +} diff --git a/android/app/src/main/java/com/happy/friendogly/domain/usecase/SaveWoofAlarmUseCase.kt b/android/app/src/main/java/com/happy/friendogly/domain/usecase/SaveWoofAlarmUseCase.kt new file mode 100644 index 000000000..4bc77e20b --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/domain/usecase/SaveWoofAlarmUseCase.kt @@ -0,0 +1,9 @@ +package com.happy.friendogly.domain.usecase + +import com.happy.friendogly.domain.repository.AlarmSettingRepository + +class SaveWoofAlarmUseCase( + private val repository: AlarmSettingRepository, +) { + suspend operator fun invoke(isSet: Boolean): Result = repository.saveWoofSetting(isSet) +} diff --git a/android/app/src/main/java/com/happy/friendogly/local/di/AlarmModule.kt b/android/app/src/main/java/com/happy/friendogly/local/di/ChatAlarmModule.kt similarity index 88% rename from android/app/src/main/java/com/happy/friendogly/local/di/AlarmModule.kt rename to android/app/src/main/java/com/happy/friendogly/local/di/ChatAlarmModule.kt index 5faaf608f..80abf6829 100644 --- a/android/app/src/main/java/com/happy/friendogly/local/di/AlarmModule.kt +++ b/android/app/src/main/java/com/happy/friendogly/local/di/ChatAlarmModule.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map import java.io.IOException -class AlarmModule(val context: Context) { +class ChatAlarmModule(val context: Context) { private val Context.dataStore: DataStore by preferencesDataStore(name = DATA_STORE_NAME) private val key = booleanPreferencesKey(ALARM_SETTING) @@ -41,7 +41,7 @@ class AlarmModule(val context: Context) { } companion object { - private const val ALARM_SETTING = "ALARM_SETTING" - private const val DATA_STORE_NAME = "alarmDataStore" + private const val ALARM_SETTING = "CHAT_ALARM_SETTING" + private const val DATA_STORE_NAME = "chatAlarmDataStore" } } diff --git a/android/app/src/main/java/com/happy/friendogly/local/di/WoofAlarmModule.kt b/android/app/src/main/java/com/happy/friendogly/local/di/WoofAlarmModule.kt new file mode 100644 index 000000000..466311479 --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/local/di/WoofAlarmModule.kt @@ -0,0 +1,47 @@ +package com.happy.friendogly.local.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import java.io.IOException + +class WoofAlarmModule(val context: Context) { + private val Context.dataStore: DataStore by preferencesDataStore(name = DATA_STORE_NAME) + + private val key = booleanPreferencesKey(ALARM_SETTING) + + var isSet: Flow = + context.dataStore.data.catch { exception -> + if (exception is IOException) { + emit(emptyPreferences()) + } else { + throw exception + } + }.map { preferences -> + preferences[key] ?: true + } + + suspend fun saveSetting(value: Boolean) { + context.dataStore.edit { preferences -> + preferences[key] = value + } + } + + suspend fun deleteSetting() { + context.dataStore.edit { prefs -> + prefs.remove(key) + } + } + + companion object { + private const val ALARM_SETTING = "WOOF_ALARM_SETTING" + private const val DATA_STORE_NAME = "woofAlarmDataStore" + } +} diff --git a/android/app/src/main/java/com/happy/friendogly/local/mapper/ChatMessagesConverter.kt b/android/app/src/main/java/com/happy/friendogly/local/mapper/ChatMessagesConverter.kt new file mode 100644 index 000000000..6731b970e --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/local/mapper/ChatMessagesConverter.kt @@ -0,0 +1,18 @@ +package com.happy.friendogly.local.mapper + +import androidx.room.TypeConverter +import com.happy.friendogly.local.model.ChatMessageEntity +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class ChatMessagesConverter { + @TypeConverter + fun fromChatMessageEntityList(messages: List): String { + return Json.encodeToString(messages) + } + + @TypeConverter + fun toChatMessageEntityList(data: String): List { + return Json.decodeFromString>(data) + } +} diff --git a/android/app/src/main/java/com/happy/friendogly/local/mapper/LocalDateTimeConverter.kt b/android/app/src/main/java/com/happy/friendogly/local/mapper/LocalDateTimeConverter.kt new file mode 100644 index 000000000..847942a1f --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/local/mapper/LocalDateTimeConverter.kt @@ -0,0 +1,19 @@ +package com.happy.friendogly.local.mapper + +import androidx.room.TypeConverter +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class LocalDateTimeConverter { + private val localDateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME + + @TypeConverter + fun fromLocalDateTime(value: LocalDateTime?): String? { + return value?.format(localDateTimeFormatter) + } + + @TypeConverter + fun toLocalDateTime(value: String?): LocalDateTime? { + return value?.let { LocalDateTime.parse(it, localDateTimeFormatter) } + } +} diff --git a/android/app/src/main/java/com/happy/friendogly/local/mapper/MessageEntityMapper.kt b/android/app/src/main/java/com/happy/friendogly/local/mapper/MessageEntityMapper.kt new file mode 100644 index 000000000..190e69dc6 --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/local/mapper/MessageEntityMapper.kt @@ -0,0 +1,107 @@ +package com.happy.friendogly.local.mapper + +import com.happy.friendogly.domain.model.ChatComponent +import com.happy.friendogly.domain.model.ChatMember +import com.happy.friendogly.domain.model.Message +import com.happy.friendogly.local.model.ChatMemberEntity +import com.happy.friendogly.local.model.ChatMessageEntity +import com.happy.friendogly.local.room.MessageTypeEntity + +fun ChatComponent.Date.toData(): ChatMessageEntity = + ChatMessageEntity( + createdAt = createdAt, + member = ChatMemberEntity.noChatMember(), + content = ChatMessageEntity.NOT_CONTENT, + type = MessageTypeEntity.DATE, + ) + +fun ChatComponent.Leave.toData(): ChatMessageEntity = + ChatMessageEntity( + createdAt = createdAt, + member = + ChatMemberEntity( + id = member.id, + name = member.name, + profileUrl = member.profileImageUrl, + ), + content = ChatMessageEntity.NOT_CONTENT, + type = MessageTypeEntity.LEAVE, + ) + +fun ChatComponent.Enter.toData(): ChatMessageEntity = + ChatMessageEntity( + createdAt = createdAt, + member = + ChatMemberEntity( + id = member.id, + name = member.name, + profileUrl = member.profileImageUrl, + ), + content = ChatMessageEntity.NOT_CONTENT, + type = MessageTypeEntity.ENTER, + ) + +fun Message.Mine.toData(): ChatMessageEntity = + ChatMessageEntity( + createdAt = createdAt, + member = + ChatMemberEntity( + id = member.id, + name = member.name, + profileUrl = member.profileImageUrl, + ), + content = content, + type = MessageTypeEntity.CHAT, + ) + +fun Message.Other.toData(): ChatMessageEntity = + ChatMessageEntity( + createdAt = createdAt, + member = + ChatMemberEntity( + id = member.id, + name = member.name, + profileUrl = member.profileImageUrl, + ), + content = content, + type = MessageTypeEntity.CHAT, + ) + +fun ChatMemberEntity.toDomain(): ChatMember = + ChatMember( + id = id, + name = name, + profileImageUrl = profileUrl, + ) + +fun ChatMessageEntity.toDomain(myMemberId: Long): ChatComponent = + when (this.type) { + MessageTypeEntity.DATE -> ChatComponent.Date(createdAt = createdAt) + MessageTypeEntity.CHAT -> { + if (myMemberId == member.id) { + Message.Mine( + content = content, + member = member.toDomain(), + createdAt = createdAt, + ) + } else { + Message.Other( + content = content, + member = member.toDomain(), + createdAt = createdAt, + ) + } + } + + MessageTypeEntity.LEAVE -> + ChatComponent.Leave( + member = member.toDomain(), + createdAt = createdAt, + ) + + MessageTypeEntity.ENTER -> + ChatComponent.Enter( + member = member.toDomain(), + createdAt = createdAt, + ) + } diff --git a/android/app/src/main/java/com/happy/friendogly/local/mapper/MessageTypeConverter.kt b/android/app/src/main/java/com/happy/friendogly/local/mapper/MessageTypeConverter.kt new file mode 100644 index 000000000..9463e5ba9 --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/local/mapper/MessageTypeConverter.kt @@ -0,0 +1,16 @@ +package com.happy.friendogly.local.mapper + +import androidx.room.TypeConverter +import com.happy.friendogly.local.room.MessageTypeEntity + +class MessageTypeConverter { + @TypeConverter + fun fromMessageType(messageTypeEntity: MessageTypeEntity): String { + return messageTypeEntity.name + } + + @TypeConverter + fun toMessageType(messageTypeEntity: String): MessageTypeEntity { + return enumValueOf(messageTypeEntity) + } +} diff --git a/android/app/src/main/java/com/happy/friendogly/local/model/ChatMemberEntity.kt b/android/app/src/main/java/com/happy/friendogly/local/model/ChatMemberEntity.kt new file mode 100644 index 000000000..0472b0b93 --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/local/model/ChatMemberEntity.kt @@ -0,0 +1,23 @@ +package com.happy.friendogly.local.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.serialization.Serializable + +@Entity(tableName = "chat_member") +@Serializable +data class ChatMemberEntity( + @PrimaryKey val id: Long, + @ColumnInfo(name = "name") val name: String, + @ColumnInfo(name = "profile_url") val profileUrl: String, +) { + companion object { + fun noChatMember() = + ChatMemberEntity( + -1, + "", + "", + ) + } +} diff --git a/android/app/src/main/java/com/happy/friendogly/local/model/ChatMessageEntity.kt b/android/app/src/main/java/com/happy/friendogly/local/model/ChatMessageEntity.kt new file mode 100644 index 000000000..95789520e --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/local/model/ChatMessageEntity.kt @@ -0,0 +1,30 @@ +package com.happy.friendogly.local.model + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.happy.friendogly.local.room.MessageTypeEntity +import com.happy.friendogly.remote.util.JavaLocalDateTimeSerializer +import kotlinx.serialization.Serializable +import java.time.LocalDateTime + +@Entity(tableName = "chat_message") +@Serializable +data class ChatMessageEntity( + @ColumnInfo(name = "created_at") + @Serializable(with = JavaLocalDateTimeSerializer::class) + val createdAt: LocalDateTime, + @Embedded(prefix = "member_") + val member: ChatMemberEntity, + @ColumnInfo(name = "content") + val content: String, + @ColumnInfo(name = "type") + val type: MessageTypeEntity, + @PrimaryKey(autoGenerate = true) + val id: Long = 0, +) { + companion object { + const val NOT_CONTENT = "" + } +} diff --git a/android/app/src/main/java/com/happy/friendogly/local/model/ChatRoomEntity.kt b/android/app/src/main/java/com/happy/friendogly/local/model/ChatRoomEntity.kt new file mode 100644 index 000000000..fefcf8708 --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/local/model/ChatRoomEntity.kt @@ -0,0 +1,13 @@ +package com.happy.friendogly.local.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.serialization.Serializable + +@Entity(tableName = "chat_room") +@Serializable +data class ChatRoomEntity( + @ColumnInfo(name = "chat_messages") val messages: List, + @PrimaryKey(autoGenerate = true) val id: Long = 0, +) diff --git a/android/app/src/main/java/com/happy/friendogly/local/room/ChatMessageDao.kt b/android/app/src/main/java/com/happy/friendogly/local/room/ChatMessageDao.kt new file mode 100644 index 000000000..d628ebafe --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/local/room/ChatMessageDao.kt @@ -0,0 +1,16 @@ +package com.happy.friendogly.local.room + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.happy.friendogly.local.model.ChatMessageEntity + +@Dao +interface ChatMessageDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(chatMessageEntity: ChatMessageEntity) + + @Query("SELECT * FROM chat_message") + suspend fun getAll(): List +} diff --git a/android/app/src/main/java/com/happy/friendogly/local/room/ChatMessageDatabase.kt b/android/app/src/main/java/com/happy/friendogly/local/room/ChatMessageDatabase.kt new file mode 100644 index 000000000..140eb9e5f --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/local/room/ChatMessageDatabase.kt @@ -0,0 +1,22 @@ +package com.happy.friendogly.local.room + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.happy.friendogly.local.mapper.ChatMessagesConverter +import com.happy.friendogly.local.mapper.LocalDateTimeConverter +import com.happy.friendogly.local.mapper.MessageTypeConverter +import com.happy.friendogly.local.model.ChatMemberEntity +import com.happy.friendogly.local.model.ChatMessageEntity +import com.happy.friendogly.local.model.ChatRoomEntity + +@Database( + entities = [ChatMessageEntity::class, ChatMemberEntity::class, ChatRoomEntity::class], + version = 1, +) +@TypeConverters(value = [LocalDateTimeConverter::class, ChatMessagesConverter::class, MessageTypeConverter::class]) +abstract class ChatMessageDatabase : RoomDatabase() { + abstract fun chatMessageDao(): ChatMessageDao + + abstract fun chatRoomDao(): ChatRoomDao +} diff --git a/android/app/src/main/java/com/happy/friendogly/local/room/ChatRoomDao.kt b/android/app/src/main/java/com/happy/friendogly/local/room/ChatRoomDao.kt new file mode 100644 index 000000000..20d21fb55 --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/local/room/ChatRoomDao.kt @@ -0,0 +1,45 @@ +package com.happy.friendogly.local.room + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import com.happy.friendogly.local.model.ChatMessageEntity +import com.happy.friendogly.local.model.ChatRoomEntity + +@Dao +interface ChatRoomDao { + @Query("SELECT * FROM chat_room WHERE id = :id") + suspend fun getChatRoomById(id: Long): ChatRoomEntity? + + @Update + suspend fun updateChatRoom(chatRoom: ChatRoomEntity) + + @Insert + suspend fun insert(chatRoom: ChatRoomEntity) + + @Transaction + suspend fun addMessageToChatRoom( + chatRoomId: Long, + newMessage: ChatMessageEntity, + ) { + val chatRoom = getChatRoomById(chatRoomId) + if (chatRoom != null) { + val updatedMessages = + chatRoom.messages.toMutableList().apply { + add(newMessage) + } + val updatedChatRoom = chatRoom.copy(messages = updatedMessages) + updateChatRoom(updatedChatRoom) + } else { + insert(ChatRoomEntity(id = chatRoomId, messages = listOf(newMessage))) + } + } + + @Transaction + suspend fun getMessagesByRoomId(chatRoomId: Long): List { + val chatRoom = getChatRoomById(chatRoomId) + return chatRoom?.messages ?: emptyList() + } +} diff --git a/android/app/src/main/java/com/happy/friendogly/local/room/MessageTypeEntity.kt b/android/app/src/main/java/com/happy/friendogly/local/room/MessageTypeEntity.kt new file mode 100644 index 000000000..a64905ece --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/local/room/MessageTypeEntity.kt @@ -0,0 +1,8 @@ +package com.happy.friendogly.local.room + +enum class MessageTypeEntity { + DATE, + CHAT, + LEAVE, + ENTER, +} diff --git a/android/app/src/main/java/com/happy/friendogly/local/source/AlarmSettingDataSourceImpl.kt b/android/app/src/main/java/com/happy/friendogly/local/source/AlarmSettingDataSourceImpl.kt index c2ff57a49..4591fad4c 100644 --- a/android/app/src/main/java/com/happy/friendogly/local/source/AlarmSettingDataSourceImpl.kt +++ b/android/app/src/main/java/com/happy/friendogly/local/source/AlarmSettingDataSourceImpl.kt @@ -1,24 +1,41 @@ package com.happy.friendogly.local.source import com.happy.friendogly.data.source.AlarmSettingDataSource -import com.happy.friendogly.local.di.AlarmModule +import com.happy.friendogly.local.di.ChatAlarmModule +import com.happy.friendogly.local.di.WoofAlarmModule import kotlinx.coroutines.flow.first class AlarmSettingDataSourceImpl( - private val alarmModule: AlarmModule, + private val chatAlarmModule: ChatAlarmModule, + private val woofAlarmModule: WoofAlarmModule, ) : AlarmSettingDataSource { - override suspend fun saveAlarm(isSet: Boolean): Result = + override suspend fun saveChatSetting(isSet: Boolean): Result = runCatching { - alarmModule.saveSetting(isSet) + chatAlarmModule.saveSetting(isSet) } - override suspend fun getAlarm(): Result = + override suspend fun getChatSetting(): Result = runCatching { - alarmModule.isSet.first() + chatAlarmModule.isSet.first() } - override suspend fun deleteAlarm(): Result = + override suspend fun deleteChatSetting(): Result = runCatching { - alarmModule.deleteSetting() + chatAlarmModule.deleteSetting() + } + + override suspend fun saveWoofSetting(isSet: Boolean): Result = + runCatching { + woofAlarmModule.saveSetting(isSet) + } + + override suspend fun getWoofSetting(): Result = + runCatching { + woofAlarmModule.isSet.first() + } + + override suspend fun deleteWoofSetting(): Result = + runCatching { + woofAlarmModule.deleteSetting() } } diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/alarm/AlarmReceiver.kt b/android/app/src/main/java/com/happy/friendogly/presentation/alarm/AlarmReceiver.kt index 3117b2236..aeab6cbbf 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/alarm/AlarmReceiver.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/alarm/AlarmReceiver.kt @@ -9,11 +9,16 @@ import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import com.happy.friendogly.R import com.happy.friendogly.application.di.AppModule +import com.happy.friendogly.domain.model.ChatComponent +import com.happy.friendogly.domain.model.ChatMember +import com.happy.friendogly.domain.model.Message import com.happy.friendogly.presentation.ui.MainActivity +import com.happy.friendogly.presentation.ui.chatlist.chat.ChatLifecycleObserver import com.happy.friendogly.presentation.ui.permission.AlarmPermission import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.time.LocalDateTime class AlarmReceiver : FirebaseMessagingService() { private lateinit var notificationManager: NotificationManager @@ -29,20 +34,84 @@ class AlarmReceiver : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { super.onMessageReceived(message) - if (message.notification != null) { - onReceive(message.notification?.title, message.notification?.body) - } else if (message.data.isNotEmpty()) { - onReceive(message.data[ALARM_TITLE], message.data[ALARM_BODY]) + + if (message.data[ALARM_TYPE] == "CHAT") { + saveMessage(message) + showChatAlarm(message.data["senderName"], message.data["content"]) + } else if (message.data[ALARM_TYPE] == "FOOTPRINT") { + showWoofAlarm(message.data[ALARM_TITLE], message.data[ALARM_BODY]) + } + } + + private fun saveMessage(message: RemoteMessage) = + CoroutineScope(Dispatchers.IO).launch { + val memberId = message.data["senderMemberId"]!!.toLong() + val name = message.data["senderName"] ?: "" + val content = message.data["content"] ?: "" + val createdAt: LocalDateTime = LocalDateTime.parse(message.data["createdAt"]) + val profileUrl = message.data["profilePictureUrl"] ?: "" + val chatRoomId: Long = message.data["chatRoomId"]?.toLong() ?: -1L + val message: ChatComponent = + when (message.data["messageType"]) { + "CHAT" -> + Message.Other( + createdAt = createdAt, + member = + ChatMember( + id = memberId, + name = name, + profileImageUrl = profileUrl, + ), + content = content, + ) + "LEAVE" -> + ChatComponent.Leave( + createdAt = createdAt, + member = + ChatMember( + id = memberId, + name = name, + profileImageUrl = profileUrl, + ), + ) + + "ENTER" -> + ChatComponent.Enter( + createdAt = createdAt, + member = + ChatMember( + id = memberId, + name = name, + profileImageUrl = profileUrl, + ), + ) + else -> error("잘못된 타입이 들어왔습니다.") + } + + AppModule.getInstance().saveChatMessageUseCase(chatRoomId, message) + } + + private fun showChatAlarm( + title: String?, + body: String?, + ) = CoroutineScope(Dispatchers.IO).launch { + notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (AppModule.getInstance().getChatAlarmUseCase.invoke() + .getOrDefault(true) && ChatLifecycleObserver.getInstance().isBackground + ) { + createNotificationChannel() + deliverNotification(title, body) } } - private fun onReceive( + private fun showWoofAlarm( title: String?, body: String?, ) = CoroutineScope(Dispatchers.IO).launch { notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - if (AppModule.getInstance().getAlarmSettingUseCase.invoke().getOrDefault(false)) { + if (AppModule.getInstance().getWoofAlarmUseCase.invoke().getOrDefault(true)) { createNotificationChannel() deliverNotification(title, body) } @@ -94,6 +163,7 @@ class AlarmReceiver : FirebaseMessagingService() { private const val CHANNEL_NAME = "alam" private const val ALARM_TITLE = "title" private const val ALARM_BODY = "body" + private const val ALARM_TYPE = "type" const val NOTIFICATION_ID = 0 const val DEFAULT_INTENT_ID = 1 diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/ChatListFragment.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/ChatListFragment.kt index c25673f73..7fa76e60b 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/ChatListFragment.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/ChatListFragment.kt @@ -20,6 +20,7 @@ class ChatListFragment : override fun initViewCreated() { initAdapter() + binding.vm = viewModel } private fun initAdapter() { diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/ChatListViewModel.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/ChatListViewModel.kt index bbdc90257..7749b2d2e 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/ChatListViewModel.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/ChatListViewModel.kt @@ -1,6 +1,7 @@ package com.happy.friendogly.presentation.ui.chatlist import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -19,6 +20,13 @@ class ChatListViewModel( var memberId: Long = 0L private set + val isChatEmpty = + MediatorLiveData().apply { + addSource(_chats) { + value = it.isEmpty() + } + } + fun getChats() { viewModelScope.launch { getChatListUseCase.invoke().onSuccess { room -> diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chat/ChatActivity.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chat/ChatActivity.kt index d44f709ff..29294ede8 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chat/ChatActivity.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chat/ChatActivity.kt @@ -11,6 +11,7 @@ import com.happy.friendogly.databinding.ActivityChatBinding import com.happy.friendogly.presentation.base.BaseActivity import com.happy.friendogly.presentation.ui.chatlist.chat.adapter.ChatAdapter import com.happy.friendogly.presentation.ui.chatlist.chatinfo.ChatInfoSideSheet +import com.happy.friendogly.presentation.ui.club.detail.ClubDetailActivity import com.happy.friendogly.presentation.ui.otherprofile.OtherProfileActivity import com.happy.friendogly.presentation.utils.hideKeyboard import kotlinx.coroutines.launch @@ -20,6 +21,7 @@ class ChatActivity : ChatNavigationAction { private val viewModel: ChatViewModel by viewModels { ChatViewModel.factory( + AppModule.getInstance().getChatMessagesUseCase, AppModule.getInstance().connectWebsocketUseCase, AppModule.getInstance().disconnectWebsocketUseCase, AppModule.getInstance().subScribeMessageUseCase, @@ -30,7 +32,7 @@ class ChatActivity : override fun initCreateView() { binding.vm = viewModel - + lifecycle.addObserver(ChatLifecycleObserver.getInstance()) initAdapter() getChatList() clickBackBtn() @@ -97,6 +99,15 @@ class ChatActivity : startActivity(OtherProfileActivity.getIntent(this, memberId)) } + override fun navigateToClub(clubId: Long) { + startActivity(ClubDetailActivity.getIntent(this, clubId)) + } + + override fun onDestroy() { + super.onDestroy() + lifecycle.removeObserver(ChatLifecycleObserver.getInstance()) + } + companion object { private const val INVALID_ID = -1L private const val EXTRA_CHAT_ID = "chatId" diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chat/ChatLifecycleObserver.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chat/ChatLifecycleObserver.kt new file mode 100644 index 000000000..909d31376 --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chat/ChatLifecycleObserver.kt @@ -0,0 +1,46 @@ +package com.happy.friendogly.presentation.ui.chatlist.chat + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner + +class ChatLifecycleObserver private constructor() : DefaultLifecycleObserver { + private var state: State = State.Background + + val isForeground + get() = state == State.Foreground + + val isBackground: Boolean + get() = state == State.Background + + override fun onStart(owner: LifecycleOwner) { + super.onStart(owner) + state = State.Foreground + } + + override fun onPause(owner: LifecycleOwner) { + super.onPause(owner) + state = State.Background + } + + override fun onDestroy(owner: LifecycleOwner) { + super.onDestroy(owner) + state = State.Background + } + + enum class State { + Foreground, + Background, + } + + companion object { + @Volatile + private var instance: ChatLifecycleObserver? = null + + fun getInstance(): ChatLifecycleObserver = + instance ?: synchronized(this) { + val newInstance = ChatLifecycleObserver() + instance = newInstance + newInstance + } + } +} diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chat/ChatNavigationAction.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chat/ChatNavigationAction.kt index 98a70990a..0c37d528f 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chat/ChatNavigationAction.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chat/ChatNavigationAction.kt @@ -2,4 +2,6 @@ package com.happy.friendogly.presentation.ui.chatlist.chat interface ChatNavigationAction { fun navigateToMemberProfile(memberId: Long) + + fun navigateToClub(clubId: Long) } diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chat/ChatViewModel.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chat/ChatViewModel.kt index d3828e0b9..7cc1a16b3 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chat/ChatViewModel.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chat/ChatViewModel.kt @@ -8,6 +8,7 @@ import com.happy.friendogly.domain.model.ChatComponent import com.happy.friendogly.domain.model.Message import com.happy.friendogly.domain.usecase.ConnectWebsocketUseCase import com.happy.friendogly.domain.usecase.DisconnectWebsocketUseCase +import com.happy.friendogly.domain.usecase.GetChatMessagesUseCase import com.happy.friendogly.domain.usecase.PublishSendMessageUseCase import com.happy.friendogly.domain.usecase.SubScribeMessageUseCase import com.happy.friendogly.presentation.base.BaseViewModel @@ -20,9 +21,11 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class ChatViewModel( + private val getChatMessagesUseCase: GetChatMessagesUseCase, private val connectWebsocketUseCase: ConnectWebsocketUseCase, private val disconnectWebsocketUseCase: DisconnectWebsocketUseCase, private val subScribeMessageUseCase: SubScribeMessageUseCase, @@ -40,18 +43,17 @@ class ChatViewModel( } } - private val initJob: Deferred = - viewModelScope.async { - connectWebsocketUseCase() - } - fun subscribeMessage( chatRoomId: Long, myMemberId: Long, ) { - viewModelScope.launch { - initJob.await() - val newChat = + launch { + val connect = connect() + val chats = initChats(chatRoomId, myMemberId) + + launch { + connect.await() + _chats.value = chats.await() subScribeMessageUseCase(chatRoomId, myMemberId).distinctUntilChanged().map { when (it) { is ChatComponent.Date -> it.toUiModel() @@ -60,14 +62,34 @@ class ChatViewModel( is Message.Mine -> it.toUiModel() is Message.Other -> it.toUiModel() } + }.collect { newChat -> + _chats.update { it.plus(newChat) } } - - newChat.collect { - _chats.value += listOf(it) } } } + private fun connect(): Deferred = + viewModelScope.async { + connectWebsocketUseCase() + } + + private fun initChats( + chatRoomId: Long, + myMemberId: Long, + ): Deferred> = + viewModelScope.async { + getChatMessagesUseCase(chatRoomId, myMemberId).getOrDefault(emptyList()).map { + when (it) { + is ChatComponent.Date -> it.toUiModel() + is ChatComponent.Enter -> it.toUiModel() + is ChatComponent.Leave -> it.toUiModel() + is Message.Mine -> it.toUiModel() + is Message.Other -> it.toUiModel() + } + } + } + fun sendMessage( chatRoomId: Long, content: String, @@ -86,6 +108,7 @@ class ChatViewModel( companion object { fun factory( + getChatMessagesUseCase: GetChatMessagesUseCase, connectWebsocketUseCase: ConnectWebsocketUseCase, disconnectWebsocketUseCase: DisconnectWebsocketUseCase, subScribeMessageUseCase: SubScribeMessageUseCase, @@ -93,6 +116,7 @@ class ChatViewModel( ): ViewModelProvider.Factory { return BaseViewModelFactory { _ -> ChatViewModel( + getChatMessagesUseCase, connectWebsocketUseCase, disconnectWebsocketUseCase, subScribeMessageUseCase, diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoMapper.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoMapper.kt index 0d55720c3..abff1f7e3 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoMapper.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoMapper.kt @@ -4,9 +4,9 @@ import com.happy.friendogly.domain.model.ChatMember fun ChatMember.toUiModel(myMemberId: Long): JoinPeople = JoinPeople( - nickName = this.memberName, - isMe = myMemberId == memberId, + nickName = this.name, + isMe = myMemberId == id, isLeader = isOwner, - profileUrl = memberProfileImageUrl, - memberId = memberId, + profileUrl = profileImageUrl, + memberId = id, ) diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoSideSheet.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoSideSheet.kt index 27dc2e6a0..e25705896 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoSideSheet.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoSideSheet.kt @@ -5,15 +5,21 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.sidesheet.SideSheetDialog import com.google.android.material.snackbar.Snackbar import com.happy.friendogly.R import com.happy.friendogly.application.di.AppModule import com.happy.friendogly.databinding.LayoutChatDrawerBinding +import com.happy.friendogly.domain.model.Gender +import com.happy.friendogly.domain.model.SizeType +import com.happy.friendogly.presentation.ui.chatlist.chat.ChatActivity import com.happy.friendogly.presentation.ui.chatlist.chat.ChatNavigationAction import com.happy.friendogly.presentation.ui.permission.AlarmPermission +import kotlinx.coroutines.launch class ChatInfoSideSheet : BottomSheetDialogFragment() { private var _binding: LayoutChatDrawerBinding? = null @@ -29,11 +35,15 @@ class ChatInfoSideSheet : BottomSheetDialogFragment() { getString(R.string.chat_setting_alarm_alert), Snackbar.LENGTH_SHORT, ).show() + binding.switchChatSettingAlarm.isChecked = false } } private val viewModel: ChatInfoViewModel by viewModels { - ChatInfoViewModel.factory(AppModule.getInstance().getChatMemberUseCase) + ChatInfoViewModel.factory( + AppModule.getInstance().getChatRoomClubUseCase, + AppModule.getInstance().getChatMemberUseCase, + ) } override fun onCreateView( @@ -66,13 +76,24 @@ class ChatInfoSideSheet : BottomSheetDialogFragment() { val memberId = requireNotNull(arguments?.getLong(EXTRA_MEMBER_ID, INVALID_ID)) val chatId = requireNotNull(arguments?.getLong(EXTRA_CHAT_ROOM_ID, INVALID_ID)) viewModel.getChatMember(myMemberId = memberId, chatRoomId = chatId) - viewModel.getClubInfo() + viewModel.getClubInfo(chatId) + observeData() + } + + private fun observeData() { viewModel.clubInfo.observe(viewLifecycleOwner) { info -> setChatInfo(info) + binding.btnChatClub.setOnClickListener { + (requireActivity() as ChatActivity).navigateToClub(info.clubId) + } } viewModel.joiningPeople.observe(viewLifecycleOwner) { adapter.submitList(it) } + lifecycleScope.launch { + binding.switchChatSettingAlarm.isChecked = + alarmPermission.hasPermissions() && AppModule.getInstance().getChatAlarmUseCase().getOrDefault(true) + } } private fun clickAlarmSetting() { @@ -80,6 +101,9 @@ class ChatInfoSideSheet : BottomSheetDialogFragment() { if (isChecked) { requestNotificationPermission() } + lifecycleScope.launch { + AppModule.getInstance().saveChatAlarmUseCase(isChecked) + } } } @@ -93,18 +117,18 @@ class ChatInfoSideSheet : BottomSheetDialogFragment() { with(binding) { info.dogSize.forEach { when (it) { - DogSize.SMALL -> chbChatDogSmall.isChecked = true - DogSize.MEDIUM -> chbChatDogMedium.isChecked = true - DogSize.LARGE -> chbChatDogLarge.isChecked = true + SizeType.SMALL -> btnChatDogSmall.isVisible = true + SizeType.MEDIUM -> btnChatDogMedium.isVisible = true + SizeType.LARGE -> btnChatDogLarge.isVisible = true } } info.dogGender.forEach { when (it) { - DogGender.MALE -> chbChatDogMale.isChecked = true - DogGender.FEMALE -> chbChatDogFemale.isChecked = true - DogGender.MALE_NEUTERED -> chbChatDogMaleNeutered.isChecked = true - DogGender.FEMALE_NEUTERED -> chbChatDogFemaleNeutered.isChecked = true + Gender.MALE -> btnChatDogMale.isVisible = true + Gender.FEMALE -> btnChatDogFemale.isVisible = true + Gender.MALE_NEUTERED -> btnChatDogMaleNeutered.isVisible = true + Gender.FEMALE_NEUTERED -> btnChatDogFemaleNeutered.isVisible = true } } } diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoUiModel.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoUiModel.kt index 1045edd64..59fb185d1 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoUiModel.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoUiModel.kt @@ -1,8 +1,12 @@ package com.happy.friendogly.presentation.ui.chatlist.chatinfo +import com.happy.friendogly.domain.model.Gender +import com.happy.friendogly.domain.model.SizeType + data class ChatInfoUiModel( - val dogSize: List, - val dogGender: List, + val clubId: Long, + val dogSize: List, + val dogGender: List, ) data class JoinPeople( @@ -12,16 +16,3 @@ data class JoinPeople( val profileUrl: String, val memberId: Long, ) - -enum class DogSize { - SMALL, - MEDIUM, - LARGE, -} - -enum class DogGender { - MALE, - FEMALE, - MALE_NEUTERED, - FEMALE_NEUTERED, -} diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoViewModel.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoViewModel.kt index 4491d3f48..64810fd67 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoViewModel.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/chatinfo/ChatInfoViewModel.kt @@ -5,11 +5,13 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.happy.friendogly.domain.usecase.GetChatMemberUseCase +import com.happy.friendogly.domain.usecase.GetChatRoomClubUseCase import com.happy.friendogly.presentation.base.BaseViewModel import com.happy.friendogly.presentation.base.BaseViewModelFactory import kotlinx.coroutines.launch class ChatInfoViewModel( + private val getChatRoomClubUseCase: GetChatRoomClubUseCase, private val getChatMemberUseCase: GetChatMemberUseCase, ) : BaseViewModel() { private val _clubInfo: MutableLiveData = MutableLiveData() @@ -31,18 +33,29 @@ class ChatInfoViewModel( } } - fun getClubInfo() { // TODO Api 연결 - _clubInfo.value = - ChatInfoUiModel( - dogSize = listOf(DogSize.SMALL), - dogGender = listOf(DogGender.FEMALE), - ) + fun getClubInfo(chatRoomId: Long) { + launch { + getChatRoomClubUseCase(chatRoomId).onSuccess { + _clubInfo.value = + ChatInfoUiModel( + clubId = it.clubId, + dogSize = it.allowedSize, + dogGender = it.allowedGender, + ) + }.onFailure { + // TODO 에러 처리 + } + } } companion object { - fun factory(getChatMemberUseCase: GetChatMemberUseCase): ViewModelProvider.Factory { + fun factory( + getChatRoomClubUseCase: GetChatRoomClubUseCase, + getChatMemberUseCase: GetChatMemberUseCase, + ): ViewModelProvider.Factory { return BaseViewModelFactory { _ -> ChatInfoViewModel( + getChatRoomClubUseCase, getChatMemberUseCase, ) } diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/uimodel/ChatUiMapper.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/uimodel/ChatUiMapper.kt index a80ce6bac..67c2e1676 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/uimodel/ChatUiMapper.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/chatlist/uimodel/ChatUiMapper.kt @@ -6,32 +6,32 @@ import com.happy.friendogly.presentation.ui.chatlist.chat.ChatUiModel fun ChatComponent.Date.toUiModel() = ChatUiModel.Date( - date = created, + date = createdAt.toLocalDate(), ) fun ChatComponent.Leave.toUiModel() = ChatUiModel.ComeOut( - nickName = name, + nickName = member.name, isCome = false, ) fun ChatComponent.Enter.toUiModel() = ChatUiModel.ComeOut( - nickName = name, + nickName = member.name, isCome = true, ) fun Message.Mine.toUiModel() = ChatUiModel.Mine( message = content, - time = dateTime.toLocalTime(), + time = createdAt.toLocalTime(), ) fun Message.Other.toUiModel() = ChatUiModel.Other( - nickName = name, + nickName = member.name, message = content, - time = dateTime.toLocalTime(), - profileUrl = profileUrl, - memberId = memberId, + time = createdAt.toLocalTime(), + profileUrl = member.profileImageUrl, + memberId = member.id, ) diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/profilesetting/ProfileSettingActivity.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/profilesetting/ProfileSettingActivity.kt index d5be6692c..104bfe15c 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/profilesetting/ProfileSettingActivity.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/profilesetting/ProfileSettingActivity.kt @@ -31,6 +31,7 @@ class ProfileSettingActivity : BaseActivity(R.layout.activity_profile_setting) { private val viewModel: ProfileSettingViewModel by viewModels { ProfileSettingViewModel.factory( + saveAlarmTokenUseCase = AppModule.getInstance().saveAlarmTokenUseCase, postMemberUseCase = AppModule.getInstance().postMemberUseCase, saveJwtTokenUseCase = AppModule.getInstance().saveJwtTokenUseCase, ) diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/profilesetting/ProfileSettingViewModel.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/profilesetting/ProfileSettingViewModel.kt index 84dff197d..ee4eae268 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/profilesetting/ProfileSettingViewModel.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/profilesetting/ProfileSettingViewModel.kt @@ -7,10 +7,13 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.createSavedStateHandle +import com.google.android.gms.tasks.Task +import com.google.firebase.messaging.FirebaseMessaging import com.happy.friendogly.domain.error.DataError import com.happy.friendogly.domain.fold import com.happy.friendogly.domain.model.JwtToken import com.happy.friendogly.domain.usecase.PostMemberUseCase +import com.happy.friendogly.domain.usecase.SaveAlamTokenUseCase import com.happy.friendogly.domain.usecase.SaveJwtTokenUseCase import com.happy.friendogly.presentation.base.BaseViewModel import com.happy.friendogly.presentation.base.BaseViewModelFactory @@ -23,6 +26,7 @@ import kotlinx.coroutines.delay import okhttp3.MultipartBody class ProfileSettingViewModel( + private val saveAlarmTokenUseCase: SaveAlamTokenUseCase, private val savedStateHandle: SavedStateHandle, private val postMemberUseCase: PostMemberUseCase, private val saveJwtTokenUseCase: SaveJwtTokenUseCase, @@ -116,6 +120,7 @@ class ProfileSettingViewModel( ).fold( onSuccess = { register -> saveJwaToken(register.tokens) + saveAlarmToken() }, onError = { error -> when (error) { @@ -140,6 +145,19 @@ class ProfileSettingViewModel( ) } + private fun saveAlarmToken() { + FirebaseMessaging.getInstance().token + .addOnCompleteListener { task: Task -> + if (!task.isSuccessful) { + return@addOnCompleteListener + } + val token = task.result + launch { // TODO 에러 핸들링 + saveAlarmTokenUseCase(token) + } + } + } + private suspend fun patchMember( nickname: String, profilePath: MultipartBody.Part?, @@ -167,11 +185,13 @@ class ProfileSettingViewModel( private val regex = "^[ㄱ-ㅎㅏ-ㅣ]+$".toRegex() fun factory( + saveAlarmTokenUseCase: SaveAlamTokenUseCase, postMemberUseCase: PostMemberUseCase, saveJwtTokenUseCase: SaveJwtTokenUseCase, ): ViewModelProvider.Factory { return BaseViewModelFactory { creator -> ProfileSettingViewModel( + saveAlarmTokenUseCase = saveAlarmTokenUseCase, savedStateHandle = creator.createSavedStateHandle(), postMemberUseCase = postMemberUseCase, saveJwtTokenUseCase = saveJwtTokenUseCase, diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/register/RegisterActivity.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/register/RegisterActivity.kt index 446c5ac71..56528a352 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/register/RegisterActivity.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/register/RegisterActivity.kt @@ -27,6 +27,7 @@ class RegisterActivity : AppCompatActivity() { getJwtTokenUseCase = AppModule.getInstance().getJwtTokenUseCase, postKakaoLoginUseCase = AppModule.getInstance().postKakaoLoginUseCase, saveJwtTokenUseCase = AppModule.getInstance().saveJwtTokenUseCase, + saveAlarmTokenUseCase = AppModule.getInstance().saveAlarmTokenUseCase, ) } diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/register/RegisterViewModel.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/register/RegisterViewModel.kt index c4f1f193d..2cecef068 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/register/RegisterViewModel.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/register/RegisterViewModel.kt @@ -15,6 +15,7 @@ import com.happy.friendogly.domain.model.KakaoAccessToken import com.happy.friendogly.domain.usecase.GetJwtTokenUseCase import com.happy.friendogly.domain.usecase.KakaoLoginUseCase import com.happy.friendogly.domain.usecase.PostKakaoLoginUseCase +import com.happy.friendogly.domain.usecase.SaveAlamTokenUseCase import com.happy.friendogly.domain.usecase.SaveJwtTokenUseCase import com.happy.friendogly.presentation.base.BaseViewModel import com.happy.friendogly.presentation.base.BaseViewModelFactory @@ -30,6 +31,7 @@ class RegisterViewModel( private val getJwtTokenUseCase: GetJwtTokenUseCase, private val postKakaoLoginUseCase: PostKakaoLoginUseCase, private val saveJwtTokenUseCase: SaveJwtTokenUseCase, + private val saveAlarmTokenUseCase: SaveAlamTokenUseCase, ) : BaseViewModel() { private val _navigateAction: MutableLiveData> = MutableLiveData(null) @@ -150,6 +152,7 @@ class RegisterViewModel( getJwtTokenUseCase: GetJwtTokenUseCase, postKakaoLoginUseCase: PostKakaoLoginUseCase, saveJwtTokenUseCase: SaveJwtTokenUseCase, + saveAlarmTokenUseCase: SaveAlamTokenUseCase, ): ViewModelProvider.Factory { return BaseViewModelFactory { _ -> RegisterViewModel( @@ -158,6 +161,7 @@ class RegisterViewModel( getJwtTokenUseCase = getJwtTokenUseCase, postKakaoLoginUseCase = postKakaoLoginUseCase, saveJwtTokenUseCase = saveJwtTokenUseCase, + saveAlarmTokenUseCase = saveAlarmTokenUseCase, ) } } diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/setting/SettingActivity.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/setting/SettingActivity.kt index 3935e0140..3a6f6d098 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/setting/SettingActivity.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/setting/SettingActivity.kt @@ -19,6 +19,8 @@ import com.happy.friendogly.presentation.ui.register.RegisterActivity class SettingActivity : BaseActivity(R.layout.activity_setting) { val viewModel: SettingViewModel by viewModels { SettingViewModel.factory( + saveChatAlarmUseCase = AppModule.getInstance().saveChatAlarmUseCase, + saveWoofAlarmUseCase = AppModule.getInstance().saveWoofAlarmUseCase, deleteTokenUseCase = AppModule.getInstance().deleteTokenUseCase, ) } diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/setting/SettingUiState.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/setting/SettingUiState.kt index 390007c4e..c0b0da5d0 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/setting/SettingUiState.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/setting/SettingUiState.kt @@ -4,4 +4,5 @@ data class SettingUiState( val totalAlarmPushPermitted: Boolean = false, val chattingAlarmPushPermitted: Boolean = false, val clubAlarmPushPermitted: Boolean = false, + val woofAlarmPushPermitted: Boolean = false, ) diff --git a/android/app/src/main/java/com/happy/friendogly/presentation/ui/setting/SettingViewModel.kt b/android/app/src/main/java/com/happy/friendogly/presentation/ui/setting/SettingViewModel.kt index 97c21b4c5..48c604f18 100644 --- a/android/app/src/main/java/com/happy/friendogly/presentation/ui/setting/SettingViewModel.kt +++ b/android/app/src/main/java/com/happy/friendogly/presentation/ui/setting/SettingViewModel.kt @@ -6,14 +6,17 @@ import androidx.lifecycle.ViewModelProvider import com.happy.friendogly.domain.error.DataError import com.happy.friendogly.domain.fold import com.happy.friendogly.domain.usecase.DeleteTokenUseCase +import com.happy.friendogly.domain.usecase.SaveChatAlarmUseCase +import com.happy.friendogly.domain.usecase.SaveWoofAlarmUseCase import com.happy.friendogly.presentation.base.BaseViewModel import com.happy.friendogly.presentation.base.BaseViewModelFactory import com.happy.friendogly.presentation.base.Event import com.happy.friendogly.presentation.base.emit -import kotlinx.coroutines.launch class SettingViewModel( private val deleteTokenUseCase: DeleteTokenUseCase, + private val saveChatAlarmUseCase: SaveChatAlarmUseCase, + private val saveWoofAlarmUseCase: SaveWoofAlarmUseCase, ) : BaseViewModel() { private val _uiState: MutableLiveData = MutableLiveData(SettingUiState()) @@ -31,10 +34,20 @@ class SettingViewModel( fun onTotalAlarmToggled(checked: Boolean) {} - fun onChattingAlarmToggled(checked: Boolean) {} + fun onChattingAlarmToggled(checked: Boolean) { + launch { + saveChatAlarmUseCase(checked) + } + } fun onClubAlarmToggled(checked: Boolean) {} + fun onWoofAlarmToggled(checked: Boolean) { + launch { + saveWoofAlarmUseCase(checked) + } + } + fun navigateToBack() { _navigateAction.emit(SettingNavigationAction.NavigateToBack) } @@ -94,9 +107,15 @@ class SettingViewModel( } companion object { - fun factory(deleteTokenUseCase: DeleteTokenUseCase): ViewModelProvider.Factory { + fun factory( + saveChatAlarmUseCase: SaveChatAlarmUseCase, + saveWoofAlarmUseCase: SaveWoofAlarmUseCase, + deleteTokenUseCase: DeleteTokenUseCase, + ): ViewModelProvider.Factory { return BaseViewModelFactory { _ -> SettingViewModel( + saveChatAlarmUseCase = saveChatAlarmUseCase, + saveWoofAlarmUseCase = saveWoofAlarmUseCase, deleteTokenUseCase = deleteTokenUseCase, ) } diff --git a/android/app/src/main/java/com/happy/friendogly/remote/api/AlarmTokenService.kt b/android/app/src/main/java/com/happy/friendogly/remote/api/AlarmTokenService.kt index ef416303d..a28b097bd 100644 --- a/android/app/src/main/java/com/happy/friendogly/remote/api/AlarmTokenService.kt +++ b/android/app/src/main/java/com/happy/friendogly/remote/api/AlarmTokenService.kt @@ -3,10 +3,10 @@ package com.happy.friendogly.remote.api import com.happy.friendogly.remote.model.request.DeviceTokenRequest import com.happy.friendogly.remote.model.response.BaseResponse import retrofit2.http.Body -import retrofit2.http.PATCH +import retrofit2.http.PUT interface AlarmTokenService { - @PATCH(ApiClient.AlarmToken.DEVICE_TOKEN) + @PUT(ApiClient.AlarmToken.DEVICE_TOKEN) suspend fun patchDeviceTokens( @Body body: DeviceTokenRequest, ): BaseResponse diff --git a/android/app/src/main/java/com/happy/friendogly/remote/api/ApiClient.kt b/android/app/src/main/java/com/happy/friendogly/remote/api/ApiClient.kt index a4658053a..673c4eb8a 100644 --- a/android/app/src/main/java/com/happy/friendogly/remote/api/ApiClient.kt +++ b/android/app/src/main/java/com/happy/friendogly/remote/api/ApiClient.kt @@ -4,7 +4,7 @@ class ApiClient { object Auth { private const val BASE_URL = "/auth" const val POST_KAKAO_LOGIN = "$BASE_URL/kakao/login" - const val POST_REFRESH = "/$BASE_URL/kakao/refresh" + const val POST_REFRESH = "$BASE_URL/kakao/refresh" } object Footprints { @@ -50,6 +50,7 @@ class ApiClient { private const val BASE_URL = "/chat-rooms" const val CHAT_LIST = "$BASE_URL/mine" const val MEMBERS = "$BASE_URL/{chatRoomId}" + const val CLUB = "$BASE_URL/{chatRoomId}/club" } object WebSocket { diff --git a/android/app/src/main/java/com/happy/friendogly/remote/api/ChatService.kt b/android/app/src/main/java/com/happy/friendogly/remote/api/ChatService.kt index 40518ba45..5d9dee191 100644 --- a/android/app/src/main/java/com/happy/friendogly/remote/api/ChatService.kt +++ b/android/app/src/main/java/com/happy/friendogly/remote/api/ChatService.kt @@ -2,8 +2,8 @@ package com.happy.friendogly.remote.api import com.happy.friendogly.remote.model.response.BaseResponse import com.happy.friendogly.remote.model.response.ChatClubMemberResponse +import com.happy.friendogly.remote.model.response.ChatRoomClubResponse import com.happy.friendogly.remote.model.response.ChatRoomListResponse -import okhttp3.Response import retrofit2.http.GET import retrofit2.http.Path @@ -14,5 +14,10 @@ interface ChatService { @GET(ApiClient.Chat.MEMBERS) suspend fun getChatMembers( @Path("chatRoomId") chatRoomId: Long, - ): retrofit2.Response> + ): BaseResponse> + + @GET(ApiClient.Chat.CLUB) + suspend fun getChatClub( + @Path("chatRoomId") chatRoomId: Long, + ): BaseResponse } diff --git a/android/app/src/main/java/com/happy/friendogly/remote/mapper/ChatRoomClubMapper.kt b/android/app/src/main/java/com/happy/friendogly/remote/mapper/ChatRoomClubMapper.kt new file mode 100644 index 000000000..6f52d5384 --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/remote/mapper/ChatRoomClubMapper.kt @@ -0,0 +1,11 @@ +package com.happy.friendogly.remote.mapper + +import com.happy.friendogly.data.model.ChatRoomClubDto +import com.happy.friendogly.remote.model.response.ChatRoomClubResponse + +fun ChatRoomClubResponse.toData(): ChatRoomClubDto = + ChatRoomClubDto( + clubId = clubId, + allowedGender = allowedGenders.map { it.toData() }, + allowedSize = allowedSizeTypes.map { it.toData() }, + ) diff --git a/android/app/src/main/java/com/happy/friendogly/remote/model/response/ChatRoomClubResponse.kt b/android/app/src/main/java/com/happy/friendogly/remote/model/response/ChatRoomClubResponse.kt new file mode 100644 index 000000000..e12a10b28 --- /dev/null +++ b/android/app/src/main/java/com/happy/friendogly/remote/model/response/ChatRoomClubResponse.kt @@ -0,0 +1,10 @@ +package com.happy.friendogly.remote.model.response + +import kotlinx.serialization.Serializable + +@Serializable +data class ChatRoomClubResponse( + val clubId: Long, + val allowedSizeTypes: List, + val allowedGenders: List, +) diff --git a/android/app/src/main/java/com/happy/friendogly/remote/source/ChatDataSourceImpl.kt b/android/app/src/main/java/com/happy/friendogly/remote/source/ChatDataSourceImpl.kt index 5cff771e7..bd03ca1c0 100644 --- a/android/app/src/main/java/com/happy/friendogly/remote/source/ChatDataSourceImpl.kt +++ b/android/app/src/main/java/com/happy/friendogly/remote/source/ChatDataSourceImpl.kt @@ -1,6 +1,7 @@ package com.happy.friendogly.remote.source import com.happy.friendogly.data.model.ChatMemberDto +import com.happy.friendogly.data.model.ChatRoomClubDto import com.happy.friendogly.data.model.ChatRoomListDto import com.happy.friendogly.data.source.ChatDataSource import com.happy.friendogly.remote.api.ChatService @@ -14,6 +15,11 @@ class ChatDataSourceImpl(private val service: ChatService) : ChatDataSource { override suspend fun getMembers(chatRoomId: Long): Result> = runCatching { - service.getChatMembers(chatRoomId).body()?.map { it.toData() } ?: emptyList() + service.getChatMembers(chatRoomId).data.map { it.toData() } + } + + override suspend fun getClubs(chatRoomId: Long): Result = + runCatching { + service.getChatClub(chatRoomId).data.toData() } } diff --git a/android/app/src/main/res/drawable/ic_hashtag.xml b/android/app/src/main/res/drawable/ic_hashtag.xml new file mode 100644 index 000000000..ae7884a9d --- /dev/null +++ b/android/app/src/main/res/drawable/ic_hashtag.xml @@ -0,0 +1,12 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_no_chat.xml b/android/app/src/main/res/drawable/ic_no_chat.xml new file mode 100644 index 000000000..ef27a96b3 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_no_chat.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/rect_coral30_fill_8.xml b/android/app/src/main/res/drawable/rect_coral30_fill_8.xml new file mode 100644 index 000000000..cf1441319 --- /dev/null +++ b/android/app/src/main/res/drawable/rect_coral30_fill_8.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/android/app/src/main/res/drawable/rect_gray100_fill_8.xml b/android/app/src/main/res/drawable/rect_gray100_fill_8.xml new file mode 100644 index 000000000..a73106f4a --- /dev/null +++ b/android/app/src/main/res/drawable/rect_gray100_fill_8.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/android/app/src/main/res/layout/activity_setting.xml b/android/app/src/main/res/layout/activity_setting.xml index 1cc62282a..073984c76 100644 --- a/android/app/src/main/res/layout/activity_setting.xml +++ b/android/app/src/main/res/layout/activity_setting.xml @@ -150,13 +150,44 @@ app:trackTint="@color/sel_switch_track" tools:checked="true" /> + + + + + + + + - + android:layout_height="wrap_content" + app:layout_constraintTop_toTopOf="parent"> @@ -33,12 +42,37 @@ - + + + + + diff --git a/android/app/src/main/res/layout/layout_chat_drawer.xml b/android/app/src/main/res/layout/layout_chat_drawer.xml index c1f61ee6d..45c1163f5 100644 --- a/android/app/src/main/res/layout/layout_chat_drawer.xml +++ b/android/app/src/main/res/layout/layout_chat_drawer.xml @@ -5,45 +5,10 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white" - android:paddingStart="12dp" + android:paddingHorizontal="12dp" app:layout_behavior="com.google.android.material.sidesheet.SideSheetBehavior" tools:openDrawer="end"> - - - - - - - + app:layout_constraintTop_toTopOf="parent" /> + +